diff --git a/Core Modules/WalletConnectSharp.Crypto/Crypto.cs b/Core Modules/WalletConnectSharp.Crypto/Crypto.cs index 0e083e6..5925fa7 100644 --- a/Core Modules/WalletConnectSharp.Crypto/Crypto.cs +++ b/Core Modules/WalletConnectSharp.Crypto/Crypto.cs @@ -30,7 +30,7 @@ namespace WalletConnectSharp.Crypto public class Crypto : ICrypto { private readonly string CRYPTO_CLIENT_SEED = $"client_ed25519_seed"; - + private const string MULTICODEC_ED25519_ENCODING = "base58btc"; private const string MULTICODEC_ED25519_BASE = "z"; private const string MULTICODEC_ED25519_HEADER = "K36"; @@ -48,7 +48,7 @@ public class Crypto : ICrypto private const int TYPE_LENGTH = 1; private const int IV_LENGTH = 12; private const int KEY_LENGTH = 32; - + /// /// The name of the crypto module /// @@ -71,12 +71,12 @@ public string Context return "walletconnectsharp"; } } - + /// /// The current KeyChain this crypto module instance is using /// public IKeyChain KeyChain { get; private set; } - + /// /// The current storage module this crypto module instance is using /// @@ -84,6 +84,7 @@ public string Context private bool _initialized; private bool _newStorage; + protected bool Disposed; /// /// Create a new instance of the crypto module, with a given storage module. @@ -97,7 +98,7 @@ public Crypto(IKeyValueStorage storage) this.KeyChain = new KeyChain(storage); this.Storage = storage; } - + /// /// Create a new instance of the crypto module, with a given keychain. /// @@ -128,7 +129,7 @@ public async Task Init() { if (_newStorage) await this.Storage.Init(); - + await this.KeyChain.Init(); this._initialized = true; } @@ -159,7 +160,7 @@ public Task GenerateKeyPair() var options = new KeyGenerationParameters(SecureRandom.GetInstance("SHA256PRNG"), 1); X25519KeyPairGenerator generator = new X25519KeyPairGenerator(); generator.Init(options); - + var keypair = generator.GenerateKeyPair(); var publicKeyData = keypair.Public as X25519PublicKeyParameters; var privateKeyData = keypair.Private as X25519PrivateKeyParameters; @@ -281,7 +282,7 @@ public Task Encrypt(EncryptParams @params) var typeRaw = Bases.Base10.Decode($"{@params.Type}"); var iv = @params.Iv; - + byte[] rawIv; if (iv == null) { @@ -308,7 +309,7 @@ public Task Encrypt(EncryptParams @params) { byte[] temp = new byte[encoded.Length * 3]; int len = aead.ProcessBytes(encoded, 0, encoded.Length, temp, 0); - + if (len > 0) { encryptedStream.Write(temp, 0, len); @@ -327,7 +328,7 @@ public Task Encrypt(EncryptParams @params) { if (senderPublicKey == null) throw new ArgumentException("Missing sender public key for type1 envelope"); - + return Task.FromResult(Convert.ToBase64String( typeRaw.Concat(senderPublicKey).Concat(rawIv).Concat(encrypted).ToArray() )); @@ -380,10 +381,7 @@ public async Task Encode(string topic, IJsonRpcPayload payload, EncodeOp var message = JsonConvert.SerializeObject(payload); var results = await Encrypt(new EncryptParams() { - Message = message, - Type = type, - SenderPublicKey = senderPublicKey, - SymKey = symKey + Message = message, Type = type, SenderPublicKey = senderPublicKey, SymKey = symKey }); return results; @@ -398,7 +396,8 @@ public async Task Encode(string topic, IJsonRpcPayload payload, EncodeOp /// (optional) Decoding options /// The type of the IJsonRpcPayload to convert the encoded Json to /// The decoded, decrypted and deserialized object of type T from an async task - public async Task Decode(string topic, string encoded, DecodeOptions options = null) where T : IJsonRpcPayload + public async Task Decode(string topic, string encoded, DecodeOptions options = null) + where T : IJsonRpcPayload { this.IsInitialized(); var @params = ValidateDecoding(encoded, options); @@ -430,7 +429,8 @@ private EncodingParams Deserialize(string encoded) var slice3 = slice2 + IV_LENGTH; var senderPublicKey = new ArraySegment(bytes, slice1, KEY_LENGTH); var iv = new ArraySegment(bytes, slice2, IV_LENGTH); - var @sealed = new ArraySegment(bytes, slice3, bytes.Length - (TYPE_LENGTH + KEY_LENGTH + IV_LENGTH)); + var @sealed = + new ArraySegment(bytes, slice3, bytes.Length - (TYPE_LENGTH + KEY_LENGTH + IV_LENGTH)); return new EncodingParams() { @@ -446,12 +446,7 @@ private EncodingParams Deserialize(string encoded) var iv = new ArraySegment(bytes, slice1, IV_LENGTH); var @sealed = new ArraySegment(bytes, slice2, bytes.Length - (IV_LENGTH + TYPE_LENGTH)); - return new EncodingParams() - { - Type = typeRaw, - Sealed = @sealed.ToArray(), - Iv = iv.ToArray() - }; + return new EncodingParams() { Type = typeRaw, Sealed = @sealed.ToArray(), Iv = iv.ToArray() }; } } @@ -493,12 +488,7 @@ public async Task SignJwt(string aud) signer.BlockUpdate(data, 0, data.Length); var signature = signer.GenerateSignature(); - return EncodeJwt(new IridiumJWTSigned() - { - Header = header, - Payload = payload, - Signature = signature - }); + return EncodeJwt(new IridiumJWTSigned() { Header = header, Payload = payload, Signature = signature }); } /// @@ -516,8 +506,8 @@ public async Task GetClientId() private string EncodeJwt(IridiumJWTSigned data) { - return string.Join(JWT_DELIMITER, - EncodeJson(data.Header), + return string.Join(JWT_DELIMITER, + EncodeJson(data.Header), EncodeJson(data.Payload), EncodeSig(data.Signature) ); @@ -549,7 +539,7 @@ private string EncodeIss(Ed25519PublicKeyParameters publicKey) private Ed25519PrivateKeyParameters KeypairFromSeed(byte[] seed) { return new Ed25519PrivateKeyParameters(seed); - + /*var options = new KeyCreationParameters() { ExportPolicy = KeyExportPolicies.AllowPlaintextExport @@ -578,10 +568,8 @@ private void IsInitialized() { if (!this._initialized) { - throw WalletConnectException.FromType(ErrorType.NOT_INITIALIZED, new Dictionary() - { - { "Name", Name } - }); + throw WalletConnectException.FromType(ErrorType.NOT_INITIALIZED, + new Dictionary() { { "Name", Name } }); } } @@ -616,7 +604,7 @@ private byte[] DeriveSharedKey(string privateKeyA, string publicKeyB) { var keyB = PublicKey.Import(KeyAgreementAlgorithm.X25519, publicKeyB.HexToByteArray(), KeyBlobFormat.RawPublicKey); - + var options = new SharedSecretCreationParameters { ExportPolicy = KeyExportPolicies.AllowPlaintextArchiving @@ -632,7 +620,7 @@ private byte[] DeriveSymmetricKey(byte[] secretKey) generator.Init(new HkdfParameters(secretKey, Array.Empty(), Array.Empty())); byte[] key = new byte[32]; - generator.GenerateBytes(key, 0,32); + generator.GenerateBytes(key, 0, 32); return key; } @@ -644,14 +632,14 @@ private string DeserializeAndDecrypt(string symKey, string encoded) var iv = param.Iv; var type = int.Parse(Bases.Base10.Encode(param.Type)); var isType1 = type == TYPE_1; - + var aead = new ChaCha20Poly1305(); aead.Init(false, new ParametersWithIV(new KeyParameter(symKey.HexToByteArray()), iv)); using MemoryStream rawDecrypted = new MemoryStream(); byte[] temp = new byte[@sealed.Length]; int len = aead.ProcessBytes(@sealed, 0, @sealed.Length, temp, 0); - + if (len > 0) { rawDecrypted.Write(temp, 0, len); @@ -663,7 +651,7 @@ private string DeserializeAndDecrypt(string symKey, string encoded) { rawDecrypted.Write(temp, 0, len); } - + return Encoding.UTF8.GetString(rawDecrypted.ToArray()); } @@ -687,7 +675,21 @@ private async Task GetClientSeed() public void Dispose() { - KeyChain?.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) + return; + + if (disposing) + { + KeyChain?.Dispose(); + } + + Disposed = true; } } } diff --git a/Core Modules/WalletConnectSharp.Network.Websocket/WebsocketConnection.cs b/Core Modules/WalletConnectSharp.Network.Websocket/WebsocketConnection.cs index 74fc224..8ff0223 100644 --- a/Core Modules/WalletConnectSharp.Network.Websocket/WebsocketConnection.cs +++ b/Core Modules/WalletConnectSharp.Network.Websocket/WebsocketConnection.cs @@ -16,6 +16,7 @@ public class WebsocketConnection : IJsonRpcConnection, IModule private string _url; private bool _registering; private Guid _context; + protected bool Disposed; /// /// The Open timeout @@ -98,7 +99,7 @@ public WebsocketConnection(string url) { if (!Validation.IsWsUrl(url)) throw new ArgumentException("Provided URL is not compatible with WebSocket connection: " + url); - + _context = Guid.NewGuid(); this._url = url; } @@ -178,7 +179,7 @@ private void OnOpen(WebsocketClient socket) { if (socket == null) return; - + socket.MessageReceived.Subscribe(OnPayload); socket.DisconnectionHappened.Subscribe(OnDisconnect); @@ -191,15 +192,15 @@ private void OnDisconnect(DisconnectionInfo obj) { if (obj.Exception != null) this.ErrorReceived?.Invoke(this, obj.Exception); - + OnClose(obj); } - + private void OnClose(DisconnectionInfo obj) { if (this._socket == null) return; - + //_socket.Dispose(); this._socket = null; this._registering = false; @@ -221,7 +222,7 @@ private void OnPayload(ResponseMessage obj) } if (string.IsNullOrWhiteSpace(json)) return; - + //Console.WriteLine($"[{Name}] Got payload {json}"); this.PayloadReceived?.Invoke(this, json); @@ -237,8 +238,9 @@ public async Task Close() throw new IOException("Connection already closed"); await _socket.Stop(WebSocketCloseStatus.NormalClosure, "Close Invoked"); - - OnClose(new DisconnectionInfo(DisconnectionType.Exit, WebSocketCloseStatus.Empty, "Close Invoked", null, null)); + + OnClose(new DisconnectionInfo(DisconnectionType.Exit, WebSocketCloseStatus.Empty, "Close Invoked", null, + null)); } /// @@ -303,32 +305,37 @@ public async Task SendError(IJsonRpcError requestPayload, object context) } } - /// - /// Dispose this websocket connection. Will automatically Close this - /// websocket if still connected. - /// public async void Dispose() { - if (Connected) + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) + return; + + if (disposing) { - await Close(); + _socket.Dispose(); } + + Disposed = true; } - private string addressNotFoundError = "getaddrinfo ENOTFOUND"; - private string connectionRefusedError = "connect ECONNREFUSED"; + private const string AddressNotFoundError = "getaddrinfo ENOTFOUND"; + private const string ConnectionRefusedError = "connect ECONNREFUSED"; + private void OnError(IJsonRpcPayload ogPayload, Exception e) { - var exception = e.Message.Contains(addressNotFoundError) || e.Message.Contains(connectionRefusedError) - ? new IOException("Unavailable WS RPC url at " + _url) : e; + var exception = e.Message.Contains(AddressNotFoundError) || e.Message.Contains(ConnectionRefusedError) + ? new IOException("Unavailable WS RPC url at " + _url) + : e; var message = exception.Message; - var payload = new JsonRpcResponse(ogPayload.Id, new Error() - { - Code = exception.HResult, - Data = null, - Message = message - }, default(T)); + var payload = new JsonRpcResponse(ogPayload.Id, + new Error() { Code = exception.HResult, Data = null, Message = message }, default(T)); //Trigger the payload event, converting the new JsonRpcResponse object to JSON string this.PayloadReceived?.Invoke(this, JsonConvert.SerializeObject(payload)); diff --git a/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcProvider.cs b/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcProvider.cs index 11aa025..9106921 100644 --- a/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcProvider.cs +++ b/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcProvider.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using WalletConnectSharp.Common; using WalletConnectSharp.Network.Models; namespace WalletConnectSharp.Network @@ -7,7 +8,7 @@ namespace WalletConnectSharp.Network /// /// The interface that represents a JSON RPC provider /// - public interface IJsonRpcProvider : IBaseJsonRpcProvider + public interface IJsonRpcProvider : IBaseJsonRpcProvider, IModule { event EventHandler PayloadReceived; @@ -18,7 +19,7 @@ public interface IJsonRpcProvider : IBaseJsonRpcProvider event EventHandler ErrorReceived; event EventHandler RawMessageReceived; - + /// /// Connect this provider to the given URL /// diff --git a/Core Modules/WalletConnectSharp.Network/JsonRpcProvider.cs b/Core Modules/WalletConnectSharp.Network/JsonRpcProvider.cs index 0a47993..eeeda1b 100644 --- a/Core Modules/WalletConnectSharp.Network/JsonRpcProvider.cs +++ b/Core Modules/WalletConnectSharp.Network/JsonRpcProvider.cs @@ -30,6 +30,7 @@ public class JsonRpcProvider : IJsonRpcProvider, IModule public event EventHandler RawMessageReceived; private GenericEventHolder jsonResponseEventHolder = new(); + protected bool Disposed; /// /// Whether the provider is currently connecting or not @@ -41,7 +42,7 @@ public bool IsConnecting return _connectingStarted && !Connecting.Task.IsCompleted; } } - + /// /// The current Connection for this provider /// @@ -100,14 +101,14 @@ public async Task Connect(string connection) { await this._connection.Close(); } - + // Reset connecting task Connecting = new TaskCompletionSource(); _connectingStarted = true; WCLogger.Log("[JsonRpcProvider] Opening connection"); await this._connection.Open(connection); - + FinalizeConnection(this._connection); } @@ -123,7 +124,7 @@ public async Task Connect(IJsonRpcConnection connection) WCLogger.Log("Current connection still open, closing connection"); await this._connection.Close(); } - + // Reset connecting task Connecting = new TaskCompletionSource(); _connectingStarted = true; @@ -133,7 +134,7 @@ public async Task Connect(IJsonRpcConnection connection) FinalizeConnection(connection); } - + private void FinalizeConnection(IJsonRpcConnection connection) { WCLogger.Log("[JsonRpcProvider] Finalizing Connection, registering event listeners"); @@ -152,7 +153,7 @@ public async Task Connect() { if (_connection == null) throw new Exception("No connection is set"); - + WCLogger.Log("[JsonRpcProvider] Connecting with given connection object"); await Connect(_connection); } @@ -179,7 +180,7 @@ public async Task Disconnect() public async Task Request(IRequestArguments requestArgs, object context = null) { WCLogger.Log("[JsonRpcProvider] Checking if connected"); - if (IsConnecting) + if (IsConnecting) await Connecting.Task; else if (!_connectingStarted && !_connection.Connected) { @@ -194,6 +195,7 @@ public async Task Request(IRequestArguments requestArgs, object co if (id == 0) id = null; // An id of 0 is null } + var request = new JsonRpcRequest(requestArgs.Method, requestArgs.Params, id); TaskCompletionSource requestTask = new TaskCompletionSource(TaskCreationOptions.None); @@ -204,7 +206,7 @@ public async Task Request(IRequestArguments requestArgs, object co return; var result = JsonConvert.DeserializeObject>(responseJson); - + if (result.Error != null) { requestTask.SetException(new IOException(result.Error.Message)); @@ -219,8 +221,7 @@ public async Task Request(IRequestArguments requestArgs, object co { if (requestTask.Task.IsCompleted) return; - - //Console.WriteLine($"[{Name}] Got Response Error {exception}"); + if (exception != null) { requestTask.SetException(exception); @@ -228,9 +229,9 @@ public async Task Request(IRequestArguments requestArgs, object co }; _lastId = request.Id; - - WCLogger.Log($"[JsonRpcProvider] Sending request {request.Method} with data {JsonConvert.SerializeObject(request)}"); - //Console.WriteLine($"[{Name}] Sending request {request.Method} with data {JsonConvert.SerializeObject(request)}"); + + WCLogger.Log( + $"[JsonRpcProvider] Sending request {request.Method} with data {JsonConvert.SerializeObject(request)}"); await _connection.SendRequest(request, context); WCLogger.Log("[JsonRpcProvider] Awaiting request result"); @@ -242,15 +243,13 @@ public async Task Request(IRequestArguments requestArgs, object co protected void RegisterEventListeners() { if (_hasRegisteredEventListeners) return; - - WCLogger.Log($"[JsonRpcProvider] Registering event listeners on connection object with context {_connection.ToString()}"); + + WCLogger.Log( + $"[JsonRpcProvider] Registering event listeners on connection object with context {_connection.ToString()}"); _connection.PayloadReceived += OnPayload; _connection.Closed += OnConnectionDisconnected; _connection.ErrorReceived += OnConnectionError; - - /*_connection.On("payload", OnPayload); - _connection.On("close", OnConnectionDisconnected); - _connection.On("error", OnConnectionError);*/ + _hasRegisteredEventListeners = true; } @@ -277,9 +276,9 @@ private void OnPayload(object sender, string json) if (payload.Id == 0) payload.Id = _lastId; - + WCLogger.Log($"[JsonRpcProvider] Payload has ID {payload.Id}"); - + this.PayloadReceived?.Invoke(this, payload); if (payload.IsRequest) @@ -304,7 +303,20 @@ private void OnPayload(object sender, string json) public void Dispose() { - _connection?.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + _connection?.Dispose(); + } + + Disposed = true; } } } diff --git a/Core Modules/WalletConnectSharp.Storage/FileSystemStorage.cs b/Core Modules/WalletConnectSharp.Storage/FileSystemStorage.cs index 2376614..5414eb9 100644 --- a/Core Modules/WalletConnectSharp.Storage/FileSystemStorage.cs +++ b/Core Modules/WalletConnectSharp.Storage/FileSystemStorage.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Text; -using System.Threading; -using System.Threading.Tasks; using Newtonsoft.Json; using WalletConnectSharp.Common.Logging; @@ -19,8 +14,9 @@ public class FileSystemStorage : InMemoryStorage /// The file path to store the JSON file /// public string FilePath { get; private set; } - private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1); - + + private readonly SemaphoreSlim _semaphoreSlim = new(1); + /// /// A new FileSystemStorage module that reads/writes all storage /// values from storage @@ -30,7 +26,7 @@ public FileSystemStorage(string filePath = null) { if (filePath == null) { - var home = + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); filePath = Path.Combine(home, ".wc", "store.json"); } @@ -62,7 +58,7 @@ public override async Task SetItem(string key, T value) await base.SetItem(key, value); await Save(); } - + /// /// The RemoveItem function deletes the value stored based off of the specified key. /// Will also update the JSON file @@ -73,7 +69,7 @@ public override async Task RemoveItem(string key) await base.RemoveItem(key); await Save(); } - + /// /// Clear all entries in this storage. WARNING: This will delete all data! /// This will also update the JSON file @@ -91,12 +87,10 @@ private async Task Save() { Directory.CreateDirectory(path); } - - var json = JsonConvert.SerializeObject(Entries, new JsonSerializerSettings() - { - TypeNameHandling = TypeNameHandling.All - }); - + + var json = JsonConvert.SerializeObject(Entries, + new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All }); + await _semaphoreSlim.WaitAsync(); await File.WriteAllTextAsync(FilePath, json, Encoding.UTF8); _semaphoreSlim.Release(); @@ -114,7 +108,7 @@ private async Task Load() try { Entries = JsonConvert.DeserializeObject>(json, - new JsonSerializerSettings() {TypeNameHandling = TypeNameHandling.Auto}); + new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto }); } catch (JsonSerializationException e) { @@ -128,5 +122,17 @@ private async Task Load() Entries = new Dictionary(); } } + + protected override void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + _semaphoreSlim.Dispose(); + } + + Disposed = true; + } } } diff --git a/Core Modules/WalletConnectSharp.Storage/InMemoryStorage.cs b/Core Modules/WalletConnectSharp.Storage/InMemoryStorage.cs index 80d6c92..8ff6bde 100644 --- a/Core Modules/WalletConnectSharp.Storage/InMemoryStorage.cs +++ b/Core Modules/WalletConnectSharp.Storage/InMemoryStorage.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using WalletConnectSharp.Common; using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Storage.Interfaces; @@ -11,6 +7,7 @@ public class InMemoryStorage : IKeyValueStorage { protected Dictionary Entries = new Dictionary(); private bool _initialized = false; + protected bool Disposed; public virtual Task Init() { @@ -41,13 +38,14 @@ public virtual Task GetItem(string key) IsInitialized(); return Task.FromResult(Entries[key] is T ? (T)Entries[key] : default); } - + public virtual Task SetItem(string key, T value) { IsInitialized(); Entries[key] = value; return Task.CompletedTask; } + public virtual Task RemoveItem(string key) { IsInitialized(); @@ -78,7 +76,13 @@ protected void IsInitialized() public void Dispose() { - Entries?.Clear(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + Disposed = true; } } } diff --git a/WalletConnectSharp.Core/Controllers/Expirer.cs b/WalletConnectSharp.Core/Controllers/Expirer.cs index e262c51..26d8f60 100644 --- a/WalletConnectSharp.Core/Controllers/Expirer.cs +++ b/WalletConnectSharp.Core/Controllers/Expirer.cs @@ -15,7 +15,9 @@ public class Expirer : IExpirer /// The version of this module /// public static readonly string Version = "0.3"; - + + protected bool Disposed; + private Dictionary _expirations = new Dictionary(); private bool initialized = false; private Expiration[] _cached = Array.Empty(); @@ -185,22 +187,14 @@ public void Set(long key, long expiry) private void SetWithTarget(string targetType, object key, long expiry) { var target = FormatTarget(targetType, key); - var expiration = new Expiration() - { - Target = target, - Expiry = expiry - }; + var expiration = new Expiration() { Target = target, Expiry = expiry }; if (_expirations.ContainsKey(target)) _expirations.Remove(target); // We cannot override, so remove first - + _expirations.Add(target, expiration); CheckExpiry(target, expiration); - this.Created?.Invoke(this, new ExpirerEventArgs() - { - Expiration = expiration, - Target = target - }); + this.Created?.Invoke(this, new ExpirerEventArgs() { Expiration = expiration, Target = target }); } /// @@ -242,7 +236,7 @@ public Task Delete(string key) return Task.CompletedTask; } - + /// /// Delete a expiration with the given long key (usually a id). /// @@ -263,11 +257,7 @@ private void DeleteWithTarget(string targetType, object key) { var expiration = GetExpiration(target); _expirations.Remove(target); - this.Deleted?.Invoke(this, new ExpirerEventArgs() - { - Target = target, - Expiration = expiration - }); + this.Deleted?.Invoke(this, new ExpirerEventArgs() { Target = target, Expiration = expiration }); } } @@ -320,11 +310,7 @@ private void CheckExpiry(string target, Expiration expiration) private void Expire(string target, Expiration expiration) { _expirations.Remove(target); - this.Expired?.Invoke(this, new ExpirerEventArgs() - { - Target = target, - Expiration = expiration - }); + this.Expired?.Invoke(this, new ExpirerEventArgs() { Target = target, Expiration = expiration }); } private void CheckExpirations(object sender, EventArgs args) @@ -372,12 +358,30 @@ private string FormatTarget(string targetType, object key) default: throw new ArgumentException($"Unknown expirer target type: ${targetType}"); } - + return $"{targetType}:{key}"; } public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + _core.HeartBeat.OnPulse -= CheckExpirations; + + this.Created -= Persist; + this.Expired -= Persist; + this.Deleted -= Persist; + } + + Disposed = true; } } } diff --git a/WalletConnectSharp.Core/Controllers/HeartBeat.cs b/WalletConnectSharp.Core/Controllers/HeartBeat.cs index 2832126..261bc64 100644 --- a/WalletConnectSharp.Core/Controllers/HeartBeat.cs +++ b/WalletConnectSharp.Core/Controllers/HeartBeat.cs @@ -9,9 +9,9 @@ namespace WalletConnectSharp.Core.Controllers public class HeartBeat : IHeartBeat { /// - /// The CancellationToken that stops the Heartbeat module + /// The CancellationTokenSource that can be used to stop the Heartbeat module /// - public CancellationToken HeartBeatCancellationToken { get; private set; } + public CancellationTokenSource CancellationTokenSource { get; private set; } = new(); public event EventHandler OnPulse; @@ -19,11 +19,11 @@ public class HeartBeat : IHeartBeat /// The interval (in milliseconds) the Pulse event gets emitted/triggered /// public int Interval { get; } - + /// - /// The context UUID that this heartboeat module uses + /// The context UUID that this heartbeat module uses /// - public readonly Guid contextGuid = Guid.NewGuid(); + public readonly Guid ContextGuid = Guid.NewGuid(); /// /// The name of this Heartbeat module @@ -32,7 +32,7 @@ public string Name { get { - return $"heartbeat-{contextGuid}"; + return $"heartbeat-{ContextGuid}"; } } @@ -47,6 +47,8 @@ public string Context } } + protected bool Disposed; + /// /// Create a new Heartbeat module, optionally specifying options /// @@ -62,19 +64,22 @@ public HeartBeat(int interval = 5000) /// HeartBeatCancellationToken is cancelled, then the interval will be halted. /// /// - public Task Init() + public Task InitAsync(CancellationToken cancellationToken = default) { - HeartBeatCancellationToken = new CancellationToken(); + if (cancellationToken != default) + { + CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + } Task.Run(async () => { - while (!HeartBeatCancellationToken.IsCancellationRequested) + while (!CancellationTokenSource.Token.IsCancellationRequested) { Pulse(); - await Task.Delay(Interval, HeartBeatCancellationToken); + await Task.Delay(Interval, CancellationTokenSource.Token); } - }, HeartBeatCancellationToken); + }, CancellationTokenSource.Token); return Task.CompletedTask; } @@ -86,6 +91,20 @@ private void Pulse() public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + CancellationTokenSource?.Dispose(); + } + + Disposed = true; } } } diff --git a/WalletConnectSharp.Core/Controllers/JsonRpcHistory.cs b/WalletConnectSharp.Core/Controllers/JsonRpcHistory.cs index 608f6a3..995c4fb 100644 --- a/WalletConnectSharp.Core/Controllers/JsonRpcHistory.cs +++ b/WalletConnectSharp.Core/Controllers/JsonRpcHistory.cs @@ -17,7 +17,7 @@ public class JsonRpcHistory : IJsonRpcHistory /// The storage version of this module /// public static readonly string Version = "0.3"; - + /// /// The name of this module instance /// @@ -51,6 +51,8 @@ public string StorageKey } } + protected bool Disposed; + private JsonRpcRecord[] _cached = Array.Empty>(); private Dictionary> _records = new Dictionary>(); private bool initialized = false; @@ -155,12 +157,7 @@ public void Set(string topic, IJsonRpcRequest request, string chainId) IsInitialized(); if (_records.ContainsKey(request.Id)) return; - var record = new JsonRpcRecord(request) - { - Id = request.Id, - Topic = topic, - ChainId = chainId, - }; + var record = new JsonRpcRecord(request) { Id = request.Id, Topic = topic, ChainId = chainId, }; _records.Add(record.Id, record); this.Created?.Invoke(this, record); } @@ -176,13 +173,13 @@ public Task> Get(string topic, long id) IsInitialized(); var record = GetRecord(id); - + // TODO Log /*if (topic != record.Topic) { throw WalletConnectException.FromType(ErrorType.MISMATCHED_TOPIC, $"{Name}: {id}"); }*/ - + return Task.FromResult>(record); } @@ -269,10 +266,8 @@ private JsonRpcRecord GetRecord(long id) if (!_records.ContainsKey(id)) { - throw WalletConnectException.FromType(ErrorType.NO_MATCHING_KEY, new Dictionary() - { - {"Tag", $"{Name}: {id}"} - }); + throw WalletConnectException.FromType(ErrorType.NO_MATCHING_KEY, + new Dictionary() { { "Tag", $"{Name}: {id}" } }); } return _records[id]; @@ -321,7 +316,22 @@ private void IsInitialized() public void Dispose() { - _core?.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + this.Created -= SaveRecordCallback; + this.Updated -= SaveRecordCallback; + this.Deleted -= SaveRecordCallback; + } + + Disposed = true; } } } diff --git a/WalletConnectSharp.Core/Controllers/Pairing.cs b/WalletConnectSharp.Core/Controllers/Pairing.cs index cce3042..33cbe9f 100644 --- a/WalletConnectSharp.Core/Controllers/Pairing.cs +++ b/WalletConnectSharp.Core/Controllers/Pairing.cs @@ -1,6 +1,7 @@ using System.Security.Cryptography; using System.Text.RegularExpressions; using WalletConnectSharp.Common.Events; +using WalletConnectSharp.Common.Logging; using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Common.Model.Relay; using WalletConnectSharp.Common.Utils; @@ -18,10 +19,12 @@ namespace WalletConnectSharp.Core.Controllers /// public class Pairing : IPairing { + protected bool Disposed; + private const int KeyLength = 32; private bool _initialized; private HashSet _registeredMethods = new HashSet(); - + /// /// The name for this module instance /// @@ -55,7 +58,7 @@ public string Context /// /// public IPairingStore Store { get; } - + /// /// Get all active and inactive pairings /// @@ -136,23 +139,17 @@ public async Task Pair(string uri, bool activatePairing = true) { throw new ArgumentException($"Topic {topic} already has keychain"); } - + var expiry = Clock.CalculateExpiry(Clock.FIVE_MINUTES); var pairing = new PairingStruct() { - Topic = topic, - Relay = relay, - Expiry = expiry, - Active = false, + Topic = topic, Relay = relay, Expiry = expiry, Active = false, }; - + await this.Store.Set(topic, pairing); await this.Core.Crypto.SetSymKey(symKey, topic); - await this.Core.Relayer.Subscribe(topic, new SubscribeOptions() - { - Relay = relay - }); - + await this.Core.Relayer.Subscribe(topic, new SubscribeOptions() { Relay = relay }); + this.Core.Expirer.Set(topic, expiry); if (activatePairing) @@ -173,7 +170,9 @@ public async Task Pair(string uri, bool activatePairing = true) public UriParameters ParseUri(string uri) { var pathStart = uri.IndexOf(":", StringComparison.Ordinal); - int? pathEnd = uri.IndexOf("?", StringComparison.Ordinal) != -1 ? uri.IndexOf("?", StringComparison.Ordinal) : (int?)null; + int? pathEnd = uri.IndexOf("?", StringComparison.Ordinal) != -1 + ? uri.IndexOf("?", StringComparison.Ordinal) + : (int?)null; var protocol = uri.Substring(0, pathStart); string path; @@ -213,16 +212,10 @@ public async Task Create() var symKey = symKeyRaw.ToHex(); var topic = await this.Core.Crypto.SetSymKey(symKey); var expiry = Clock.CalculateExpiry(Clock.FIVE_MINUTES); - var relay = new ProtocolOptions() - { - Protocol = RelayProtocols.Default - }; + var relay = new ProtocolOptions() { Protocol = RelayProtocols.Default }; var pairing = new PairingStruct() { - Topic = topic, - Expiry = expiry, - Relay = relay, - Active = false, + Topic = topic, Expiry = expiry, Relay = relay, Active = false, }; var uri = $"{ICore.Protocol}:{topic}@{ICore.Version}?" .AddQueryParam("symKey", symKey) @@ -235,11 +228,7 @@ public async Task Create() await this.Core.Relayer.Subscribe(topic); this.Core.Expirer.Set(topic, expiry); - return new CreatePairingData() - { - Topic = topic, - Uri = uri - }; + return new CreatePairingData() { Topic = topic, Uri = uri }; } /// @@ -275,7 +264,7 @@ public Task Register(string[] methods) public Task UpdateExpiry(string topic, long expiration) { IsInitialized(); - return this.Store.Update(topic, new PairingStruct() {Expiry = expiration}); + return this.Store.Update(topic, new PairingStruct() { Expiry = expiration }); } /// @@ -286,7 +275,7 @@ public Task UpdateExpiry(string topic, long expiration) public Task UpdateMetadata(string topic, Metadata metadata) { IsInitialized(); - return this.Store.Update(topic, new PairingStruct() {PeerMetadata = metadata}); + return this.Store.Update(topic, new PairingStruct() { PeerMetadata = metadata }); } /// @@ -309,7 +298,7 @@ public async Task Ping(string topic) else done.SetResult(args.Result); }); - + await done.Task; } } @@ -327,7 +316,7 @@ public async Task Disconnect(string topic) { var error = Error.FromErrorType(ErrorType.USER_DISCONNECTED); await Core.MessageHandler.SendRequest(topic, - new PairingDelete() {Code = error.Code, Message = error.Message}); + new PairingDelete() { Code = error.Code, Message = error.Message }); await DeletePairing(topic); } } @@ -335,24 +324,22 @@ await Core.MessageHandler.SendRequest(topic, private async Task ActivatePairing(string topic) { var expiry = Clock.CalculateExpiry(Clock.THIRTY_DAYS); - await this.Store.Update(topic, new PairingStruct() - { - Active = true, - Expiry = expiry - }); + await this.Store.Update(topic, new PairingStruct() { Active = true, Expiry = expiry }); this.Core.Expirer.Set(topic, expiry); } - + private async Task DeletePairing(string topic) { bool expirerHasDeleted = !this.Core.Expirer.Has(topic); bool pairingHasDeleted = !this.Store.Keys.Contains(topic); bool symKeyHasDeleted = !(await this.Core.Crypto.HasKeys(topic)); - + await this.Core.Relayer.Unsubscribe(topic); await Task.WhenAll( - pairingHasDeleted ? Task.CompletedTask : this.Store.Delete(topic, Error.FromErrorType(ErrorType.USER_DISCONNECTED)), + pairingHasDeleted + ? Task.CompletedTask + : this.Store.Delete(topic, Error.FromErrorType(ErrorType.USER_DISCONNECTED)), symKeyHasDeleted ? Task.CompletedTask : this.Core.Crypto.DeleteSymKey(topic), expirerHasDeleted ? Task.CompletedTask : this.Core.Expirer.Delete(topic) ); @@ -360,13 +347,15 @@ await Task.WhenAll( private Task Cleanup() { - List pairingTopics = (from pair in this.Store.Values.Where(e => e.Expiry != null) where Clock.IsExpired(pair.Expiry.Value) select pair.Topic).ToList(); - + List pairingTopics = (from pair in this.Store.Values.Where(e => e.Expiry != null) + where Clock.IsExpired(pair.Expiry.Value) + select pair.Topic).ToList(); + return Task.WhenAll( pairingTopics.Select(DeletePairing) ); } - + private async Task IsValidPairingTopic(string topic) { if (string.IsNullOrWhiteSpace(topic)) @@ -384,11 +373,11 @@ private async Task IsValidPairingTopic(string topic) throw WalletConnectException.FromType(ErrorType.EXPIRED, $"pairing topic: {topic}"); } } - + private bool IsValidUrl(string url) { if (string.IsNullOrWhiteSpace(url)) return false; - + try { new Uri(url); @@ -399,14 +388,14 @@ private bool IsValidUrl(string url) return false; } } - + private Task IsValidPair(string uri) { if (!IsValidUrl(uri)) throw WalletConnectException.FromType(ErrorType.MISSING_OR_INVALID, $"pair() uri: {uri}"); return Task.CompletedTask; } - + private void IsInitialized() { if (!_initialized) @@ -414,7 +403,7 @@ private void IsInitialized() throw WalletConnectException.FromType(ErrorType.NOT_INITIALIZED, this.Name); } } - + private async Task OnPairingPingRequest(string topic, JsonRpcRequest payload) { var id = payload.Id; @@ -423,11 +412,7 @@ private async Task OnPairingPingRequest(string topic, JsonRpcRequest(id, topic, true); - this.PairingPinged?.Invoke(this, new PairingEvent() - { - Topic = topic, - Id = id - }); + this.PairingPinged?.Invoke(this, new PairingEvent() { Topic = topic, Id = id }); } catch (WalletConnectException e) { @@ -438,20 +423,16 @@ private async Task OnPairingPingRequest(string topic, JsonRpcRequest payload) { var id = payload.Id; - + // put at the end of the stack to avoid a race condition // where session_ping listener is not yet initialized await Task.Delay(500); - this.PairingPinged?.Invoke(this, new PairingEvent() - { - Id = id, - Topic = topic - }); + this.PairingPinged?.Invoke(this, new PairingEvent() { Id = id, Topic = topic }); PairingPingResponseEvents[$"pairing_ping{id}"](this, payload); } - + private async Task OnPairingDeleteRequest(string topic, JsonRpcRequest payload) { var id = payload.Id; @@ -461,18 +442,14 @@ private async Task OnPairingDeleteRequest(string topic, JsonRpcRequest(id, topic, true); await DeletePairing(topic); - this.PairingDeleted?.Invoke(this, new PairingEvent() - { - Topic = topic, - Id = id - }); + this.PairingDeleted?.Invoke(this, new PairingEvent() { Topic = topic, Id = id }); } catch (WalletConnectException e) { await Core.MessageHandler.SendError(id, topic, Error.FromException(e)); } } - + private async Task IsValidDisconnect(string topic, Error reason) { if (string.IsNullOrWhiteSpace(topic)) @@ -482,9 +459,10 @@ private async Task IsValidDisconnect(string topic, Error reason) await IsValidPairingTopic(topic); } - + private async void ExpiredCallback(object sender, ExpirerEventArgs e) { + WCLogger.Log($"Expired topic {e.Target}"); var target = new ExpirerTarget(e.Target); if (string.IsNullOrWhiteSpace(target.Topic)) return; @@ -493,16 +471,26 @@ private async void ExpiredCallback(object sender, ExpirerEventArgs e) if (this.Store.Keys.Contains(topic)) { await DeletePairing(topic); - this.PairingExpired?.Invoke(this, new PairingEvent() - { - Topic = topic, - }); + this.PairingExpired?.Invoke(this, new PairingEvent() { Topic = topic, }); } } public void Dispose() { - Store?.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + Store?.Dispose(); + } + + Disposed = true; } } } diff --git a/WalletConnectSharp.Core/Controllers/Relayer.cs b/WalletConnectSharp.Core/Controllers/Relayer.cs index 1e11f87..219fa8d 100644 --- a/WalletConnectSharp.Core/Controllers/Relayer.cs +++ b/WalletConnectSharp.Core/Controllers/Relayer.cs @@ -122,6 +122,7 @@ public bool TransportExplicitlyClosed private string projectId; private bool initialized; private bool reconnecting = false; + protected bool Disposed; /// /// Create a new Relayer with the given RelayerOptions. @@ -488,9 +489,23 @@ private async Task ToEstablishConnection() public void Dispose() { - Subscriber?.Dispose(); - Publisher?.Dispose(); - Messages?.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + Subscriber?.Dispose(); + Publisher?.Dispose(); + Messages?.Dispose(); + Provider?.Dispose(); + } + + Disposed = true; } } } diff --git a/WalletConnectSharp.Core/Controllers/Store.cs b/WalletConnectSharp.Core/Controllers/Store.cs index 9cfb68f..4e6091c 100644 --- a/WalletConnectSharp.Core/Controllers/Store.cs +++ b/WalletConnectSharp.Core/Controllers/Store.cs @@ -17,15 +17,17 @@ namespace WalletConnectSharp.Core.Controllers /// The type of the values stored, the value must contain the key public class Store : IStore where TValue : IKeyHolder { + protected bool Disposed; + private bool initialized; private Dictionary map = new Dictionary(); private TValue[] cached = Array.Empty(); - + /// /// The ICore module using this Store module /// public ICore Core { get; } - + /// /// The StoragePrefix this Store module will prepend to the storage key /// @@ -41,12 +43,12 @@ public string Version return "0.3"; } } - + /// /// The Name of this Store module /// public string Name { get; } - + /// /// The context string of this Store module /// @@ -109,7 +111,7 @@ public Store(ICore core, string name, string storagePrefix = null) name = $"{core.Name}-{name}"; Name = name; Context = name; - + if (storagePrefix == null) StoragePrefix = WalletConnectCore.STORAGE_PREFIX; else @@ -152,6 +154,7 @@ public Task Set(TKey key, TValue value) { return Update(key, value); } + map.Add(key, value); return Persist(); } @@ -179,7 +182,7 @@ public TValue Get(TKey key) public Task Update(TKey key, TValue update) { IsInitialized(); - + // Partial updates aren't built into C# // However, we can use reflection to sort of // get the same thing @@ -205,7 +208,7 @@ public Task Update(TKey key, TValue update) previousValue = (TValue)test; } } - + var fields = t.GetFields(); // Loop through all of them @@ -221,7 +224,7 @@ public Task Update(TKey key, TValue update) previousValue = (TValue)test; } } - + // Now, set the update variable to be the new modified // previousValue object update = previousValue; @@ -312,7 +315,22 @@ protected virtual void IsInitialized() public void Dispose() { - Core?.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + map.Clear(); + map = null; + cached = null; + } + + Disposed = true; } } } diff --git a/WalletConnectSharp.Core/Controllers/TypedMessageHandler.cs b/WalletConnectSharp.Core/Controllers/TypedMessageHandler.cs index c88102f..fbaa626 100644 --- a/WalletConnectSharp.Core/Controllers/TypedMessageHandler.cs +++ b/WalletConnectSharp.Core/Controllers/TypedMessageHandler.cs @@ -18,7 +18,9 @@ public class TypedMessageHandler : ITypedMessageHandler public event EventHandler RawMessage; private EventHandlerMap messageEventHandlerMap = new(); - + + protected bool Disposed; + public ICore Core { get; } /// @@ -47,7 +49,7 @@ public TypedMessageHandler(ICore core) { this.Core = core; } - + public Task Init() { if (!_initialized) @@ -58,7 +60,7 @@ public Task Init() _initialized = true; return Task.CompletedTask; } - + async void RelayerMessageCallback(object sender, MessageEvent e) { var topic = e.Topic; @@ -73,12 +75,8 @@ async void RelayerMessageCallback(object sender, MessageEvent e) } else if (payload.IsResponse) { - this.RawMessage?.Invoke(this, new DecodedMessageEvent() - { - Topic = topic, - Message = message, - Payload = payload - }); + this.RawMessage?.Invoke(this, + new DecodedMessageEvent() { Topic = topic, Message = message, Payload = payload }); } } @@ -90,34 +88,35 @@ 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, Func, Task> responseCallback) + public async void HandleMessageType(Func, Task> requestCallback, + Func, Task> responseCallback) { var method = RpcMethodAttribute.MethodForType(); var rpcHistory = await this.Core.History.JsonRpcHistoryOfType(); - + async void RequestCallback(object sender, MessageEvent e) { if (requestCallback == null) return; - + var topic = e.Topic; var message = e.Message; - + var options = DecodeOptionForTopic(topic); var payload = await this.Core.Crypto.Decode>(topic, message, options); - + (await this.Core.History.JsonRpcHistoryOfType()).Set(topic, payload, null); await requestCallback(topic, payload); } - + async void ResponseCallback(object sender, MessageEvent e) { if (responseCallback == null) return; - + var topic = e.Topic; var message = e.Message; - + var options = DecodeOptionForTopic(topic); var rawResultPayload = await this.Core.Crypto.Decode(topic, message, options); @@ -128,7 +127,7 @@ async void ResponseCallback(object sender, MessageEvent e) try { var payload = await this.Core.Crypto.Decode>(topic, message, options); - + await history.Resolve(payload); await responseCallback(topic, payload); @@ -155,26 +154,23 @@ async void InspectResponseRaw(object sender, DecodedMessageEvent e) // ignored if we can't find anything in the history if (record == null) return; var resMethod = record.Request.Method; - + // Trigger the true response event, which will trigger ResponseCallback - messageEventHandlerMap[$"response_{resMethod}"](this, new MessageEvent() - { - Topic = topic, - Message = message - }); + messageEventHandlerMap[$"response_{resMethod}"](this, + new MessageEvent() { Topic = topic, Message = message }); } - catch(WalletConnectException err) + catch (WalletConnectException err) { if (err.CodeType != ErrorType.NO_MATCHING_KEY) throw; - + // ignored if we can't find anything in the history } } messageEventHandlerMap[$"request_{method}"] += RequestCallback; messageEventHandlerMap[$"response_{method}"] += ResponseCallback; - + // Handle response_raw in this context // This will allow us to examine response_raw in every typed context registered this.RawMessage += InspectResponseRaw; @@ -197,7 +193,8 @@ public PublishOptions RpcRequestOptionsFromType() opts = RpcRequestOptionsForType(); if (opts == null) { - throw new Exception($"No RpcRequestOptions attribute found in either {typeof(T1).FullName} or {typeof(T2).FullName}!"); + throw new Exception( + $"No RpcRequestOptions attribute found in either {typeof(T1).FullName} or {typeof(T2).FullName}!"); } } @@ -222,11 +219,7 @@ public PublishOptions RpcRequestOptionsForType() var opts = attributes.Cast().First(); - return new PublishOptions() - { - Tag = opts.Tag, - TTL = opts.TTL - }; + return new PublishOptions() { Tag = opts.Tag, TTL = opts.TTL }; } /// @@ -249,12 +242,13 @@ public PublishOptions RpcResponseOptionsFromTypes() opts = RpcResponseOptionsForType(); if (opts == null) { - throw new Exception($"No RpcResponseOptions attribute found in either {typeof(T1).FullName} or {typeof(T2).FullName}!"); + throw new Exception( + $"No RpcResponseOptions attribute found in either {typeof(T1).FullName} or {typeof(T2).FullName}!"); } return opts; } - + /// /// Build from an from /// the given type T @@ -273,11 +267,7 @@ public PublishOptions RpcResponseOptionsForType() var opts = attributes.Cast().First(); - return new PublishOptions() - { - Tag = opts.Tag, - TTL = opts.TTL - }; + return new PublishOptions() { Tag = opts.Tag, TTL = opts.TTL }; } public void SetDecodeOptionsForTopic(DecodeOptions options, string topic) @@ -287,9 +277,7 @@ public void SetDecodeOptionsForTopic(DecodeOptions options, string topic) public DecodeOptions DecodeOptionForTopic(string topic) { - if (_decodeOptionsMap.ContainsKey(topic)) - return _decodeOptionsMap[topic]; - return null; + return _decodeOptionsMap.TryGetValue(topic, out var option) ? option : null; } /// @@ -301,14 +289,15 @@ public DecodeOptions DecodeOptionForTopic(string topic) /// The request type /// The response type /// The id of the request sent - public async Task SendRequest(string topic, T parameters, long? expiry = null, EncodeOptions options = null) + public async Task SendRequest(string topic, T parameters, long? expiry = null, + EncodeOptions options = null) { EnsureTypeIsSerializerSafe(parameters); - + var method = RpcMethodAttribute.MethodForType(); var payload = new JsonRpcRequest(method, parameters); - + WCLogger.Log(JsonConvert.SerializeObject(payload)); var message = await this.Core.Crypto.Encode(topic, payload, options); @@ -319,7 +308,7 @@ public async Task SendRequest(string topic, T parameters, long? exp { opts.TTL = (long)expiry; } - + (await this.Core.History.JsonRpcHistoryOfType()).Set(topic, payload, null); // await is intentionally omitted here because of a possible race condition @@ -342,7 +331,7 @@ public async Task SendRequest(string topic, T parameters, long? exp public async Task SendResult(long id, string topic, TR result, EncodeOptions options = null) { EnsureTypeIsSerializerSafe(result); - + var payload = new JsonRpcResponse(id, null, result); var message = await this.Core.Crypto.Encode(topic, payload, options); var opts = RpcResponseOptionsFromTypes(); @@ -362,7 +351,7 @@ public async Task SendError(long id, string topic, Error error, EncodeOpt { // Type Error is always serializer safe // EnsureTypeIsSerializerSafe(error); - + var payload = new JsonRpcResponse(id, error, default); var message = await this.Core.Crypto.Encode(topic, payload, options); var opts = RpcResponseOptionsFromTypes(); @@ -370,21 +359,36 @@ public async Task SendError(long id, string topic, Error error, EncodeOpt await (await this.Core.History.JsonRpcHistoryOfType()).Resolve(payload); } - public void Dispose() - { - } - private void EnsureTypeIsSerializerSafe(T testObject) { var typeString = typeof(T).FullName; if (_typeSafeCache.Contains(typeString)) return; - + // Throw any serialization exceptions now // before it's too late TypeSafety.EnsureTypeSerializerSafe(testObject); _typeSafeCache.Add(typeString); } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) + return; + + if (disposing) + { + this.Core.Relayer.OnMessageReceived -= RelayerMessageCallback; + } + + Disposed = true; + } } } diff --git a/WalletConnectSharp.Core/Interfaces/IHeartBeat.cs b/WalletConnectSharp.Core/Interfaces/IHeartBeat.cs index f010a09..2d2093f 100644 --- a/WalletConnectSharp.Core/Interfaces/IHeartBeat.cs +++ b/WalletConnectSharp.Core/Interfaces/IHeartBeat.cs @@ -9,7 +9,7 @@ namespace WalletConnectSharp.Core.Interfaces public interface IHeartBeat : IModule { event EventHandler OnPulse; - + /// /// The interval (in milliseconds) the Pulse event gets emitted/triggered /// @@ -21,6 +21,6 @@ public interface IHeartBeat : IModule /// HeartBeatCancellationToken is cancelled, then the interval will be halted. /// /// - public Task Init(); + public Task InitAsync(CancellationToken cancellationToken = default); } } diff --git a/WalletConnectSharp.Core/WalletConnectCore.cs b/WalletConnectSharp.Core/WalletConnectCore.cs index ad00dcf..e3d06af 100644 --- a/WalletConnectSharp.Core/WalletConnectCore.cs +++ b/WalletConnectSharp.Core/WalletConnectCore.cs @@ -112,6 +112,8 @@ public string Context public CoreOptions Options { get; } + protected bool Disposed; + /// /// Create a new Core with the given options. /// @@ -192,7 +194,7 @@ private async Task Initialize() await Storage.Init(); await Crypto.Init(); await Relayer.Init(); - await HeartBeat.Init(); + await HeartBeat.InitAsync(); await Expirer.Init(); await MessageHandler.Init(); await Pairing.Init(); @@ -200,13 +202,26 @@ private async Task Initialize() public void Dispose() { - HeartBeat?.Dispose(); - Crypto?.Dispose(); - Relayer?.Dispose(); - Storage?.Dispose(); - MessageHandler?.Dispose(); - Expirer?.Dispose(); - Pairing?.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + HeartBeat?.Dispose(); + Crypto?.Dispose(); + Relayer?.Dispose(); + Storage?.Dispose(); + MessageHandler?.Dispose(); + Expirer?.Dispose(); + Pairing?.Dispose(); + } + + Disposed = true; } } } diff --git a/WalletConnectSharp.Sign/Engine.cs b/WalletConnectSharp.Sign/Engine.cs index 94b1ddd..7bdda1b 100644 --- a/WalletConnectSharp.Sign/Engine.cs +++ b/WalletConnectSharp.Sign/Engine.cs @@ -24,12 +24,14 @@ namespace WalletConnectSharp.Sign /// public partial class Engine : IEnginePrivate, IEngine, IModule { + protected bool Disposed; + private const long ProposalExpiry = Clock.THIRTY_DAYS; private const long SessionExpiry = Clock.SEVEN_DAYS; private const int KeyLength = 32; private bool _initialized = false; - + /// /// The using this Engine /// @@ -80,7 +82,7 @@ public async Task Init() if (!this._initialized) { SetupEvents(); - + await PrivateThis.Cleanup(); this.RegisterRelayerEvents(); this.RegisterExpirerEvents(); @@ -108,100 +110,105 @@ private void RegisterExpirerEvents() private void RegisterRelayerEvents() { // 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.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); + MessageHandler.HandleMessageType(PrivateThis.OnSessionPingRequest, + PrivateThis.OnSessionPingResponse); } - + /// /// This event is invoked when the given session has expired /// Event Side: dApp & Wallet /// public event EventHandler SessionExpired; - + /// /// This event is invoked when the given pairing has expired /// Event Side: Wallet /// public event EventHandler PairingExpired; - + /// /// This event is invoked when a new session is proposed. This is usually invoked /// after a new pairing has been activated from a URI /// Event Side: Wallet /// public event EventHandler SessionProposed; - + /// /// This event is invoked when a proposed session has been connected to a wallet. This event is /// triggered after the session has been approved by a wallet /// Event Side: dApp /// public event EventHandler SessionConnected; - + /// /// This event is invoked when a proposed session connection failed with an error /// Event Side: dApp /// public event EventHandler SessionConnectionErrored; - + /// /// This event is invoked when a given session sent a update request. /// Event Side: Wallet /// public event EventHandler SessionUpdateRequest; - + /// /// This event is invoked when a given session sent a extend request. /// Event Side: Wallet /// public event EventHandler SessionExtendRequest; - + /// /// This event is invoked when a given session update request was successful. /// Event Side: dApp /// public event EventHandler SessionUpdated; - + /// /// This event is invoked when a given session extend request was successful. /// Event Side: dApp /// public event EventHandler SessionExtended; - + /// /// This event is invoked when a given session has been pinged /// Event Side: dApp & Wallet /// public event EventHandler SessionPinged; - + /// /// This event is invoked whenever a session has been deleted /// Event Side: dApp & Wallet /// public event EventHandler SessionDeleted; - + /// /// This event is invoked whenever a session has been rejected /// Event Side: Wallet /// public event EventHandler SessionRejected; - + /// /// This event is invoked whenever a session has been approved /// Event Side: Wallet /// public event EventHandler SessionApproved; - + /// /// This event is invoked whenever a pairing is pinged /// Event Side: dApp & Wallet /// public event EventHandler PairingPinged; - + /// /// This event is invoked whenever a pairing is deleted /// Event Side: dApp & Wallet @@ -228,11 +235,13 @@ 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(Func>, Task> requestCallback, Func, Task> responseCallback) + public void HandleSessionRequestMessageType( + Func>, Task> requestCallback, + Func, Task> responseCallback) { Client.Core.MessageHandler.HandleMessageType(requestCallback, responseCallback); } - + /// /// An alias for where T is and /// TR is unchanged @@ -240,7 +249,8 @@ public void HandleSessionRequestMessageType(FuncThe 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, Func, Task> responseCallback) + public void HandleEventMessageType(Func>, Task> requestCallback, + Func, Task> responseCallback) { Client.Core.MessageHandler.HandleMessageType(requestCallback, responseCallback); } @@ -292,7 +302,9 @@ public Task Disconnect(Error reason = null) public UriParameters ParseUri(string uri) { var pathStart = uri.IndexOf(":", StringComparison.Ordinal); - int? pathEnd = uri.IndexOf("?", StringComparison.Ordinal) != -1 ? uri.IndexOf("?", StringComparison.Ordinal) : (int?)null; + int? pathEnd = uri.IndexOf("?", StringComparison.Ordinal) != -1 + ? uri.IndexOf("?", StringComparison.Ordinal) + : (int?)null; var protocol = uri.Substring(0, pathStart); string path; @@ -344,7 +356,7 @@ public async Task Connect(ConnectOptions options) await PrivateThis.IsValidConnect(options); var requiredNamespaces = options.RequiredNamespaces; var optionalNamespaces = options.OptionalNamespaces; - var sessionProperties = options.SessionProperties; + var sessionProperties = options.SessionProperties; var relays = options.Relays; var topic = options.PairingTopic; string uri = ""; @@ -355,7 +367,7 @@ 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}"); } @@ -364,7 +376,7 @@ 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}"); } @@ -374,22 +386,12 @@ public async Task Connect(ConnectOptions options) RequiredNamespaces = requiredNamespaces, Relays = relays != null ? new[] { relays } - : new[] - { - new ProtocolOptions() - { - Protocol = RelayProtocols.Default - } - }, - Proposer = new Participant() - { - PublicKey = publicKey, - Metadata = this.Client.Metadata - }, + : new[] { new ProtocolOptions() { Protocol = RelayProtocols.Default } }, + Proposer = new Participant() { PublicKey = publicKey, Metadata = this.Client.Metadata }, OptionalNamespaces = optionalNamespaces, SessionProperties = sessionProperties, }; - + WCLogger.Log($"Created public key pair"); TaskCompletionSource approvalTask = new TaskCompletionSource(); @@ -406,11 +408,12 @@ public async Task Connect(ConnectOptions options) var completeSession = session with { RequiredNamespaces = requiredNamespaces }; await PrivateThis.SetExpiry(session.Topic, session.Expiry.Value); await Client.Session.Set(session.Topic, completeSession); - + if (!string.IsNullOrWhiteSpace(topic)) { await this.Client.Core.Pairing.UpdateMetadata(topic, session.Peer.Metadata); } + approvalTask.SetResult(completeSession); }; @@ -438,9 +441,9 @@ public async Task Connect(ConnectOptions options) } logger.Log($"Sending request JSON {JsonConvert.SerializeObject(proposal)} to topic {topic}"); - + var id = await MessageHandler.SendRequest(topic, proposal); - + logger.Log($"Got back {id} as request pending id"); var expiry = Clock.CalculateExpiry(options.Expiry); @@ -456,11 +459,7 @@ public async Task Connect(ConnectOptions options) SessionProperties = proposal.SessionProperties, }); - return new ConnectedData() - { - Uri = uri, - Approval = approvalTask.Task - }; + return new ConnectedData() { Uri = uri, Approval = approvalTask.Task }; } /// @@ -524,16 +523,9 @@ public async Task Approve(ApproveParams @params) var sessionSettle = new SessionSettle() { - Relay = new ProtocolOptions() - { - Protocol = relayProtocol != null ? relayProtocol : "irn" - }, + Relay = new ProtocolOptions() { Protocol = relayProtocol != null ? relayProtocol : "irn" }, Namespaces = namespaces, - Controller = new Participant() - { - PublicKey = selfPublicKey, - Metadata = this.Client.Metadata - }, + Controller = new Participant() { PublicKey = selfPublicKey, Metadata = this.Client.Metadata }, Expiry = Clock.CalculateExpiry(SessionExpiry) }; @@ -573,16 +565,13 @@ public async Task Approve(ApproveParams @params) await MessageHandler.SendResult(id, pairingTopic, new SessionProposeResponse() { - Relay = new ProtocolOptions() - { - Protocol = relayProtocol != null ? relayProtocol : "irn" - }, + Relay = new ProtocolOptions() { Protocol = relayProtocol != null ? relayProtocol : "irn" }, ResponderPublicKey = selfPublicKey }); await this.Client.Proposal.Delete(id, Error.FromErrorType(ErrorType.USER_DISCONNECTED)); await this.Client.Core.Pairing.Activate(pairingTopic); } - + return IApprovedData.FromTask(sessionTopic, acknowledgedTask.Task); } @@ -620,10 +609,8 @@ public async Task UpdateSession(string topic, Namespaces names { IsInitialized(); await PrivateThis.IsValidUpdate(topic, namespaces); - var id = await MessageHandler.SendRequest(topic, new SessionUpdate() - { - Namespaces = namespaces - }); + var id = await MessageHandler.SendRequest(topic, + new SessionUpdate() { Namespaces = namespaces }); TaskCompletionSource acknowledgedTask = new TaskCompletionSource(); this.sessionEventsHandlerMap.ListenOnce($"session_update{id}", (sender, args) => @@ -634,11 +621,8 @@ public async Task UpdateSession(string topic, Namespaces names acknowledgedTask.SetResult(args.Result); }); - await this.Client.Session.Update(topic, new SessionStruct() - { - Namespaces = namespaces - }); - + await this.Client.Session.Update(topic, new SessionStruct() { Namespaces = namespaces }); + return IAcknowledgement.FromTask(acknowledgedTask.Task); } @@ -652,7 +636,7 @@ public async Task Extend(string topic) IsInitialized(); await PrivateThis.IsValidExtend(topic); var id = await MessageHandler.SendRequest(topic, new SessionExtend()); - + TaskCompletionSource acknowledgedTask = new TaskCompletionSource(); this.sessionEventsHandlerMap.ListenOnce($"session_extend{id}", (sender, args) => @@ -664,7 +648,7 @@ public async Task Extend(string topic) }); await PrivateThis.SetExpiry(topic, Clock.CalculateExpiry(SessionExpiry)); - + return IAcknowledgement.FromTask(acknowledgedTask.Task); } @@ -704,13 +688,13 @@ public async Task Request(string topic, T data, string chainId = null } var request = new JsonRpcRequest(method, data); - + IsInitialized(); await PrivateThis.IsValidRequest(topic, request, defaultChainId); long[] id = new long[1]; - + var taskSource = new TaskCompletionSource(); - + SessionRequestEvents() .FilterResponses((e) => e.Topic == topic && e.Response.Id == id[0]) .OnResponse += args => @@ -723,12 +707,9 @@ public async Task Request(string topic, T data, string chainId = null return Task.CompletedTask; }; - id[0] = await MessageHandler.SendRequest, TR>(topic, new SessionRequest() - { - ChainId = defaultChainId, - Request = request - }); - + id[0] = await MessageHandler.SendRequest, TR>(topic, + new SessionRequest() { ChainId = defaultChainId, Request = request }); + return await taskSource.Task; } @@ -770,12 +751,8 @@ public async Task Respond(string topic, JsonRpcResponse response) public async Task Emit(string topic, EventData @event, string chainId = null) { IsInitialized(); - await MessageHandler.SendRequest, object>(topic, new SessionEvent() - { - ChainId = chainId, - Event = @event, - Topic = topic, - }); + await MessageHandler.SendRequest, object>(topic, + new SessionEvent() { ChainId = chainId, Event = @event, Topic = topic, }); } /// @@ -786,7 +763,7 @@ public async Task Ping(string topic) { IsInitialized(); await PrivateThis.IsValidPing(topic); - + if (this.Client.Session.Keys.Contains(topic)) { var id = await MessageHandler.SendRequest(topic, new SessionPing()); @@ -799,7 +776,7 @@ public async Task Ping(string topic) done.SetResult(args.Result); }); await done.Task; - } + } else if (this.Client.Core.Pairing.Store.Keys.Contains(topic)) { await this.Client.Core.Pairing.Ping(topic); @@ -816,22 +793,14 @@ public async Task Disconnect(string topic, Error reason) IsInitialized(); var error = reason ?? Error.FromErrorType(ErrorType.USER_DISCONNECTED); await PrivateThis.IsValidDisconnect(topic, error); - + if (this.Client.Session.Keys.Contains(topic)) { - var id = await MessageHandler.SendRequest(topic, new SessionDelete() - { - Code = error.Code, - Message = error.Message, - Data = error.Data - }); + var id = await MessageHandler.SendRequest(topic, + new SessionDelete() { Code = error.Code, Message = error.Message, Data = error.Data }); await PrivateThis.DeleteSession(topic); - this.SessionDeleted?.Invoke(this, new SessionEvent() - { - Topic = topic, - Id = id - }); - } + this.SessionDeleted?.Invoke(this, new SessionEvent() { Topic = topic, Id = id }); + } else if (this.Client.Core.Pairing.Store.Keys.Contains(topic)) { await this.Client.Core.Pairing.Disconnect(topic); @@ -885,7 +854,20 @@ public Task Reject(ProposalStruct proposalStruct, Error error) public void Dispose() { - Client?.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + Client?.Dispose(); + } + + Disposed = true; } } } diff --git a/WalletConnectSharp.Sign/WalletConnectSignClient.cs b/WalletConnectSharp.Sign/WalletConnectSignClient.cs index 7f8cb82..6a793ee 100644 --- a/WalletConnectSharp.Sign/WalletConnectSignClient.cs +++ b/WalletConnectSharp.Sign/WalletConnectSignClient.cs @@ -27,12 +27,12 @@ public class WalletConnectSignClient : ISignClient /// The protocol ALL Sign Client will use as a protocol string /// public static readonly string PROTOCOL = "wc"; - + /// /// The protocol version ALL Sign Client use /// public static readonly int VERSION = 2; - + /// /// The base context string ALL Sign Client use /// @@ -52,7 +52,7 @@ public class WalletConnectSignClient : ISignClient /// The context string for this Sign Client module /// public string Context { get; } - + /// /// The Metadata for this instance of the Sign Client module /// @@ -64,23 +64,23 @@ public class WalletConnectSignClient : ISignClient /// The module this Sign Client module is using /// public ICore Core { get; } - + /// /// The module this Sign Client module is using. Used to do all /// protocol activities behind the scenes, should not be used directly. /// public IEngine Engine { get; } - + /// /// The module this Sign Client module is using. Used for storing pairing data /// public IPairingStore PairingStore { get; } - + /// /// The module this Sign Client module is using. Used for storing session data /// public ISession Session { get; } - + /// /// The module this Sign Client module is using. Used for storing proposal data /// @@ -115,6 +115,7 @@ public int Version } } + protected bool Disposed; public event EventHandler SessionExpired; public event EventHandler PairingExpired; @@ -165,7 +166,7 @@ private WalletConnectSignClient(SignClientOptions options) throw new ArgumentException("The Metadata field must be set in the SignClientOptions object"); else Metadata = options.Metadata; - + Options = options; if (string.IsNullOrWhiteSpace(options.Name)) @@ -175,7 +176,7 @@ private WalletConnectSignClient(SignClientOptions options) else throw new ArgumentException("The Name field in Metadata must be set"); } - + Name = options.Name; if (string.IsNullOrWhiteSpace(options.BaseContext)) @@ -204,7 +205,7 @@ private WalletConnectSignClient(SignClientOptions options) Proposal = new Proposal(Core); Engine = new Engine(this); AddressProvider = new AddressProvider(this); - + SetupEvents(); } @@ -293,7 +294,7 @@ public Task Reject(RejectParams @params) { return Engine.Reject(@params); } - + /// /// Reject a proposal that was recently paired. If the given proposal was not from a recent pairing, /// or the proposal has expired, then an Exception will be thrown. @@ -308,13 +309,9 @@ public Task Reject(ProposalStruct proposalStruct, string message = null) if (message == null) message = "Proposal denied by remote host"; - return Reject(proposalStruct, new Error() - { - Message = message, - Code = (long) ErrorType.USER_DISCONNECTED, - }); + return Reject(proposalStruct, new Error() { Message = message, Code = (long)ErrorType.USER_DISCONNECTED, }); } - + /// /// Reject a proposal that was recently paired. If the given proposal was not from a recent pairing, /// or the proposal has expired, then an Exception will be thrown. @@ -326,12 +323,8 @@ public Task Reject(ProposalStruct proposalStruct, Error error) if (proposalStruct.Id == null) throw new ArgumentException("No proposal Id given"); - var rejectParams = new RejectParams() - { - Id = (long) proposalStruct.Id, - Reason = error - }; - + var rejectParams = new RejectParams() { Id = (long)proposalStruct.Id, Reason = error }; + return Reject(rejectParams); } @@ -434,7 +427,8 @@ public SessionStruct[] Find(RequiredNamespaces requiredNamespaces) return Engine.Find(requiredNamespaces); } - public void HandleEventMessageType(Func>, Task> requestCallback, Func, Task> responseCallback) + public void HandleEventMessageType(Func>, Task> requestCallback, + Func, Task> responseCallback) { this.Engine.HandleEventMessageType(requestCallback, responseCallback); } @@ -486,11 +480,24 @@ private async Task Initialize() public void Dispose() { - Core?.Dispose(); - PairingStore?.Dispose(); - Session?.Dispose(); - Proposal?.Dispose(); - PendingRequests?.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) + { + Core?.Dispose(); + PairingStore?.Dispose(); + Session?.Dispose(); + Proposal?.Dispose(); + PendingRequests?.Dispose(); + } + + Disposed = true; } } }