Skip to content

feat(mwa): API parity, auth cache, disconnect/reconnect UX#264

Open
Pratikkale26 wants to merge 4 commits intomagicblock-labs:mainfrom
Pratikkale26:feature/mwa-api-parity
Open

feat(mwa): API parity, auth cache, disconnect/reconnect UX#264
Pratikkale26 wants to merge 4 commits intomagicblock-labs:mainfrom
Pratikkale26:feature/mwa-api-parity

Conversation

@Pratikkale26
Copy link
Copy Markdown

@Pratikkale26 Pratikkale26 commented Mar 23, 2026

⚠️ This is a Draft PR submitted as proof of work alongside a Solana Seeker Grant application.
The implementation is complete and functional. Please review the changes as part of the grant evaluation.

Status Type ⚠️ Core Change Issue
Ready Feature Yes N/A

Problem

The Solana.Unity-SDK MWA implementation is missing three features that the React Native MWA SDK has:

  1. deauthorize — No way to explicitly revoke a wallet session token. Logout() just clears local state without telling the wallet app, leaking the token indefinitely.
  2. get_capabilities — No way for apps to query a wallet's supported features (max transaction batch sizes, supported versions).
  3. Auth token persistence — The _authToken is only stored in memory. Every app restart forces the user to approve wallet authorization again, even if they connected yesterday. This is the single biggest UX problem for Solana Unity games on Android.

Solution

1. IMwaAuthCache — Extensible Auth Token Cache Layer

Introduced a new IMwaAuthCache interface that decouples token persistence from the adapter. Developers can inject their own encrypted or platform-specific storage backends.

  • PlayerPrefsAuthCache — Default Unity PlayerPrefs implementation (works out of the box)
  • EncryptedAuthCache — Stub template for secure storage backends
  • Cache is injected via constructor: new SolanaWalletAdapter(options, authCache: myCache)

2. Silent Reconnect via Cached Token

Login() / Connect() now first checks the cache for a stored token and attempts a reauthorize silently (no user prompt). Falls back to full authorize only if the token is missing or expired.

3. Deauthorize — Clean Session Termination

  • Added Deauthorize(string authToken) to IAdapterOperations and implemented the deauthorize JSON-RPC call in MobileWalletAdapterClient
  • Logout() now calls Deauthorize before clearing local state, properly revoking the session in the wallet app
  • New DisconnectWallet() public async method wraps this for clean UI-triggered logout

4. GetCapabilities — Wallet Feature Detection

  • Added GetCapabilities() to IAdapterOperations and implemented get_capabilities JSON-RPC call
  • Returns WalletCapabilities model with MaxTransactionsPerRequest, MaxMessagesPerRequest, SupportedTransactionVersions

5. New Public API on SolanaWalletAdapter

// Explicit disconnect — deauthorizes, clears cache, fires event
await wallet.DisconnectWallet();

// Silent reconnect using cached token (no user prompt if valid)
await wallet.ReconnectWallet();

// Query wallet feature limits before sending batch requests
var caps = await wallet.GetCapabilities();

// Subscribe to lifecycle events for UI updates
wallet.OnWalletDisconnected += () => ShowConnectButton();
wallet.OnWalletReconnected += () => ShowWalletAddress();

Before & After Screenshots

BEFORE — User forced to approve wallet on every app launch:

App Launch → [Phantom opens] → "Allow connection?" → [User taps Approve] → Connected
App Launch → [Phantom opens] → "Allow connection?" → [User taps Approve] → Connected  ← AGAIN!
App Launch → [Phantom opens] → "Allow connection?" → [User taps Approve] → Connected  ← EVERY TIME!

AFTER — Silent reconnect with cached token:

App Launch 1 → [Phantom opens] → "Allow connection?" → [User taps Approve] → Connected ← Token cached
App Launch 2 → Silent reauthorize → Connected instantly                                 ← No popup!
App Launch 3 → Silent reauthorize → Connected instantly                                 ← No popup!

Other Changes

  • IAdapterOperations.cs — Added XML doc comments to all existing methods for better IDE support
  • SolanaMobileWalletAdapter.cs — Auth token is now refreshed in the cache after every SignTransaction and SignMessage call (wallets may rotate tokens)
  • SolanaWalletAdapter.cs — Constructor now accepts optional IMwaAuthCache authCache parameter (backward compatible, defaults to PlayerPrefsAuthCache)

