diff --git a/Core Modules/WalletConnectSharp.Crypto/KeyChain.cs b/Core Modules/WalletConnectSharp.Crypto/KeyChain.cs index 7c83a88..d58720e 100644 --- a/Core Modules/WalletConnectSharp.Crypto/KeyChain.cs +++ b/Core Modules/WalletConnectSharp.Crypto/KeyChain.cs @@ -193,7 +193,9 @@ private async Task> GetKeyChain() private async Task SaveKeyChain() { - await Storage.SetItem(StorageKey, this._keyChain); + // We need to copy the contents, otherwise Dispose() + // may clear the reference stored inside InMemoryStorage + await Storage.SetItem(StorageKey, new Dictionary(this._keyChain)); } public void Dispose() diff --git a/Core Modules/WalletConnectSharp.Network/JsonRpcProvider.cs b/Core Modules/WalletConnectSharp.Network/JsonRpcProvider.cs index eeeda1b..cec4514 100644 --- a/Core Modules/WalletConnectSharp.Network/JsonRpcProvider.cs +++ b/Core Modules/WalletConnectSharp.Network/JsonRpcProvider.cs @@ -253,6 +253,19 @@ protected void RegisterEventListeners() _hasRegisteredEventListeners = true; } + protected void UnregisterEventListeners() + { + if (!_hasRegisteredEventListeners) return; + + WCLogger.Log( + $"[JsonRpcProvider] Unregistering event listeners on connection object with context {_connection.ToString()} inside {Context}"); + _connection.PayloadReceived -= OnPayload; + _connection.Closed -= OnConnectionDisconnected; + _connection.ErrorReceived -= OnConnectionError; + + _hasRegisteredEventListeners = false; + } + private void OnConnectionError(object sender, Exception e) { this.ErrorReceived?.Invoke(this, e); @@ -313,6 +326,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { + UnregisterEventListeners(); _connection?.Dispose(); } diff --git a/Core Modules/WalletConnectSharp.Storage/FileSystemStorage.cs b/Core Modules/WalletConnectSharp.Storage/FileSystemStorage.cs index 9583b61..2bcc5fd 100644 --- a/Core Modules/WalletConnectSharp.Storage/FileSystemStorage.cs +++ b/Core Modules/WalletConnectSharp.Storage/FileSystemStorage.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Text; using Newtonsoft.Json; using WalletConnectSharp.Common.Logging; @@ -41,7 +42,11 @@ public FileSystemStorage(string filePath = null) /// public override async Task Init() { + if (Initialized) + return; + _semaphoreSlim = new SemaphoreSlim(1, 1); + await Task.WhenAll( Load(), base.Init() ); @@ -89,12 +94,39 @@ private async Task Save() Directory.CreateDirectory(path); } - var json = JsonConvert.SerializeObject(Entries, + string json; + json = JsonConvert.SerializeObject(Entries, new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All }); - await _semaphoreSlim.WaitAsync(); - await File.WriteAllTextAsync(FilePath, json, Encoding.UTF8); - _semaphoreSlim.Release(); + try + { + if (!Disposed) + await _semaphoreSlim.WaitAsync(); + int count = 5; + IOException lastException; + do + { + try + { + await File.WriteAllTextAsync(FilePath, json, Encoding.UTF8); + return; + } + catch (IOException e) + { + WCLogger.LogError($"Got error saving storage file: retries left {count}"); + await Task.Delay(100); + count--; + lastException = e; + } + } while (count > 0); + + throw lastException; + } + finally + { + if (!Disposed) + _semaphoreSlim.Release(); + } } private async Task Load() @@ -102,25 +134,28 @@ private async Task Load() if (!File.Exists(FilePath)) return; - await _semaphoreSlim.WaitAsync(); - var json = await File.ReadAllTextAsync(FilePath, Encoding.UTF8); - _semaphoreSlim.Release(); + string json; + try + { + await _semaphoreSlim.WaitAsync(); + json = await File.ReadAllTextAsync(FilePath, Encoding.UTF8); + } + finally + { + _semaphoreSlim.Release(); + } + // Hard fail here if the storage file is bad, unless it's serialized as a Dictionary (for backwards compatibility) + var jsonSerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }; try { - Entries = JsonConvert.DeserializeObject>(json, - new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto }); + Entries = JsonConvert.DeserializeObject>(json, + jsonSerializerSettings); } - catch (JsonSerializationException e) + catch (JsonSerializationException) { - // Move the file to a .unsupported file - // and start fresh - WCLogger.LogError(e); - WCLogger.LogError("Cannot load JSON file, moving data to .unsupported file to force continue"); - if (File.Exists(FilePath + ".unsupported")) - File.Move(FilePath + ".unsupported", FilePath + "." + Guid.NewGuid() + ".unsupported"); - File.Move(FilePath, FilePath + ".unsupported"); - Entries = new Dictionary(); + var dict = JsonConvert.DeserializeObject>(json, jsonSerializerSettings); + Entries = new ConcurrentDictionary(dict); } } diff --git a/Core Modules/WalletConnectSharp.Storage/InMemoryStorage.cs b/Core Modules/WalletConnectSharp.Storage/InMemoryStorage.cs index 8ff6bde..46b0f2b 100644 --- a/Core Modules/WalletConnectSharp.Storage/InMemoryStorage.cs +++ b/Core Modules/WalletConnectSharp.Storage/InMemoryStorage.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Storage.Interfaces; @@ -5,13 +6,16 @@ namespace WalletConnectSharp.Storage { public class InMemoryStorage : IKeyValueStorage { - protected Dictionary Entries = new Dictionary(); - private bool _initialized = false; + protected ConcurrentDictionary Entries = new ConcurrentDictionary(); + protected bool Initialized = false; protected bool Disposed; public virtual Task Init() { - _initialized = true; + if (Initialized) + return Task.CompletedTask; + + Initialized = true; return Task.CompletedTask; } @@ -24,6 +28,7 @@ public virtual Task GetKeys() public virtual async Task GetEntriesOfType() { IsInitialized(); + // GetEntries is thread-safe return (await GetEntries()).OfType().ToArray(); } @@ -43,13 +48,15 @@ public virtual Task SetItem(string key, T value) { IsInitialized(); Entries[key] = value; + return Task.CompletedTask; } public virtual Task RemoveItem(string key) { IsInitialized(); - Entries.Remove(key); + Entries.Remove(key, out _); + return Task.CompletedTask; } @@ -63,12 +70,13 @@ public virtual Task Clear() { IsInitialized(); Entries.Clear(); + return Task.CompletedTask; } protected void IsInitialized() { - if (!_initialized) + if (!Initialized) { throw WalletConnectException.FromType(ErrorType.NOT_INITIALIZED, "Storage"); } diff --git a/Tests/WalletConnectSharp.Auth.Tests/AuthClientTest.cs b/Tests/WalletConnectSharp.Auth.Tests/AuthClientTest.cs index cbbc1ce..1c5b15f 100644 --- a/Tests/WalletConnectSharp.Auth.Tests/AuthClientTest.cs +++ b/Tests/WalletConnectSharp.Auth.Tests/AuthClientTest.cs @@ -11,6 +11,7 @@ using WalletConnectSharp.Storage; using WalletConnectSharp.Tests.Common; using Xunit; +using Xunit.Abstractions; using ErrorResponse = WalletConnectSharp.Auth.Models.ErrorResponse; namespace WalletConnectSharp.Auth.Tests @@ -26,6 +27,7 @@ public class AuthClientTests : IClassFixture, IAsyncLifetim }; private readonly CryptoWalletFixture _cryptoWalletFixture; + private readonly ITestOutputHelper _testOutputHelper; private IAuthClient PeerA; public IAuthClient PeerB; @@ -54,13 +56,14 @@ public string WalletAddress } } - public AuthClientTests(CryptoWalletFixture cryptoFixture) + public AuthClientTests(CryptoWalletFixture cryptoFixture, ITestOutputHelper testOutputHelper) { this._cryptoWalletFixture = cryptoFixture; + _testOutputHelper = testOutputHelper; } [Fact, Trait("Category", "unit")] - public async void TestInit() + public async Task TestInit() { Assert.NotNull(PeerA); Assert.NotNull(PeerB); @@ -77,7 +80,7 @@ public async void TestInit() } [Fact, Trait("Category", "unit")] - public async void TestPairs() + public async Task TestPairs() { var ogPairSize = PeerA.Core.Pairing.Pairings.Length; @@ -110,7 +113,7 @@ public async void TestPairs() } [Fact, Trait("Category", "unit")] - public async void TestKnownPairings() + public async Task TestKnownPairings() { var ogSizeA = PeerA.Core.Pairing.Pairings.Length; var history = await PeerA.AuthHistory(); @@ -121,7 +124,7 @@ public async void TestKnownPairings() var ogHistorySizeB = historyB.Keys.Length; List responses = new List(); - TaskCompletionSource responseTask = new TaskCompletionSource(); + TaskCompletionSource knownPairingTask = new TaskCompletionSource(); async void OnPeerBOnAuthRequested(object sender, AuthRequest request) { @@ -145,9 +148,9 @@ void OnPeerAOnAuthResponded(object sender, AuthResponse args) var sessionTopic = args.Topic; var cacao = args.Response.Result; var signature = cacao.Signature; - Console.WriteLine($"{sessionTopic}: {signature}"); + _testOutputHelper.WriteLine($"{sessionTopic}: {signature}"); responses.Add(args); - responseTask.SetResult(args); + knownPairingTask.SetResult(args); } PeerA.AuthResponded += OnPeerAOnAuthResponded; @@ -156,9 +159,9 @@ void OnPeerAOnAuthError(object sender, AuthErrorResponse args) { var sessionTopic = args.Topic; var error = args.Error; - Console.WriteLine($"{sessionTopic}: {error}"); + _testOutputHelper.WriteLine($"{sessionTopic}: {error}"); responses.Add(args); - responseTask.SetResult(args); + knownPairingTask.SetResult(args); } PeerA.AuthError += OnPeerAOnAuthError; @@ -167,18 +170,18 @@ void OnPeerAOnAuthError(object sender, AuthErrorResponse args) await PeerB.Core.Pairing.Pair(requestData.Uri); - await responseTask.Task; - + await knownPairingTask.Task; + // Reset - responseTask = new TaskCompletionSource(); + knownPairingTask = new TaskCompletionSource(); // Get last pairing, that is the one we just made var knownPairing = PeerA.Core.Pairing.Pairings[^1]; var requestData2 = await PeerA.Request(DefaultRequestParams, knownPairing.Topic); - await responseTask.Task; - + await knownPairingTask.Task; + Assert.Null(requestData2.Uri); Assert.Equal(ogSizeA + 1, PeerA.Core.Pairing.Pairings.Length); @@ -195,7 +198,7 @@ void OnPeerAOnAuthError(object sender, AuthErrorResponse args) } [Fact, Trait("Category", "unit")] - public async void HandlesAuthRequests() + public async Task HandlesAuthRequests() { var ogSize = PeerB.Requests.Length; @@ -218,7 +221,7 @@ public async void HandlesAuthRequests() } [Fact, Trait("Category", "unit")] - public async void TestErrorResponses() + public async Task TestErrorResponses() { var ogPSize = PeerA.Core.Pairing.Pairings.Length; @@ -263,7 +266,7 @@ void OnPeerAOnAuthResponded(object sender, AuthResponse response) } [Fact, Trait("Category", "unit")] - public async void HandlesSuccessfulResponse() + public async Task HandlesSuccessfulResponse() { var ogPSize = PeerA.Core.Pairing.Pairings.Length; @@ -313,7 +316,7 @@ void OnPeerAOnAuthResponded(object sender, AuthResponse response) => } [Fact, Trait("Category", "unit")] - public async void TestCustomRequestExpiry() + public async Task TestCustomRequestExpiry() { var uri = ""; var expiry = 1000; @@ -360,7 +363,7 @@ await PeerB.Respond( } [Fact, Trait("Category", "unit")] - public async void TestGetPendingPairings() + public async Task TestGetPendingPairings() { var ogCount = PeerB.PendingRequests.Count; @@ -386,7 +389,7 @@ public async void TestGetPendingPairings() } [Fact, Trait("Category", "unit")] - public async void TestGetPairings() + public async Task TestGetPairings() { var peerAOgSize = PeerA.Core.Pairing.Pairings.Length; var peerBOgSize = PeerB.Core.Pairing.Pairings.Length; @@ -414,7 +417,7 @@ public async void TestGetPairings() } [Fact, Trait("Category", "unit")] - public async void TestPing() + public async Task TestPing() { TaskCompletionSource receivedAuthRequest = new TaskCompletionSource(); TaskCompletionSource receivedClientPing = new TaskCompletionSource(); @@ -453,7 +456,7 @@ public async void TestPing() } [Fact, Trait("Category", "unit")] - public async void TestDisconnectedPairing() + public async Task TestDisconnectedPairing() { var peerAOgSize = PeerA.Core.Pairing.Pairings.Length; var peerBOgSize = PeerB.Core.Pairing.Pairings.Length; @@ -493,7 +496,7 @@ public async void TestDisconnectedPairing() } [Fact, Trait("Category", "unit")] - public async void TestReceivesMetadata() + public async Task TestReceivesMetadata() { var receivedMetadataName = ""; var ogPairingSize = PeerA.Core.Pairing.Pairings.Length; diff --git a/Tests/WalletConnectSharp.Auth.Tests/SignatureTest.cs b/Tests/WalletConnectSharp.Auth.Tests/SignatureTest.cs index 4ba407d..f322668 100644 --- a/Tests/WalletConnectSharp.Auth.Tests/SignatureTest.cs +++ b/Tests/WalletConnectSharp.Auth.Tests/SignatureTest.cs @@ -22,7 +22,7 @@ public class SignatureTest Expiration Time: 2022-10-11T23:03:35.700Z".Replace("\r", ""); [Fact, Trait("Category", "unit")] - public async void TestValidEip1271Signature() + public async Task TestValidEip1271Signature() { var signature = new Cacao.CacaoSignature.EIP1271CacaoSignature( "0xc1505719b2504095116db01baaf276361efd3a73c28cf8cc28dabefa945b8d536011289ac0a3b048600c1e692ff173ca944246cf7ceb319ac2262d27b395c82b1c"); @@ -34,7 +34,7 @@ public async void TestValidEip1271Signature() } [Fact, Trait("Category", "unit")] - public async void TestBadEip1271Signature() + public async Task TestBadEip1271Signature() { var signature = new Cacao.CacaoSignature.EIP1271CacaoSignature( "0xdead5719b2504095116db01baaf276361efd3a73c28cf8cc28dabefa945b8d536011289ac0a3b048600c1e692ff173ca944246cf7ceb319ac2262d27b395c82b1c"); diff --git a/Tests/WalletConnectSharp.Crypto.Tests/CryptoTests.cs b/Tests/WalletConnectSharp.Crypto.Tests/CryptoTests.cs index 051ef54..ecee01d 100644 --- a/Tests/WalletConnectSharp.Crypto.Tests/CryptoTests.cs +++ b/Tests/WalletConnectSharp.Crypto.Tests/CryptoTests.cs @@ -32,7 +32,7 @@ public CryptoTests(CryptoFixture cryptoFixture) } [Fact, Trait("Category", "unit")] - public async void TestEncodeDecode() + public async Task TestEncodeDecode() { await _cryptoFixture.WaitForModulesReady(); diff --git a/Tests/WalletConnectSharp.Network.Tests/RelayTests.cs b/Tests/WalletConnectSharp.Network.Tests/RelayTests.cs index 2d47e0d..806cb63 100644 --- a/Tests/WalletConnectSharp.Network.Tests/RelayTests.cs +++ b/Tests/WalletConnectSharp.Network.Tests/RelayTests.cs @@ -47,7 +47,7 @@ public async Task BuildGoodURL() } [Fact, Trait("Category", "integration")] - public async void ConnectAndRequest() + public async Task ConnectAndRequest() { var url = await BuildGoodURL(); var connection = new WebsocketConnection(url); @@ -60,7 +60,7 @@ public async void ConnectAndRequest() } [Fact, Trait("Category", "integration")] - public async void RequestWithoutConnect() + public async Task RequestWithoutConnect() { var url = await BuildGoodURL(); var connection = new WebsocketConnection(url); @@ -72,7 +72,7 @@ public async void RequestWithoutConnect() } [Fact, Trait("Category", "integration")] - public async void ThrowOnJsonRpcError() + public async Task ThrowOnJsonRpcError() { var url = await BuildGoodURL(); var connection = new WebsocketConnection(url); @@ -83,7 +83,7 @@ await Assert.ThrowsAsync(() => } [Fact, Trait("Category", "integration")] - public async void ThrowsOnUnavailableHost() + public async Task ThrowsOnUnavailableHost() { var connection = new WebsocketConnection(BAD_WS_URL); var provider = new JsonRpcProvider(connection); @@ -92,7 +92,7 @@ public async void ThrowsOnUnavailableHost() } [Fact, Trait("Category", "integration")] - public async void ReconnectsWithNewProvidedHost() + public async Task ReconnectsWithNewProvidedHost() { var url = await BuildGoodURL(); var connection = new WebsocketConnection(BAD_WS_URL); @@ -107,7 +107,7 @@ public async void ReconnectsWithNewProvidedHost() } [Fact, Trait("Category", "integration")] - public async void DoesNotDoubleRegisterListeners() + public async Task DoesNotDoubleRegisterListeners() { var url = await BuildGoodURL(); var connection = new WebsocketConnection(url); diff --git a/Tests/WalletConnectSharp.Sign.Test/SignClientFixture.cs b/Tests/WalletConnectSharp.Sign.Test/SignClientFixture.cs index 594f397..5ef576c 100644 --- a/Tests/WalletConnectSharp.Sign.Test/SignClientFixture.cs +++ b/Tests/WalletConnectSharp.Sign.Test/SignClientFixture.cs @@ -1,12 +1,17 @@ -using WalletConnectSharp.Core; +using WalletConnectSharp.Common.Logging; +using WalletConnectSharp.Core; using WalletConnectSharp.Sign.Models; using WalletConnectSharp.Storage; +using WalletConnectSharp.Storage.Interfaces; using WalletConnectSharp.Tests.Common; namespace WalletConnectSharp.Sign.Test; public class SignClientFixture : TwoClientsFixture { + public IKeyValueStorage StorageOverrideA; + public IKeyValueStorage StorageOverrideB; + public SignClientOptions OptionsA { get; protected set; } public SignClientOptions OptionsB { get; protected set; } @@ -26,11 +31,11 @@ public override async Task Init() { Description = "An example dapp to showcase WalletConnectSharpv2", Icons = new[] { "https://walletconnect.com/meta/favicon.ico" }, - Name = $"WalletConnectSharpv2 Dapp Example - {Guid.NewGuid().ToString()}", + Name = $"WalletConnectSharpv2 Dapp Example", Url = "https://walletconnect.com" }, // Omit if you want persistant storage - Storage = new InMemoryStorage() + Storage = StorageOverrideA ?? new InMemoryStorage() }; OptionsB = new SignClientOptions() @@ -41,14 +46,31 @@ public override async Task Init() { Description = "An example wallet to showcase WalletConnectSharpv2", Icons = new[] { "https://walletconnect.com/meta/favicon.ico" }, - Name = $"WalletConnectSharpv2 Wallet Example - {Guid.NewGuid().ToString()}", + Name = $"WalletConnectSharpv2 Wallet Example", Url = "https://walletconnect.com" }, // Omit if you want persistant storage - Storage = new InMemoryStorage() + Storage = StorageOverrideB ?? new InMemoryStorage() }; ClientA = await WalletConnectSignClient.Init(OptionsA); ClientB = await WalletConnectSignClient.Init(OptionsB); } + + public override async Task DisposeAndReset() + { + await WaitForNoPendingRequests(ClientA); + await WaitForNoPendingRequests(ClientB); + + await base.DisposeAndReset(); + } + + protected async Task WaitForNoPendingRequests(WalletConnectSignClient client) + { + while (client.PendingSessionRequests.Length > 0) + { + WCLogger.Log($"Waiting for {client.PendingSessionRequests.Length} requests to finish sending"); + await Task.Delay(100); + } + } } diff --git a/Tests/WalletConnectSharp.Sign.Test/SignTests.cs b/Tests/WalletConnectSharp.Sign.Test/SignTests.cs index c3cc145..c9c5f98 100644 --- a/Tests/WalletConnectSharp.Sign.Test/SignTests.cs +++ b/Tests/WalletConnectSharp.Sign.Test/SignTests.cs @@ -1,17 +1,21 @@ using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Common.Utils; +using WalletConnectSharp.Crypto; using WalletConnectSharp.Network.Models; using WalletConnectSharp.Sign.Interfaces; using WalletConnectSharp.Sign.Models; using WalletConnectSharp.Sign.Models.Engine; +using WalletConnectSharp.Storage; using WalletConnectSharp.Tests.Common; using Xunit; +using Xunit.Abstractions; namespace WalletConnectSharp.Sign.Test { public class SignTests : IClassFixture { private SignClientFixture _cryptoFixture; + private readonly ITestOutputHelper _testOutputHelper; private const string AllowedChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; [RpcMethod("test_method"), RpcRequestOptions(Clock.ONE_MINUTE, 99998)] @@ -52,9 +56,10 @@ public WalletConnectSignClient ClientB } } - public SignTests(SignClientFixture cryptoFixture) + public SignTests(SignClientFixture cryptoFixture, ITestOutputHelper testOutputHelper) { this._cryptoFixture = cryptoFixture; + _testOutputHelper = testOutputHelper; } public static async Task TestConnectMethod(ISignClient clientA, ISignClient clientB) @@ -114,7 +119,7 @@ public static async Task TestConnectMethod(ISignClient clientA, I } [Fact, Trait("Category", "integration")] - public async void TestApproveSession() + public async Task TestApproveSession() { await _cryptoFixture.WaitForClientsReady(); @@ -122,7 +127,7 @@ public async void TestApproveSession() } [Fact, Trait("Category", "integration")] - public async void TestRejectSession() + public async Task TestRejectSession() { await _cryptoFixture.WaitForClientsReady(); @@ -167,7 +172,7 @@ public async void TestRejectSession() } [Fact, Trait("Category", "integration")] - public async void TestSessionRequestResponse() + public async Task TestSessionRequestResponse() { await _cryptoFixture.WaitForClientsReady(); @@ -267,7 +272,7 @@ public async void TestSessionRequestResponse() } [Fact, Trait("Category", "integration")] - public async void TestTwoUniqueSessionRequestResponse() + public async Task TestTwoUniqueSessionRequestResponse() { await _cryptoFixture.WaitForClientsReady(); @@ -384,50 +389,50 @@ public async void TestTwoUniqueSessionRequestResponse() } [Fact, Trait("Category", "integration")] - public async void TestTwoUniqueSessionRequestResponseUsingAddressProviderDefaults() + public async Task TestTwoUniqueSessionRequestResponseUsingAddressProviderDefaults() { await _cryptoFixture.WaitForClientsReady(); - var testAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; - var testMethod = "test_method"; - var testMethod2 = "test_method_2"; - - var dappConnectOptions = new ConnectOptions() + var dappClient = ClientA; + var walletClient = ClientB; + if (!dappClient.AddressProvider.HasDefaultSession && !walletClient.AddressProvider.HasDefaultSession) { - RequiredNamespaces = new RequiredNamespaces() + var testAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; + var testMethod = "test_method"; + var testMethod2 = "test_method_2"; + + var dappConnectOptions = new ConnectOptions() { + RequiredNamespaces = new RequiredNamespaces() { - "eip155", new ProposedNamespace() { - Methods = new[] - { - testMethod, - testMethod2 - }, - Chains = new[] - { - "eip155:1" - }, - Events = new[] + "eip155", + new ProposedNamespace() { - "chainChanged", "accountsChanged" + Methods = new[] { testMethod, testMethod2 }, + Chains = new[] { "eip155:1" }, + Events = new[] { "chainChanged", "accountsChanged" } } } } - } - }; + }; - var dappClient = ClientA; - var connectData = await dappClient.Connect(dappConnectOptions); + var connectData = await dappClient.Connect(dappConnectOptions); - var walletClient = ClientB; - var proposal = await walletClient.Pair(connectData.Uri); + var proposal = await walletClient.Pair(connectData.Uri); - var approveData = await walletClient.Approve(proposal, testAddress); + var approveData = await walletClient.Approve(proposal, testAddress); - var sessionData = await connectData.Approval; - await approveData.Acknowledged(); + await connectData.Approval; + await approveData.Acknowledged(); + } + else + { + Assert.True(dappClient.AddressProvider.HasDefaultSession); + Assert.True(walletClient.AddressProvider.HasDefaultSession); + } + var defaultSessionTopic = dappClient.AddressProvider.DefaultSession.Topic; var rnd = new Random(); var a = rnd.Next(100); var b = rnd.Next(100); @@ -474,9 +479,11 @@ public async void TestTwoUniqueSessionRequestResponseUsingAddressProviderDefault // from the dappClient.Engine.Request function call (the response Result or throws an Exception) // We do it here for the sake of testing dappClient.Engine.SessionRequestEvents() - .FilterResponses((r) => r.Topic == sessionData.Topic) + .FilterResponses((r) => r.Topic == defaultSessionTopic) .OnResponse += (responseData) => { + Assert.True(responseData.Topic == defaultSessionTopic); + var response = responseData.Response; var data = response.Result; @@ -501,7 +508,7 @@ public async void TestTwoUniqueSessionRequestResponseUsingAddressProviderDefault } [Fact, Trait("Category", "integration")] - public async void TestAddressProviderDefaults() + public async Task TestAddressProviderDefaults() { await _cryptoFixture.WaitForClientsReady(); @@ -557,5 +564,32 @@ public async void TestAddressProviderDefaults() Assert.Equal("eip155:1", dappClient.AddressProvider.DefaultChain); Assert.Equal("eip155", dappClient.AddressProvider.DefaultNamespace); } + + [Fact, Trait("Category", "integration")] + public async Task TestAddressProviderDefaultsSaving() + { + await _cryptoFixture.WaitForClientsReady(); + + await TestTwoUniqueSessionRequestResponseUsingAddressProviderDefaults(); + + var defaultSessionTopic = _cryptoFixture.ClientA.AddressProvider.DefaultSession.Topic; + + _cryptoFixture.StorageOverrideA = _cryptoFixture.ClientA.Core.Storage; + _cryptoFixture.StorageOverrideB = _cryptoFixture.ClientB.Core.Storage; + + await Task.Delay(100); + + await _cryptoFixture.DisposeAndReset(); + + _testOutputHelper.WriteLine(string.Join(",", _cryptoFixture.ClientB.Core.Crypto.KeyChain.Keychain.Values)); + + await Task.Delay(100); + + var reloadedDefaultSessionTopic = _cryptoFixture.ClientA.AddressProvider.DefaultSession.Topic; + + Assert.Equal(defaultSessionTopic, reloadedDefaultSessionTopic); + + await TestTwoUniqueSessionRequestResponseUsingAddressProviderDefaults(); + } } } diff --git a/Tests/WalletConnectSharp.Storage.Test/FileSystemStorageTest.cs b/Tests/WalletConnectSharp.Storage.Test/FileSystemStorageTest.cs index 7713112..3656db1 100644 --- a/Tests/WalletConnectSharp.Storage.Test/FileSystemStorageTest.cs +++ b/Tests/WalletConnectSharp.Storage.Test/FileSystemStorageTest.cs @@ -9,7 +9,7 @@ namespace WalletConnectSharp.Storage.Test public class FileSystemStorageTest { [Fact, Trait("Category", "unit")] - public async void GetSetRemoveTest() + public async Task GetSetRemoveTest() { using (var tempFolder = new TempFolder()) { @@ -23,7 +23,7 @@ public async void GetSetRemoveTest() } [Fact, Trait("Category", "unit")] - public async void GetKeysTest() + public async Task GetKeysTest() { using (var tempFolder = new TempFolder()) { @@ -36,7 +36,7 @@ public async void GetKeysTest() } [Fact, Trait("Category", "unit")] - public async void GetEntriesTests() + public async Task GetEntriesTests() { using (var tempFolder = new TempFolder()) { @@ -51,7 +51,7 @@ public async void GetEntriesTests() } [Fact, Trait("Category", "unit")] - public async void HasItemTest() + public async Task HasItemTest() { using (var tempFolder = new TempFolder()) { diff --git a/Tests/WalletConnectSharp.Tests.Common/TwoClientsFixture.cs b/Tests/WalletConnectSharp.Tests.Common/TwoClientsFixture.cs index e630033..78962be 100644 --- a/Tests/WalletConnectSharp.Tests.Common/TwoClientsFixture.cs +++ b/Tests/WalletConnectSharp.Tests.Common/TwoClientsFixture.cs @@ -1,6 +1,6 @@ namespace WalletConnectSharp.Tests.Common; -public abstract class TwoClientsFixture +public abstract class TwoClientsFixture where TClient : IDisposable { public TClient ClientA { get; protected set; } public TClient ClientB { get; protected set; } @@ -19,4 +19,21 @@ public async Task WaitForClientsReady() while (ClientA == null || ClientB == null) await Task.Delay(10); } + + public virtual async Task DisposeAndReset() + { + if (ClientA != null) + { + ClientA.Dispose(); + ClientA = default; + } + + if (ClientB != null) + { + ClientB.Dispose(); + ClientB = default; + } + + await Init(); + } } diff --git a/Tests/WalletConnectSharp.Web3Wallet.Tests/AuthTests.cs b/Tests/WalletConnectSharp.Web3Wallet.Tests/AuthTests.cs index fb70c84..bc3799c 100644 --- a/Tests/WalletConnectSharp.Web3Wallet.Tests/AuthTests.cs +++ b/Tests/WalletConnectSharp.Web3Wallet.Tests/AuthTests.cs @@ -93,7 +93,7 @@ public async Task DisposeAsync() } [Fact, Trait("Category", "unit")] - public async void TestRespondToAuthRequest() + public async Task TestRespondToAuthRequest() { var request = await _dapp.Request(DefaultRequestParams); uriString = request.Uri; @@ -134,7 +134,7 @@ await Task.WhenAll( } [Fact, Trait("Category", "unit")] - public async void TestShouldRejectAuthRequest() + public async Task TestShouldRejectAuthRequest() { var request = await _dapp.Request(DefaultRequestParams); uriString = request.Uri; @@ -181,7 +181,7 @@ await Task.WhenAll( } [Fact, Trait("Category", "unit")] - public async void TestGetPendingAuthRequest() + public async Task TestGetPendingAuthRequest() { var request = await _dapp.Request(DefaultRequestParams); uriString = request.Uri; diff --git a/Tests/WalletConnectSharp.Web3Wallet.Tests/SignTests.cs b/Tests/WalletConnectSharp.Web3Wallet.Tests/SignTests.cs index 208ea19..92a09f4 100644 --- a/Tests/WalletConnectSharp.Web3Wallet.Tests/SignTests.cs +++ b/Tests/WalletConnectSharp.Web3Wallet.Tests/SignTests.cs @@ -82,7 +82,8 @@ public class ChainChangedEvent "eth_signTypedData" }, Accounts = TestAccounts, - Events = TestEvents + Events = TestEvents, + Chains = new[] { TestEthereumChain }, } } }; @@ -91,7 +92,8 @@ public class ChainChangedEvent { Methods = new[] { "eth_signTransaction", }, Accounts = new[] { TestAccounts[0] }, - Events = new[] { TestEvents[0] } + Events = new[] { TestEvents[0] }, + Chains = new[] { TestEthereumChain }, }; private static readonly Namespaces TestNamespaces = new Namespaces() @@ -183,7 +185,7 @@ public async Task DisposeAsync() } [Fact, Trait("Category", "unit")] - public async void TestShouldApproveSessionProposal() + public async Task TestShouldApproveSessionProposal() { TaskCompletionSource task1 = new TaskCompletionSource(); _wallet.SessionProposed += async (sender, @event) => @@ -207,7 +209,7 @@ await Task.WhenAll( } [Fact, Trait("Category", "unit")] - public async void TestShouldRejectSessionProposal() + public async Task TestShouldRejectSessionProposal() { var rejectionError = Error.FromErrorType(ErrorType.USER_DISCONNECTED); @@ -246,7 +248,7 @@ await Task.WhenAll( } [Fact, Trait("Category", "unit")] - public async void TestUpdateSession() + public async Task TestUpdateSession() { TaskCompletionSource task1 = new TaskCompletionSource(); _wallet.SessionProposed += async (sender, @event) => @@ -285,7 +287,7 @@ await Task.WhenAll( } [Fact, Trait("Category", "unit")] - public async void TestExtendSession() + public async Task TestExtendSession() { TaskCompletionSource task1 = new TaskCompletionSource(); _wallet.SessionProposed += async (sender, @event) => @@ -321,7 +323,7 @@ await Task.WhenAll( } [Fact, Trait("Category", "unit")] - public async void TestRespondToSessionRequest() + public async Task TestRespondToSessionRequest() { TaskCompletionSource task1 = new TaskCompletionSource(); _wallet.SessionProposed += async (sender, @event) => @@ -337,7 +339,8 @@ public async void TestRespondToSessionRequest() { Methods = TestNamespace.Methods, Events = TestNamespace.Events, - Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" } + Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" }, + Chains = new[] { TestEthereumChain }, } } }); @@ -399,7 +402,7 @@ await Task.WhenAll( } [Fact, Trait("Category", "unit")] - public async void TestWalletDisconnectFromSession() + public async Task TestWalletDisconnectFromSession() { TaskCompletionSource task1 = new TaskCompletionSource(); _wallet.SessionProposed += async (sender, @event) => @@ -415,7 +418,8 @@ public async void TestWalletDisconnectFromSession() { Methods = TestNamespace.Methods, Events = TestNamespace.Events, - Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" } + Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" }, + Chains = new [] { TestEthereumChain } } } }); @@ -446,7 +450,7 @@ await Task.WhenAll( } [Fact, Trait("Category", "unit")] - public async void TestDappDisconnectFromSession() + public async Task TestDappDisconnectFromSession() { TaskCompletionSource task1 = new TaskCompletionSource(); _wallet.SessionProposed += async (sender, @event) => @@ -462,7 +466,8 @@ public async void TestDappDisconnectFromSession() { Methods = TestNamespace.Methods, Events = TestNamespace.Events, - Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" } + Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" }, + Chains = new [] { TestEthereumChain } } } }); @@ -493,7 +498,7 @@ await Task.WhenAll( } [Fact, Trait("Category", "unit")] - public async void TestEmitSessionEvent() + public async Task TestEmitSessionEvent() { TaskCompletionSource task1 = new TaskCompletionSource(); _wallet.SessionProposed += async (sender, @event) => @@ -509,7 +514,8 @@ public async void TestEmitSessionEvent() { Methods = TestNamespace.Methods, Events = TestNamespace.Events, - Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" } + Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" }, + Chains = new [] { TestEthereumChain } } } }); @@ -534,7 +540,7 @@ await Task.WhenAll( }; TaskCompletionSource task2 = new TaskCompletionSource(); - _dapp.HandleEventMessageType(async (s, request) => + var handler = await _dapp.HandleEventMessageType(async (s, request) => { var eventData = request.Params.Event; var topic = request.Params.Topic; @@ -548,10 +554,12 @@ await Task.WhenAll( task2.Task, _wallet.EmitSessionEvent(session.Topic, sentData, TestRequiredNamespaces["eip155"].Chains[0]) ); + + handler.Dispose(); } [Fact, Trait("Category", "unit")] - public async void TestGetActiveSessions() + public async Task TestGetActiveSessions() { TaskCompletionSource task1 = new TaskCompletionSource(); _wallet.SessionProposed += async (sender, @event) => @@ -569,7 +577,8 @@ public async void TestGetActiveSessions() { Methods = TestNamespace.Methods, Events = TestNamespace.Events, - Accounts = new[] { $"{TestEthereumChain}:{WalletAddress}" } + Accounts = new[] { $"{TestEthereumChain}:{WalletAddress}" }, + Chains = new [] { TestEthereumChain } } } }); @@ -591,7 +600,7 @@ await Task.WhenAll( } [Fact, Trait("Category", "unit")] - public async void TestGetPendingSessionProposals() + public async Task TestGetPendingSessionProposals() { TaskCompletionSource task1 = new TaskCompletionSource(); _wallet.SessionProposed += (sender, @event) => @@ -610,7 +619,7 @@ await Task.WhenAll( } [Fact, Trait("Category", "unit")] - public async void TestGetPendingSessionRequests() + public async Task TestGetPendingSessionRequests() { TaskCompletionSource task1 = new TaskCompletionSource(); _wallet.SessionProposed += async (sender, @event) => @@ -628,7 +637,8 @@ public async void TestGetPendingSessionRequests() { Methods = TestNamespace.Methods, Events = TestNamespace.Events, - Accounts = new[] { $"{TestEthereumChain}:{WalletAddress}" } + Accounts = new[] { $"{TestEthereumChain}:{WalletAddress}" }, + Chains = new [] { TestEthereumChain } } } }); diff --git a/WalletConnectSharp.Auth/Controllers/AuthEngine.cs b/WalletConnectSharp.Auth/Controllers/AuthEngine.cs index e7ac368..1652a35 100644 --- a/WalletConnectSharp.Auth/Controllers/AuthEngine.cs +++ b/WalletConnectSharp.Auth/Controllers/AuthEngine.cs @@ -5,6 +5,7 @@ using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Common.Utils; using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Verify; using WalletConnectSharp.Crypto.Models; using WalletConnectSharp.Network.Models; @@ -25,6 +26,9 @@ public partial class AuthEngine : IAuthEngine $"{AUTH_CLIENT_PROTOCOL}@{AUTH_CLIENT_VERSION}:{AUTH_CLIENT_CONTEXT}"; public static readonly string AUTH_CLIENT_PUBLIC_KEY_NAME = $"{AUTH_CLIENT_STORAGE_PREFIX}:PUB_KEY"; + + private DisposeHandlerToken messageHandler; + protected bool Disposed; public string Name { @@ -57,11 +61,11 @@ public AuthEngine(IAuthClient client) Client = client; } - public void Init() + public async Task Init() { if (!initialized) { - RegisterRelayerEvents(); + await RegisterRelayerEvents(); this.initialized = true; } } @@ -234,10 +238,10 @@ protected async Task SetExpiry(string topic, long expiry) this.Client.Core.Expirer.Set(topic, expiry); } - private void RegisterRelayerEvents() + private async Task RegisterRelayerEvents() { // MessageHandler will handle all topic tracking - this.Client.Core.MessageHandler.HandleMessageType(OnAuthRequest, OnAuthResponse); + this.messageHandler = await this.Client.Core.MessageHandler.HandleMessageType(OnAuthRequest, OnAuthResponse); } private async Task OnAuthResponse(string topic, JsonRpcResponse response) @@ -392,5 +396,19 @@ private void IsInitialized() public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + this.messageHandler.Dispose(); + } + + Disposed = true; } } diff --git a/WalletConnectSharp.Auth/Interfaces/IAuthEngine.cs b/WalletConnectSharp.Auth/Interfaces/IAuthEngine.cs index 01df8d2..968fa54 100644 --- a/WalletConnectSharp.Auth/Interfaces/IAuthEngine.cs +++ b/WalletConnectSharp.Auth/Interfaces/IAuthEngine.cs @@ -9,7 +9,7 @@ public interface IAuthEngine : IModule IDictionary PendingRequests { get; } - void Init(); + Task Init(); Task Request(RequestParams @params, string topic = null); diff --git a/WalletConnectSharp.Auth/WalletConnectAuthClient.cs b/WalletConnectSharp.Auth/WalletConnectAuthClient.cs index eda24b9..f76d0b0 100644 --- a/WalletConnectSharp.Auth/WalletConnectAuthClient.cs +++ b/WalletConnectSharp.Auth/WalletConnectAuthClient.cs @@ -85,7 +85,7 @@ private async Task Initialize() await this.AuthKeys.Init(); await this.Requests.Init(); await this.PairingTopics.Init(); - this.Engine.Init(); + await this.Engine.Init(); } private WalletConnectAuthClient(AuthOptions options) diff --git a/WalletConnectSharp.Core/Controllers/MessageTracker.cs b/WalletConnectSharp.Core/Controllers/MessageTracker.cs index 2b629e3..242ce48 100644 --- a/WalletConnectSharp.Core/Controllers/MessageTracker.cs +++ b/WalletConnectSharp.Core/Controllers/MessageTracker.cs @@ -177,7 +177,9 @@ public async Task Delete(string topic) private async Task SetRelayerMessages(Dictionary messages) { - await _core.Storage.SetItem(StorageKey, messages); + // Clone dictionary for Storage, otherwise we'll be saving + // the reference + await _core.Storage.SetItem(StorageKey, new Dictionary(messages)); } private async Task> GetRelayerMessages() diff --git a/WalletConnectSharp.Core/Controllers/Pairing.cs b/WalletConnectSharp.Core/Controllers/Pairing.cs index 33cbe9f..f5017cf 100644 --- a/WalletConnectSharp.Core/Controllers/Pairing.cs +++ b/WalletConnectSharp.Core/Controllers/Pairing.cs @@ -6,6 +6,7 @@ using WalletConnectSharp.Common.Model.Relay; using WalletConnectSharp.Common.Utils; using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Expirer; using WalletConnectSharp.Core.Models.Pairing; using WalletConnectSharp.Core.Models.Pairing.Methods; @@ -52,6 +53,8 @@ public string Context public event EventHandler PairingDeleted; private EventHandlerMap> PairingPingResponseEvents = new(); + private DisposeHandlerToken pairingDeleteMessageHandler; + private DisposeHandlerToken pairingPingMessageHandler; /// /// Get the module that is handling the storage of @@ -95,7 +98,7 @@ public async Task Init() { await this.Store.Init(); await Cleanup(); - RegisterTypedMessages(); + await RegisterTypedMessages(); RegisterExpirerEvents(); this._initialized = true; } @@ -106,10 +109,10 @@ private void RegisterExpirerEvents() this.Core.Expirer.Expired += ExpiredCallback; } - private void RegisterTypedMessages() + private async Task RegisterTypedMessages() { - Core.MessageHandler.HandleMessageType(OnPairingDeleteRequest, null); - Core.MessageHandler.HandleMessageType(OnPairingPingRequest, OnPairingPingResponse); + this.pairingDeleteMessageHandler = await Core.MessageHandler.HandleMessageType(OnPairingDeleteRequest, null); + this.pairingPingMessageHandler = await Core.MessageHandler.HandleMessageType(OnPairingPingRequest, OnPairingPingResponse); } /// @@ -488,6 +491,8 @@ protected virtual void Dispose(bool disposing) if (disposing) { Store?.Dispose(); + this.pairingDeleteMessageHandler.Dispose(); + this.pairingPingMessageHandler.Dispose(); } Disposed = true; diff --git a/WalletConnectSharp.Core/Controllers/Relayer.cs b/WalletConnectSharp.Core/Controllers/Relayer.cs index cc09af9..f61681a 100644 --- a/WalletConnectSharp.Core/Controllers/Relayer.cs +++ b/WalletConnectSharp.Core/Controllers/Relayer.cs @@ -3,6 +3,7 @@ using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Common.Utils; using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Relay; using WalletConnectSharp.Core.Models.Subscriber; using WalletConnectSharp.Network; @@ -123,6 +124,8 @@ public bool TransportExplicitlyClosed private bool initialized; private bool reconnecting = false; protected bool Disposed; + private DateTime lastSyncTime; + private bool isSyncing; /// /// Create a new Relayer with the given RelayerOptions. @@ -146,8 +149,11 @@ public Relayer(RelayerOptions opts) ConnectionTimeout = opts.ConnectionTimeout; RelayUrlBuilder = opts.RelayUrlBuilder; + MessageFetchInterval = opts.MessageFetchInterval; } + public TimeSpan? MessageFetchInterval { get; set; } + /// /// Initialize this Relayer module. This will initialize all sub-modules /// and connect the backing IJsonRpcProvider. @@ -199,33 +205,85 @@ protected virtual Task BuildConnection(string url) protected virtual void RegisterProviderEventListeners() { - Provider.RawMessageReceived += (sender, s) => - { - OnProviderPayload(s); - }; + Provider.RawMessageReceived += OnProviderRawMessageReceived; + Provider.Connected += OnProviderConnected; + Provider.Disconnected += OnProviderDisconnected; + Provider.ErrorReceived += OnProviderErrorReceived; + + Core.HeartBeat.OnPulse += HeartBeatOnOnPulse; + } - Provider.Connected += (sender, connection) => + private async void HeartBeatOnOnPulse(object sender, EventArgs e) + { + var interval = this.MessageFetchInterval; + if (interval == null) return; + + var topics = Subscriber.Topics; + if (topics.Length <= 0 || isSyncing || !(DateTime.Now - lastSyncTime >= interval)) return; + + isSyncing = true; + bool hasMore; + do { - this.OnConnected?.Invoke(this, EventArgs.Empty); - }; + var request = new BatchFetchMessageRequest() + { + Topics = topics + }; - Provider.Disconnected += async (sender, args) => - { - this.OnDisconnected?.Invoke(this, EventArgs.Empty); + var response = + await Request(new RequestArguments() + { + Method = "irn_batchFetchMessages", + Params = request + }); - if (this._transportExplicitlyClosed) - return; + if (response?.Messages == null) + break; - // Attempt to reconnect after one second - await Task.Delay(1000); + await Task.WhenAll(response.Messages.Select(message => new MessageEvent() { Message = message.Message, Topic = message.Topic }) + .Select(OnMessageEvent)); - await RestartTransport(); - }; + hasMore = response.HasMore; + } while (hasMore); - Provider.ErrorReceived += (sender, args) => - { - this.OnErrored?.Invoke(this, args); - }; + isSyncing = false; + lastSyncTime = DateTime.Now; + } + + private void OnProviderErrorReceived(object sender, Exception e) + { + if (Disposed) return; + + this.OnErrored?.Invoke(this, e); + } + + private async void OnProviderDisconnected(object sender, EventArgs e) + { + if (Disposed) return; + + this.OnDisconnected?.Invoke(this, EventArgs.Empty); + + if (this._transportExplicitlyClosed) + return; + + // Attempt to reconnect after one second + await Task.Delay(1000); + + await RestartTransport(); + } + + private void OnProviderConnected(object sender, IJsonRpcConnection e) + { + if (Disposed) return; + + this.OnConnected?.Invoke(sender, EventArgs.Empty); + } + + private void OnProviderRawMessageReceived(object sender, string e) + { + if (Disposed) return; + + OnProviderPayload(e); } protected virtual void RegisterEventListeners() @@ -511,6 +569,15 @@ protected virtual void Dispose(bool disposing) Subscriber?.Dispose(); Publisher?.Dispose(); Messages?.Dispose(); + + // Un-listen to events + Provider.Connected -= OnProviderConnected; + Provider.Disconnected -= OnProviderDisconnected; + Provider.RawMessageReceived -= OnProviderRawMessageReceived; + Provider.ErrorReceived -= OnProviderErrorReceived; + + Core.HeartBeat.OnPulse -= HeartBeatOnOnPulse; + Provider?.Dispose(); } diff --git a/WalletConnectSharp.Core/Controllers/Subscriber.cs b/WalletConnectSharp.Core/Controllers/Subscriber.cs index 1454465..9504ad4 100644 --- a/WalletConnectSharp.Core/Controllers/Subscriber.cs +++ b/WalletConnectSharp.Core/Controllers/Subscriber.cs @@ -582,8 +582,7 @@ private void OnBatchSubscribe(ActiveSubscription[] subscriptions) if (subscriptions.Length == 0) return; foreach (var sub in subscriptions) { - SetSubscription(sub.Id, sub); - this.pending.Remove(sub.Topic); + OnSubscribe(sub.Id, sub); } } diff --git a/WalletConnectSharp.Core/Controllers/TypedMessageHandler.cs b/WalletConnectSharp.Core/Controllers/TypedMessageHandler.cs index fbaa626..94af826 100644 --- a/WalletConnectSharp.Core/Controllers/TypedMessageHandler.cs +++ b/WalletConnectSharp.Core/Controllers/TypedMessageHandler.cs @@ -4,6 +4,7 @@ using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Common.Utils; using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Relay; using WalletConnectSharp.Crypto.Models; using WalletConnectSharp.Network.Models; @@ -88,7 +89,7 @@ async void RelayerMessageCallback(object sender, MessageEvent e) /// The callback function to invoke when a response is received with the given response type /// The request type to trigger the requestCallback for /// The response type to trigger the responseCallback for - public async void HandleMessageType(Func, Task> requestCallback, + public async Task HandleMessageType(Func, Task> requestCallback, Func, Task> responseCallback) { var method = RpcMethodAttribute.MethodForType(); @@ -102,6 +103,8 @@ async void RequestCallback(object sender, MessageEvent e) var message = e.Message; var options = DecodeOptionForTopic(topic); + + if (options == null && !await this.Core.Crypto.HasKeys(topic)) return; var payload = await this.Core.Crypto.Decode>(topic, message, options); @@ -118,6 +121,8 @@ async void ResponseCallback(object sender, MessageEvent e) var message = e.Message; var options = DecodeOptionForTopic(topic); + + if (options == null && !await this.Core.Crypto.HasKeys(topic)) return; var rawResultPayload = await this.Core.Crypto.Decode(topic, message, options); @@ -174,6 +179,14 @@ async void InspectResponseRaw(object sender, DecodedMessageEvent e) // Handle response_raw in this context // This will allow us to examine response_raw in every typed context registered this.RawMessage += InspectResponseRaw; + + return new DisposeHandlerToken(() => + { + this.RawMessage -= InspectResponseRaw; + + messageEventHandlerMap[$"request_{method}"] -= RequestCallback; + messageEventHandlerMap[$"response_{method}"] -= ResponseCallback; + }); } /// @@ -298,8 +311,6 @@ public async Task SendRequest(string topic, T parameters, long? exp var payload = new JsonRpcRequest(method, parameters); - WCLogger.Log(JsonConvert.SerializeObject(payload)); - var message = await this.Core.Crypto.Encode(topic, payload, options); var opts = RpcRequestOptionsFromType(); diff --git a/WalletConnectSharp.Core/Interfaces/ICore.cs b/WalletConnectSharp.Core/Interfaces/ICore.cs index 5ba2545..e71f15c 100644 --- a/WalletConnectSharp.Core/Interfaces/ICore.cs +++ b/WalletConnectSharp.Core/Interfaces/ICore.cs @@ -59,7 +59,7 @@ public interface ICore : IModule /// SDK executions /// public IKeyValueStorage Storage { get; } - + /// /// The module this Core module is using. Use this for handling /// custom message types (request or response) and for sending messages (request, responses or errors) diff --git a/WalletConnectSharp.Core/Interfaces/ITypedMessageHandler.cs b/WalletConnectSharp.Core/Interfaces/ITypedMessageHandler.cs index b73400a..4b4e51c 100644 --- a/WalletConnectSharp.Core/Interfaces/ITypedMessageHandler.cs +++ b/WalletConnectSharp.Core/Interfaces/ITypedMessageHandler.cs @@ -1,4 +1,5 @@ using WalletConnectSharp.Common; +using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Relay; using WalletConnectSharp.Crypto.Models; using WalletConnectSharp.Network.Models; @@ -31,7 +32,7 @@ public interface ITypedMessageHandler : IModule /// The callback function to invoke when a response is received with the given response type /// The request type to trigger the requestCallback for /// The response type to trigger the responseCallback for - void HandleMessageType(Func, Task> requestCallback, + Task HandleMessageType(Func, Task> requestCallback, Func, Task> responseCallback); /// diff --git a/WalletConnectSharp.Core/Models/BatchFetchMessageRequest.cs b/WalletConnectSharp.Core/Models/BatchFetchMessageRequest.cs new file mode 100644 index 0000000..e404b8f --- /dev/null +++ b/WalletConnectSharp.Core/Models/BatchFetchMessageRequest.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Network.Models; + +namespace WalletConnectSharp.Core.Models; + +public class BatchFetchMessageRequest +{ + [JsonProperty("topics")] + public string[] Topics; +} diff --git a/WalletConnectSharp.Core/Models/BatchFetchMessagesResponse.cs b/WalletConnectSharp.Core/Models/BatchFetchMessagesResponse.cs new file mode 100644 index 0000000..e64bc19 --- /dev/null +++ b/WalletConnectSharp.Core/Models/BatchFetchMessagesResponse.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace WalletConnectSharp.Core.Models; + +public class BatchFetchMessagesResponse +{ + public class ReceivedMessage + { + [JsonProperty("topic")] + public string Topic; + + [JsonProperty("message")] + public string Message; + + [JsonProperty("publishedAt")] + public long PublishedAt; + + [JsonProperty("tag")] + public long Tag; + } + + [JsonProperty("messages")] + public ReceivedMessage[] Messages; + + [JsonProperty("hasMore")] + public bool HasMore; +} diff --git a/WalletConnectSharp.Core/Models/DisposeHandlerToken.cs b/WalletConnectSharp.Core/Models/DisposeHandlerToken.cs new file mode 100644 index 0000000..5671cec --- /dev/null +++ b/WalletConnectSharp.Core/Models/DisposeHandlerToken.cs @@ -0,0 +1,33 @@ +namespace WalletConnectSharp.Core.Models; + +public class DisposeHandlerToken : IDisposable +{ + private readonly Action _onDispose; + + public DisposeHandlerToken(Action onDispose) + { + if (onDispose == null) + throw new ArgumentException("onDispose must be non-null"); + this._onDispose = onDispose; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + this._onDispose(); + } + + Disposed = true; + } + + protected bool Disposed; +} diff --git a/WalletConnectSharp.Core/Models/MessageHandler/TypedEventHandler.cs b/WalletConnectSharp.Core/Models/MessageHandler/TypedEventHandler.cs index 157cb24..c9da69c 100644 --- a/WalletConnectSharp.Core/Models/MessageHandler/TypedEventHandler.cs +++ b/WalletConnectSharp.Core/Models/MessageHandler/TypedEventHandler.cs @@ -1,7 +1,9 @@ using Newtonsoft.Json; +using WalletConnectSharp.Common.Logging; using WalletConnectSharp.Common.Utils; using WalletConnectSharp.Core; using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Verify; using WalletConnectSharp.Network.Models; @@ -15,10 +17,11 @@ namespace WalletConnectSharp.Sign.Models /// /// The request type to filter for /// The response typ to filter for - public class TypedEventHandler + public class TypedEventHandler : IDisposable { protected static readonly Dictionary> Instances = new(); protected readonly ICore Ref; + protected List _disposeActions = new List(); protected Func, bool> RequestPredicate; protected Func, bool> ResponsePredicate; @@ -66,6 +69,7 @@ public delegate Task private event ResponseMethod _onResponse; private object _eventLock = new object(); private int _activeCount; + protected DisposeHandlerToken messageHandler; /// /// The event handler that triggers when a new request of type @@ -143,6 +147,8 @@ public event ResponseMethod OnResponse } } + public bool Disposed { get; protected set; } + protected TypedEventHandler(ICore engine) { Ref = engine; @@ -186,24 +192,33 @@ protected virtual TypedEventHandler BuildNew(ICore _ref, Func, bool> requestPredicate, Func, bool> responsePredicate) { - return new TypedEventHandler(_ref) + var wrappedRef = new TypedEventHandler(_ref) { RequestPredicate = requestPredicate, ResponsePredicate = responsePredicate }; + + _disposeActions.Add(wrappedRef.Dispose); + + return wrappedRef; } - protected virtual void Setup() + protected virtual async void Setup() { - Ref.MessageHandler.HandleMessageType(RequestCallback, ResponseCallback); + this.messageHandler = await Ref.MessageHandler.HandleMessageType(RequestCallback, ResponseCallback); } - protected virtual void Teardown() + protected virtual async void Teardown() { - // TODO Unsubscribe from HandleMessageType from above + if (this.messageHandler != null) + { + this.messageHandler.Dispose(); + this.messageHandler = null; + } } protected virtual Task ResponseCallback(string arg1, JsonRpcResponse arg2) { + WCLogger.Log($"Got generic response for type {typeof(TR)}"); var rea = new ResponseEventArgs(arg2, arg1); return ResponsePredicate != null && !ResponsePredicate(rea) ? Task.CompletedTask : _onResponse != null ? _onResponse(rea) : Task.CompletedTask; @@ -227,7 +242,23 @@ protected virtual async Task RequestCallback(string arg1, JsonRpcRequest arg2 if (RequestPredicate != null && !RequestPredicate(rea)) return; if (_onRequest == null) return; + var isDisposed = ((WalletConnectCore)Ref).Disposed; + + if (isDisposed) + { + WCLogger.Log($"Too late to process request {typeof(T)} in topic {arg1}, the WalletConnect instance {Ref.Context} was disposed before we could"); + return; + } + await _onRequest(rea); + + var nextIsDisposed = ((WalletConnectCore)Ref).Disposed; + + if (nextIsDisposed) + { + WCLogger.Log($"Too late to send a result for request {typeof(T)} in topic {arg1}, the WalletConnect instance {Ref.Context} was disposed before we could"); + return; + } if (rea.Error != null) { @@ -262,5 +293,34 @@ async Task VerifyContext(string hash, Metadata metadata) return context; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + var context = Ref.Context; + foreach (var action in _disposeActions) + { + action(); + } + + _disposeActions.Clear(); + + if (Instances.ContainsKey(context)) + Instances.Remove(context); + + Teardown(); + } + + Disposed = true; + } } } diff --git a/WalletConnectSharp.Core/Models/Relay/RelayerOptions.cs b/WalletConnectSharp.Core/Models/Relay/RelayerOptions.cs index 4fbfbba..023ac1d 100644 --- a/WalletConnectSharp.Core/Models/Relay/RelayerOptions.cs +++ b/WalletConnectSharp.Core/Models/Relay/RelayerOptions.cs @@ -33,6 +33,13 @@ public class RelayerOptions /// public TimeSpan? ConnectionTimeout = TimeSpan.FromSeconds(30); + /// + /// The interval at which the Relayer should request new (unsent) messages from the Relay server. If + /// this field is null, then the Relayer will never ask for new messages, so all new messages will only + /// come directly from Relay server + /// + public TimeSpan? MessageFetchInterval = TimeSpan.FromSeconds(15); + /// /// The module to use for building the Relay RPC URL. /// diff --git a/WalletConnectSharp.Core/WalletConnectCore.cs b/WalletConnectSharp.Core/WalletConnectCore.cs index e3d06af..5b5a78b 100644 --- a/WalletConnectSharp.Core/WalletConnectCore.cs +++ b/WalletConnectSharp.Core/WalletConnectCore.cs @@ -112,7 +112,7 @@ public string Context public CoreOptions Options { get; } - protected bool Disposed; + public bool Disposed { get; protected set; } /// /// Create a new Core with the given options. diff --git a/WalletConnectSharp.Sign/Controllers/AddressProvider.cs b/WalletConnectSharp.Sign/Controllers/AddressProvider.cs index 47b7383..dc546f9 100644 --- a/WalletConnectSharp.Sign/Controllers/AddressProvider.cs +++ b/WalletConnectSharp.Sign/Controllers/AddressProvider.cs @@ -1,4 +1,6 @@ -using WalletConnectSharp.Sign.Interfaces; +using Newtonsoft.Json; +using WalletConnectSharp.Common.Logging; +using WalletConnectSharp.Sign.Interfaces; using WalletConnectSharp.Sign.Models; using WalletConnectSharp.Sign.Models.Engine.Events; @@ -6,11 +8,20 @@ namespace WalletConnectSharp.Sign.Controllers; public class AddressProvider : IAddressProvider { + public struct DefaultData + { + public SessionStruct Session; + public string Namespace; + public string ChainId; + } + + public event EventHandler DefaultsLoaded; + public bool HasDefaultSession { get { - return !string.IsNullOrWhiteSpace(DefaultSession.Topic) && DefaultSession.RequiredNamespaces != null; + return !string.IsNullOrWhiteSpace(DefaultSession.Topic) && DefaultSession.Namespaces != null; } } @@ -30,9 +41,44 @@ public string Context } } - public SessionStruct DefaultSession { get; set; } - public string DefaultNamespace { get; set; } - public string DefaultChain { get; set; } + private DefaultData _state; + + public SessionStruct DefaultSession + { + get + { + return _state.Session; + } + set + { + _state.Session = value; + } + } + + public string DefaultNamespace + { + get + { + return _state.Namespace; + } + set + { + _state.Namespace = value; + } + } + + public string DefaultChain + { + get + { + return _state.ChainId; + } + set + { + _state.ChainId = value; + } + } + public ISession Sessions { get; private set; } private ISignClient _client; @@ -41,14 +87,34 @@ public AddressProvider(ISignClient client) { this._client = client; this.Sessions = client.Session; - + // set the first connected session to the default one client.SessionConnected += ClientOnSessionConnected; client.SessionDeleted += ClientOnSessionDeleted; client.SessionUpdated += ClientOnSessionUpdated; - client.SessionApproved += ClientOnSessionConnected; + client.SessionApproved += ClientOnSessionConnected; } - + + public virtual async Task SaveDefaults() + { + await _client.Core.Storage.SetItem($"{Context}-default-session", _state); + } + + public virtual async Task LoadDefaults() + { + var key = $"{Context}-default-session"; + if (await _client.Core.Storage.HasItem(key)) + { + _state = await _client.Core.Storage.GetItem(key); + } + else + { + _state = new DefaultData(); + } + + DefaultsLoaded?.Invoke(this, new DefaultsLoadingEventArgs(_state)); + } + private void ClientOnSessionUpdated(object sender, SessionEvent e) { if (DefaultSession.Topic == e.Topic) @@ -75,45 +141,80 @@ private void ClientOnSessionConnected(object sender, SessionStruct e) } } - private void UpdateDefaultChainAndNamespace() + private async void UpdateDefaultChainAndNamespace() { - if (HasDefaultSession) + try { - var currentDefault = DefaultNamespace; - if (currentDefault != null && DefaultSession.RequiredNamespaces.ContainsKey(currentDefault)) + if (HasDefaultSession) { - // DefaultNamespace is still valid - var currentChain = DefaultChain; - if (currentChain == null || - DefaultSession.RequiredNamespaces[DefaultNamespace].Chains.Contains(currentChain)) + var currentDefault = DefaultNamespace; + if (currentDefault != null && DefaultSession.Namespaces.ContainsKey(currentDefault)) { - // DefaultChain is still valid + // 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; } - DefaultChain = DefaultSession.RequiredNamespaces[DefaultNamespace].Chains[0]; - return; - } - - // DefaultNamespace is null or not found in RequiredNamespaces, update it - DefaultNamespace = DefaultSession.RequiredNamespaces.OrderedKeys.FirstOrDefault(); - if (DefaultNamespace != null) - { - DefaultChain = DefaultSession.RequiredNamespaces[DefaultNamespace].Chains[0]; - } - else - { - // TODO The Keys property is unordered! Maybe this needs to be updated - DefaultNamespace = DefaultSession.RequiredNamespaces.Keys.FirstOrDefault(); + // DefaultNamespace is null or not found in current available spaces, update it + DefaultNamespace = DefaultSession.Namespaces.Keys.FirstOrDefault(); if (DefaultNamespace != null) { - DefaultChain = DefaultSession.RequiredNamespaces[DefaultNamespace].Chains[0]; + 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]; + } + } + 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"); + } + } } } + else + { + DefaultNamespace = null; + } + + await SaveDefaults(); } - else + catch (Exception e) { - DefaultNamespace = null; + WCLogger.LogError(e); + throw; } } @@ -126,6 +227,11 @@ public Caip25Address CurrentAddress(string @namespace = null, SessionStruct sess return session.CurrentAddress(@namespace); } + public async Task Init() + { + await this.LoadDefaults(); + } + public Caip25Address[] AllAddresses(string @namespace = null, SessionStruct session = default) { @namespace ??= DefaultNamespace; @@ -140,7 +246,7 @@ public void Dispose() _client.SessionConnected -= ClientOnSessionConnected; _client.SessionDeleted -= ClientOnSessionDeleted; _client.SessionUpdated -= ClientOnSessionUpdated; - _client.SessionApproved -= ClientOnSessionConnected; + _client.SessionApproved -= ClientOnSessionConnected; _client = null; Sessions = null; diff --git a/WalletConnectSharp.Sign/Engine.cs b/WalletConnectSharp.Sign/Engine.cs index 8006a5d..8b8d328 100644 --- a/WalletConnectSharp.Sign/Engine.cs +++ b/WalletConnectSharp.Sign/Engine.cs @@ -7,6 +7,7 @@ using WalletConnectSharp.Common.Model.Relay; using WalletConnectSharp.Common.Utils; using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Pairing; using WalletConnectSharp.Core.Models.Relay; using WalletConnectSharp.Core.Models.Verify; @@ -31,6 +32,7 @@ public partial class Engine : IEnginePrivate, IEngine, IModule private const int KeyLength = 32; private bool _initialized = false; + private Dictionary _disposeActions = new Dictionary(); /// /// The using this Engine @@ -42,6 +44,7 @@ public partial class Engine : IEnginePrivate, IEngine, IModule private ITypedMessageHandler MessageHandler => Client.Core.MessageHandler; private EventHandlerMap> sessionEventsHandlerMap = new(); + private List messageDisposeHandlers; /// /// The name of this Engine module @@ -84,7 +87,7 @@ public async Task Init() SetupEvents(); await PrivateThis.Cleanup(); - this.RegisterRelayerEvents(); + await this.RegisterRelayerEvents(); this.RegisterExpirerEvents(); this._initialized = true; } @@ -107,20 +110,33 @@ private void RegisterExpirerEvents() this.Client.Core.Expirer.Expired += ExpiredCallback; } - private void RegisterRelayerEvents() + private async Task RegisterRelayerEvents() { + this.messageDisposeHandlers = new List(); + // Register all Request Types - MessageHandler.HandleMessageType( - PrivateThis.OnSessionProposeRequest, PrivateThis.OnSessionProposeResponse); - MessageHandler.HandleMessageType(PrivateThis.OnSessionSettleRequest, - PrivateThis.OnSessionSettleResponse); - MessageHandler.HandleMessageType(PrivateThis.OnSessionUpdateRequest, - PrivateThis.OnSessionUpdateResponse); - MessageHandler.HandleMessageType(PrivateThis.OnSessionExtendRequest, - PrivateThis.OnSessionExtendResponse); - MessageHandler.HandleMessageType(PrivateThis.OnSessionDeleteRequest, null); - MessageHandler.HandleMessageType(PrivateThis.OnSessionPingRequest, - PrivateThis.OnSessionPingResponse); + this.messageDisposeHandlers.Add( + await MessageHandler.HandleMessageType( + PrivateThis.OnSessionProposeRequest, PrivateThis.OnSessionProposeResponse)); + + this.messageDisposeHandlers.Add(await MessageHandler.HandleMessageType( + PrivateThis.OnSessionSettleRequest, + PrivateThis.OnSessionSettleResponse)); + + this.messageDisposeHandlers.Add(await MessageHandler.HandleMessageType( + PrivateThis.OnSessionUpdateRequest, + PrivateThis.OnSessionUpdateResponse)); + + this.messageDisposeHandlers.Add(await MessageHandler.HandleMessageType( + PrivateThis.OnSessionExtendRequest, + PrivateThis.OnSessionExtendResponse)); + + this.messageDisposeHandlers.Add( + await MessageHandler.HandleMessageType(PrivateThis.OnSessionDeleteRequest, null)); + + this.messageDisposeHandlers.Add(await MessageHandler.HandleMessageType( + PrivateThis.OnSessionPingRequest, + PrivateThis.OnSessionPingResponse)); } /// @@ -224,7 +240,11 @@ private void RegisterRelayerEvents() /// The managing events for the given types T, TR public TypedEventHandler SessionRequestEvents() { - return SessionRequestEventHandler.GetInstance(Client.Core, PrivateThis); + var uniqueKey = typeof(T).FullName + "--" + typeof(TR).FullName; + var instance = SessionRequestEventHandler.GetInstance(Client.Core, PrivateThis); + if (!_disposeActions.ContainsKey(uniqueKey)) + _disposeActions.Add(uniqueKey, () => instance.Dispose()); + return instance; } /// @@ -235,11 +255,11 @@ public TypedEventHandler SessionRequestEvents() /// The callback function to invoke when a response is received with the given response type /// The request type to trigger the requestCallback for. Will be wrapped in /// The response type to trigger the responseCallback for - public void HandleSessionRequestMessageType( + public Task HandleSessionRequestMessageType( Func>, Task> requestCallback, Func, Task> responseCallback) { - Client.Core.MessageHandler.HandleMessageType(requestCallback, responseCallback); + return Client.Core.MessageHandler.HandleMessageType(requestCallback, responseCallback); } /// @@ -249,10 +269,11 @@ public void HandleSessionRequestMessageType( /// The callback function to invoke when a request is received with the given request type /// The callback function to invoke when a response is received with the given response type /// The request type to trigger the requestCallback for. Will be wrapped in - public void HandleEventMessageType(Func>, Task> requestCallback, + public Task HandleEventMessageType( + Func>, Task> requestCallback, Func, Task> responseCallback) { - Client.Core.MessageHandler.HandleMessageType(requestCallback, responseCallback); + return Client.Core.MessageHandler.HandleMessageType(requestCallback, responseCallback); } public Task UpdateSession(Namespaces namespaces) @@ -367,8 +388,6 @@ public async Task Connect(ConnectOptions options) var pairing = this.Client.Core.Pairing.Store.Get(topic); if (pairing.Active != null) active = pairing.Active.Value; - - WCLogger.Log($"Loaded pairing for {topic}"); } if (string.IsNullOrEmpty(topic) || !active) @@ -376,8 +395,6 @@ public async Task Connect(ConnectOptions options) var CreatePairing = await this.Client.Core.Pairing.Create(); topic = CreatePairing.Topic; uri = CreatePairing.Uri; - - WCLogger.Log($"Created pairing for new topic: {topic}"); } var publicKey = await this.Client.Core.Crypto.GenerateKeyPair(); @@ -392,8 +409,6 @@ public async Task Connect(ConnectOptions options) SessionProperties = sessionProperties, }; - WCLogger.Log($"Created public key pair"); - TaskCompletionSource approvalTask = new TaskCompletionSource(); this.SessionConnected += async (sender, session) => { @@ -683,8 +698,10 @@ public async Task Request(string topic, T data, string chainId = null if (string.IsNullOrWhiteSpace(chainId)) { var sessionData = Client.Session.Get(topic); - var firstRequiredNamespace = sessionData.RequiredNamespaces.OrderedKeys[0]; - defaultChainId = sessionData.RequiredNamespaces[firstRequiredNamespace].Chains[0]; + var defaultNamespace = Client.AddressProvider.DefaultNamespace ?? + sessionData.Namespaces.Keys.FirstOrDefault(); + defaultChainId = Client.AddressProvider.DefaultChain ?? + sessionData.Namespaces[defaultNamespace].Chains[0]; } else { @@ -868,7 +885,12 @@ protected virtual void Dispose(bool disposing) if (disposing) { - Client?.Dispose(); + foreach (var action in _disposeActions.Values) + { + action(); + } + + _disposeActions.Clear(); } Disposed = true; diff --git a/WalletConnectSharp.Sign/Interfaces/IAddressProvider.cs b/WalletConnectSharp.Sign/Interfaces/IAddressProvider.cs index c5d107f..2ce881a 100644 --- a/WalletConnectSharp.Sign/Interfaces/IAddressProvider.cs +++ b/WalletConnectSharp.Sign/Interfaces/IAddressProvider.cs @@ -5,6 +5,8 @@ namespace WalletConnectSharp.Sign.Interfaces; public interface IAddressProvider : IModule { + event EventHandler DefaultsLoaded; + bool HasDefaultSession { get; } SessionStruct DefaultSession { get; set; } @@ -17,5 +19,7 @@ public interface IAddressProvider : IModule Caip25Address CurrentAddress( string chain = null, SessionStruct session = default); + Task Init(); + Caip25Address[] AllAddresses(string chain = null, SessionStruct session = default); } diff --git a/WalletConnectSharp.Sign/Interfaces/IEngine.cs b/WalletConnectSharp.Sign/Interfaces/IEngine.cs index 7f9ae03..35c3977 100644 --- a/WalletConnectSharp.Sign/Interfaces/IEngine.cs +++ b/WalletConnectSharp.Sign/Interfaces/IEngine.cs @@ -15,7 +15,7 @@ namespace WalletConnectSharp.Sign.Interfaces /// is an sub-type of and represents the actual Engine. This is /// different than the Sign client. /// - public interface IEngine : IEngineAPI + public interface IEngine : IEngineAPI, IDisposable { /// /// The this Engine is using diff --git a/WalletConnectSharp.Sign/Interfaces/IEngineAPI.cs b/WalletConnectSharp.Sign/Interfaces/IEngineAPI.cs index fdb63f0..9fbd1c2 100644 --- a/WalletConnectSharp.Sign/Interfaces/IEngineAPI.cs +++ b/WalletConnectSharp.Sign/Interfaces/IEngineAPI.cs @@ -1,3 +1,4 @@ +using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Pairing; using WalletConnectSharp.Core.Models.Relay; using WalletConnectSharp.Network.Models; @@ -258,7 +259,7 @@ public interface IEngineAPI /// All sessions that have a namespace that match the given SessionStruct[] Find(RequiredNamespaces requiredNamespaces); - void HandleEventMessageType(Func>, Task> requestCallback, + Task HandleEventMessageType(Func>, Task> requestCallback, Func, Task> responseCallback); /// diff --git a/WalletConnectSharp.Sign/Models/DefaultsLoadingEventArgs.cs b/WalletConnectSharp.Sign/Models/DefaultsLoadingEventArgs.cs new file mode 100644 index 0000000..c93c114 --- /dev/null +++ b/WalletConnectSharp.Sign/Models/DefaultsLoadingEventArgs.cs @@ -0,0 +1,13 @@ +using WalletConnectSharp.Sign.Controllers; + +namespace WalletConnectSharp.Sign.Models; + +public class DefaultsLoadingEventArgs : EventArgs +{ + public DefaultsLoadingEventArgs(in AddressProvider.DefaultData defaults) + { + Defaults = defaults; + } + + public AddressProvider.DefaultData Defaults { get; } +} diff --git a/WalletConnectSharp.Sign/Models/Namespace.cs b/WalletConnectSharp.Sign/Models/Namespace.cs index 1e1a345..1dbd6e0 100644 --- a/WalletConnectSharp.Sign/Models/Namespace.cs +++ b/WalletConnectSharp.Sign/Models/Namespace.cs @@ -63,10 +63,15 @@ public Namespace WithAccount(string account) Accounts = Accounts.Append(account).ToArray(); return this; } + + protected bool ArrayEquals(string[] a, string[] b) + { + return a.Length == b.Length && a.All(b.Contains) && b.All(a.Contains); + } protected bool Equals(Namespace other) { - return Equals(Accounts, other.Accounts) && Equals(Methods, other.Methods) && Equals(Events, other.Events); + return ArrayEquals(Accounts, other.Accounts) && ArrayEquals(Methods, other.Methods) && ArrayEquals(Events, other.Events); } public override bool Equals(object obj) diff --git a/WalletConnectSharp.Sign/Models/Namespaces.cs b/WalletConnectSharp.Sign/Models/Namespaces.cs index a154d64..b2c6d31 100644 --- a/WalletConnectSharp.Sign/Models/Namespaces.cs +++ b/WalletConnectSharp.Sign/Models/Namespaces.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; -using WalletConnectSharp.Common.Utils; - namespace WalletConnectSharp.Sign.Models { /// @@ -10,55 +7,24 @@ namespace WalletConnectSharp.Sign.Models /// namespace: [-a-z0-9]{3,8} /// reference: [-_a-zA-Z0-9]{1,32} /// - public class Namespaces : Dictionary, IEquatable + public class Namespaces : SortedDictionary { public Namespaces() : base() { } public Namespaces(Namespaces namespaces) : base(namespaces) { - } public Namespaces(RequiredNamespaces requiredNamespaces) { WithProposedNamespaces(requiredNamespaces); } - + public Namespaces(Dictionary proposedNamespaces) { WithProposedNamespaces(proposedNamespaces); } - public bool Equals(Namespaces other) - { - return new DictionaryComparer(Namespace.NamespaceComparer).Equals(this, other); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != this.GetType()) - { - return false; - } - - return Equals((Namespaces)obj); - } - - public override int GetHashCode() - { - throw new NotImplementedException(); - } - public Namespaces WithNamespace(string chainNamespace, Namespace nm) { Add(chainNamespace, nm); @@ -69,14 +35,11 @@ public Namespace At(string chainNamespace) { return this[chainNamespace]; } - - public Namespaces WithProposedNamespaces(Dictionary proposedNamespaces) + + public Namespaces WithProposedNamespaces(IDictionary proposedNamespaces) { - foreach (var pair in proposedNamespaces) + foreach (var (chainNamespace, requiredNamespace) in proposedNamespaces) { - var chainNamespace = pair.Key; - var requiredNamespace = pair.Value; - Add(chainNamespace, new Namespace(requiredNamespace)); } diff --git a/WalletConnectSharp.Sign/Models/ProposalStruct.cs b/WalletConnectSharp.Sign/Models/ProposalStruct.cs index 43d129e..714228c 100644 --- a/WalletConnectSharp.Sign/Models/ProposalStruct.cs +++ b/WalletConnectSharp.Sign/Models/ProposalStruct.cs @@ -121,7 +121,8 @@ public ApproveParams ApproveProposal(string[] approvedAccounts, ProtocolOptions { Accounts = allAccounts, Events = rn.Events, - Methods = rn.Methods + Methods = rn.Methods, + Chains = rn.Chains, }); } if (OptionalNamespaces != null) @@ -135,7 +136,8 @@ public ApproveParams ApproveProposal(string[] approvedAccounts, ProtocolOptions { Accounts = allAccounts, Events = rn.Events, - Methods = rn.Methods + Methods = rn.Methods, + Chains = rn.Chains, }); } diff --git a/WalletConnectSharp.Sign/Models/ProposedNamespace.cs b/WalletConnectSharp.Sign/Models/ProposedNamespace.cs index dd1a0f4..f2611db 100644 --- a/WalletConnectSharp.Sign/Models/ProposedNamespace.cs +++ b/WalletConnectSharp.Sign/Models/ProposedNamespace.cs @@ -75,9 +75,14 @@ public Namespace WithAccount(string account) return new Namespace(this).WithAccount(account); } + protected bool ArrayEquals(string[] a, string[] b) + { + return a.Length == b.Length && a.All(b.Contains) && b.All(a.Contains); + } + protected bool Equals(ProposedNamespace other) { - return Equals(Chains, other.Chains) && Equals(Methods, other.Methods) && Equals(Events, other.Events); + return ArrayEquals(Chains, other.Chains) && ArrayEquals(Methods, other.Methods) && ArrayEquals(Events, other.Events); } public override bool Equals(object obj) diff --git a/WalletConnectSharp.Sign/Models/RequiredNamespaces.cs b/WalletConnectSharp.Sign/Models/RequiredNamespaces.cs index bbd78d4..0b53ed6 100644 --- a/WalletConnectSharp.Sign/Models/RequiredNamespaces.cs +++ b/WalletConnectSharp.Sign/Models/RequiredNamespaces.cs @@ -10,65 +10,8 @@ namespace WalletConnectSharp.Sign.Models /// namespace: [-a-z0-9]{3,8} /// reference: [-_a-zA-Z0-9]{1,32} /// - public class RequiredNamespaces : Dictionary, IEquatable + public class RequiredNamespaces : SortedDictionary { - private List _orderedKeys = new(); - - public List OrderedKeys => _orderedKeys; - - public new void Add(string key, ProposedNamespace value) - { - base.Add(key, value); - _orderedKeys.Add(key); - } - - public new void Remove(string key) - { - base.Remove(key); - _orderedKeys.Remove(key); - } - - public bool Equals(RequiredNamespaces other) - { - return new DictionaryComparer(ProposedNamespace.RequiredNamespaceComparer).Equals(this, other); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != this.GetType()) - { - return false; - } - - return Equals((RequiredNamespaces)obj); - } - - public override int GetHashCode() - { - throw new NotImplementedException(); - } - - - public bool Equals(RequiredNamespaces x, RequiredNamespaces y) - { - return new DictionaryComparer(ProposedNamespace.RequiredNamespaceComparer).Equals(x, y); - } - - public int GetHashCode(RequiredNamespaces obj) - { - throw new NotImplementedException(); - } - public RequiredNamespaces WithProposedNamespace(string chainNamespace, ProposedNamespace proposedNamespace) { Add(chainNamespace, proposedNamespace); diff --git a/WalletConnectSharp.Sign/Models/SessionRequestEventHandler.cs b/WalletConnectSharp.Sign/Models/SessionRequestEventHandler.cs index 5fd7de1..91e1ec3 100644 --- a/WalletConnectSharp.Sign/Models/SessionRequestEventHandler.cs +++ b/WalletConnectSharp.Sign/Models/SessionRequestEventHandler.cs @@ -1,4 +1,6 @@ -using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Common.Logging; +using WalletConnectSharp.Common.Model.Errors; +using WalletConnectSharp.Core.Interfaces; using WalletConnectSharp.Network.Models; using WalletConnectSharp.Sign.Interfaces; using WalletConnectSharp.Sign.Models.Engine.Methods; @@ -14,7 +16,7 @@ namespace WalletConnectSharp.Sign.Models public class SessionRequestEventHandler : TypedEventHandler { private readonly IEnginePrivate _enginePrivate; - + /// /// Get a singleton instance of this class for the given context. The context /// string of the given will be used to determine the singleton instance to @@ -46,10 +48,14 @@ protected SessionRequestEventHandler(ICore engine, IEnginePrivate enginePrivate) protected override TypedEventHandler BuildNew(ICore @ref, Func, bool> requestPredicate, Func, bool> responsePredicate) { - return new SessionRequestEventHandler(@ref, _enginePrivate) + var instance = new SessionRequestEventHandler(@ref, _enginePrivate) { RequestPredicate = requestPredicate, ResponsePredicate = responsePredicate }; + + _disposeActions.Add(instance.Dispose); + + return instance; } protected override void Setup() @@ -58,10 +64,18 @@ protected override void Setup() wrappedRef.OnRequest += WrappedRefOnOnRequest; wrappedRef.OnResponse += WrappedRefOnOnResponse; + + _disposeActions.Add(() => + { + wrappedRef.OnRequest -= WrappedRefOnOnRequest; + wrappedRef.OnResponse -= WrappedRefOnOnResponse; + wrappedRef.Dispose(); + }); } private Task WrappedRefOnOnResponse(ResponseEventArgs e) { + WCLogger.Log($"Got response for type {typeof(TR)}"); return base.ResponseCallback(e.Topic, e.Response); } @@ -96,6 +110,8 @@ await _enginePrivate.SetPendingSessionRequest(new PendingRequestStruct() }); await base.RequestCallback(e.Topic, sessionRequest); + + await _enginePrivate.DeletePendingSessionRequest(e.Request.Id, Error.FromErrorType(ErrorType.GENERIC)); } } } diff --git a/WalletConnectSharp.Sign/WalletConnectSignClient.cs b/WalletConnectSharp.Sign/WalletConnectSignClient.cs index 6a793ee..8665fd2 100644 --- a/WalletConnectSharp.Sign/WalletConnectSignClient.cs +++ b/WalletConnectSharp.Sign/WalletConnectSignClient.cs @@ -2,6 +2,7 @@ using WalletConnectSharp.Core; using WalletConnectSharp.Core.Controllers; using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Pairing; using WalletConnectSharp.Core.Models.Relay; using WalletConnectSharp.Crypto; @@ -427,10 +428,10 @@ public SessionStruct[] Find(RequiredNamespaces requiredNamespaces) return Engine.Find(requiredNamespaces); } - public void HandleEventMessageType(Func>, Task> requestCallback, + public Task HandleEventMessageType(Func>, Task> requestCallback, Func, Task> responseCallback) { - this.Engine.HandleEventMessageType(requestCallback, responseCallback); + return this.Engine.HandleEventMessageType(requestCallback, responseCallback); } public Task UpdateSession(Namespaces namespaces) @@ -476,6 +477,7 @@ private async Task Initialize() await Session.Init(); await Proposal.Init(); await Engine.Init(); + await AddressProvider.Init(); } public void Dispose() @@ -495,6 +497,7 @@ protected virtual void Dispose(bool disposing) Session?.Dispose(); Proposal?.Dispose(); PendingRequests?.Dispose(); + Engine?.Dispose(); } Disposed = true;