Skip to content

Commit

Permalink
Merge pull request #25 from reown-com/fix/metamask-chain-switch
Browse files Browse the repository at this point in the history
fix: chain switching not working in mobile MM
  • Loading branch information
skibitsky authored Nov 29, 2024
2 parents af94d73 + d8932ae commit 6e7436d
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 47 deletions.
6 changes: 6 additions & 0 deletions Reown.sln
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reown.WalletKit.Test", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reown.Core.Storage.Test", "test\Reown.Core.Storage.Test\Reown.Core.Storage.Test.csproj", "{F13FA074-D00C-46B9-A7AC-78FCF940165D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reown.Core.Common.Test", "test\Reown.Core.Common.Test\Reown.Core.Common.Test.csproj", "{4C513826-A313-40FA-A6ED-F74A33139809}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -114,5 +116,9 @@ Global
{F13FA074-D00C-46B9-A7AC-78FCF940165D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F13FA074-D00C-46B9-A7AC-78FCF940165D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F13FA074-D00C-46B9-A7AC-78FCF940165D}.Release|Any CPU.Build.0 = Release|Any CPU
{4C513826-A313-40FA-A6ED-F74A33139809}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4C513826-A313-40FA-A6ED-F74A33139809}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4C513826-A313-40FA-A6ED-F74A33139809}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4C513826-A313-40FA-A6ED-F74A33139809}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@ protected override Task InitializeAsyncCore(IEnumerable<Chain> supportedChains)
protected override async Task ChangeActiveChainAsyncCore(Chain chain)
{
if (AppKit.ConnectorController.IsAccountConnected)
{
// Request connector to change active chain.
// If connector approves the change, it will trigger the ChainChanged event.
await AppKit.ConnectorController.ChangeActiveChainAsync(chain);

var previousChain = ActiveChain;
ActiveChain = chain;
OnChainChanged(new ChainChangedEventArgs(previousChain, chain));
}
else
{
ActiveChain = chain;
}

AppKit.EventsController.SendEvent(new Event
{
Expand All @@ -38,6 +46,9 @@ protected override async Task ChangeActiveChainAsyncCore(Chain chain)

protected override void ConnectorChainChangedHandlerCore(object sender, Connector.ChainChangedEventArgs e)
{
if (ActiveChain?.ChainId == e.ChainId)
return;

var chain = Chains.GetValueOrDefault(e.ChainId);

var previousChain = ActiveChain;
Expand Down
94 changes: 90 additions & 4 deletions src/Reown.Core.Common/Runtime/Utils/HexByteConvertorExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,105 @@
using System;
using System.Linq;
using System.Numerics;
using System.Text;

namespace Reown.Core.Common.Utils
{
//From article http://blogs.msdn.com/b/heikkiri/archive/2012/07/17/hex-string-to-corresponding-byte-array.aspx
public static class HexByteConvertorExtensions
{
private static readonly byte[] Empty = Array.Empty<byte>();

public static string ToHex(this byte[] value, bool prefix = false)
{
var strPrex = prefix ? "0x" : "";
return strPrex + string.Concat(value.Select(b => b.ToString("x2")).ToArray());
var prefixLength = prefix ? 2 : 0;
var bufferLength = prefixLength + value.Length * 2; // Each byte becomes two hex characters

var buffer = bufferLength <= 256
? stackalloc char[bufferLength]
: new char[bufferLength];

var index = 0;
if (prefix)
{
buffer[0] = '0';
buffer[1] = 'x';
index = 2;
}

foreach (var b in value)
{
buffer[index++] = GetHexChar(b >> 4); // High nibble
buffer[index++] = GetHexChar(b & 0x0F); // Low nibble
}

return new string(buffer);
}

public static string ToHex(this string value, bool prefix = false)
{
return ToHex(Encoding.UTF8.GetBytes(value), prefix);
}

public static string ToHex(this int value, bool prefix = false)
{
Span<char> buffer = stackalloc char[10]; // 8 characters for the value + 2 for the prefix

var index = 0;
if (prefix)
{
buffer[0] = '0';
buffer[1] = 'x';
index = 2;
}

var success = value.TryFormat(buffer[index..], out var charsWritten, "x");

if (!success)
{
throw new InvalidOperationException("Failed to convert value to hex");
}

return new string(buffer[..(index + charsWritten)]);
}

public static string ToHex(this BigInteger value, bool prefix = false)
{
if (value.IsZero)
return prefix ? "0x0" : "0";

var byteCount = value.GetByteCount();
var prefixLength = prefix ? 2 : 0;
var bufferLength = prefixLength + byteCount * 2; // Each byte becomes two hex characters

var buffer = bufferLength <= 256
? stackalloc char[bufferLength]
: new char[bufferLength];

var success = value.TryFormat(buffer, out var charsWritten, "x");
if (!success)
{
throw new InvalidOperationException("Failed to convert value to hexadecimal.");
}

// Remove unnecessary leading zeros
var nonZeroStartIndex = 0;
while (nonZeroStartIndex < charsWritten && buffer[nonZeroStartIndex] == '0')
nonZeroStartIndex++;

if (nonZeroStartIndex == charsWritten)
return prefix ? "0x0" : "0";

if (!prefix)
return new string(buffer.Slice(nonZeroStartIndex, charsWritten - nonZeroStartIndex));

var resultLength = charsWritten - nonZeroStartIndex;
var resultBuffer = bufferLength <= 256 ? stackalloc char[2 + resultLength] : new char[2 + resultLength];
resultBuffer[0] = '0';
resultBuffer[1] = 'x';
buffer.Slice(nonZeroStartIndex, resultLength).CopyTo(resultBuffer[2..]);
return new string(resultBuffer);
}

public static bool HasHexPrefix(this string value)
{
return value.StartsWith("0x");
Expand Down Expand Up @@ -128,10 +208,16 @@ public static byte[] HexToByteArray(this string value)
}
}

// Maps 0-15 to '0'-'9' and 'a'-'f'
private static char GetHexChar(int value)
{
return (char)(value < 10 ? '0' + value : 'a' + (value - 10));
}

private static byte FromCharacterToByte(char character, int index, int shift = 0)
{
var value = (byte)character;
if (0x40 < value && 0x47 > value || 0x60 < value && 0x67 > value)
if (value is > 0x40 and < 0x47 or > 0x60 and < 0x67)
{
if (0x40 == (0x40 & value))
if (0x20 == (0x20 & value))
Expand Down
57 changes: 15 additions & 42 deletions src/Reown.Sign.Nethereum/Runtime/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using Reown.Core.Common.Model.Errors;
using Reown.Sign.Interfaces;
using Reown.Sign.Models.Engine.Methods;
using Reown.Sign.Nethereum.Model;

namespace Reown.Sign.Nethereum
Expand All @@ -22,60 +21,34 @@ public static async Task SwitchEthereumChainAsync(this ISignClient signClient, E
if (ethereumChain == null)
throw new ArgumentNullException(nameof(ethereumChain));

var tcs = new TaskCompletionSource<bool>();
signClient.SubscribeToSessionEvent("chainChanged", OnChainChanged);

var caip2ChainId = $"eip155:{ethereumChain.chainIdDecimal}";
if (!signClient.AddressProvider.DefaultSession.Namespaces.TryGetValue("eip155", out var @namespace)
|| !@namespace.Chains.Contains(caip2ChainId))
{
var request = new WalletAddEthereumChain(ethereumChain);

try
{
await signClient.Request<WalletAddEthereumChain, string>(request);

// Try to switch chain. This will only work if the chain is already added to the MetaMask
var data = new WalletSwitchEthereumChain(ethereumChain.chainIdHex);

var switchChainTask = signClient.Request<WalletSwitchEthereumChain, string>(data);
var chainChangedEventTask = tcs.Task;

await signClient.Request<WalletSwitchEthereumChain, string>(data);
}
catch (ReownNetworkException e)
{
try
{
await Task.WhenAll(switchChainTask, chainChangedEventTask);
var metaMaskError = JsonConvert.DeserializeObject<MetaMaskError>(e.Message);
if (metaMaskError is { Code: 4001 }) // If user rejected
throw;
}
finally
catch (Exception)
{
_ = signClient.TryUnsubscribeFromSessionEvent("chainChanged", OnChainChanged);
// If requested chain is not added to the MetaMask, it returns an error that can't be deserialized
}
}
catch (ReownNetworkException)
{
// Wallet can decline if chain has already been added
}
}

async void OnChainChanged(object sender, SessionEvent<JToken> sessionEvent)
{
if (sessionEvent.ChainId == "eip155:0")
return;

if (sessionEvent.ChainId != $"eip155:{ethereumChain.chainIdDecimal}")
return;

// Wait for the session to be updated before changing the default chain id
await Task.Delay(TimeSpan.FromSeconds(1));

try
{
await signClient.AddressProvider.SetDefaultChainIdAsync(sessionEvent.ChainId);
}
catch (Exception e)
{
tcs.SetException(e);
// If the chain is not added to the MetaMask, add it
// MetaMask will also prompt the user to switch to the new chain
var request = new WalletAddEthereumChain(ethereumChain);
await signClient.Request<WalletAddEthereumChain, string>(request);
}

tcs.SetResult(true);
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/Reown.Sign.Nethereum/Runtime/Model/EthereumChain.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Numerics;
using Newtonsoft.Json;
using Reown.Core.Common.Utils;

Expand Down Expand Up @@ -33,7 +34,7 @@ public EthereumChain()
public EthereumChain(string chainIdDecimal, string name, in Currency nativeCurrency, string[] rpcUrls, string[] blockExplorerUrls = null)
{
this.chainIdDecimal = chainIdDecimal;
chainIdHex = chainIdDecimal.ToHex();
chainIdHex = BigInteger.Parse(chainIdDecimal).ToHex(true);
this.name = name;
this.nativeCurrency = nativeCurrency;
this.rpcUrls = rpcUrls;
Expand Down
15 changes: 15 additions & 0 deletions src/Reown.Sign.Nethereum/Runtime/Model/MetaMaskError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using Newtonsoft.Json;

namespace Reown.Sign.Nethereum.Model
{
[Serializable]
public class MetaMaskError
{
[JsonProperty("code")]
public int Code { get; set; }

[JsonProperty("message")]
public string Message { get; set; }
}
}
3 changes: 3 additions & 0 deletions src/Reown.Sign.Nethereum/Runtime/Model/MetaMaskError.cs.meta

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

29 changes: 29 additions & 0 deletions test/Reown.Core.Common.Test/HexByteConvertorExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Numerics;
using Reown.Core.Common.Utils;
using Xunit;

namespace Reown.Core.Common.Test;

public class HexByteConvertorExtensionsTests
{
[Theory]
[InlineData(137, true, "0x89")]
[InlineData(16, true, "0x10")]
[InlineData(256, true, "0x100")]
[InlineData(0, true, "0x0")]
[InlineData(1, true, "0x1")]
[InlineData(4096, true, "0x1000")]
[InlineData(37714555429, true, "0x8c7f67225")]
[InlineData(137, false, "89")]
[InlineData(16, false, "10")]
[InlineData(256, false, "100")]
[InlineData(0, false, "0")]
[InlineData(1, false, "1")]
[InlineData(4096, false, "1000")]
[InlineData(37714555429, false, "8c7f67225")]
public void ToHex_Values_ReturnsExpectedHex(BigInteger value, bool prefix, string expected)
{
var result = value.ToHex(prefix);
Assert.Equal(expected, result);
}
}
24 changes: 24 additions & 0 deletions test/Reown.Core.Common.Test/Reown.Core.Common.Test.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<PackageReference Include="xunit"/>
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Reown.Core.Common\Reown.Core.Common.csproj" />
</ItemGroup>
</Project>

0 comments on commit 6e7436d

Please sign in to comment.