Deploy Notes

New files:

  • Runtime/codebase/SolanaMobileStack/MwaAuthCache/IMwaAuthCache.cs — Cache interface
  • Runtime/codebase/SolanaMobileStack/MwaAuthCache/PlayerPrefsAuthCache.cs — Default impl
  • Runtime/codebase/SolanaMobileStack/MwaAuthCache/EncryptedAuthCache.cs — Extensible stub
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/WalletCapabilities.cs — Response model

New dependencies: None. All implementations use existing Unity APIs (PlayerPrefs) and existing SDK infrastructure.

Breaking changes: None. All changes are additive. Existing constructor calls continue to work identically.

Summary by CodeRabbit

  • New Features

    • Disconnect/Reconnect wallet APIs and OnWalletDisconnected/OnWalletReconnected events
    • Silent reauthorization with cached tokens; deauthorize and get_capabilities RPCs
    • Queryable wallet capabilities model (limits, supported versions)
    • Pluggable auth cache API with a default PlayerPrefs implementation and encrypted-cache placeholder
    • Async entrypoint for executing queued adapter actions
  • Documentation

    • Proposal for productionization, auth-cache pattern, UX and lifecycle changes

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 23, 2026

Walkthrough

Adds wallet lifecycle RPCs (deauthorize, get_capabilities), a WalletCapabilities response model, an auth-token cache abstraction (IMwaAuthCache) with PlayerPrefs and encrypted stubs, silent reauthorize and disconnect/reconnect flows, and wiring in adapters/clients to use the new cache and APIs.

Changes

