Skip to content

Commit 8fc607a

Browse files
authored
Merge pull request #288 from lopeselio/fix/mwa-2.0-caip2-chain-network-mismatch
fix(mwa): send CAIP-2 chain so MWA 2.0 wallets (Seeker Seed Vault) use the correct network
2 parents 90dab97 + 105f416 commit 8fc607a

6 files changed

Lines changed: 131 additions & 32 deletions

File tree

Runtime/codebase/SolanaMobileStack/Interfaces/IAdapterOperations.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99
public interface IAdapterOperations
1010
{
1111
[Preserve]
12-
public Task<AuthorizationResult> Authorize(Uri identityUri, Uri iconUri, string identityName, string rpcCluster);
12+
// chain: optional CAIP-2 identifier (e.g. "solana:devnet") for MWA 2.0 wallets.
13+
public Task<AuthorizationResult> Authorize(Uri identityUri, Uri iconUri, string identityName, string rpcCluster, string chain = null);
1314
[Preserve]
14-
public Task<AuthorizationResult> Reauthorize(Uri identityUri, Uri iconUri, string identityName, string authToken);
15+
// rpcCluster/chain: MWA 2.0 requires the network identifier on reauthorize too.
16+
// The spec deprecates the standalone reauthorize in favour of authorize carrying an
17+
// auth_token; when chain is absent the wallet (e.g. Seeker Seed Vault) defaults the
18+
// re-established session to solana:mainnet, causing a "Network mismatch" at sign time.
19+
public Task<AuthorizationResult> Reauthorize(Uri identityUri, Uri iconUri, string identityName, string authToken, string rpcCluster = null, string chain = null);
1520
[Preserve]
1621
public Task Deauthorize(string authToken);
1722
[Preserve]

Runtime/codebase/SolanaMobileStack/JsonRpcClient/JsonRequest.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ public class JsonRequestParams
4848
[RequiredMember]
4949
public string Cluster { get; set; }
5050

51+
// MWA 2.0 renamed the network identifier from "cluster" to "chain" using
52+
// CAIP-2 values (e.g. "solana:devnet"). Wallets implementing the 2.0 spec
53+
// (e.g. Seeker Seed Vault) ignore the legacy "cluster" field and default to
54+
// "solana:mainnet" when "chain" is absent. Both are serialized for
55+
// backward/forward compatibility across MWA 1.x and 2.0 wallets.
56+
[JsonProperty("chain", NullValueHandling = NullValueHandling.Ignore)]
57+
[RequiredMember]
58+
public string Chain { get; set; }
59+
5160
[JsonProperty("auth_token", NullValueHandling = NullValueHandling.Ignore)]
5261
[RequiredMember]
5362

Runtime/codebase/SolanaMobileStack/MobileWalletAdapterClient.cs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,35 @@ public MobileWalletAdapterClient(IMessageSender messageSender) : base(messageSen
2121
}
2222

2323
[Preserve]
24-
public Task<AuthorizationResult> Authorize(Uri identityUri, Uri iconUri, string identityName, string cluster)
24+
public Task<AuthorizationResult> Authorize(Uri identityUri, Uri iconUri, string identityName, string cluster, string chain = null)
2525
{
2626
var request = PrepareAuthRequest(
2727
identityUri,
28-
iconUri,
29-
identityName,
28+
iconUri,
29+
identityName,
3030
cluster,
31-
"authorize");
32-
31+
"authorize",
32+
chain);
33+
3334
return SendRequest<AuthorizationResult>(request);
3435
}
3536

36-
public Task<AuthorizationResult> Reauthorize(Uri identityUri, Uri iconUri, string identityName, string authToken)
37+
public Task<AuthorizationResult> Reauthorize(Uri identityUri, Uri iconUri, string identityName, string authToken, string cluster = null, string chain = null)
3738
{
39+
// MWA 2.0 deprecated the standalone `reauthorize` method in favour of `authorize`
40+
// carrying an `auth_token`. The `chain` MUST be re-sent here: when it is absent the
41+
// wallet (e.g. Seeker Seed Vault) defaults the re-established session to
42+
// solana:mainnet, producing a "Network mismatch" at sign time even though the
43+
// original authorize was devnet. When the chain matches the token's binding the
44+
// wallet silently reuses the existing auth_token (no extra user prompt).
3845
var request = PrepareAuthRequest(
3946
identityUri,
40-
iconUri,
41-
identityName,
42-
null,
43-
"reauthorize");
44-
47+
iconUri,
48+
identityName,
49+
cluster,
50+
"authorize",
51+
chain);
52+
4553
request.Params.AuthToken = authToken;
4654

