diff --git a/Tests/WalletConnectSharp.Sign.Test/SignTests.cs b/Tests/WalletConnectSharp.Sign.Test/SignTests.cs index c9c5f98..1af7f4d 100644 --- a/Tests/WalletConnectSharp.Sign.Test/SignTests.cs +++ b/Tests/WalletConnectSharp.Sign.Test/SignTests.cs @@ -84,7 +84,7 @@ public static async Task TestConnectMethod(ISignClient clientA, I }, Chains = new[] { - "eip155:1" + "eip155:1", "eip155:10" }, Events = new[] { @@ -149,7 +149,7 @@ public async Task TestRejectSession() }, Chains = new[] { - "eip155:1" + "eip155:1", "eip155:10" }, Events = new[] { @@ -192,7 +192,7 @@ public async Task TestSessionRequestResponse() }, Chains = new[] { - "eip155:1" + "eip155:1", "eip155:10" }, Events = new[] { @@ -294,7 +294,7 @@ public async Task TestTwoUniqueSessionRequestResponse() }, Chains = new[] { - "eip155:1" + "eip155:1", "eip155:10" }, Events = new[] { @@ -555,14 +555,19 @@ public async Task TestAddressProviderDefaults() var address = dappClient.AddressProvider.CurrentAddress(); Assert.Equal(testAddress, address.Address); Assert.Equal("eip155:1", address.ChainId); - Assert.Equal("eip155:1", dappClient.AddressProvider.DefaultChain); + Assert.Equal("eip155:1", dappClient.AddressProvider.DefaultChainId); Assert.Equal("eip155", dappClient.AddressProvider.DefaultNamespace); address = walletClient.AddressProvider.CurrentAddress(); Assert.Equal(testAddress, address.Address); Assert.Equal("eip155:1", address.ChainId); - Assert.Equal("eip155:1", dappClient.AddressProvider.DefaultChain); + Assert.Equal("eip155:1", dappClient.AddressProvider.DefaultChainId); Assert.Equal("eip155", dappClient.AddressProvider.DefaultNamespace); + + var allAddresses = dappClient.AddressProvider.AllAddresses("eip155").ToArray(); + Assert.Single(allAddresses); + Assert.Equal(testAddress, allAddresses[0].Address); + Assert.Equal("eip155:1", allAddresses[0].ChainId); } [Fact, Trait("Category", "integration")] @@ -591,5 +596,35 @@ public async Task TestAddressProviderDefaultsSaving() await TestTwoUniqueSessionRequestResponseUsingAddressProviderDefaults(); } + + [Fact] [Trait("Category", "integration")] + public async Task TestAddressProviderChainIdChange() + { + await _cryptoFixture.WaitForClientsReady(); + + _ = await TestConnectMethod(ClientA, ClientB); + + const string badChainId = "invalid:invalid"; + await Assert.ThrowsAsync(() => ClientA.AddressProvider.SetDefaultChainIdAsync(badChainId)); + + // Change the default chain id + const string newChainId = "eip155:10"; + await ClientA.AddressProvider.SetDefaultChainIdAsync(newChainId); + Assert.Equal(newChainId, ClientA.AddressProvider.DefaultChainId); + } + + [Fact] [Trait("Category", "integration")] + public async Task TestAddressProviderDisconnect() + { + await _cryptoFixture.WaitForClientsReady(); + + _ = await TestConnectMethod(ClientA, ClientB); + + Assert.True(ClientA.AddressProvider.HasDefaultSession); + + await ClientA.Disconnect(); + + Assert.False(ClientA.AddressProvider.HasDefaultSession); + } } } diff --git a/WalletConnectSharp.Sign/Controllers/AddressProvider.cs b/WalletConnectSharp.Sign/Controllers/AddressProvider.cs index dc546f9..e942d54 100644 --- a/WalletConnectSharp.Sign/Controllers/AddressProvider.cs +++ b/WalletConnectSharp.Sign/Controllers/AddressProvider.cs @@ -1,6 +1,4 @@ -using Newtonsoft.Json; -using WalletConnectSharp.Common.Logging; -using WalletConnectSharp.Sign.Interfaces; +using WalletConnectSharp.Sign.Interfaces; using WalletConnectSharp.Sign.Models; using WalletConnectSharp.Sign.Models.Engine.Events; @@ -8,6 +6,8 @@ namespace WalletConnectSharp.Sign.Controllers; public class AddressProvider : IAddressProvider { + private bool _disposed; + public struct DefaultData { public SessionStruct Session; @@ -67,7 +67,7 @@ public string DefaultNamespace } } - public string DefaultChain + public string DefaultChainId { get { @@ -91,7 +91,7 @@ public AddressProvider(ISignClient client) // set the first connected session to the default one client.SessionConnected += ClientOnSessionConnected; client.SessionDeleted += ClientOnSessionDeleted; - client.SessionUpdated += ClientOnSessionUpdated; + client.SessionUpdateRequest += ClientOnSessionUpdated; client.SessionApproved += ClientOnSessionConnected; } @@ -115,124 +115,121 @@ public virtual async Task LoadDefaults() DefaultsLoaded?.Invoke(this, new DefaultsLoadingEventArgs(_state)); } - private void ClientOnSessionUpdated(object sender, SessionEvent e) + private async void ClientOnSessionUpdated(object sender, SessionEvent e) { if (DefaultSession.Topic == e.Topic) { - UpdateDefaultChainAndNamespace(); + DefaultSession = Sessions.Get(e.Topic); + await UpdateDefaultChainIdAndNamespaceAsync(); } } - private void ClientOnSessionDeleted(object sender, SessionEvent e) + private async void ClientOnSessionDeleted(object sender, SessionEvent e) { if (DefaultSession.Topic == e.Topic) { DefaultSession = default; - UpdateDefaultChainAndNamespace(); + await UpdateDefaultChainIdAndNamespaceAsync(); } } - private void ClientOnSessionConnected(object sender, SessionStruct e) + private async void ClientOnSessionConnected(object sender, SessionStruct e) { if (!HasDefaultSession) { DefaultSession = e; - UpdateDefaultChainAndNamespace(); + await UpdateDefaultChainIdAndNamespaceAsync(); } } - private async void UpdateDefaultChainAndNamespace() + private async Task UpdateDefaultChainIdAndNamespaceAsync() { - try + if (HasDefaultSession) { - if (HasDefaultSession) + // Check if current default namespace is still valid with the current session + var currentDefault = DefaultNamespace; + if (currentDefault != null && DefaultSession.Namespaces.ContainsKey(currentDefault)) { - var currentDefault = DefaultNamespace; - if (currentDefault != null && DefaultSession.Namespaces.ContainsKey(currentDefault)) + // Check if current default chain is still valid with the current session + var currentChain = DefaultChainId; + if (currentChain == null || !DefaultSession.Namespaces[DefaultNamespace].Chains.Contains(currentChain)) { - // DefaultNamespace is still valid - var currentChain = DefaultChain; - if (currentChain == null || - DefaultSession.Namespaces[DefaultNamespace].Chains.Contains(currentChain)) - { - // DefaultChain is still valid - await SaveDefaults(); - return; - } - - DefaultChain = DefaultSession.Namespaces[DefaultNamespace].Chains[0]; - await SaveDefaults(); - return; + // If the current default chain is not valid, let's use the first one + DefaultChainId = DefaultSession.Namespaces[DefaultNamespace].Chains[0]; } - - // DefaultNamespace is null or not found in current available spaces, update it + } + else + { + // If DefaultNamespace is null or not found in current available spaces, update it DefaultNamespace = DefaultSession.Namespaces.Keys.FirstOrDefault(); - if (DefaultNamespace != null) + if (DefaultNamespace != null && DefaultSession.Namespaces[DefaultNamespace].Chains != null) { - if (DefaultSession.Namespaces.ContainsKey(DefaultNamespace) && - DefaultSession.Namespaces[DefaultNamespace].Chains != null) - { - DefaultChain = DefaultSession.Namespaces[DefaultNamespace].Chains[0]; - } - else if (DefaultSession.RequiredNamespaces.ContainsKey(DefaultNamespace) && - DefaultSession.RequiredNamespaces[DefaultNamespace].Chains != null) - { - // We don't know what chain to use? Let's use the required one as a fallback - DefaultChain = DefaultSession.RequiredNamespaces[DefaultNamespace].Chains[0]; - } + DefaultChainId = DefaultSession.Namespaces[DefaultNamespace].Chains[0]; } else { - DefaultNamespace = DefaultSession.Namespaces.Keys.FirstOrDefault(); - if (DefaultNamespace != null && DefaultSession.Namespaces[DefaultNamespace].Chains != null) - { - DefaultChain = DefaultSession.Namespaces[DefaultNamespace].Chains[0]; - } - else - { - // We don't know what chain to use? Let's use the required one as a fallback - DefaultNamespace = DefaultSession.RequiredNamespaces.Keys.FirstOrDefault(); - if (DefaultNamespace != null && - DefaultSession.RequiredNamespaces[DefaultNamespace].Chains != null) - { - DefaultChain = DefaultSession.RequiredNamespaces[DefaultNamespace].Chains[0]; - } - else - { - WCLogger.LogError("Could not figure out default chain to use"); - } - } + throw new InvalidOperationException("Could not figure out default chain and namespace"); } } - else - { - DefaultNamespace = null; - } await SaveDefaults(); } - catch (Exception e) + else { - WCLogger.LogError(e); - throw; + DefaultNamespace = null; + DefaultChainId = null; } } - public Caip25Address CurrentAddress(string @namespace = null, SessionStruct session = default) + public async Task InitAsync() { - @namespace ??= DefaultNamespace; - if (string.IsNullOrWhiteSpace(session.Topic)) // default - session = DefaultSession; + await this.LoadDefaults(); + } + + public async Task SetDefaultNamespaceAsync(string @namespace) + { + if (string.IsNullOrWhiteSpace(@namespace)) + { + throw new ArgumentNullException(nameof(@namespace)); + } - return session.CurrentAddress(@namespace); + if (!DefaultSession.Namespaces.ContainsKey(@namespace)) + { + throw new InvalidOperationException($"Namespace {@namespace} is not available in the current session"); + } + + DefaultNamespace = @namespace; + await SaveDefaults(); } + + public async Task SetDefaultChainIdAsync(string chainId) + { + if (string.IsNullOrWhiteSpace(chainId)) + { + throw new ArgumentNullException(nameof(chainId)); + } - public async Task Init() + if (!DefaultSession.Namespaces[DefaultNamespace].Chains.Contains(chainId)) + { + throw new InvalidOperationException($"Chain {chainId} is not available in the current session"); + } + + DefaultChainId = chainId; + await SaveDefaults(); + } + + public Caip25Address CurrentAddress(string chainId = null, SessionStruct session = default) { - await this.LoadDefaults(); + chainId ??= DefaultChainId; + if (string.IsNullOrWhiteSpace(session.Topic)) + { + session = DefaultSession; + } + + return session.CurrentAddress(chainId); } - public Caip25Address[] AllAddresses(string @namespace = null, SessionStruct session = default) + public IEnumerable AllAddresses(string @namespace = null, SessionStruct session = default) { @namespace ??= DefaultNamespace; if (string.IsNullOrWhiteSpace(session.Topic)) // default @@ -240,17 +237,33 @@ public Caip25Address[] AllAddresses(string @namespace = null, SessionStruct sess return session.AllAddresses(@namespace); } - + public void Dispose() { - _client.SessionConnected -= ClientOnSessionConnected; - _client.SessionDeleted -= ClientOnSessionDeleted; - _client.SessionUpdated -= ClientOnSessionUpdated; - _client.SessionApproved -= ClientOnSessionConnected; - - _client = null; - Sessions = null; - DefaultNamespace = null; - DefaultSession = default; + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _client.SessionConnected -= ClientOnSessionConnected; + _client.SessionDeleted -= ClientOnSessionDeleted; + _client.SessionUpdateRequest -= ClientOnSessionUpdated; + _client.SessionApproved -= ClientOnSessionConnected; + + _client = null; + Sessions = null; + DefaultNamespace = null; + DefaultSession = default; + } + + _disposed = true; } } diff --git a/WalletConnectSharp.Sign/Engine.cs b/WalletConnectSharp.Sign/Engine.cs index 3e5f1a0..b6066a8 100644 --- a/WalletConnectSharp.Sign/Engine.cs +++ b/WalletConnectSharp.Sign/Engine.cs @@ -325,7 +325,7 @@ public Task Extend() public Task Request(T data, string chainId = null, long? expiry = null) { return Request(Client.AddressProvider.DefaultSession.Topic, data, - chainId ?? Client.AddressProvider.DefaultChain, expiry); + chainId ?? Client.AddressProvider.DefaultChainId, expiry); } public Task Respond(JsonRpcResponse response) @@ -336,7 +336,7 @@ public Task Respond(JsonRpcResponse response) public Task Emit(EventData eventData, string chainId = null) { return Emit(Client.AddressProvider.DefaultSession.Topic, eventData, - chainId ?? Client.AddressProvider.DefaultChain); + chainId ?? Client.AddressProvider.DefaultChainId); } public Task Ping() @@ -735,7 +735,7 @@ public async Task Request(string topic, T data, string chainId = null var sessionData = Client.Session.Get(topic); var defaultNamespace = Client.AddressProvider.DefaultNamespace ?? sessionData.Namespaces.Keys.FirstOrDefault(); - defaultChainId = Client.AddressProvider.DefaultChain ?? + defaultChainId = Client.AddressProvider.DefaultChainId ?? sessionData.Namespaces[defaultNamespace].Chains[0]; } else diff --git a/WalletConnectSharp.Sign/Interfaces/IAddressProvider.cs b/WalletConnectSharp.Sign/Interfaces/IAddressProvider.cs index 2ce881a..adc4738 100644 --- a/WalletConnectSharp.Sign/Interfaces/IAddressProvider.cs +++ b/WalletConnectSharp.Sign/Interfaces/IAddressProvider.cs @@ -10,16 +10,21 @@ public interface IAddressProvider : IModule bool HasDefaultSession { get; } SessionStruct DefaultSession { get; set; } - - string DefaultNamespace { get; set; } - - string DefaultChain { get; set; } + + string DefaultNamespace { get; } + + string DefaultChainId { get; } ISession Sessions { get; } - - Caip25Address CurrentAddress( string chain = null, SessionStruct session = default); - Task Init(); - Caip25Address[] AllAddresses(string chain = null, SessionStruct session = default); + Task InitAsync(); + + Task SetDefaultNamespaceAsync(string @namespace); + + Task SetDefaultChainIdAsync(string chainId); + + Caip25Address CurrentAddress(string chainId = null, SessionStruct session = default); + + IEnumerable AllAddresses(string @namespace = null, SessionStruct session = default); } diff --git a/WalletConnectSharp.Sign/Interfaces/IEngineAPI.cs b/WalletConnectSharp.Sign/Interfaces/IEngineAPI.cs index 8a19450..4d0ee22 100644 --- a/WalletConnectSharp.Sign/Interfaces/IEngineAPI.cs +++ b/WalletConnectSharp.Sign/Interfaces/IEngineAPI.cs @@ -50,7 +50,7 @@ public interface IEngineAPI /// /// This event is invoked when a given session sent a update request. - /// Event Side: Wallet + /// Event Side: dApp /// event EventHandler SessionUpdateRequest; @@ -62,7 +62,7 @@ public interface IEngineAPI /// /// This event is invoked when a given session update request was successful. - /// Event Side: dApp + /// Event Side: Wallet /// event EventHandler SessionUpdated; diff --git a/WalletConnectSharp.Sign/Models/SessionStruct.cs b/WalletConnectSharp.Sign/Models/SessionStruct.cs index 48035f4..d92a29a 100644 --- a/WalletConnectSharp.Sign/Models/SessionStruct.cs +++ b/WalletConnectSharp.Sign/Models/SessionStruct.cs @@ -82,51 +82,77 @@ public string Key return Topic; } } - - public Caip25Address CurrentAddress(string @namespace) + + public Caip25Address CurrentAddress(string chainId) { - // double check - if (@namespace == null) - throw new ArgumentException("SessionStruct.CurrentAddress: @namespace is null"); - if (string.IsNullOrWhiteSpace(Topic)) - throw new ArgumentException("SessionStruct.CurrentAddress: Session is undefined"); - - var defaultNamespace = Namespaces[@namespace]; + ValidateChainIdAndTopic(chainId); + + var namespaceStr = chainId.Split(':')[0]; + if (!Namespaces.TryGetValue(namespaceStr, out var defaultNamespace)) + { + throw new InvalidOperationException( + $"SessionStruct.CurrentAddress: Given namespace {namespaceStr} is not available in the current session"); + } + if (defaultNamespace.Accounts.Length == 0) - throw new Exception( - $"SessionStruct.CurrentAddress: Given namespace {@namespace} has no connected addresses"); + throw new InvalidOperationException( + $"SessionStruct.CurrentAddress: Given namespace {namespaceStr} has no connected addresses"); - var fullAddress = defaultNamespace.Accounts[0]; - var addressParts = fullAddress.Split(":"); + var fullAddress = Array.Find(defaultNamespace.Accounts, addr => addr.StartsWith(chainId)); + if (fullAddress == default) + { + throw new InvalidOperationException( + $"SessionStruct.CurrentAddress: No address found for chain {chainId}"); + } + + var address = fullAddress.Split(":")[2]; + return new Caip25Address { Address = address, ChainId = chainId }; + } + + public IEnumerable AllAddresses(string @namespace) + { + ValidateNamespaceAndTopic(@namespace); + + var defaultNamespace = Namespaces[@namespace]; + return defaultNamespace.Accounts.Length == 0 + ? [] + : defaultNamespace.Accounts.Select(CreateCaip25Address); + } + public static Caip25Address CreateCaip25Address(string fullAddress) + { + var addressParts = fullAddress.Split(":"); var address = addressParts[2]; var chainId = string.Join(':', addressParts.Take(2)); - return new Caip25Address() - { - Address = address, - ChainId = chainId, - }; + return new Caip25Address { Address = address, ChainId = chainId }; } - - public Caip25Address[] AllAddresses(string @namespace) + + private void ValidateNamespaceAndTopic(string @namespace) { - // double check if (@namespace == null) - throw new ArgumentException("SessionStruct.AllAddresses: @namespace is null"); + { + throw new ArgumentException("@namespace is null"); + } + if (string.IsNullOrWhiteSpace(Topic)) - throw new ArgumentException("SessionStruct.AllAddresses: Session is undefined"); - - var defaultNamespace = Namespaces[@namespace]; + { + throw new ArgumentException("Session is undefined"); + } + } - if (defaultNamespace.Accounts.Length == 0) - return null; //The namespace {@namespace} has no addresses connected") + private void ValidateChainIdAndTopic(string chainId) + { + if (chainId == null) + { + throw new ArgumentException("chainId is null"); + } - return defaultNamespace.Accounts.Select(addr => new Caip25Address() + if (string.IsNullOrWhiteSpace(Topic)) { - Address = addr.Split(":")[2], ChainId = string.Join(":", addr.Split(":").Take(2)) - }).ToArray(); + throw new ArgumentException("Session is undefined"); + } } } } diff --git a/WalletConnectSharp.Sign/WalletConnectSignClient.cs b/WalletConnectSharp.Sign/WalletConnectSignClient.cs index d011736..e61b7cf 100644 --- a/WalletConnectSharp.Sign/WalletConnectSignClient.cs +++ b/WalletConnectSharp.Sign/WalletConnectSignClient.cs @@ -488,7 +488,7 @@ private async Task Initialize() await Session.Init(); await Proposal.Init(); await Engine.Init(); - await AddressProvider.Init(); + await AddressProvider.InitAsync(); } public void Dispose() @@ -503,12 +503,13 @@ protected virtual void Dispose(bool disposing) if (disposing) { + AddressProvider?.Dispose(); Core?.Dispose(); + Engine?.Dispose(); PairingStore?.Dispose(); Session?.Dispose(); Proposal?.Dispose(); PendingRequests?.Dispose(); - Engine?.Dispose(); } Disposed = true;