Skip to content

Auth 2.0 SIWS (Sign In With Solana)#278

Open
mstevens843 wants to merge 4 commits into
magicblock-labs:mainfrom
mstevens843:feat/authorize-mwa-2.0
Open

Auth 2.0 SIWS (Sign In With Solana)#278
mstevens843 wants to merge 4 commits into
magicblock-labs:mainfrom
mstevens843:feat/authorize-mwa-2.0

Conversation

@mstevens843
Copy link
Copy Markdown
Contributor

@mstevens843 mstevens843 commented Apr 16, 2026

NOTE: This PR adds SIWS support to the MWA authorize flow, matching the React Native SDK's signIn() behavior. Wallets that don't support SIWS natively get a fallback via sign_messages. Phantom and Solflare have wallet-side issues with the fallback path (details below). The React Native SDK hits the same behavior from these wallets.

Problem

The MWA 2.0 spec defines sign_in_payload as part of the authorize request, letting apps authenticate users and verify wallet ownership in a single step. The Unity SDK had no support for this.
Developers had to authorize first, then manually construct and sign a SIWS message in a separate session. That doesn't match what the React Native SDK offers out of the box.

Solution

Added SIWS support to _Login() in SolanaMobileWalletAdapter using a two-action approach that matches the React Native SDK.

Action 1 sends authorize with sign_in_payload containing domain, statement, and URI. Wallets that support SIWS natively (like Backpack) return sign_in_result directly in the authorize response. One prompt, done.

Action 2 is the fallback. If the wallet doesn't return sign_in_result, we construct a CAIP-122 SIWS message and send it via sign_messages. This covers wallets like Jupiter that support sign_messages but don't handle sign_in_payload in authorize. The signed payload is parsed correctly per the React Native reference: last 64 bytes are the ed25519 signature, everything before is the signed message.

LastSignInResult is exposed on both SolanaMobileWalletAdapter and SolanaWalletAdapter so apps can read the SIWS result after login.

When siwsDomain is not set, the flow falls back to plain authorize with no SIWS. No behavior change for existing users.

Test Results

Tested on Solana Seeker (Android 15):

  • Backpack - native sign_in_result returned, single prompt, works perfectly
  • Jupiter - fallback sign_messages works, two prompts (connect + sign), SIWS verified
  • Phantom - authorize works but Phantom closes the WebSocket when it receives sign_messages as a second RPC in the same session. This is a wallet-side limitation, not an SDK issue.
  • Solflare - crashes on its own with a Flutter bug (Reply already submitted in onActivityResult). Unrelated to this PR.

Other Changes

  • Removed IsV1Session from MobileWalletAdapterSession (no longer needed)
  • Added MAC check workaround in session decryption for BouncyCastle GCM compatibility
  • Removed &v=1 from the association intent URL
  • Cleaned up MobileWalletAdapterClient (extracted helper methods, removed excessive logging)
  • Renamed SignInPayload property to SignInPayloadData in JsonRequestParams to avoid naming collision
  • Removed unnecessary [Preserve] constructor from SignInResult

Deploy Notes

No new dependencies. No new scripts. Backwards compatible. SIWS is opt-in via siwsDomain and siwsStatement on SolanaMobileWalletAdapterOptions.

Summary by CodeRabbit

  • New Features

    • Sign-in authentication flow with customizable domain and statement support
    • Query wallet capabilities endpoint
    • Direct transaction signing and sending functionality
    • Access to wallet sign-in result data
  • Improvements

    • Enhanced connection stability with improved WebSocket lifecycle management
    • Added request tracking and response timeout handling for better reliability

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

Warning

Rate limit exceeded

@mstevens843 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 59 minutes and 59 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b0cd7a6a-997e-4430-9c4f-80cda87b59ad

📥 Commits

Reviewing files that changed from the base of the PR and between 08a870d and af06468.

📒 Files selected for processing (14)
  • Runtime/codebase/SolanaMobileStack/Interfaces/IAdapterOperations.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpc20Client.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/JsonRequest.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/AuthorizationResult.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/CapabilitiesResult.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/CapabilitiesResult.cs.meta
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/SignAndSendResult.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/SignAndSendResult.cs.meta
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/SignInResult.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/SignInResult.cs.meta
  • Runtime/codebase/SolanaMobileStack/LocalAssociationScenario.cs
  • Runtime/codebase/SolanaMobileStack/MobileWalletAdapterClient.cs
  • Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs
  • Runtime/codebase/SolanaWalletAdapter.cs

Walkthrough

This PR extends the Solana Mobile Stack with new authorization and transaction signing workflows. It introduces SIWS (Sign In With Solana) support, capability discovery via GetCapabilities(), direct transaction signing and sending via SignAndSendTransactions(), improved RPC request tracking and logging, enhanced connection timeout handling, and exposes sign-in results through a new LastSignInResult property.

Changes

Cohort / File(s) Summary
Interface & Client API Definitions
Runtime/codebase/SolanaMobileStack/Interfaces/IAdapterOperations.cs, Runtime/codebase/SolanaMobileStack/MobileWalletAdapterClient.cs
Added new interface methods and implementations: overloaded Authorize() with SIWS payload support, SignAndSendTransactions() for direct signing/sending, and GetCapabilities() for capability discovery. Includes URI validation and message ID assignment.
JSON-RPC Request Models
Runtime/codebase/SolanaMobileStack/JsonRpcClient/JsonRequest.cs
Introduced new JSON request parameters (chain, features, sign_in_payload, options) and two new nested serializable classes: SignInPayload (SIWS domain/statement/nonce details) and SignAndSendOptions (transaction send configuration).
JSON-RPC Response Models
Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/AuthorizationResult.cs, Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/CapabilitiesResult.cs, Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/SignAndSendResult.cs, Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/SignInResult.cs, Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/*\\.meta
Extended AuthorizationResult with chains/features/icon/sign-in-result properties. Added CapabilitiesResult (max transactions/messages, supported versions, features), SignAndSendResult (signatures with base64 decoding), and SignInResult (address/signed message/signature details). Includes 4 Unity meta files.
Core Client & Connection Management
Runtime/codebase/SolanaMobileStack/JsonRpc20Client.cs, Runtime/codebase/SolanaMobileStack/LocalAssociationScenario.cs
Enhanced RPC request tracking via PendingRequests counter; improved logging with method names and payload details. Added structured logging, timeout handling (45s response timeout), and reconnection/closure logic that distinguishes between completed actions and pending RPC requests.
Adapter SIWS Support & Sign-In Exposure
Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs, Runtime/codebase/SolanaWalletAdapter.cs
Introduced SIWS configuration options (siwsDomain, siwsStatement), CAIP-2 chain mapping, and fallback message signing. Exposed LastSignInResult property at both mobile adapter and main adapter levels with optional fallback construction from signature payloads.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.45% 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 'Auth 2.0 SIWS (Sign In With Solana)' is specific and clearly describes the main feature being added. It accurately reflects the primary change: implementing SIWS support for the authorize flow in the MWA 2.0 spec.
Description check ✅ Passed The PR description provides clear problem statement, solution overview with two-action approach, test results with specific wallet behaviors, and additional changes. However, it lacks explicit Before & After Screenshots section and Deploy Notes follow the template structure inconsistently.

✏️ 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: 10

🤖 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/JsonRpc20Client.cs`:
- Line 23: PendingRequests is not thread-safe; introduce a private backing field
(e.g., int _pendingRequests) and update the PendingRequests getter to return
that field, then replace all ++/-- operations on PendingRequests (in SendRequest
and in the ContinueWith callbacks referenced) with Interlocked.Increment(ref
_pendingRequests) and Interlocked.Decrement(ref _pendingRequests) respectively
so all updates are atomic and safe across thread-pool callbacks.

In `@Runtime/codebase/SolanaMobileStack/JsonRpcClient/JsonRequest.cs`:
- Around line 149-151: The MinContextSlot property is typed as int? but Solana's
min_context_slot is a u64; change the property declaration on JsonRequest
(property MinContextSlot) from int? to ulong? so it can represent the full
0..2^64-1 range, keep the existing JsonProperty and RequiredMember attributes
unchanged, and run/adjust any callers or deserialization code that assume an int
to accept ulong? (or cast safely) to prevent overflow during JSON parsing.

In
`@Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/AuthorizationResult.cs`:
- Around line 46-49: The attributes marking optional response properties as
required must be removed or replaced: remove the [RequiredMember] on
AuthorizationResult.SignInResult (property SignInResult) since it can be null
for wallets without SIWS, and likewise remove or change [RequiredMember] on
AuthorizationResultAccounts properties Chains, Features, and Icon so they are
treated as optional; if your project has an [OptionalMember] or
nullable-handling attribute, use that instead and ensure the types remain
nullable to match the fallback logic in SolanaMobileWalletAdapter._Login().

In
`@Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/SignAndSendResult.cs`:
- Around line 16-19: The SignaturesBytes accessor currently returns null when
there are no signatures; update the SignaturesBytes property in
SignAndSendResult to return an empty List<byte[]> instead of null so callers can
safely iterate without null checks—use Signatures (or Signatures?.Count) to
decide whether to Select(Convert.FromBase64String) into a List<byte[]> or return
new List<byte[]>() when empty or null, preserving the [RequiredMember] attribute
and the property's type.

In `@Runtime/codebase/SolanaMobileStack/LocalAssociationScenario.cs`:
- Around line 240-254: StartResponseTimeout currently awaits Task.Delay and then
checks _closed which leaves a race where a response can arrive between delay
completion and the _closed check causing confusing logs; modify
StartResponseTimeout to accept/use a CancellationToken from a
CancellationTokenSource that you create when starting the timeout and cancel in
the response path (e.g., in HandleEncryptedSessionPayload or wherever you set
results) so the delay is aborted as soon as a response is processed; reference
StartResponseTimeout, HandleEncryptedSessionPayload, _closed and use a
CancellationTokenSource (e.g., _responseCts) that is canceled on successful
response handling and passed to Task.Delay to avoid running the timeout action
after a response.
- Around line 178-182: The catch block currently calls CloseAssociation(null)
which causes _startAssociationTaskCompletionSource.SetResult(null) and leads to
a NullReferenceException when the caller checks result.WasSuccessful; change the
catch to call CloseAssociation with a non-null failure result object (e.g., an
AssociationResult/StartAssociationResult instance indicating failure and
containing the exception info) so
_startAssociationTaskCompletionSource.SetResult receives a valid error result;
update CloseAssociation usage in LocalAssociationScenario.cs and ensure the
failure result includes WasSuccessful=false and the exception message/stack so
SolanaMobileWalletAdapter._Login() can safely inspect result.WasSuccessful.

In `@Runtime/codebase/SolanaMobileStack/MobileWalletAdapterClient.cs`:
- Around line 66-73: Extract the duplicated URI validation into a private helper
on MobileWalletAdapterClient (e.g., ValidateUri or ValidateIdentityAndIconUris)
and replace the two inline checks in the constructor/initializer and in
PrepareAuthRequest with calls to that helper; the helper should accept a Uri and
a mode/enum or bool to indicate expected absolute vs relative (use names
identityUri and iconUri to preserve intent), throw the same ArgumentException
messages when validation fails, and update both the constructor code path and
PrepareAuthRequest to call this single helper to remove duplication.
- Line 96: The call to SendRequest is missing the methodName argument causing
logs to show "Unknown"; update the three call
sites—SendRequest<AuthorizationResult>(request) in the authorize path, the
SendRequest call in SignAndSendTransactions, and the SendRequest call in
GetCapabilities—to pass the appropriate methodName strings (e.g., "authorize",
"signAndSendTransactions", "getCapabilities") as the first parameter so
SendRequest logs and telemetry contain the correct method context.

In `@Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs`:
- Around line 92-119: The two async lambdas added via actions.Add share and
mutate the captured variable authorization (and read it later for siws
fallback), which is unsafe with the current Action<IAdapterOperations> async
pattern; fix by combining both steps into a single async delegate so the
Authorize result is read synchronously after awaiting the authorize call:
replace the two separate actions.Add(...) delegates with one async delegate that
calls client.Authorize (assigning authorization), then immediately checks
authorization.SignInResult and authorization.PublicKey and, if needed,
constructs siwsMessageText and calls client.SignMessages to set siwsFallbackSig;
alternatively, migrate the actions collection to use Func<IAdapterOperations,
Task> so captured mutations are reliably visible across awaits (preferred
long-term).
- Around line 30-35: ClusterToChain currently lacks a mapping for "localnet",
causing a wrong fallback to "solana:mainnet"; update the code so
RpcCluster.LocalNet is handled specially rather than using the static
ClusterToChain fallback: add logic (e.g., a ResolveLocalnetChainId method called
where ClusterToChain is read or where the fallback on line 83 occurs) that
queries the local RPC node for its genesis hash (or uses the default
test-validator genesis hash if you choose a hardcoded fallback) and returns the
chain id formatted as "solana:<first-32-chars-of-genesis-hash>"; ensure
ClusterToChain usage or the code path that reads it (references to
ClusterToChain and RpcCluster.LocalNet) uses this resolved value for localnet so
SIWS and wallet authorization receive the correct chain identifier.
🪄 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: f2bf6a7d-1cdb-4d3f-a5fe-9c23d89b4ed8

📥 Commits

Reviewing files that changed from the base of the PR and between 054503e and 08a870d.

📒 Files selected for processing (14)
  • Runtime/codebase/SolanaMobileStack/Interfaces/IAdapterOperations.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpc20Client.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/JsonRequest.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/AuthorizationResult.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/CapabilitiesResult.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/CapabilitiesResult.cs.meta
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/SignAndSendResult.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/SignAndSendResult.cs.meta
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/SignInResult.cs
  • Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/SignInResult.cs.meta
  • Runtime/codebase/SolanaMobileStack/LocalAssociationScenario.cs
  • Runtime/codebase/SolanaMobileStack/MobileWalletAdapterClient.cs
  • Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs
  • Runtime/codebase/SolanaWalletAdapter.cs

Comment thread Runtime/codebase/SolanaMobileStack/JsonRpc20Client.cs Outdated
Comment thread Runtime/codebase/SolanaMobileStack/JsonRpcClient/JsonRequest.cs Outdated
Comment thread Runtime/codebase/SolanaMobileStack/JsonRpcClient/Responses/SignAndSendResult.cs Outdated
Comment thread Runtime/codebase/SolanaMobileStack/LocalAssociationScenario.cs
Comment thread Runtime/codebase/SolanaMobileStack/LocalAssociationScenario.cs
Comment thread Runtime/codebase/SolanaMobileStack/MobileWalletAdapterClient.cs
Comment thread Runtime/codebase/SolanaMobileStack/MobileWalletAdapterClient.cs Outdated
Comment thread Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs
Comment thread Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs
mstevens843 and others added 4 commits April 27, 2026 13:12
Adds MWA 2.0 authorize support with Sign In With Solana (SIWS), enabling
one-shot connect + prove ownership. Also adds signAndSendTransactions and
getCapabilities client methods per the MWA 2.0 spec.

SDK plumbing:
- New Authorize overload with chain, features, addresses, authToken, signInPayload
- New SignAndSendTransactions method with options (commitment, skipPreflight)
- New GetCapabilities method (non-privileged)
- New response models: SignInResult, SignAndSendResult, CapabilitiesResult
- Extended AuthorizationResult with accounts[].chains/features/icon + sign_in_result
- Extended JsonRequest with Chain, Features, SignInPayload, SignAndSendOptions

Adapter-level SIWS:
- SolanaMobileWalletAdapterOptions: added siwsDomain, siwsStatement
- SolanaMobileWalletAdapter._Login(): SIWS path when siwsDomain is set, legacy path otherwise
- SolanaMobileWalletAdapter.LastSignInResult: stores SIWS result from authorize
- SolanaWalletAdapter.LastSignInResult: delegation to internal adapter

Tested on Solana Seeker with Phantom and Backpack via example app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…The React Native SDK's signIn() method does:

  1. Authorize with sign_in_payload
  2. If wallet doesn't return sign_in_result → fallback to sign_messages with a CAIP-122 message
  3. Parse the signed payload: last 64 bytes = signature, rest = signed message
- thread-safe PendingRequests counter via Interlocked over a backing field
- remove [RequiredMember] from optional MWA response fields (SignInResult,
  Chains, Features, Icon) and add NullValueHandling.Ignore so JSON serialization
  matches their wallet-optional nature
- SignaturesBytes returns an empty list instead of null when there are no signatures
- pass methodName to SendRequest at every call site for clearer debug logs
- map localnet cluster to default solana-test-validator genesis hash to avoid
  silent fall-through to solana:mainnet during local development
- combine the SIWS authorize and CAIP-122 fallback into a single delegate so
  the captured `authorization` mutation is reliably visible to the fallback
  step (avoids cross-lambda capture issues with the async-void
  Action<IAdapterOperations> dispatch pattern)
@mstevens843 mstevens843 force-pushed the feat/authorize-mwa-2.0 branch from 0ef117f to af06468 Compare April 27, 2026 20:16
@mstevens843
Copy link
Copy Markdown
Contributor Author

Hey, pushed an update on this one. Rebased onto current main and addressed the CodeRabbit feedback in one followup commit.

Rebase notes:

CodeRabbit fixes in af06468:

  • PendingRequests is now thread safe via Interlocked over a private backing field
  • Removed [RequiredMember] from optional MWA response fields (SignInResult, Chains, Features, Icon) and switched them to NullValueHandling.Ignore so the JSON contract matches the wallet-optional reality
  • SignaturesBytes returns an empty list instead of null when there are no signatures
  • methodName is now passed at every SendRequest call site so the debug logs show the actual method instead of "Unknown"
  • localnet maps to the default solana-test-validator genesis hash so it no longer falls through to solana:mainnet during local dev (non-default validators would still need dynamic resolution, noted with a comment)
  • Combined the SIWS authorize and CAIP-122 fallback into a single delegate to avoid the cross-lambda capture issue with the async-void Action pattern

Ready for another look when you have time.

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