Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
bb5aa28
fix(mwa): clear _authToken and authToken cache on Logout
JoshhSandhu Mar 25, 2026
a13bd76
fix(mwa): enforce auth session restore invariant in keepConnectionAli…
JoshhSandhu Mar 25, 2026
eef35e6
feat(mwa): add Deauthorize() and GetCapabilities() to IAdapterOperations
JoshhSandhu Mar 25, 2026
4b03eec
feat(mwa): implement Deauthorize() RPC in MobileWalletAdapterClient
JoshhSandhu Mar 25, 2026
e46a668
feat(mwa): implement GetCapabilities() RPC in MobileWalletAdapterClient
JoshhSandhu Mar 25, 2026
74db465
feat(mwa): add DisconnectWallet(), ReconnectWallet(), and lifecycle e…
JoshhSandhu Mar 25, 2026
0726e11
feat(mwa): add DisconnectWallet() and ReconnectWallet() passthroughs …
JoshhSandhu Mar 25, 2026
3a9774e
fix(mwa): restore _authToken from cache at start of signing methods
JoshhSandhu Mar 25, 2026
98a2234
fix(mwa): replace using var with var for LocalAssociationScenario
JoshhSandhu Mar 25, 2026
6bd4acc
fix(mwa): guard reauth.AuthToken null assignment in _Login reauthoriz…
JoshhSandhu Mar 25, 2026
df71c17
fix(mwa): fix major lifecycle issues identified in code audit
JoshhSandhu Mar 25, 2026
0b2aa3e
fix(mwa): enhance error handling in Deauthorize() and update GetCapab…
JoshhSandhu Mar 25, 2026
15b0ccf
feat(mwa): added meta file for CapabilitiesResult.cs
JoshhSandhu Mar 25, 2026
534c235
feat(mwa): fixed in editor error 'response' not found
JoshhSandhu Mar 25, 2026
3c84d89
fix(mwa): removed deauth error handling cause of errors
JoshhSandhu Mar 25, 2026
8ef67ae
Merge branch 'magicblock-labs:main' into fix/mwa-keepConnectionAlive-…
JoshhSandhu Mar 28, 2026
a0cefb2
fix(mwa): address CodeRabbit review comments
JoshhSandhu Mar 29, 2026
05273e3
fix(mwa): address remaining CodeRabbit nitpicks
JoshhSandhu Mar 29, 2026
b730145
fix(mwa): address CodeRabbit critical and major review issues
JoshhSandhu Mar 29, 2026
e3954d2
fix(mwa): address CodeRabbit minor issues
JoshhSandhu Mar 29, 2026
175ad9c
fix(mwa): address CodeRabbit nitpick and defensive null checks
JoshhSandhu Mar 29, 2026
ac90023
fix(mwa): guard signed result against null before dereferencing
JoshhSandhu Mar 29, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public interface IAdapterOperations
[Preserve]
public Task<AuthorizationResult> Reauthorize(Uri identityUri, Uri iconUri, string identityName, string authToken);
[Preserve]
public Task Deauthorize(string authToken);
[Preserve]
public Task<CapabilitiesResult> GetCapabilities();
[Preserve]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
public Task<SignedResult> SignTransactions(IEnumerable<byte[]> transactions);
[Preserve]
public Task<SignedResult> SignMessages(IEnumerable<byte[]> messages, IEnumerable<byte[]> addresses);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Newtonsoft.Json;
using UnityEngine.Scripting;

// ReSharper disable once CheckNamespace

[Preserve]
public class CapabilitiesResult
{
[JsonProperty("supports_clone_authorization")]
public bool? SupportsCloneAuthorization { get; set; }

[JsonProperty("max_transactions_per_request")]
public int? MaxTransactionsPerRequest { get; set; }

[JsonProperty("max_messages_per_request")]
public int? MaxMessagesPerRequest { get; set; }

[JsonProperty("supported_transaction_versions")]
public string[] SupportedTransactionVersions { get; set; }
}

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

39 changes: 39 additions & 0 deletions Runtime/codebase/SolanaMobileStack/MobileWalletAdapterClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ public Task<AuthorizationResult> Reauthorize(Uri identityUri, Uri iconUri, strin

return SendRequest<AuthorizationResult>(request);
}

public async Task Deauthorize(string authToken)
{
var request = PrepareDeauthorizeRequest(authToken);
await SendRequest<object>(request);
}

public Task<CapabilitiesResult> GetCapabilities()
{
var request = PrepareGetCapabilitiesRequest();
return SendRequest<CapabilitiesResult>(request);
}

public Task<SignedResult> SignTransactions(IEnumerable<byte[]> transactions)
{
Expand Down Expand Up @@ -85,6 +97,33 @@ private JsonRequest PrepareAuthRequest(Uri uriIdentity, Uri icon, string name, s
};
return request;
}

private JsonRequest PrepareDeauthorizeRequest(string authToken)
{
var request = new JsonRequest
{
JsonRpc = "2.0",
Method = "deauthorize",
Params = new JsonRequest.JsonRequestParams
{
AuthToken = authToken
},
Id = NextMessageId()
};
return request;
}

private JsonRequest PrepareGetCapabilitiesRequest()
{
var request = new JsonRequest
{
JsonRpc = "2.0",
Method = "get_capabilities",
Params = new JsonRequest.JsonRequestParams(),
Id = NextMessageId()
};
return request;
}

private JsonRequest PrepareSignTransactionsRequest(IEnumerable<byte[]> transactions)
{
Expand Down
184 changes: 182 additions & 2 deletions Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public class SolanaMobileWalletAdapter : WalletBase
private readonly WalletBase _internalWallet;
private string _authToken;

public event Action OnWalletDisconnected;
public event Action OnWalletReconnected;

public SolanaMobileWalletAdapter(
SolanaMobileWalletAdapterOptions solanaWalletOptions,
RpcCluster rpcCluster = RpcCluster.DevNet,
Expand All @@ -54,7 +57,56 @@ protected override async Task<Account> _Login(string password = null)
if (_walletOptions.keepConnectionAlive)
{
string pk = PlayerPrefs.GetString("pk", null);
if (!pk.IsNullOrEmpty()) return new Account(string.Empty, new PublicKey(pk));
string authToken = PlayerPrefs.GetString("authToken", null);
if (!pk.IsNullOrEmpty() && !authToken.IsNullOrEmpty())
{
string reauthPublicKey = null;
// TODO: change to using var after PR #260 merges (IDisposable not yet on LocalAssociationScenario)
var reauthorizeScenario = new LocalAssociationScenario();
var reauthorizeResult = await reauthorizeScenario.StartAndExecute(
new List<Action<IAdapterOperations>>
{
async client =>
{
var reauth = await client.Reauthorize(
new Uri(_walletOptions.identityUri),
new Uri(_walletOptions.iconUri, UriKind.Relative),
_walletOptions.name, authToken);
if (reauth != null && !string.IsNullOrEmpty(reauth.AuthToken))
{
_authToken = reauth.AuthToken;
reauthPublicKey = reauth.PublicKey != null
? new PublicKey(reauth.PublicKey).ToString()
: null;
}
}
}
);
if (reauthorizeResult.WasSuccessful)
{
if (string.IsNullOrEmpty(_authToken))
{
// Reauthorize RPC succeeded but wallet returned no token - treat as failure
// Fall through to cleanup below
}
else
{
PlayerPrefs.SetString("authToken", _authToken);
PlayerPrefs.Save();
var resolvedKey = !string.IsNullOrEmpty(reauthPublicKey) ? reauthPublicKey : pk;
return new Account(string.Empty, new PublicKey(resolvedKey));
}
}
// Reauthorize failed or returned empty token - clear cached credentials
PlayerPrefs.DeleteKey("pk");
PlayerPrefs.DeleteKey("authToken");
PlayerPrefs.Save();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
else if (!pk.IsNullOrEmpty())
{
PlayerPrefs.DeleteKey("pk");
PlayerPrefs.Save();
}
}
AuthorizationResult authorization = null;
var localAssociationScenario = new LocalAssociationScenario();
Expand All @@ -76,11 +128,17 @@ protected override async Task<Account> _Login(string password = null)
Debug.LogError(result.Error.Message);
throw new Exception(result.Error.Message);
}
if (authorization == null)
{
throw new Exception("[MWA] Login: authorization was not populated");
}
_authToken = authorization.AuthToken;
var publicKey = new PublicKey(authorization.PublicKey);
if (_walletOptions.keepConnectionAlive)
{
PlayerPrefs.SetString("pk", publicKey.ToString());
PlayerPrefs.SetString("authToken", _authToken);
PlayerPrefs.Save();
Comment thread
JoshhSandhu marked this conversation as resolved.
}
return new Account(string.Empty, publicKey);
}
Expand All @@ -94,6 +152,8 @@ protected override async Task<Transaction> _SignTransaction(Transaction transact

protected override async Task<Transaction[]> _SignAllTransactions(Transaction[] transactions)
{
if (_authToken.IsNullOrEmpty() && _walletOptions.keepConnectionAlive)
_authToken = PlayerPrefs.GetString("authToken", null);

var cluster = RPCNameMap[(int)RpcCluster];
SignedResult res = null;
Expand Down Expand Up @@ -130,7 +190,20 @@ protected override async Task<Transaction[]> _SignAllTransactions(Transaction[]
Debug.LogError(result.Error.Message);
throw new Exception(result.Error.Message);
}
if (authorization == null)
{
throw new Exception("[MWA] SignAllTransactions: authorization was not populated");
}
if (res == null)
{
throw new Exception("[MWA] SignAllTransactions: signed payloads were not populated");
}
_authToken = authorization.AuthToken;
if (_walletOptions.keepConnectionAlive)
{
PlayerPrefs.SetString("authToken", _authToken);
PlayerPrefs.Save();
}
return res.SignedPayloads.Select(transaction => Transaction.Deserialize(transaction)).ToArray();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

Expand All @@ -139,11 +212,105 @@ public override void Logout()
{
base.Logout();
PlayerPrefs.DeleteKey("pk");
_authToken = null;
PlayerPrefs.DeleteKey("authToken");
PlayerPrefs.Save();
}

public async Task DisconnectWallet()
{
string authToken = _authToken;
if (authToken.IsNullOrEmpty())
authToken = PlayerPrefs.GetString("authToken", null);

if (!authToken.IsNullOrEmpty())
{
try
{
// TODO: change to using var after PR #260 merges (IDisposable not yet on LocalAssociationScenario)
var localAssociationScenario = new LocalAssociationScenario();
var result = await localAssociationScenario.StartAndExecute(
new List<Action<IAdapterOperations>>
{
async client =>
{
await client.Deauthorize(authToken);
}
}
);
if (!result.WasSuccessful)
{
Debug.LogWarning($"[MWA] Deauthorize returned error: {result.Error.Message}");
}
}
catch (Exception e)
{
Debug.LogWarning($"[MWA] Deauthorize transport failed (best-effort): {e}");
}
}

Logout();
OnWalletDisconnected?.Invoke();
}

public async Task ReconnectWallet()
{
try
{
var account = await Login();
if (account != null)
{
OnWalletReconnected?.Invoke();
}
else
{
Debug.LogWarning("[MWA] ReconnectWallet: Login returned null, not firing OnWalletReconnected");
throw new Exception("ReconnectWallet failed: Login returned null");
}
}
catch (Exception e)
{
Debug.LogError(e.Message);
throw;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public async Task<CapabilitiesResult> GetCapabilities()
{
CapabilitiesResult capabilities = null;
// TODO: change to using var after PR #260 merges (IDisposable not yet on LocalAssociationScenario)
var localAssociationScenario = new LocalAssociationScenario();
var result = await localAssociationScenario.StartAndExecute(
new List<Action<IAdapterOperations>>
{
async client =>
{
capabilities = await client.GetCapabilities();
}
}
);
if (!result.WasSuccessful)
{
Debug.LogError(result.Error.Message);
throw new Exception(result.Error.Message);
}
if (capabilities == null)
{
throw new Exception("[MWA] GetCapabilities RPC succeeded but returned no data");
}
return capabilities;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

public override async Task<byte[]> SignMessage(byte[] message)
{
if (_authToken.IsNullOrEmpty() && _walletOptions.keepConnectionAlive)
_authToken = PlayerPrefs.GetString("authToken", null);

string cachedPk = Account?.PublicKey?.ToString()
?? PlayerPrefs.GetString("pk", null);
if (string.IsNullOrEmpty(cachedPk))
throw new Exception("[MWA] Cannot sign message: no account available");
Comment thread
coderabbitai[bot] marked this conversation as resolved.

SignedResult signedMessages = null;
var localAssociationScenario = new LocalAssociationScenario();
AuthorizationResult authorization = null;
Expand Down Expand Up @@ -172,7 +339,7 @@ public override async Task<byte[]> SignMessage(byte[] message)
{
signedMessages = await client.SignMessages(
messages: new List<byte[]> { message },
addresses: new List<byte[]> { Account.PublicKey.KeyBytes }
addresses: new List<byte[]> { new PublicKey(cachedPk).KeyBytes }
);
}
}
Expand All @@ -182,7 +349,20 @@ public override async Task<byte[]> SignMessage(byte[] message)
Debug.LogError(result.Error.Message);
throw new Exception(result.Error.Message);
}
if (authorization == null)
{
throw new Exception("[MWA] SignMessage: authorization was not populated");
}
if (signedMessages == null)
{
throw new Exception("[MWA] SignMessage: signed payloads were not populated");
}
_authToken = authorization.AuthToken;
if (_walletOptions.keepConnectionAlive)
{
PlayerPrefs.SetString("authToken", _authToken);
PlayerPrefs.Save();
}
return signedMessages.SignedPayloadsBytes[0];
}

Expand Down
Loading