4755
return SendRequest<AuthorizationResult>(request);
@@ -71,7 +79,7 @@ public Task<SignedResult> SignMessages(IEnumerable<byte[]> messages, IEnumerable
7179
return SendRequest<SignedResult>(request);
7280
}
7381

74-
private JsonRequest PrepareAuthRequest(Uri uriIdentity, Uri icon, string name, string cluster, string method)
82+
private JsonRequest PrepareAuthRequest(Uri uriIdentity, Uri icon, string name, string cluster, string method, string chain = null)
7583
{
7684
if (uriIdentity != null && !uriIdentity.IsAbsoluteUri)
7785
{
@@ -93,7 +101,8 @@ private JsonRequest PrepareAuthRequest(Uri uriIdentity, Uri icon, string name, s
93101
Icon = icon,
94102
Name = name
95103
},
96-
Cluster = cluster
104+
Cluster = cluster,
105+
Chain = chain
97106
},
98107
Id = NextMessageId()
99108
};

Runtime/codebase/SolanaMobileStack/SolanaMobileWalletAdapter.cs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ private void OnApplicationFocus(bool hasFocus)
4242
// below still touches PlayerPrefs directly. Live reads and writes
4343
// for the auth token go through _authCache (see IMwaAuthCache).
4444
private const string PrefKeyAuthToken = PlayerPrefsAuthCache.DefaultKey;
45-
45+
4646
private readonly SolanaMobileWalletAdapterOptions _walletOptions;
4747

4848
private Transaction _currentTransaction;
@@ -59,6 +59,17 @@ private void OnApplicationFocus(bool hasFocus)
5959
public event Action OnWalletDisconnected;
6060
public event Action OnWalletReconnected;
6161

