Skip to content

Commit 4d6dc69

Browse files
committed
Merge origin/main into MWA CAIP-2 chain fix
Resolve conflicts in SolanaMobileWalletAdapter.cs against main's MWA refactor (IMwaAuthCache / AuthorizeOperation / ReauthorizeOperation / RunPrivileged). - Re-thread the CAIP-2 `chain` parameter through main's AuthorizeOperation helper (new optional `chain` ctor arg) and its call sites: _Login and the authorize path in RunPrivileged. SignAllTransactions / SignMessage now reach authorize via RunPrivileged, which carries chain. - Drop the AuthCacheVersion / InvalidateStaleAuthCache self-heal mechanism per maintainer review (PR #288 lines 30/37/83). Main's IMwaAuthCache layer supersedes the PlayerPrefs version hack. - Add MobileWalletAdapterClient tests asserting Params.Chain is forwarded and omitted when null (CodeRabbit feedback).
2 parents 0c3193d + 90dab97 commit 4d6dc69

29 files changed

Lines changed: 1788 additions & 318 deletions

Runtime/Plugins/SolanaMobileStack.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Runtime/Plugins/SolanaMobileStack/Android.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.solana.unity.mwa;
2+
3+
import android.app.Activity;
4+
import android.app.PendingIntent;
5+
import android.content.BroadcastReceiver;
6+
import android.content.ComponentName;
7+
import android.content.Context;
8+
import android.content.Intent;
9+
import android.content.IntentFilter;
10+
import android.os.Build;
11+
12+
/** System wallet chooser + {@link Intent#EXTRA_CHOSEN_COMPONENT} capture. No manifest entry */
13+
public final class MwaChooserHelper {
14+
15+
private static final String ACTION_CHOSEN = "com.solana.unity.mwa.WALLET_CHOSEN";
16+
17+
// RECEIVER_NOT_EXPORTED (API 33) as literal for older compileSdk.
18+
private static final int RECEIVER_NOT_EXPORTED = 0x4;
19+
20+
private static final String EXTRA_NONCE = "chooser_nonce";
21+
22+
private static volatile String sChosenPackage = null;
23+
private static volatile String sChooserNonce = null;
24+
private static BroadcastReceiver sReceiver = null;
25+
26+
private MwaChooserHelper() {}
27+
28+
/** Last chosen package, then clear. Null if none. */
29+
public static synchronized String consumeChosenPackage() {
30+
String pkg = sChosenPackage;
31+
sChosenPackage = null;
32+
return pkg;
33+
}
34+
35+
/** createChooser + IntentSender callback = launches MWA target intent */
36+
public static void launchWithChooser(Activity activity, Intent target, String title) {
37+
sChosenPackage = null;
38+
sChooserNonce = java.util.UUID.randomUUID().toString();
39+
registerReceiver(activity.getApplicationContext());
40+
41+
Intent broadcast = new Intent(ACTION_CHOSEN)
42+
.setPackage(activity.getPackageName())
43+
.putExtra(EXTRA_NONCE, sChooserNonce);
44+
45+
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
46+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
47+
flags |= PendingIntent.FLAG_MUTABLE;
48+
}
49+
PendingIntent pendingIntent = PendingIntent.getBroadcast(activity, 0, broadcast, flags);
50+
51+
Intent chooser = Intent.createChooser(target, title, pendingIntent.getIntentSender());
52+
activity.startActivity(chooser);
53+
}
54+
55+
private static synchronized void registerReceiver(Context context) {
56+
if (sReceiver != null) {
57+
return;
58+
}
59+
sReceiver = new BroadcastReceiver() {
60+
@Override
61+
public void onReceive(Context ctx, Intent intent) {
62+
String nonce = intent.getStringExtra(EXTRA_NONCE);
63+
if (nonce == null || !nonce.equals(sChooserNonce)) {
64+
return;
65+
}
66+
ComponentName chosen = intent.getParcelableExtra(Intent.EXTRA_CHOSEN_COMPONENT);
67+
if (chosen != null) {
68+
sChosenPackage = chosen.getPackageName();
69+
}
70+
}
71+
};
72+
IntentFilter filter = new IntentFilter(ACTION_CHOSEN);
73+
if (Build.VERSION.SDK_INT >= 33) {
74+
context.registerReceiver(sReceiver, filter, RECEIVER_NOT_EXPORTED);
75+
} else {
76+
context.registerReceiver(sReceiver, filter);
77+
}
78+
}
79+
}

Runtime/Plugins/SolanaMobileStack/Android/MwaChooserHelper.java.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Runtime/Plugins/SolanaWalletAdapterWebGL/SolanaWalletAdapterWebGL.jslib

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,125 @@ mergeInto(LibraryManager.library, {
168168
{{{ makeDynCall('vi', 'callback') }}}(null);
169169
}
170170
},
171+
ExternSubscribeWalletEvents: function (walletNamePtr, callback) {
172+
try {
173+
const walletName = UTF8ToString(walletNamePtr);
174+
window.unityWalletEventUnsubs = window.unityWalletEventUnsubs || {};
175+
176+
if (window.unityWalletEventUnsubs[walletName]) {
177+
window.unityWalletEventUnsubs[walletName]();
178+
delete window.unityWalletEventUnsubs[walletName];
179+
}
180+
181+
const toPkString = (v) => {
182+
try {
183+
if (!v) return null;
184+
if (typeof v === "string") return v;
185+
if (Array.isArray(v) && v.length > 0) {
186+
const first = v[0];
187+
if (!first) return null;
188+
return typeof first === "string" ? first : (first.toString ? first.toString() : null);
189+
}
190+
return v.toString ? v.toString() : null;
191+
} catch (_) {
192+
return null;
193+
}
194+
};
195+
196+
const emit = (evt, data) => {
197+
const payload = {
198+
event: evt,
199+
walletName: walletName,
200+
publicKey: data && data.publicKey ? toPkString(data.publicKey) : toPkString(data),
201+
account: data && data.account ? toPkString(data.account) : null,
202+
accounts: data && data.accounts ? data.accounts.map(toPkString).filter(Boolean) : null,
203+
error: data && data.message ? String(data.message) : null,
204+
};
205+
const json = JSON.stringify(payload);
206+
const len = lengthBytesUTF8(json) + 1;
207+
const ptr = _malloc(len);
208+
stringToUTF8(json, ptr, len);
209+
{{{ makeDynCall('vi', 'callback') }}}(ptr);
210+
_free(ptr);
211+
};
212+
213+
const tryResolveProvider = () => {
214+
if (walletName === "XNFT" && window.xnft && window.xnft.solana) {
215+
return window.xnft.solana;
216+
}
217+
218+
if (window.walletAdapterLib) {
219+
if (typeof window.walletAdapterLib.getWalletAdapter === "function") {
220+
const a = window.walletAdapterLib.getWalletAdapter(walletName);
221+
if (a) return a;
222+
}
223+
if (typeof window.walletAdapterLib.getWalletByName === "function") {
224+
const w = window.walletAdapterLib.getWalletByName(walletName);
225+
if (w && w.adapter) return w.adapter;
226+
if (w) return w;
227+
}
228+
const pools = [window.walletAdapterLib.walletAdapters, window.walletAdapterLib.wallets];
229+
for (const pool of pools) {
230+
if (!Array.isArray(pool)) continue;
231+
for (const item of pool) {
232+
if (!item) continue;
233+
if (item.name === walletName) return item.adapter || item;
234+
if (item.adapter && item.adapter.name === walletName) return item.adapter;
235+
if (item.wallet && item.wallet.name === walletName) return item.wallet.adapter || item.wallet;
236+
}
237+
}
238+
}
239+
240+
if (window.solana && walletName.toLowerCase().includes("phantom")) {
241+
return window.solana;
242+
}
243+
244+
return null;
245+
};
246+
247+
const provider = tryResolveProvider();
248+
if (!provider || typeof provider.on !== "function") {
249+
return;
250+
}
251+
252+
const subscriptions = [];
253+
const on = (evt, fn) => {
254+
try {
255+
provider.on(evt, fn);
256+
subscriptions.push([evt, fn]);
257+
} catch (_) {}
258+
};
259+
260+
on("accountsChanged", (accounts) => emit("accountsChanged", { accounts }));
261+
on("accountChanged", (account) => emit("accountChanged", { account }));
262+
on("disconnect", (info) => emit("disconnect", info));
263+
on("connect", (info) => emit("connect", info));
264+
on("change", (info) => emit("change", info));
265+
266+
window.unityWalletEventUnsubs[walletName] = () => {
267+
if (typeof provider.off === "function") {
268+
subscriptions.forEach(([evt, fn]) => {
269+
try {
270+
provider.off(evt, fn);
271+
} catch (_) {}
272+
});
273+
}
274+
};
275+
} catch (err) {
276+
console.error(err && err.message ? err.message : err);
277+
}
278+
},
279+
280+
ExternUnsubscribeWalletEvents: function (walletNamePtr) {
281+
try {
282+
const walletName = UTF8ToString(walletNamePtr);
283+
if (!window.unityWalletEventUnsubs || !window.unityWalletEventUnsubs[walletName]) {
284+
return;
285+
}
286+
window.unityWalletEventUnsubs[walletName]();
287+
delete window.unityWalletEventUnsubs[walletName];
288+
} catch (err) {
289+
console.error(err && err.message ? err.message : err);
290+
}
291+
},
171292
});

Runtime/codebase/SessionWallet.cs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,12 @@ private SessionWallet(RpcCluster rpcCluster = RpcCluster.DevNet,
2828
string customRpcUri = null, string customStreamingRpcUri = null,
2929
bool autoConnectOnStartup = false) : base(rpcCluster, customRpcUri, customStreamingRpcUri, autoConnectOnStartup)
3030
{
31-
if (Instance == null)
31+
if (Instance != null)
3232
{
33-
Instance = this;
34-
}
35-
else
36-
{
37-
throw new Exception("SessionWallet already exists");
33+
Debug.LogWarning($"there are more than one {nameof(SessionWallet)} in the scene");
3834
}
35+
36+
Instance = this;
3937
}
4038

4139
/// <summary>
@@ -65,6 +63,26 @@ public void SignInitSessionTx(Transaction tx)
6563
{
6664
tx.PartialSign(new[] { _externalWallet.Account, Account });
6765
}
66+
67+
public static SessionWallet GetSessionWallet(string publicKey, string privateKey, PublicKey targetProgram)
68+
{
69+
_externalWallet = Web3.Wallet;
70+
if (_externalWallet?.ActiveRpcClient == null)
71+
throw new InvalidOperationException("Web3.Wallet and its ActiveRpcClient must be initialized before creating a SessionWallet from raw keys.");
72+
73+
var sessionAccount = new Account(privateKey, publicKey);
74+
75+
// TODO: ActiveRpcClient can be null, get node address some other way
76+
var sessionWallet = new SessionWallet(_externalWallet.RpcCluster, _externalWallet.ActiveRpcClient.NodeAddress.ToString())
77+
{
78+
TargetProgram = targetProgram,
79+
EncryptedKeystoreKey = $"{_externalWallet.Account.PublicKey}_SessionKeyStore",
80+
SessionTokenPDA = FindSessionToken(targetProgram, sessionAccount, _externalWallet.Account),
81+
Account = sessionAccount,
82+
};
83+
84+
return sessionWallet;
85+
}
6886

6987
/// <summary>
7088
/// Creates a new SessionWallet instance based on the signature we get from signing a specific message.
@@ -75,9 +93,6 @@ public void SignInitSessionTx(Transaction tx)
7593
/// <returns>A SessionWallet instance.</returns>
7694
public static async Task<SessionWallet> GetSessionWallet(PublicKey targetProgram, WalletBase externalWallet = null, string seed = "MagicBlock.SessionKey")
7795
{
78-
// TODO: This class behaves like a singleton, what if I want multiple session wallets active at the same time?
79-
if (Instance != null) return Instance;
80-
8196
externalWallet ??= Web3.Wallet;
8297
_externalWallet = externalWallet;
8398

@@ -108,8 +123,6 @@ public static async Task<SessionWallet> GetSessionWallet(PublicKey targetProgram
108123
/// <returns>A SessionWallet instance.</returns>
109124
public static async Task<SessionWallet> GetSessionWallet(PublicKey targetProgram, string password, WalletBase externalWallet = null)
110125
{
111-
if (Instance != null) return Instance;
112-
113126
externalWallet ??= Web3.Wallet;
114127
_externalWallet = externalWallet;
115128

@@ -131,6 +144,7 @@ public static async Task<SessionWallet> GetSessionWallet(PublicKey targetProgram
131144
sessionWallet.DeleteSessionWallet();
132145
sessionWallet.Logout();
133146
Instance = null;
147+
134148
return await GetSessionWallet(targetProgram, password, externalWallet);
135149
}
136150

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Threading.Tasks;
2+
3+
// ReSharper disable once CheckNamespace
4+
5+
namespace Solana.Unity.SDK
6+
{
7+
/// <summary>
8+
/// Stores the selected Android wallet package used for MWA association.
9+
/// </summary>
10+
public interface IMwaWalletSelectionCache
11+
{
12+
Task<string> GetSelectedWalletPackage();
13+
Task SetSelectedWalletPackage(string packageName);
14+
Task ClearSelectedWalletPackage();
15+
}
16+
}

Runtime/codebase/SolanaMobileStack/IMwaWalletSelectionCache.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Runtime/codebase/SolanaMobileStack/LocalAssociationIntentCreator.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,24 @@
55
public static class LocalAssociationIntentCreator
66
{
77

8-
public static AndroidJavaObject CreateAssociationIntent(string associationToken, int port)
8+
public static AndroidJavaObject CreateAssociationIntent(
9+
string associationToken, int port, string targetPackage = null)
910
{
1011
var intent = new AndroidJavaObject("android.content.Intent");
1112
intent.Call<AndroidJavaObject>("setAction", "android.intent.action.VIEW");
1213
intent.Call<AndroidJavaObject>("addCategory", "android.intent.category.BROWSABLE");
1314
var url = $"{AssociationContract.SchemeMobileWalletAdapter}:/" +
1415
$"{AssociationContract.LocalPathSuffix}?association={associationToken}&port={port}";
1516
var uriClass = new AndroidJavaClass("android.net.Uri");
16-
var uriData = uriClass.CallStatic<AndroidJavaObject>("parse", url);
17+
var uriData = uriClass.CallStatic<AndroidJavaObject>("parse", url);
1718
intent.Call<AndroidJavaObject>("setData", uriData);
18-
//intent.Call<AndroidJavaObject>("addFlags", 0x14000000);
19+
20+
if (!string.IsNullOrEmpty(targetPackage))
21+
{
22+
intent.Call<AndroidJavaObject>("setPackage", targetPackage);
23+
UnityEngine.Debug.Log($"[MWA] Intent targeting package: {targetPackage}");
24+
}
25+
1926
return intent;
2027
}
2128
}

0 commit comments

Comments
 (0)