Cohort / File(s) Summary
Interface & Response Model
Runtime/codebase/SolanaMobileStack/Interfaces/IAdapterOperations.cs, Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/WalletCapabilities.cs, Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/WalletCapabilities.cs.meta
Added Deauthorize(string authToken) and GetCapabilities() to the adapter interface (with XML docs); added WalletCapabilities DTO and its Unity .meta.
RPC Client
Runtime/codebase/SolanaMobileStack/MobileWalletAdapterClient.cs
Implemented Deauthorize and GetCapabilities RPC calls and refactored request-building helpers (stylistic).
Auth Cache API & Implementations
Runtime/codebase/SolanaMobileStack/MwaAuthCache/IMwaAuthCache.cs, Runtime/codebase/SolanaMobileStack/MwaAuthCache/PlayerPrefsAuthCache.cs, Runtime/codebase/SolanaMobileStack/MwaAuthCache/EncryptedAuthCache.cs, Runtime/codebase/SolanaMobileStack/MwaAuthCache/*.meta
Introduced IMwaAuthCache abstraction and two implementations: PlayerPrefsAuthCache (concrete storage) and EncryptedAuthCache (NotImplemented stub). Added Unity .meta files.
Mobile Adapter Core
Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs
Injected IMwaAuthCache; added silent reauthorize flow, DisconnectWallet, ReconnectWallet, GetCapabilities, events OnWalletDisconnected/OnWalletReconnected, updated token persistence and Logout behavior.
Routing Adapter / Public Wiring
Runtime/codebase/SolanaWalletAdapter.cs
Constructor gains optional IMwaAuthCache authCache; forwards cache and mobile events on Android. Added public DisconnectWallet, ReconnectWallet, GetCapabilities, and related events.
Association Flow
Runtime/codebase/SolanaMobileStack/LocalAssociationScenario.cs
Added async action entrypoint StartAndExecuteAsync and async-aware action execution (ExecuteNextActionAsync) to support async adapter operations.
Docs / Proposal
PITCH.md
New proposal describing productionization plan, API parity, auth cache design, and UX/flow changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: API parity features (deauthorize, get_capabilities), auth cache infrastructure, and disconnect/reconnect UX improvements.
Description check ✅ Passed The description is comprehensive and well-structured, covering problem statement, detailed solution with code examples, before/after comparison, deployment notes, and acknowledgment of draft status. All template sections are adequately addressed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs (1)

110-122: ⚠️ Potential issue | 🔴 Critical

Critical: StartAndExecute and ExecuteNextAction do not properly await async actions, creating a fire-and-forget pattern.

The issue is in LocalAssociationScenario.ExecuteNextAction() (line 137):

private void ExecuteNextAction(Response<object> response = null)
{
    // ...
    action.Invoke(_client);  // Does not await
}

When an async lambda is passed as Action<IAdapterOperations>, invoking it without await results in:

  1. The async operation runs as async void (fire-and-forget)
  2. Exceptions inside the lambda are lost
  3. The caller continues immediately—before the async work completes
  4. In the code snippet at lines 110-122, authorization is assigned inside an unawaited async void lambda, so it remains uninitialized when code after StartAndExecute executes

This affects all action lambdas: lines 113-120 (_Login), 156-158 (DisconnectWallet), 199-202 (GetCapabilities), 233-248 (_SignAllTransactions), 285-300 (SignMessage), 354-361 (TryReauthorize).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs` around lines
110 - 122, LocalAssociationScenario currently takes Action<IAdapterOperations>
and calls action.Invoke(_client) in ExecuteNextAction, causing async lambdas to
run fire-and-forget; change the action type to Func<IAdapterOperations, Task>
(or ValueTask) across the API (e.g., the StartAndExecute parameter and any
callers using Action<IAdapterOperations>), update
LocalAssociationScenario.ExecuteNextAction to be async Task and await the
invoked func (await action.Invoke(_client)), and propagate async/await through
StartAndExecute so callers (e.g., the code assigning authorization) properly
await completion and exceptions are propagated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/WalletCapabilities.cs`:
- Around line 17-41: Remove the [RequiredMember] attribute from
MaxTransactionsPerRequest, MaxMessagesPerRequest, SupportedTransactionVersions,
and SupportsCloneAuthorization so nullable semantics remain consistent with the
XML docs and optional JSON fields; update the XML comments: change
SupportedTransactionVersions to describe supported transaction version
identifiers (e.g., "legacy", "0") instead of "feature set identifiers", and
change SupportsCloneAuthorization to state it indicates whether the wallet
supports clone authorization (not the sign_and_send_transactions endpoint); keep
the properties as nullable (int?, List<string>, bool?) to reflect optional
reporting by wallets.

In `@Runtime/codebase/SolanaMobileStack/MwaAuthCache/EncryptedAuthCache.cs`:
- Around line 17-44: The EncryptedAuthCache class currently silently no-ops
GetAuthToken, SetAuthToken, and ClearAuthToken which drops tokens; change this
to fail fast by throwing a clear exception (e.g., NotImplementedException or
InvalidOperationException) from GetAuthToken, SetAuthToken, and ClearAuthToken
in the EncryptedAuthCache class so callers immediately know the secure backend
is not implemented; optionally add a constructor that accepts an
encryption/secure storage provider interface (e.g., ISecureStorage) and document
that these methods should delegate to that provider once implemented.

In `@Runtime/codebase/SolanaMobileStack/MwaAuthCache/PlayerPrefsAuthCache.cs`:
- Around line 20-46: Validate and sanitize inputs to avoid silent key
collisions: in GetAuthToken, SetAuthToken, and ClearAuthToken check that
walletIdentity is not null/empty and does not contain reserved characters (or
encode it) before calling BuildKey; in SetAuthToken also validate token is not
null/empty (and optionally enforce a max length). Throw
ArgumentNullException/ArgumentException immediately for invalid inputs. Update
BuildKey (and KeyPrefix usage) to produce an unambiguous key by encoding or
escaping walletIdentity (e.g., URI-escape or Base64) to prevent collisions
between different identities that could form the same concatenated key.

In `@Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs`:
- Around line 183-186: ReconnectWallet currently just calls Login, which may
confuse readers; update the method to include a brief inline comment or expand
its XML doc comment above ReconnectWallet explaining that it intentionally
delegates to Login (which performs silent reauthorization) to provide semantic
clarity for callers and preserve a distinct API surface; reference the
ReconnectWallet() method and the Login() method in the comment so future
maintainers understand the intentional wrapper behavior.
- Around line 147-176: Update DisconnectWallet to log the full exception when
deauthorize fails: inside the catch for the
LocalAssociationScenario.StartAndExecute call (where you currently reference
e.Message), include the full exception details (e.ToString()) or use
Debug.LogWarningException so the stack trace is preserved; keep the same
best-effort behavior (do not rethrow), still clear _authToken, call
_authCache.ClearAuthToken(WalletIdentity), invoke OnWalletDisconnected and
base.Logout as before.
- Line 8: Remove the unused using directive "using UnityEngine.Events;" from the
top of the file; the code uses System.Action-based events (e.g., the Action
delegates in SolanaMobileWalletAdapter class) so the UnityEngine.Events
namespace is unnecessary—delete that import to clean up unused references.
- Around line 333-337: The override Logout currently fire-and-forgets
DisconnectWallet(), causing swallowed exceptions and race conditions; change it
to (1) call base.Logout() to synchronously clear Account and Mnemonic, then (2)
synchronously wait for DisconnectWallet() to complete using
DisconnectWallet().ConfigureAwait(false).GetAwaiter().GetResult() (wrap in
try/catch to log or rethrow as appropriate) so exceptions surface and callers
observe completion before Logout returns.

---

Outside diff comments:
In `@Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs`:
- Around line 110-122: LocalAssociationScenario currently takes
Action<IAdapterOperations> and calls action.Invoke(_client) in
ExecuteNextAction, causing async lambdas to run fire-and-forget; change the
action type to Func<IAdapterOperations, Task> (or ValueTask) across the API
(e.g., the StartAndExecute parameter and any callers using
Action<IAdapterOperations>), update LocalAssociationScenario.ExecuteNextAction
to be async Task and await the invoked func (await action.Invoke(_client)), and
propagate async/await through StartAndExecute so callers (e.g., the code
assigning authorization) properly await completion and exceptions are
propagated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8261121a-e991-41be-8351-cd010039f32d

📥 Commits

Reviewing files that changed from the base of the PR and between 91941ec and 6d9f699.

📒 Files selected for processing (13)
  • Runtime/codebase/SolanaMobileStack/Interfaces/IAdapterOperations.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/WalletCapabilities.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/WalletCapabilities.cs.meta
  • Runtime/codebase/SolanaMobileStack/MobileWalletAdapterClient.cs
  • Runtime/codebase/SolanaMobileStack/MwaAuthCache.meta
  • Runtime/codebase/SolanaMobileStack/MwaAuthCache/EncryptedAuthCache.cs
  • Runtime/codebase/SolanaMobileStack/MwaAuthCache/EncryptedAuthCache.cs.meta
  • Runtime/codebase/SolanaMobileStack/MwaAuthCache/IMwaAuthCache.cs
  • Runtime/codebase/SolanaMobileStack/MwaAuthCache/IMwaAuthCache.cs.meta
  • Runtime/codebase/SolanaMobileStack/MwaAuthCache/PlayerPrefsAuthCache.cs
  • Runtime/codebase/SolanaMobileStack/MwaAuthCache/PlayerPrefsAuthCache.cs.meta
  • Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs
  • Runtime/codebase/SolanaWalletAdapter.cs

Comment on lines +20 to +46
public Task<string> GetAuthToken(string walletIdentity)
{
var key = BuildKey(walletIdentity);
var token = PlayerPrefs.GetString(key, null);
return Task.FromResult(string.IsNullOrEmpty(token) ? null : token);
}

/// <inheritdoc/>
public Task SetAuthToken(string walletIdentity, string token)
{
var key = BuildKey(walletIdentity);
PlayerPrefs.SetString(key, token);
PlayerPrefs.Save();
return Task.CompletedTask;
}

/// <inheritdoc/>
public Task ClearAuthToken(string walletIdentity)
{
var key = BuildKey(walletIdentity);
PlayerPrefs.DeleteKey(key);
PlayerPrefs.Save();
return Task.CompletedTask;
}

private static string BuildKey(string walletIdentity) => KeyPrefix + walletIdentity;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add input validation for cache keys/tokens to avoid silent key collisions.

walletIdentity and token are currently accepted unchecked. Invalid inputs can produce shared keys or ambiguous cache state.

🛡️ Proposed defensive checks
+using System;
 using System.Threading.Tasks;
 using UnityEngine;
@@
         public Task<string> GetAuthToken(string walletIdentity)
         {
             var key = BuildKey(walletIdentity);
             var token = PlayerPrefs.GetString(key, null);
             return Task.FromResult(string.IsNullOrEmpty(token) ? null : token);
         }
@@
         public Task SetAuthToken(string walletIdentity, string token)
         {
+            if (string.IsNullOrEmpty(token))
+                throw new ArgumentException("Auth token must be non-empty.", nameof(token));
+
             var key = BuildKey(walletIdentity);
             PlayerPrefs.SetString(key, token);
             PlayerPrefs.Save();
             return Task.CompletedTask;
         }
@@
-        private static string BuildKey(string walletIdentity) => KeyPrefix + walletIdentity;
+        private static string BuildKey(string walletIdentity)
+        {
+            if (string.IsNullOrWhiteSpace(walletIdentity))
+                throw new ArgumentException("Wallet identity must be non-empty.", nameof(walletIdentity));
+
+            return KeyPrefix + walletIdentity;
+        }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Runtime/codebase/SolanaMobileStack/MwaAuthCache/PlayerPrefsAuthCache.cs`
around lines 20 - 46, Validate and sanitize inputs to avoid silent key
collisions: in GetAuthToken, SetAuthToken, and ClearAuthToken check that
walletIdentity is not null/empty and does not contain reserved characters (or
encode it) before calling BuildKey; in SetAuthToken also validate token is not
null/empty (and optionally enforce a max length). Throw
ArgumentNullException/ArgumentException immediately for invalid inputs. Update
BuildKey (and KeyPrefix usage) to produce an unambiguous key by encoding or
escaping walletIdentity (e.g., URI-escape or Base64) to prevent collisions
between different identities that could form the same concatenated key.

Comment on lines +183 to +186
public async Task<Account> ReconnectWallet()
{
return await Login();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

ReconnectWallet() is just a wrapper around Login() - consider documenting the distinction.

The method body simply delegates to Login(), which already handles silent reauthorization. The value of this separate method is the semantic clarity for callers. Consider adding an inline comment or expanding the doc comment to clarify this is intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs` around lines
183 - 186, ReconnectWallet currently just calls Login, which may confuse
readers; update the method to include a brief inline comment or expand its XML
doc comment above ReconnectWallet explaining that it intentionally delegates to
Login (which performs silent reauthorization) to provide semantic clarity for
callers and preserve a distinct API surface; reference the ReconnectWallet()
method and the Login() method in the comment so future maintainers understand
the intentional wrapper behavior.

Comment on lines +333 to +337
public override void Logout()
{
// Fire-and-forget the async disconnect (can't await in override void)
_ = DisconnectWallet();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fire-and-forget async call in synchronous Logout() override.

_ = DisconnectWallet() discards the Task, meaning:

  1. Any exceptions in DisconnectWallet are silently swallowed
  2. The caller cannot determine when the disconnect operation completes
  3. Account and Mnemonic (cleared in base.Logout() at line 175) may still be non-null immediately after Logout() returns, since DisconnectWallet runs asynchronously

This can cause race conditions in calling code that expects Logout() to be synchronous.

Proposed mitigation: Add synchronous cache clear as fallback
 public override void Logout()
 {
-    // Fire-and-forget the async disconnect (can't await in override void)
-    _ = DisconnectWallet();
+    // Clear local state synchronously for immediate consistency
+    _authToken = null;
+    base.Logout();
+    
+    // Fire-and-forget the network deauthorize (best-effort cleanup)
+    _ = Task.Run(async () =>
+    {
+        try
+        {
+            await _authCache.ClearAuthToken(WalletIdentity);
+            // Optionally send deauthorize to wallet
+        }
+        catch (Exception e)
+        {
+            Debug.LogWarning($"[MWA] Async logout cleanup failed: {e.Message}");
+        }
+    });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs` around lines
333 - 337, The override Logout currently fire-and-forgets DisconnectWallet(),
causing swallowed exceptions and race conditions; change it to (1) call
base.Logout() to synchronously clear Account and Mnemonic, then (2)
synchronously wait for DisconnectWallet() to complete using
DisconnectWallet().ConfigureAwait(false).GetAwaiter().GetResult() (wrap in
try/catch to log or rethrow as appropriate) so exceptions surface and callers
observe completion before Logout returns.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@PITCH.md`:
- Around line 121-131: The budget total header "## 💰 Budget — $6,500 USD (in
SKR)" and the table's "**Total** | **$6,500**" are inconsistent with the sum of
line items (3500+800+1200+700+800+500 = $7,500); update either the individual
line item values or change both the header and the Total row to "$7,500" so the
header, table content, and Total row all match (locate the budget section title
"## 💰 Budget — $6,500 USD (in SKR)", the item rows "Core SDK implementation",
"PR review rounds + integration polish", "Full SDK documentation", "Open-source
Example Android App", "Community support + post-merge maintenance", "Buffer /
contingency", and the "**Total**" row to apply the fix).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 20a9b271-42aa-4c46-abd5-0cf3ac81286a

📥 Commits

Reviewing files that changed from the base of the PR and between 6d9f699 and 093c785.

📒 Files selected for processing (1)
  • PITCH.md

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Runtime/codebase/SolanaMobileStack/LocalAssociationScenario.cs (1)

154-168: ⚠️ Potential issue | 🔴 Critical

Fix async queue dispatch before this lands.

With StartAndExecuteAsync(), _actions is never initialized, so Line 156 dereferences null on the first call from ReceivePublicKeyHandler(). This block also falls through after CloseAssociation(response), which can double-complete _startAssociationTaskCompletionSource or dequeue from an empty queue on the terminal response. That breaks every new async MWA flow built on this path.

Suggested fix
 private void ExecuteNextAction(Response<object> response = null)
 {
-    if (_actions.Count == 0 || response is { Failed: true })
-        CloseAssociation(response);
-
-    if (_useAsyncActions)
-    {
-        // Properly await the async action before proceeding
-        ExecuteNextActionAsync(response);
-    }
-    else
-    {
-        var action = _actions.Dequeue();
-        action.Invoke(_client);
-    }
+    var isTerminal = response is { Failed: true };
+    var hasPendingActions = _useAsyncActions
+        ? _asyncActions != null && _asyncActions.Count > 0
+        : _actions != null && _actions.Count > 0;
+
+    if (!hasPendingActions || isTerminal)
+    {
+        CloseAssociation(response);
+        return;
+    }
+
+    if (_useAsyncActions)
+    {
+        ExecuteNextActionAsync(response);
+        return;
+    }
+
+    var action = _actions.Dequeue();
+    action.Invoke(_client);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Runtime/codebase/SolanaMobileStack/LocalAssociationScenario.cs` around lines
154 - 168, ExecuteNextAction currently dereferences a possibly null _actions
(StartAndExecuteAsync never initializes it), falls through after
CloseAssociation(response) causing double-complete of
_startAssociationTaskCompletionSource and dequeues on empty queue; fix by
ensuring _actions is initialized in StartAndExecuteAsync (or lazily in
ExecuteNextAction) before any use, make ExecuteNextAction return immediately
after calling CloseAssociation when response is null/failed or _actions.Count ==
0, and when _useAsyncActions is true await the async helper (await
ExecuteNextActionAsync(response)) instead of fire-and-forget; also guard
completion of _startAssociationTaskCompletionSource in CloseAssociation (check
IsCompleted/try-set) to prevent double-complete.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs`:
- Around line 95-99: The lifecycle events are raised before the adapter's local
state is updated, causing handlers to see stale session data; move the event
invocations so state changes happen first: for the reauthorization path, assign
_authToken, await _authCache.SetAuthToken(WalletIdentity, _authToken), construct
and set the new Account (using new PublicKey(reauthorizeResult.PublicKey)) and
only then invoke OnWalletReconnected; for logout, call base.Logout() (which
clears Account and Mnemonic) and only after that invoke OnWalletDisconnected so
handlers observe the cleared state.
- Around line 146-170: DisconnectWallet currently only deauthorizes when
_authToken is populated, leaving wallet-side sessions alive if the app restarted
and the token is only in _authCache; update DisconnectWallet so it reads any
cached token from _authCache (e.g., await
_authCache.GetAuthToken(WalletIdentity)) when _authToken is null, capture that
token into a local variable, call LocalAssociationScenario.StartAndExecuteAsync
with client => client.Deauthorize(capturedToken) inside the existing try/catch,
and then clear both _authToken and await
_authCache.ClearAuthToken(WalletIdentity); keep the same best-effort error
handling and ensure you never attempt to deauthorize with a null/empty token.

---

Outside diff comments:
In `@Runtime/codebase/SolanaMobileStack/LocalAssociationScenario.cs`:
- Around line 154-168: ExecuteNextAction currently dereferences a possibly null
_actions (StartAndExecuteAsync never initializes it), falls through after
CloseAssociation(response) causing double-complete of
_startAssociationTaskCompletionSource and dequeues on empty queue; fix by
ensuring _actions is initialized in StartAndExecuteAsync (or lazily in
ExecuteNextAction) before any use, make ExecuteNextAction return immediately
after calling CloseAssociation when response is null/failed or _actions.Count ==
0, and when _useAsyncActions is true await the async helper (await
ExecuteNextActionAsync(response)) instead of fire-and-forget; also guard
completion of _startAssociationTaskCompletionSource in CloseAssociation (check
IsCompleted/try-set) to prevent double-complete.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4dddca93-0175-455b-b975-a80633258db9

📥 Commits

Reviewing files that changed from the base of the PR and between 093c785 and 1f6602f.

📒 Files selected for processing (6)
  • PITCH.md
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/WalletCapabilities.cs
  • Runtime/codebase/SolanaMobileStack/LocalAssociationScenario.cs
  • Runtime/codebase/SolanaMobileStack/MwaAuthCache/EncryptedAuthCache.cs
  • Runtime/codebase/SolanaMobileStack/MwaAuthCache/PlayerPrefsAuthCache.cs
  • Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs

Comment on lines +95 to +99
_authToken = reauthorizeResult.AuthToken;
await _authCache.SetAuthToken(WalletIdentity, _authToken);
var cachedPublicKey = new PublicKey(reauthorizeResult.PublicKey);
OnWalletReconnected?.Invoke();
return new Account(string.Empty, cachedPublicKey);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Emit lifecycle events after local state changes.

At Line 98, OnWalletReconnected fires before the adapter exposes the new Account; at Line 173, OnWalletDisconnected fires before base.Logout() clears Account and Mnemonic. Handlers that inspect adapter state inside these callbacks will observe the old session.

Also applies to: 172-174

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs` around lines
95 - 99, The lifecycle events are raised before the adapter's local state is
updated, causing handlers to see stale session data; move the event invocations
so state changes happen first: for the reauthorization path, assign _authToken,
await _authCache.SetAuthToken(WalletIdentity, _authToken), construct and set the
new Account (using new PublicKey(reauthorizeResult.PublicKey)) and only then
invoke OnWalletReconnected; for logout, call base.Logout() (which clears Account
and Mnemonic) and only after that invoke OnWalletDisconnected so handlers
observe the cleared state.

Comment on lines +146 to +170
public async Task DisconnectWallet()
{
if (!string.IsNullOrEmpty(_authToken))
{
try
{
var localAssociationScenario = new LocalAssociationScenario();
var currentToken = _authToken; // capture before clearing
await localAssociationScenario.StartAndExecuteAsync(
new List<Func<IAdapterOperations, Task>>
{
async client => await client.Deauthorize(currentToken)
}
);
}
catch (Exception e)
{
// Best-effort — don't block logout if deauthorize fails (e.g. wallet not installed)
Debug.LogWarning($"[MWA] Deauthorize failed during disconnect: {e}");
}
}

// Clear all local auth state
_authToken = null;
await _authCache.ClearAuthToken(WalletIdentity);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Disconnect should revoke cached sessions after a cold start.

If the app restarts, the only auth token may be in _authCache. DisconnectWallet() only checks _authToken, so an explicit sign-out just clears the local cache and leaves the wallet-side authorization alive.

Suggested fix
 public async Task DisconnectWallet()
 {
-    if (!string.IsNullOrEmpty(_authToken))
+    var tokenToRevoke = _authToken;
+    if (string.IsNullOrEmpty(tokenToRevoke) && _walletOptions.keepConnectionAlive)
+        tokenToRevoke = await _authCache.GetAuthToken(WalletIdentity);
+
+    if (!string.IsNullOrEmpty(tokenToRevoke))
     {
         try
         {
             var localAssociationScenario = new LocalAssociationScenario();
-            var currentToken = _authToken; // capture before clearing
             await localAssociationScenario.StartAndExecuteAsync(
                 new List<Func<IAdapterOperations, Task>>
                 {
-                    async client => await client.Deauthorize(currentToken)
+                    async client => await client.Deauthorize(tokenToRevoke)
                 }
             );
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs` around lines
146 - 170, DisconnectWallet currently only deauthorizes when _authToken is
populated, leaving wallet-side sessions alive if the app restarted and the token
is only in _authCache; update DisconnectWallet so it reads any cached token from
_authCache (e.g., await _authCache.GetAuthToken(WalletIdentity)) when _authToken
is null, capture that token into a local variable, call
LocalAssociationScenario.StartAndExecuteAsync with client =>
client.Deauthorize(capturedToken) inside the existing try/catch, and then clear
both _authToken and await _authCache.ClearAuthToken(WalletIdentity); keep the
same best-effort error handling and ensure you never attempt to deauthorize with
a null/empty token.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant