Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions PITCH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# 🎮 Unity Mobile Wallet Adapter SDK – API Parity & UX Improvements

> **Applicant:** Pratik Kale
> **GitHub:** https://github.com/Pratikkale26/Solana.Unity-SDK
> **Proposal for:** Solana Seeker – Unity Mobile Wallet Adapter SDK RFP

---

## 🧠 TL;DR

This proposal delivers full API parity for the Unity Mobile Wallet Adapter SDK, introduces a persistent authorization cache layer, and improves wallet lifecycle UX (connect, disconnect, reconnect).

The core SDK implementation is already completed and submitted as a Draft PR, significantly reducing execution risk. This grant focuses on productionizing the work through documentation, example apps, and final integration.

---

## 🔴 Problem

The current Mobile Wallet Adapter (MWA) implementation in the Unity SDK lacks key functionality already available in the React Native SDK.

As a result, Unity-based Solana mobile games suffer from poor wallet UX:

- Missing `deauthorize` and `get_capabilities` APIs
- No persistent auth token storage (tokens are lost on app restart)
- Users must re-approve wallet connections every session
- No clean disconnect or silent reconnect flow

This leads to:
- Friction in player onboarding
- Poor user retention
- Inconsistent developer experience compared to React Native

---

## ✅ Solution

This proposal introduces a complete upgrade to the Unity MWA stack, aligning it with React Native capabilities while improving developer ergonomics.

### 1. API Parity

Added missing methods:
- `deauthorize` (revoke wallet session)
- `get_capabilities` (wallet feature detection)

Implemented in:
- `IAdapterOperations`
- `MobileWalletAdapterClient`

---

### 2. Extensible Auth Cache Layer

Introduced a pluggable caching system:

```text
IMwaAuthCache
├── PlayerPrefsAuthCache (default)
└── EncryptedAuthCache (extensible template)
```

Developers can inject custom secure storage:

```csharp
var wallet = new SolanaWalletAdapter(options, authCache: new MySecureCache());
```

---

### 3. Seamless Reconnect UX

* `Login()` attempts silent `reauthorize` using cached token
* Falls back to full authorization only if needed

👉 Eliminates repeated wallet approval popups

---

### 4. Clean Disconnect Flow

```csharp
await wallet.DisconnectWallet(); // revokes token + clears cache
await wallet.ReconnectWallet(); // silent or full auth

wallet.OnWalletDisconnected += () => ShowConnectScreen();
wallet.OnWalletReconnected += () => RestoreGameState();

await wallet.GetCapabilities();
```

---

## 🔬 Proof of Work

The full implementation is already completed and submitted as a Draft PR:

👉 [https://github.com/magicblock-labs/Solana.Unity-SDK/pull/264](https://github.com/magicblock-labs/Solana.Unity-SDK/pull/264)

This includes:

* API parity implementation
* Auth cache system
* Wallet lifecycle improvements
* Unit test coverage for cache layer

This significantly reduces delivery risk and ensures the grant funds are used for production readiness rather than initial development.

---

## 🛠 Scope of Work (Grant Deliverables)

The grant will fund the completion and productionization of this work:

| Deliverable | Timeline |
| ----------------------------------------------------------------------- | -------- |
| 📄 SDK Documentation (installation, API reference, cache customization) | Week 1–3 |
| 📱 Open-source Unity Android Example App (demonstrating full MWA flow) | Week 4–5 |
| 🔧 PR review cycles, fixes, and upstream merge support | Ongoing |

---

## 💰 Budget — $6,500 USD (in SKR)

| Item | Cost |
| -------------------------------------------------------------- | ---------- |
| Core SDK implementation (API parity, auth cache, lifecycle UX) | $2,200 |
| PR review rounds + integration polish | $800 |
| Full SDK documentation | $1,200 |
| Open-source Example Android App | $1,200 |
| Community support + post-merge maintenance | $600 |
| Buffer / contingency | $500 |
| **Total** | **$6,500** |
Comment thread
coderabbitai[bot] marked this conversation as resolved.

---

## 🚀 Impact

This upgrade improves the developer experience for Unity game developers building on Solana Mobile by:

* Enabling persistent wallet sessions across app restarts
* Reducing friction in player onboarding
* Providing API parity with the React Native SDK
* Making wallet lifecycle management predictable and production-ready

By aligning Unity tooling with Solana Mobile standards, this work helps unlock broader adoption of Solana in mobile gaming.

---

## 👤 About Me

**Pratik Kale**
Full-Stack Developer & Solana Builder

* GitHub: [https://github.com/Pratikkale26](https://github.com/Pratikkale26)
* Twitter: [https://x.com/PratikKale26](https://x.com/PratikKale26)

I focus on building developer tooling and infrastructure in the Solana ecosystem, with an emphasis on improving developer experience and real-world usability.
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,27 @@
[Preserve]
public interface IAdapterOperations
{
/// <summary>Requests authorization from the wallet. Returns an auth token on success.</summary>
[Preserve]
public Task<AuthorizationResult> Authorize(Uri identityUri, Uri iconUri, string identityName, string rpcCluster);

/// <summary>Re-uses a previously issued auth token to reauthorize without user re-prompt.</summary>
[Preserve]
public Task<AuthorizationResult> Reauthorize(Uri identityUri, Uri iconUri, string identityName, string authToken);

/// <summary>Revokes an auth token so the wallet forgets the session. Always call this before Logout.</summary>
[Preserve]
public Task Deauthorize(string authToken);

/// <summary>Queries the wallet for its supported features and limits.</summary>
[Preserve]
public Task<WalletCapabilities> GetCapabilities();

/// <summary>Requests signing of one or more serialized transactions.</summary>
[Preserve]
public Task<SignedResult> SignTransactions(IEnumerable<byte[]> transactions);

/// <summary>Requests signing of one or more arbitrary messages.</summary>
[Preserve]
public Task<SignedResult> SignMessages(IEnumerable<byte[]> messages, IEnumerable<byte[]> addresses);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEngine.Scripting;

// ReSharper disable once CheckNamespace

/// <summary>
/// Represents the capabilities reported by a connected wallet via the get_capabilities MWA endpoint.
/// All properties are optional — wallets may omit fields they do not support.
/// </summary>
[Preserve]
public class WalletCapabilities
{
/// <summary>
/// Maximum number of transaction payloads that can be signed in a single request.
/// Null if the wallet does not report this limit.
/// </summary>
[JsonProperty("max_transactions_per_request")]
public int? MaxTransactionsPerRequest { get; set; }

/// <summary>
/// Maximum number of message payloads that can be signed in a single request.
/// Null if the wallet does not report this limit.
/// </summary>
[JsonProperty("max_messages_per_request")]
public int? MaxMessagesPerRequest { get; set; }

/// <summary>
/// Supported Solana transaction versions (e.g. "legacy", "0").
/// Null or empty if the wallet does not report this capability.
/// </summary>
[JsonProperty("supported_transaction_versions")]
public List<string> SupportedTransactionVersions { get; set; }

/// <summary>
/// Whether the wallet supports clone authorization, which allows
/// one authorization context to extend to another app instance.
/// Null if the wallet does not report this capability.
/// </summary>
[JsonProperty("supports_clone_authorization")]
public bool? SupportsCloneAuthorization { get; set; }
Comment thread
coderabbitai[bot] marked this conversation as resolved.

[Preserve]
public WalletCapabilities() { }
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 52 additions & 2 deletions Runtime/codebase/SolanaMobileStack/LocalAssociationScenario.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public class LocalAssociationScenario
private MobileWalletAdapterClient _client;
private readonly AndroidJavaObject _currentActivity;
private Queue<Action<IAdapterOperations>> _actions;
private Queue<Func<IAdapterOperations, Task>> _asyncActions;
private bool _useAsyncActions;

public LocalAssociationScenario(int clientTimeoutMs = 9000)
{
Expand Down Expand Up @@ -69,6 +71,26 @@ public Task<Response<object>> StartAndExecute(List<Action<IAdapterOperations>> a
_startAssociationTaskCompletionSource = new TaskCompletionSource<Response<object>>();
return _startAssociationTaskCompletionSource.Task;
}

/// <summary>
/// Async-compatible overload of <see cref="StartAndExecute"/>.
/// Accepts async delegate actions (<see cref="Func{IAdapterOperations, Task}"/>) and properly
/// awaits each one before executing the next, preventing fire-and-forget race conditions.
/// </summary>
public Task<Response<object>> StartAndExecuteAsync(List<Func<IAdapterOperations, Task>> asyncActions)
{
if (asyncActions == null || asyncActions.Count == 0)
throw new ArgumentException("Actions must be non-null and non-empty");
_asyncActions = new Queue<Func<IAdapterOperations, Task>>(asyncActions);
_useAsyncActions = true;
var intent = LocalAssociationIntentCreator.CreateAssociationIntent(
_session.AssociationToken,
_port);
_currentActivity.Call("startActivityForResult", intent, 0);
_currentActivity.Call("runOnUiThread", new AndroidJavaRunnable(TryConnectWs));
_startAssociationTaskCompletionSource = new TaskCompletionSource<Response<object>>();
return _startAssociationTaskCompletionSource.Task;
}

private async void TryConnectWs()
{
Expand Down Expand Up @@ -133,8 +155,36 @@ private void ExecuteNextAction(Response<object> response = null)
{
if (_actions.Count == 0 || response is { Failed: true })
CloseAssociation(response);
var action = _actions.Dequeue();
action.Invoke(_client);

if (_useAsyncActions)
{
// Properly await the async action before proceeding
ExecuteNextActionAsync(response);
}
else
{
var action = _actions.Dequeue();
action.Invoke(_client);
}
}

private async void ExecuteNextActionAsync(Response<object> response = null)
{
if (_asyncActions.Count == 0 || response is { Failed: true })
{
CloseAssociation(response);
return;
}
var action = _asyncActions.Dequeue();
try
{
await action.Invoke(_client);
}
catch (Exception e)
{
Debug.LogError($"[MWA] Async action failed: {e}");
CloseAssociation(new Response<object> { Error = new Response<object>.ResponseError { Message = e.Message } });
}
}

private async void CloseAssociation(Response<object> response)
Expand Down
Loading