62+
// CAIP-2 chain identifiers for MWA 2.0 wallets (e.g. Seeker Seed Vault).
63+
// Keyed to match RpcCluster / RPCNameMap. LocalNet has no standard CAIP-2
64+
// value, so it maps to null and only the legacy "cluster" field is sent.
65+
private static readonly Dictionary<int, string> ChainNameMap = new ()
66+
{
67+
{ 0, "solana:mainnet" },
68+
{ 1, "solana:devnet" },
69+
{ 2, "solana:testnet" },
70+
{ 3, null },
71+
};
72+
6273
public SolanaMobileWalletAdapter(
6374
SolanaMobileWalletAdapterOptions solanaWalletOptions,
6475
RpcCluster rpcCluster = RpcCluster.DevNet,
@@ -178,7 +189,8 @@ private async Task RunPrivileged(Action<IAdapterOperations> privilegedAction, st
178189
// operation in one session, so only the operation itself prompts.
179190
if (!_authToken.IsNullOrEmpty())
180191
{
181-
var reauthorize = new ReauthorizeOperation(_walletOptions, _authToken);
192+
var reauthorize = new ReauthorizeOperation(
193+
_walletOptions, _authToken, RPCNameMap[(int)RpcCluster], ChainNameMap[(int)RpcCluster]);
182194
using (var scenario = await CreateTargetedScenario())
183195
{
184196
var actions = reauthorize.BuildActions();
@@ -205,7 +217,8 @@ private async Task RunPrivileged(Action<IAdapterOperations> privilegedAction, st
205217
}
206218

207219
// No usable token: authorize (prompts) and run the operation in one session.
208-
var authorize = new AuthorizeOperation(_walletOptions, RPCNameMap[(int)RpcCluster]);
220+
var authorize = new AuthorizeOperation(
221+
_walletOptions, RPCNameMap[(int)RpcCluster], ChainNameMap[(int)RpcCluster]);
209222
using (var scenario = await CreateTargetedScenario())
210223
{
211224
var actions = authorize.BuildActions();
@@ -300,8 +313,9 @@ private async Task<Account> _LoginInternal(string password = null)
300313
using var localAssociationScenario = await CreateTargetedScenario();
301314

302315
var cluster = RPCNameMap[(int)RpcCluster];
303-
var authorizationOperation = new AuthorizeOperation(_walletOptions, cluster);
304-
316+
var chain = ChainNameMap[(int)RpcCluster];
317+
var authorizationOperation = new AuthorizeOperation(_walletOptions, cluster, chain);
318+
305319
var result = await localAssociationScenario.StartAndExecute(authorizationOperation.BuildActions());
306320
if (!result.WasSuccessful)
307321
{
@@ -564,13 +578,15 @@ internal sealed class AuthorizeOperation
564578
{
565579
private readonly SolanaMobileWalletAdapterOptions _opts;
566580
private readonly string _cluster;
581+
private readonly string _chain;
567582

568583
public AuthorizationResult Authorization { get; private set; }
569584

570-
public AuthorizeOperation(SolanaMobileWalletAdapterOptions opts, string cluster)
585+
public AuthorizeOperation(SolanaMobileWalletAdapterOptions opts, string cluster, string chain = null)
571586
{
572587
_opts = opts;
573588
_cluster = cluster;
589+
_chain = chain;
574590
}
575591

576592
public List<Action<IAdapterOperations>> BuildActions()
@@ -583,7 +599,8 @@ public List<Action<IAdapterOperations>> BuildActions()
583599
new Uri(_opts.identityUri),
584600
new Uri(_opts.iconUri, UriKind.Relative),
585601
_opts.name,
586-
_cluster);
602+
_cluster,
603+
_chain);
587604
}
588605
};
589606
}
@@ -593,13 +610,17 @@ internal sealed class ReauthorizeOperation
593610
{
594611
private readonly SolanaMobileWalletAdapterOptions _opts;
595612
private readonly string _authToken;
596-
613+
private readonly string _cluster;
614+
private readonly string _chain;
615+
597616
public AuthorizationResult Authorization { get; private set; }
598617

599-
public ReauthorizeOperation(SolanaMobileWalletAdapterOptions opts, string authToken)
618+
public ReauthorizeOperation(SolanaMobileWalletAdapterOptions opts, string authToken, string cluster = null, string chain = null)
600619
{
601620
_opts = opts;
602621
_authToken = authToken;
622+
_cluster = cluster;
623+
_chain = chain;
603624
}
604625

605626
public List<Action<IAdapterOperations>> BuildActions()
@@ -611,7 +632,7 @@ public List<Action<IAdapterOperations>> BuildActions()
611632
Authorization = await client.Reauthorize(
612633
new Uri(_opts.identityUri),
613634
new Uri(_opts.iconUri, UriKind.Relative),
614-
_opts.name, _authToken);
635+
_opts.name, _authToken, _cluster, _chain);
615636
}
616637
};
617638
}

Tests/EditMode/Contracts/IAdapterOperationsContractTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ public void Interface_Has_Authorize_WithExpectedSignature()
4646
Assert.IsNotNull(method, "IAdapterOperations.Authorize must exist");
4747
Assert.AreEqual(typeof(Task<AuthorizationResult>), method.ReturnType,
4848
"Authorize must return Task<AuthorizationResult>");
49-
Assert.IsTrue(HasParams(method, typeof(Uri), typeof(Uri), typeof(string), typeof(string)),
50-
"Authorize params must be (Uri identityUri, Uri iconUri, string identityName, string rpcCluster)");
49+
Assert.IsTrue(HasParams(method, typeof(Uri), typeof(Uri), typeof(string), typeof(string), typeof(string)),
50+
"Authorize params must be (Uri identityUri, Uri iconUri, string identityName, string rpcCluster, string chain)");
5151
}
5252

5353

@@ -60,8 +60,8 @@ public void Interface_Has_Reauthorize_WithExpectedSignature()
6060
Assert.IsNotNull(method, "IAdapterOperations.Reauthorize must exist");
6161
Assert.AreEqual(typeof(Task<AuthorizationResult>), method.ReturnType,
6262
"Reauthorize must return Task<AuthorizationResult>");
63-
Assert.IsTrue(HasParams(method, typeof(Uri), typeof(Uri), typeof(string), typeof(string)),
64-
"Reauthorize params must be (Uri identityUri, Uri iconUri, string identityName, string authToken)");
63+
Assert.IsTrue(HasParams(method, typeof(Uri), typeof(Uri), typeof(string), typeof(string), typeof(string), typeof(string)),
64+
"Reauthorize params must be (Uri identityUri, Uri iconUri, string identityName, string authToken, string rpcCluster, string chain)");
6565
}
6666

6767

Tests/EditMode/MwaClient/MobileWalletAdapterClientTests.cs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,40 @@ public void Authorize_SendsJsonRpc_WithCorrectCluster()
119119
"Params.Cluster must match the supplied cluster string");
120120
}
121121

122+
[Test]
123+
public void Authorize_SendsJsonRpc_WithCorrectChain()
124+
{
125+
// Arrange
126+
var identityUri = new Uri("https://example.com");
127+
const string cluster = "devnet";
128+
const string chain = "solana:devnet";
129+
130+
// Act
131+
_ = _client.Authorize(identityUri, null, "TestApp", cluster, chain);
132+
133+
// Assert
134+
var request = DecodeLastRequest();
135+
Assert.AreEqual(chain, request.Params.Chain,
136+
"Params.Chain must match the supplied CAIP-2 chain string");
137+
}
138+
139+
[Test]
140+
public void Authorize_OmitsChain_WhenChainIsNull()
141+
{
142+
// Arrange
143+
var identityUri = new Uri("https://example.com");
144+
const string cluster = "mainnet-beta";
145+
146+
// Act: no chain supplied (e.g. LocalNet) — Chain must stay null so
147+
// NullValueHandling.Ignore drops it from the serialized request.
148+
_ = _client.Authorize(identityUri, null, "TestApp", cluster);
149+
150+
// Assert
151+
var request = DecodeLastRequest();
152+
Assert.IsNull(request.Params.Chain,
153+
"Params.Chain must be null when no chain is supplied");
154+
}
155+
122156
[Test]
123157
public void Authorize_MessageIds_AreIncrementing()
124158
{
@@ -176,8 +210,10 @@ public void Authorize_ThrowsArgumentException_WhenIconUri_IsAbsolute()
176210

177211
// Reauthorize
178212
[Test]
179-
public void Reauthorize_SendsJsonRpc_WithCorrectMethod()
213+
public void Reauthorize_SendsJsonRpc_AsAuthorizeWithAuthToken()
180214
{
215+
// MWA 2.0 deprecated the standalone `reauthorize` method; reauthorization is
216+
// performed via `authorize` carrying an auth_token.
181217
// Arrange
182218
var identityUri = new Uri("https://example.com");
183219
const string authToken = "test-auth-token-abc123";
@@ -187,8 +223,8 @@ public void Reauthorize_SendsJsonRpc_WithCorrectMethod()
187223

188224
// Assert
189225
var request = DecodeLastRequest();
190-
Assert.AreEqual("reauthorize", request.Method,
191-
"Method must be 'reauthorize'");
226+
Assert.AreEqual("authorize", request.Method,
227+
"Method must be 'authorize' (MWA 2.0 reauthorize-via-authorize)");
192228
}
193229

194230
[Test]
@@ -206,5 +242,24 @@ public void Reauthorize_SendsJsonRpc_WithAuthToken()
206242
Assert.AreEqual(authToken, request.Params.AuthToken,
207243
"Params.AuthToken must match the supplied auth token");
208244
}
245+
246+
[Test]
247+
public void Reauthorize_SendsJsonRpc_WithChain()
248+
{
249+
// The chain MUST be forwarded on reauthorize, or the wallet defaults the
250+
// re-established session to solana:mainnet (Network mismatch at sign time).
251+
// Arrange
252+
var identityUri = new Uri("https://example.com");
253+
const string authToken = "test-auth-token-abc123";
254+
const string chain = "solana:devnet";
255+
256+
// Act
257+
_ = _client.Reauthorize(identityUri, null, "TestApp", authToken, "devnet", chain);
258+
259+
// Assert
260+
var request = DecodeLastRequest();
261+
Assert.AreEqual(chain, request.Params.Chain,
262+
"Params.Chain must match the supplied CAIP-2 chain string on reauthorize");
263+
}
209264
}
210265
}

0 commit comments

Comments
 (0)