diff --git a/Core Modules/WalletConnectSharp.Common/Logging/ILogger.cs b/Core Modules/WalletConnectSharp.Common/Logging/ILogger.cs new file mode 100644 index 0000000..c8dee21 --- /dev/null +++ b/Core Modules/WalletConnectSharp.Common/Logging/ILogger.cs @@ -0,0 +1,11 @@ +namespace WalletConnectSharp.Common.Logging +{ + public interface ILogger + { + void Log(string message); + + void LogError(string message); + + void LogError(Exception e); + } +} diff --git a/Core Modules/WalletConnectSharp.Common/Logging/WCLogger.cs b/Core Modules/WalletConnectSharp.Common/Logging/WCLogger.cs new file mode 100644 index 0000000..271e5e9 --- /dev/null +++ b/Core Modules/WalletConnectSharp.Common/Logging/WCLogger.cs @@ -0,0 +1,36 @@ +namespace WalletConnectSharp.Common.Logging +{ + public class WCLogger + { + public static ILogger Logger; + + public static ILogger WithContext(string context) + { + return new WrapperLogger(Logger, context); + } + + public static void Log(string message) + { + if (Logger == null) + return; + + Logger.Log(message); + } + + public static void LogError(string message) + { + if (Logger == null) + return; + + Logger.LogError(message); + } + + public static void LogError(Exception e) + { + if (Logger == null) + return; + + Logger.LogError(e); + } + } +} diff --git a/Core Modules/WalletConnectSharp.Common/Logging/WrapperLogger.cs b/Core Modules/WalletConnectSharp.Common/Logging/WrapperLogger.cs new file mode 100644 index 0000000..5e86f96 --- /dev/null +++ b/Core Modules/WalletConnectSharp.Common/Logging/WrapperLogger.cs @@ -0,0 +1,31 @@ +namespace WalletConnectSharp.Common.Logging; + +public class WrapperLogger : ILogger +{ + private ILogger _logger; + private string prefix; + + public WrapperLogger(ILogger logger, string prefix) + { + _logger = logger; + this.prefix = prefix; + } + + public void Log(string message) + { + if (_logger == null) return; + _logger.Log($"[{prefix}] {message}"); + } + + public void LogError(string message) + { + if (_logger == null) return; + _logger.LogError($"[{prefix}] {message}"); + } + + public void LogError(Exception e) + { + if (_logger == null) return; + _logger.LogError(e); + } +} diff --git a/Core Modules/WalletConnectSharp.Common/Model/Errors/SdkErrors.cs b/Core Modules/WalletConnectSharp.Common/Model/Errors/SdkErrors.cs index 5133bd4..db44bf6 100644 --- a/Core Modules/WalletConnectSharp.Common/Model/Errors/SdkErrors.cs +++ b/Core Modules/WalletConnectSharp.Common/Model/Errors/SdkErrors.cs @@ -27,9 +27,9 @@ public static class SdkErrors /// The error type message to generate /// A dictionary (or anonymous type) of parameters for the error message /// The error message as a string - public static string MessageFromType(ErrorType type, object @params = null) + public static string MessageFromType(ErrorType type, Dictionary @params = null) { - return MessageFromType(type, null, @params.AsDictionary()); + return MessageFromType(type, null, @params); } /// @@ -47,8 +47,8 @@ public static string MessageFromType(ErrorType type, string message = null, Dict @params = new Dictionary(); } - if (!string.IsNullOrWhiteSpace(message) && !@params.ContainsKey("message")) - @params.Add("message", message); + if (!string.IsNullOrWhiteSpace(message)) + @params.TryAdd("message", message); string errorMessage; switch (type) @@ -231,4 +231,4 @@ private static string FormatErrorText(string formattedText, DictionaryThe error type of the exception /// Additional (optional) parameters for the generated error message /// A new exception - public static WalletConnectException FromType(ErrorType type, object @params = null) + public static WalletConnectException FromType(ErrorType type, Dictionary @params = null) { - return FromType(type, null, @params.AsDictionary()); + return FromType(type, null, @params); } } -} \ No newline at end of file +} diff --git a/Core Modules/WalletConnectSharp.Common/Utils/Clock.cs b/Core Modules/WalletConnectSharp.Common/Utils/Clock.cs index 0393a92..99913e0 100644 --- a/Core Modules/WalletConnectSharp.Common/Utils/Clock.cs +++ b/Core Modules/WalletConnectSharp.Common/Utils/Clock.cs @@ -164,5 +164,19 @@ public static long Now() { return ((DateTimeOffset)DateTime.Now).ToUnixTimeSeconds(); } + + /// + /// Current DateTime.Now as unix timestamp milliseconds + /// + /// + public static long NowMilliseconds() + { + return ((DateTimeOffset)DateTime.Now).ToUnixTimeMilliseconds(); + } + + public static TimeSpan AsTimeSpan(long seconds) + { + return TimeSpan.FromSeconds(seconds); + } } -} \ No newline at end of file +} diff --git a/Core Modules/WalletConnectSharp.Common/Utils/DictionaryComparer.cs b/Core Modules/WalletConnectSharp.Common/Utils/DictionaryComparer.cs new file mode 100644 index 0000000..6ebe73c --- /dev/null +++ b/Core Modules/WalletConnectSharp.Common/Utils/DictionaryComparer.cs @@ -0,0 +1,30 @@ +namespace WalletConnectSharp.Common.Utils +{ + public class DictionaryComparer : + IEqualityComparer> + { + private IEqualityComparer valueComparer; + public DictionaryComparer(IEqualityComparer valueComparer = null) + { + this.valueComparer = valueComparer ?? EqualityComparer.Default; + } + public bool Equals(Dictionary x, Dictionary y) + { + if (x.Count != y.Count) + return false; + if (x.Keys.Except(y.Keys).Any()) + return false; + if (y.Keys.Except(x.Keys).Any()) + return false; + foreach (var pair in x) + if (!valueComparer.Equals(pair.Value, y[pair.Key])) + return false; + return true; + } + + public int GetHashCode(Dictionary obj) + { + throw new NotImplementedException(); + } + } +} diff --git a/Core Modules/WalletConnectSharp.Common/Utils/Extensions.cs b/Core Modules/WalletConnectSharp.Common/Utils/Extensions.cs index 4d562c5..e7ae344 100644 --- a/Core Modules/WalletConnectSharp.Common/Utils/Extensions.cs +++ b/Core Modules/WalletConnectSharp.Common/Utils/Extensions.cs @@ -11,34 +11,6 @@ namespace WalletConnectSharp.Common.Utils /// public static class Extensions { - /// - /// Convert an anonymous type to a Dictionary - /// - /// The anonymous type instance to convert to a dictionary - /// Enforce all keys to be lowercased - /// A dictionary where each key is the property name of the anonymous type - /// and each value is the property's value - public static Dictionary AsDictionary(this object obj, bool enforceLowercase = true) - { - if (obj is Dictionary objects) - return objects; - - var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - - if (obj != null) - { - foreach (PropertyInfo propertyDescriptor in obj.GetType().GetProperties()) - { - object value = propertyDescriptor.GetValue(obj, null); - var key = enforceLowercase ? propertyDescriptor.Name.ToLower() : propertyDescriptor.Name; - - dict.Add(key, value); - } - } - - return dict; - } - /// /// Returns true if the given object is a numeric type /// @@ -129,5 +101,12 @@ public static async Task WithTimeout(this Task task, TimeSpan timeout, string me throw new TimeoutException(message.Replace("%t", timeout.ToString())); } } + + public static bool SetEquals(this IEnumerable first, IEnumerable second, + IEqualityComparer comparer) + { + return new HashSet(second, comparer ?? EqualityComparer.Default) + .SetEquals(first); + } } -} \ No newline at end of file +} diff --git a/Core Modules/WalletConnectSharp.Common/Utils/ListComparer.cs b/Core Modules/WalletConnectSharp.Common/Utils/ListComparer.cs new file mode 100644 index 0000000..0a485f2 --- /dev/null +++ b/Core Modules/WalletConnectSharp.Common/Utils/ListComparer.cs @@ -0,0 +1,21 @@ +namespace WalletConnectSharp.Common.Utils +{ + public class ListComparer : IEqualityComparer> + { + private IEqualityComparer valueComparer; + public ListComparer(IEqualityComparer valueComparer = null) + { + this.valueComparer = valueComparer ?? EqualityComparer.Default; + } + + public bool Equals(List x, List y) + { + return x.SetEquals(y, valueComparer); + } + + public int GetHashCode(List obj) + { + throw new NotImplementedException(); + } + } +} diff --git a/Core Modules/WalletConnectSharp.Crypto/Crypto.cs b/Core Modules/WalletConnectSharp.Crypto/Crypto.cs index 5486724..6b1ecc8 100644 --- a/Core Modules/WalletConnectSharp.Crypto/Crypto.cs +++ b/Core Modules/WalletConnectSharp.Crypto/Crypto.cs @@ -43,8 +43,8 @@ public class Crypto : ICrypto private static readonly Encoding DATA_ENCODING = Encoding.UTF8; private static readonly Encoding JSON_ENCODING = Encoding.UTF8; - private const int TYPE_0 = 0; - private const int TYPE_1 = 1; + public const int TYPE_0 = 0; + public const int TYPE_1 = 1; private const int TYPE_LENGTH = 1; private const int IV_LENGTH = 12; private const int KEY_LENGTH = 32; @@ -578,11 +578,20 @@ private void IsInitialized() { if (!this._initialized) { - throw WalletConnectException.FromType(ErrorType.NOT_INITIALIZED, new {Name}); + throw WalletConnectException.FromType(ErrorType.NOT_INITIALIZED, new Dictionary() + { + { "Name", Name } + }); } } - private string HashKey(string key) + /// + /// Hash a hex key string using SHA256. The input key string must be a hex + /// string and the returned hash is represented as a hex string + /// + /// The input hex key string to hash using SHA256 + /// The hash of the given input as a hex string + public string HashKey(string key) { using (SHA256 sha256 = SHA256.Create()) { diff --git a/Core Modules/WalletConnectSharp.Crypto/Interfaces/ICrypto.cs b/Core Modules/WalletConnectSharp.Crypto/Interfaces/ICrypto.cs index 32f5ea8..5ed84f4 100644 --- a/Core Modules/WalletConnectSharp.Crypto/Interfaces/ICrypto.cs +++ b/Core Modules/WalletConnectSharp.Crypto/Interfaces/ICrypto.cs @@ -121,5 +121,13 @@ public interface ICrypto : IModule /// /// The client id as a string Task GetClientId(); + + /// + /// Hash a hex key string using SHA256. The input key string must be a hex + /// string and the returned hash is represented as a hex string + /// + /// The input hex key string to hash using SHA256 + /// The hash of the given input as a hex string + string HashKey(string key); } } diff --git a/Core Modules/WalletConnectSharp.Crypto/KeyChain.cs b/Core Modules/WalletConnectSharp.Crypto/KeyChain.cs index c3671af..511b57a 100644 --- a/Core Modules/WalletConnectSharp.Crypto/KeyChain.cs +++ b/Core Modules/WalletConnectSharp.Crypto/KeyChain.cs @@ -142,7 +142,10 @@ public async Task Get(string tag) if (!await Has(tag)) { - throw WalletConnectException.FromType(ErrorType.NO_MATCHING_KEY, new {tag}); + throw WalletConnectException.FromType(ErrorType.NO_MATCHING_KEY, new Dictionary() + { + {"tag", tag} + }); } return this._keyChain[tag]; @@ -160,7 +163,10 @@ public async Task Delete(string tag) if (!await Has(tag)) { - throw WalletConnectException.FromType(ErrorType.NO_MATCHING_KEY, new {tag}); + throw WalletConnectException.FromType(ErrorType.NO_MATCHING_KEY, new Dictionary() + { + {"tag", tag} + }); } _keyChain.Remove(tag); @@ -172,7 +178,10 @@ private void IsInitialized() { if (!this._initialized) { - throw WalletConnectException.FromType(ErrorType.NOT_INITIALIZED, new {Name}); + throw WalletConnectException.FromType(ErrorType.NOT_INITIALIZED, new Dictionary() + { + {"Name", Name} + }); } } diff --git a/Core Modules/WalletConnectSharp.Crypto/Models/DecodeOptions.cs b/Core Modules/WalletConnectSharp.Crypto/Models/DecodeOptions.cs index b071425..1bcec66 100644 --- a/Core Modules/WalletConnectSharp.Crypto/Models/DecodeOptions.cs +++ b/Core Modules/WalletConnectSharp.Crypto/Models/DecodeOptions.cs @@ -11,6 +11,6 @@ public class DecodeOptions /// The public key that received this encoded message /// [JsonProperty("receiverPublicKey")] - public string ReceiverPublicKey { get; set; } + public string ReceiverPublicKey; } } diff --git a/Core Modules/WalletConnectSharp.Crypto/Models/EncodeOptions.cs b/Core Modules/WalletConnectSharp.Crypto/Models/EncodeOptions.cs index 498663d..3c280c0 100644 --- a/Core Modules/WalletConnectSharp.Crypto/Models/EncodeOptions.cs +++ b/Core Modules/WalletConnectSharp.Crypto/Models/EncodeOptions.cs @@ -11,18 +11,18 @@ public class EncodeOptions /// The envelope type to use /// [JsonProperty("type")] - public int Type { get; set; } + public int Type; /// /// The public key that is sending the encoded message /// [JsonProperty("senderPublicKey")] - public string SenderPublicKey { get; set; } + public string SenderPublicKey; /// /// The public key that is receiving the encoded message /// [JsonProperty("receiverPublicKey")] - public string ReceiverPublicKey { get; set; } + public string ReceiverPublicKey; } } diff --git a/Core Modules/WalletConnectSharp.Crypto/Models/EncodingParams.cs b/Core Modules/WalletConnectSharp.Crypto/Models/EncodingParams.cs index bc28987..2e91cbf 100644 --- a/Core Modules/WalletConnectSharp.Crypto/Models/EncodingParams.cs +++ b/Core Modules/WalletConnectSharp.Crypto/Models/EncodingParams.cs @@ -12,24 +12,24 @@ public class EncodingParams /// The envelope type as raw bytes /// [JsonProperty("type")] - public byte[] Type { get; set; } + public byte[] Type; /// /// The sealed encoded message as raw bytes /// [JsonProperty("sealed")] - public byte[] Sealed { get; set; } + public byte[] Sealed; /// /// The IV of the encoded message as raw bytes /// [JsonProperty("iv")] - public byte[] Iv { get; set; } + public byte[] Iv; /// /// The public key of the sender as raw bytes /// [JsonProperty("senderPublicKey")] - public byte[] SenderPublicKey { get; set; } + public byte[] SenderPublicKey; } } diff --git a/Core Modules/WalletConnectSharp.Crypto/Models/EncodingValidation.cs b/Core Modules/WalletConnectSharp.Crypto/Models/EncodingValidation.cs index c4c4118..b0aa633 100644 --- a/Core Modules/WalletConnectSharp.Crypto/Models/EncodingValidation.cs +++ b/Core Modules/WalletConnectSharp.Crypto/Models/EncodingValidation.cs @@ -11,18 +11,18 @@ public class EncodingValidation /// The envelope type to validate /// [JsonProperty("type")] - public int Type { get; set; } + public int Type; /// /// The sender public key to validate /// [JsonProperty("senderPublicKey")] - public string SenderPublicKey { get; set; } + public string SenderPublicKey; /// /// The receiver public key to validate /// [JsonProperty("receiverPublicKey")] - public string ReceiverPublicKey { get; set; } + public string ReceiverPublicKey; } } diff --git a/Core Modules/WalletConnectSharp.Crypto/Models/EncryptParams.cs b/Core Modules/WalletConnectSharp.Crypto/Models/EncryptParams.cs index 986b971..5cd154f 100644 --- a/Core Modules/WalletConnectSharp.Crypto/Models/EncryptParams.cs +++ b/Core Modules/WalletConnectSharp.Crypto/Models/EncryptParams.cs @@ -11,30 +11,30 @@ public class EncryptParams /// The message to encrypt /// [JsonProperty("message")] - public string Message { get; set; } + public string Message; /// /// The Sym key to use for encrypting /// [JsonProperty("symKey")] - public string SymKey { get; set; } + public string SymKey; /// /// The envelope type to use when encrypting /// [JsonProperty("type")] - public int Type { get; set; } + public int Type; /// /// The IV to use for the encryption /// [JsonProperty("iv")] - public string Iv { get; set; } + public string Iv; /// /// The public key of the sender of this encrypted message /// [JsonProperty("senderPublicKey")] - public string SenderPublicKey { get; set; } + public string SenderPublicKey; } } diff --git a/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTData.cs b/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTData.cs index 181e412..58345c9 100644 --- a/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTData.cs +++ b/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTData.cs @@ -11,12 +11,12 @@ public class IridiumJWTData /// The Iridium JWT header data /// [JsonProperty("header")] - public IridiumJWTHeader Header { get; set; } + public IridiumJWTHeader Header; /// /// The Iridium JWT payload /// [JsonProperty("payload")] - public IridiumJWTPayload Payload { get; set; } + public IridiumJWTPayload Payload; } } diff --git a/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTDecoded.cs b/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTDecoded.cs index 8908f74..952eb27 100644 --- a/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTDecoded.cs +++ b/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTDecoded.cs @@ -6,6 +6,6 @@ namespace WalletConnectSharp.Crypto.Models public class IridiumJWTDecoded : IridiumJWTSigned { [JsonProperty("data")] - public byte[] Data { get; set; } + public byte[] Data; } } diff --git a/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTHeader.cs b/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTHeader.cs index 42999c6..cc5f1aa 100644 --- a/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTHeader.cs +++ b/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTHeader.cs @@ -5,6 +5,7 @@ namespace WalletConnectSharp.Crypto.Models /// /// The header data for an Iridium JWT header /// + [Serializable] public class IridiumJWTHeader { /// @@ -15,17 +16,15 @@ public class IridiumJWTHeader Alg = "EdDSA", Typ = "JWT" }; - + /// /// The encoding algorithm to use /// - [JsonProperty("alg")] - public string Alg { get; set; } - + [JsonProperty("alg")] public string Alg; + /// /// The encoding type to use /// - [JsonProperty("typ")] - public string Typ { get; set; } + [JsonProperty("typ")] public string Typ; } } diff --git a/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTPayload.cs b/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTPayload.cs index 30149c2..9897b3b 100644 --- a/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTPayload.cs +++ b/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTPayload.cs @@ -5,36 +5,32 @@ namespace WalletConnectSharp.Crypto.Models /// /// The data for an Iridium JWT payload /// + [Serializable] public class IridiumJWTPayload { /// /// The iss value /// - [JsonProperty("iss")] - public string Iss { get; set; } - + [JsonProperty("iss")] public string Iss; + /// /// The sub value /// - [JsonProperty("sub")] - public string Sub { get; set; } - + [JsonProperty("sub")] public string Sub; + /// /// The aud value /// - [JsonProperty("aud")] - public string Aud { get; set; } - + [JsonProperty("aud")] public string Aud; + /// /// The iat value /// - [JsonProperty("iat")] - public long Iat { get; set; } - + [JsonProperty("iat")] public long Iat; + /// /// The exp value /// - [JsonProperty("exp")] - public long Exp { get; set; } + [JsonProperty("exp")] public long Exp; } } diff --git a/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTSigned.cs b/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTSigned.cs index 6b01948..e684da4 100644 --- a/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTSigned.cs +++ b/Core Modules/WalletConnectSharp.Crypto/Models/IridiumJWTSigned.cs @@ -8,6 +8,6 @@ namespace WalletConnectSharp.Crypto.Models public class IridiumJWTSigned : IridiumJWTData { [JsonProperty("signature")] - public byte[] Signature { get; set; } + public byte[] Signature; } } diff --git a/Core Modules/WalletConnectSharp.Events/EventDelegator.cs b/Core Modules/WalletConnectSharp.Events/EventDelegator.cs index a616d4d..da15cfc 100644 --- a/Core Modules/WalletConnectSharp.Events/EventDelegator.cs +++ b/Core Modules/WalletConnectSharp.Events/EventDelegator.cs @@ -4,6 +4,7 @@ using System.Reflection; using Newtonsoft.Json; using WalletConnectSharp.Common; +using WalletConnectSharp.Common.Logging; using WalletConnectSharp.Common.Model; using WalletConnectSharp.Events.Model; @@ -74,7 +75,7 @@ public void ListenFor(string eventId, Action callback) /// The callback to invoke when the event is triggered /// The type of event data the callback MUST be given public void ListenFor(string eventId, EventHandler> callback) - { + { EventManager>.InstanceOf(Context).EventTriggers[eventId] += callback; } @@ -231,8 +232,10 @@ from type in assembly.GetTypes() propagateEventMethod.Invoke(genericProvider, new object[] { eventId, eventData }); wasTriggered = true; } - catch (Exception) + catch (Exception e) { + WCLogger.LogError(e); + WCLogger.Log($"[EventDelegator] Error Invoking EventFactory<{type.FullName}>.Provider.PropagateEvent({eventId}, {eventData})"); if (raiseOnException) throw; } diff --git a/Core Modules/WalletConnectSharp.Network.Websocket/WalletConnectSharp.Network.Websocket.csproj b/Core Modules/WalletConnectSharp.Network.Websocket/WalletConnectSharp.Network.Websocket.csproj index 428bd01..6070f63 100644 --- a/Core Modules/WalletConnectSharp.Network.Websocket/WalletConnectSharp.Network.Websocket.csproj +++ b/Core Modules/WalletConnectSharp.Network.Websocket/WalletConnectSharp.Network.Websocket.csproj @@ -19,6 +19,16 @@ Apache-2.0 + + TRACE +WC_DEF_WEBSOCKET + + + + TRACE +WC_DEF_WEBSOCKET + + diff --git a/Core Modules/WalletConnectSharp.Network.Websocket/WebsocketConnection.cs b/Core Modules/WalletConnectSharp.Network.Websocket/WebsocketConnection.cs index 643c33d..487a631 100644 --- a/Core Modules/WalletConnectSharp.Network.Websocket/WebsocketConnection.cs +++ b/Core Modules/WalletConnectSharp.Network.Websocket/WebsocketConnection.cs @@ -36,6 +36,12 @@ public string Url } } + public bool IsPaused + { + get; + internal set; + } + /// /// The name of this websocket connection module /// @@ -180,6 +186,9 @@ private async Task Register(string url) private void OnOpen(WebsocketClient socket) { + if (socket == null) + return; + socket.MessageReceived.Subscribe(OnPayload); socket.DisconnectionHappened.Subscribe(OnDisconnect); @@ -222,6 +231,8 @@ private void OnPayload(ResponseMessage obj) } if (string.IsNullOrWhiteSpace(json)) return; + + //Console.WriteLine($"[{Name}] Got payload {json}"); Events.Trigger(WebsocketConnectionEvents.Payload, json); } @@ -322,7 +333,7 @@ private void OnError(IJsonRpcPayload ogPayload, Exception e) ? new IOException("Unavailable WS RPC url at " + _url) : e; var message = exception.Message; - var payload = new JsonRpcResponse(ogPayload.Id, new ErrorResponse() + var payload = new JsonRpcResponse(ogPayload.Id, new Error() { Code = exception.HResult, Data = null, diff --git a/Core Modules/WalletConnectSharp.Network.Websocket/WebsocketConnectionBuilder.cs b/Core Modules/WalletConnectSharp.Network.Websocket/WebsocketConnectionBuilder.cs new file mode 100644 index 0000000..7bc1cec --- /dev/null +++ b/Core Modules/WalletConnectSharp.Network.Websocket/WebsocketConnectionBuilder.cs @@ -0,0 +1,12 @@ +using WalletConnectSharp.Network.Interfaces; + +namespace WalletConnectSharp.Network.Websocket +{ + public class WebsocketConnectionBuilder : IConnectionBuilder + { + public Task CreateConnection(string url) + { + return Task.FromResult(new WebsocketConnection(url)); + } + } +} diff --git a/Core Modules/WalletConnectSharp.Network/Interfaces/IConnectionBuilder.cs b/Core Modules/WalletConnectSharp.Network/Interfaces/IConnectionBuilder.cs new file mode 100644 index 0000000..f7a49cd --- /dev/null +++ b/Core Modules/WalletConnectSharp.Network/Interfaces/IConnectionBuilder.cs @@ -0,0 +1,7 @@ +namespace WalletConnectSharp.Network.Interfaces +{ + public interface IConnectionBuilder + { + Task CreateConnection(string url); + } +} diff --git a/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcConnection.cs b/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcConnection.cs index 8693d69..33d72a5 100644 --- a/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcConnection.cs +++ b/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcConnection.cs @@ -24,6 +24,8 @@ public interface IJsonRpcConnection : IEvents, IDisposable /// string Url { get; } + bool IsPaused { get; } + /// /// Open this connection /// @@ -74,4 +76,4 @@ public interface IJsonRpcConnection : IEvents, IDisposable /// A task that is performing the send Task SendError(IJsonRpcError errorPayload, object context); } -} \ No newline at end of file +} diff --git a/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcError.cs b/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcError.cs index 02c99e3..d622751 100644 --- a/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcError.cs +++ b/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcError.cs @@ -11,7 +11,6 @@ public interface IJsonRpcError : IJsonRpcPayload /// /// The error for this JSON RPC response or null if no error is present /// - [JsonProperty("error")] - ErrorResponse Error { get; } + Error Error { get; } } -} \ No newline at end of file +} diff --git a/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcPayload.cs b/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcPayload.cs index a4cc31d..59b7571 100644 --- a/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcPayload.cs +++ b/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcPayload.cs @@ -10,13 +10,11 @@ public interface IJsonRpcPayload /// /// The unique id for this JSON RPC payload /// - [JsonProperty("id")] long Id { get; } /// /// The version of this JSON RPC payload /// - [JsonProperty("jsonrpc")] string JsonRPC { get; } } -} \ No newline at end of file +} diff --git a/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcResult.cs b/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcResult.cs index 9dfeca1..94aacf3 100644 --- a/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcResult.cs +++ b/Core Modules/WalletConnectSharp.Network/Interfaces/IJsonRpcResult.cs @@ -12,7 +12,6 @@ public interface IJsonRpcResult : IJsonRpcError /// /// The result data of the response to the request /// - [JsonProperty("result")] T Result { get; } } -} \ No newline at end of file +} diff --git a/Core Modules/WalletConnectSharp.Network/Interfaces/IRequestArguments.cs b/Core Modules/WalletConnectSharp.Network/Interfaces/IRequestArguments.cs index e7152fa..a74dfae 100644 --- a/Core Modules/WalletConnectSharp.Network/Interfaces/IRequestArguments.cs +++ b/Core Modules/WalletConnectSharp.Network/Interfaces/IRequestArguments.cs @@ -11,13 +11,11 @@ public interface IRequestArguments /// /// The method for this request /// - [JsonProperty("method")] string Method { get; } /// /// The parameter for this request /// - [JsonProperty("params")] T Params { get; } } -} \ No newline at end of file +} diff --git a/Core Modules/WalletConnectSharp.Network/JsonRpcProvider.cs b/Core Modules/WalletConnectSharp.Network/JsonRpcProvider.cs index 1b6c299..9082c96 100644 --- a/Core Modules/WalletConnectSharp.Network/JsonRpcProvider.cs +++ b/Core Modules/WalletConnectSharp.Network/JsonRpcProvider.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using WalletConnectSharp.Common; +using WalletConnectSharp.Common.Logging; using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Events; using WalletConnectSharp.Events.Model; @@ -106,7 +107,9 @@ public async Task Connect(string connection) Connecting = new TaskCompletionSource(); _connectingStarted = true; + WCLogger.Log("[JsonRpcProvider] Opening connection"); await this._connection.Open(connection); + FinalizeConnection(this._connection); } @@ -116,9 +119,10 @@ public async Task Connect(string connection) /// The connection object to use to connect public async Task Connect(IJsonRpcConnection connection) { - if (this._connection == connection && connection.Connected) return; + if (this._connection.Url == connection.Url && connection.Connected) return; if (this._connection.Connected) { + WCLogger.Log("Current connection still open, closing connection"); await this._connection.Close(); } @@ -126,6 +130,7 @@ public async Task Connect(IJsonRpcConnection connection) Connecting = new TaskCompletionSource(); _connectingStarted = true; + WCLogger.Log("[JsonRpcProvider] Opening connection"); await connection.Open(); FinalizeConnection(connection); @@ -133,6 +138,7 @@ public async Task Connect(IJsonRpcConnection connection) private void FinalizeConnection(IJsonRpcConnection connection) { + WCLogger.Log("[JsonRpcProvider] Finalizing Connection, registering event listeners"); this._connection = connection; RegisterEventListeners(); Events.Trigger(ProviderEvents.Connect, connection); @@ -148,6 +154,8 @@ 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); } @@ -172,10 +180,12 @@ public async Task Disconnect() /// A Task that will resolve when a response is received public async Task Request(IRequestArguments requestArgs, object context = null) { - if (IsConnecting) + WCLogger.Log("[JsonRpcProvider] Checking if connected"); + if (IsConnecting) await Connecting.Task; else if (!_connectingStarted && !_connection.Connected) { + WCLogger.Log("[JsonRpcProvider] Not connected, connecting now"); await Connect(_connection); } @@ -225,9 +235,11 @@ 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)}"); await _connection.SendRequest(request, context); + WCLogger.Log("[JsonRpcProvider] Awaiting request resuult"); await requestTask.Task; return requestTask.Task.Result; @@ -237,6 +249,7 @@ protected void RegisterEventListeners() { if (_hasRegisteredEventListeners) return; + WCLogger.Log($"[JsonRpcProvider] Registering event listeners on connection object with context {_connection.Events.Context}"); _connection.On("payload", OnPayload); _connection.On("close", OnConnectionDisconnected); _connection.On("error", OnConnectionError); @@ -256,6 +269,8 @@ private void OnConnectionDisconnected(object sender, GenericEvent e) private void OnPayload(object sender, GenericEvent e) { var json = e.EventData; + + WCLogger.Log($"[JsonRpcProvider] Got payload {json}"); var payload = JsonConvert.DeserializeObject(json); @@ -267,6 +282,8 @@ private void OnPayload(object sender, GenericEvent e) if (payload.Id == 0) payload.Id = _lastId; + WCLogger.Log($"[JsonRpcProvider] Payload has ID {payload.Id}"); + Events.Trigger(ProviderEvents.Payload, payload); if (payload.IsRequest) @@ -282,6 +299,7 @@ private void OnPayload(object sender, GenericEvent e) } else { + WCLogger.Log($"Triggering event for ID {payload.Id.ToString()}"); Events.Trigger(payload.Id.ToString(), json); } } diff --git a/Core Modules/WalletConnectSharp.Network/Models/Error.cs b/Core Modules/WalletConnectSharp.Network/Models/Error.cs new file mode 100644 index 0000000..aa68838 --- /dev/null +++ b/Core Modules/WalletConnectSharp.Network/Models/Error.cs @@ -0,0 +1,151 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; +using WalletConnectSharp.Common; +using WalletConnectSharp.Common.Model.Errors; + +namespace WalletConnectSharp.Network.Models +{ + /// + /// Indicates an error + /// + public class Error + { + /// + /// The error code of this error + /// + [JsonProperty("code")] + public long Code; + + /// + /// The message for this error + /// + [JsonProperty("message")] + public string Message; + + /// + /// Any extra data for this error + /// + [JsonProperty("data")] + public string Data; + + /// + /// Create an ErrorResponse with a given ErrorType and (optional) parameters + /// + /// The error type of the ErrorResponse to create + /// The message to attach to the error + /// A new ErrorResponse + public static Error FromErrorType(ErrorType type, string message) + { + return FromErrorType(type, new Dictionary() { { "message", message } }); + } + + /// + /// Create an ErrorResponse with a given ErrorType and (optional) parameters + /// + /// The error type of the ErrorResponse to create + /// Extra parameters for the error message + /// Extra data that is stored in the Data field of the newly created ErrorResponse + /// A new ErrorResponse + public static Error FromErrorType(ErrorType type, Dictionary @params = null, string extraData = null) + { + string message = SdkErrors.MessageFromType(type, @params); + + return new Error() + { + Code = (long) type, + Message = message, + Data = extraData + }; + } + + /// + /// Create an ErrorResponse from a WalletConnectException + /// + /// The exception to grab error values from + /// A new ErrorResponse object using values from the given exception + public static Error FromException(WalletConnectException walletConnectException) + { + return new Error() + { + Code = walletConnectException.Code, + Message = walletConnectException.Message, + Data = walletConnectException.ToString() + }; + } + + /// + /// Convert this ErrorResponse to a WalletConnectException + /// + /// A new WalletConnectException using values from this ErrorResponse + public WalletConnectException ToException() + { + return WalletConnectException.FromType((ErrorType)Code, Message); + } + + private sealed class CodeMessageDataEqualityComparer : IEqualityComparer + { + public bool Equals(Error x, Error y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (ReferenceEquals(x, null)) + { + return false; + } + + if (ReferenceEquals(y, null)) + { + return false; + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + return x.Code == y.Code && x.Message == y.Message && x.Data == y.Data; + } + + public int GetHashCode(Error obj) + { + return HashCode.Combine(obj.Code, obj.Message, obj.Data); + } + } + + public static IEqualityComparer CodeMessageDataComparer { get; } = new CodeMessageDataEqualityComparer(); + + protected bool Equals(Error other) + { + return Code == other.Code && Message == other.Message && Data == other.Data; + } + + 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((Error)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Code, Message, Data); + } + } +} diff --git a/Core Modules/WalletConnectSharp.Network/Models/ErrorResponse.cs b/Core Modules/WalletConnectSharp.Network/Models/ErrorResponse.cs deleted file mode 100644 index cf9e0fe..0000000 --- a/Core Modules/WalletConnectSharp.Network/Models/ErrorResponse.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; -using WalletConnectSharp.Common; -using WalletConnectSharp.Common.Model.Errors; - -namespace WalletConnectSharp.Network.Models -{ - /// - /// Indicates an error - /// - public class ErrorResponse - { - /// - /// The error code of this error - /// - [JsonProperty("code")] - public long Code; - - /// - /// The message for this error - /// - [JsonProperty("message")] - public string Message; - - /// - /// Any extra data for this error - /// - [JsonProperty("data")] - public string Data; - - /// - /// Create an ErrorResponse with a given ErrorType and (optional) parameters - /// - /// The error type of the ErrorResponse to create - /// Extra parameters for the error message - /// Extra data that is stored in the Data field of the newly created ErrorResponse - /// A new ErrorResponse - public static ErrorResponse FromErrorType(ErrorType type, object @params = null, string extraData = null) - { - string message = SdkErrors.MessageFromType(type, @params); - - return new ErrorResponse() - { - Code = (long) type, - Message = message, - Data = extraData - }; - } - - /// - /// Create an ErrorResponse from a WalletConnectException - /// - /// The exception to grab error values from - /// A new ErrorResponse object using values from the given exception - public static ErrorResponse FromException(WalletConnectException walletConnectException) - { - return new ErrorResponse() - { - Code = walletConnectException.Code, - Message = walletConnectException.Message, - Data = walletConnectException.ToString() - }; - } - - /// - /// Convert this ErrorResponse to a WalletConnectException - /// - /// A new WalletConnectException using values from this ErrorResponse - public WalletConnectException ToException() - { - return WalletConnectException.FromType((ErrorType)Code, Message); - } - } -} diff --git a/Core Modules/WalletConnectSharp.Network/Models/JsonRpcError.cs b/Core Modules/WalletConnectSharp.Network/Models/JsonRpcError.cs index 9503580..52d1abe 100644 --- a/Core Modules/WalletConnectSharp.Network/Models/JsonRpcError.cs +++ b/Core Modules/WalletConnectSharp.Network/Models/JsonRpcError.cs @@ -11,25 +11,35 @@ public class JsonRpcError : IJsonRpcError /// The id field /// [JsonProperty("id")] - public long Id { get; set; } + private long _id; + + [JsonIgnore] + public long Id => _id; + + [JsonProperty("jsonrpc")] + private string _jsonRpc = "2.0"; /// /// The jsonrpc field /// - [JsonProperty("jsonrpc")] + [JsonIgnore] public string JsonRPC { get { - return "2.0"; + return _jsonRpc; } } + + [JsonProperty("error", NullValueHandling = NullValueHandling.Ignore)] + private Error _error; + /// /// The error field /// - [JsonProperty("error", NullValueHandling = NullValueHandling.Ignore)] - public ErrorResponse Error { get; set; } + [JsonIgnore] + public Error Error => _error; /// /// Create a blank JSON rpc error response @@ -43,10 +53,10 @@ public JsonRpcError() /// /// The id of the response /// The error value - public JsonRpcError(long id, ErrorResponse error) + public JsonRpcError(long id, Error error) { - Id = id; - Error = error; + _id = id; + _error = error; } } } diff --git a/Core Modules/WalletConnectSharp.Network/Models/JsonRpcPayload.cs b/Core Modules/WalletConnectSharp.Network/Models/JsonRpcPayload.cs index 0cc47fb..7e9e09c 100644 --- a/Core Modules/WalletConnectSharp.Network/Models/JsonRpcPayload.cs +++ b/Core Modules/WalletConnectSharp.Network/Models/JsonRpcPayload.cs @@ -11,15 +11,27 @@ namespace WalletConnectSharp.Network.Models /// public class JsonRpcPayload : IJsonRpcPayload { + [JsonProperty("id")] + private long _id; + + [JsonProperty("jsonrpc")] + private string _jsonRPC = "2.0"; + /// /// The JSON RPC id for this payload /// - public long Id { get; set; } - + [JsonIgnore] + public long Id + { + get => _id; + set => _id = value; + } + /// /// The JSON RPC version for this payload /// - public string JsonRPC { get; set; } + [JsonIgnore] + public string JsonRPC => _jsonRPC; [JsonExtensionData] #pragma warning disable CS0649 diff --git a/Core Modules/WalletConnectSharp.Network/Models/JsonRpcRequest.cs b/Core Modules/WalletConnectSharp.Network/Models/JsonRpcRequest.cs index 428f015..3946884 100644 --- a/Core Modules/WalletConnectSharp.Network/Models/JsonRpcRequest.cs +++ b/Core Modules/WalletConnectSharp.Network/Models/JsonRpcRequest.cs @@ -9,35 +9,59 @@ namespace WalletConnectSharp.Network.Models /// The parameter type for this JSON RPC request public class JsonRpcRequest : IJsonRpcRequest { + [JsonProperty("method")] + private string _method; + /// /// The method of this Json rpc request /// - public string Method { get; set; } - + [JsonIgnore] + public string Method + { + get => _method; + set => _method = value; + } + + [JsonProperty("params")] + private T _params; + /// /// The parameters of this Json rpc request /// - public T Params { get; set; } - + [JsonIgnore] + public T Params + { + get => _params; + set => _params = value; + } + + [JsonProperty("id")] + private long _id; + /// /// The id of this Json rpc request /// - public long Id { get; set; } + [JsonIgnore] + public long Id + { + get => _id; + set => _id = value; + } + [JsonProperty("jsonrpc")] + private string _jsonRPC = "2.0"; + /// - /// The jsonrpc field + /// The JSON RPC version for this payload /// - public string JsonRPC - { - get - { - return "2.0"; - } - } + [JsonIgnore] + public string JsonRPC => _jsonRPC; + /// /// Create a blank Json rpc request /// + [JsonConstructor] public JsonRpcRequest() { } @@ -56,9 +80,9 @@ public JsonRpcRequest(string method, T param, long? id = null) id = RpcPayloadId.Generate(); } - this.Method = method; - this.Params = param; - this.Id = (long)id; + this._method = method; + this._params = param; + this._id = (long)id; } } } diff --git a/Core Modules/WalletConnectSharp.Network/Models/JsonRpcResponse.cs b/Core Modules/WalletConnectSharp.Network/Models/JsonRpcResponse.cs index 30594e1..ee55dbc 100644 --- a/Core Modules/WalletConnectSharp.Network/Models/JsonRpcResponse.cs +++ b/Core Modules/WalletConnectSharp.Network/Models/JsonRpcResponse.cs @@ -8,35 +8,51 @@ namespace WalletConnectSharp.Network.Models /// The type of the result property for this JSON RPC response public class JsonRpcResponse : IJsonRpcResult { - /// - /// The id of this Json rpc response, should match the original request - /// + [JsonProperty("id")] - public long Id { get; set; } + private long _id; /// - /// The jsonrpc field + /// The id of this Json rpc response, should match the original request /// - [JsonProperty("jsonrpc")] - public string JsonRPC + [JsonIgnore] + public long Id { - get - { - return "2.0"; - } + get => _id; + set => _id = value; } + + [JsonProperty("jsonrpc")] + private string _jsonRPC = "2.0"; /// - /// The error field for this response, if one is present + /// The JSON RPC version for this payload /// + [JsonIgnore] + public string JsonRPC => _jsonRPC; + [JsonProperty("error", NullValueHandling = NullValueHandling.Ignore)] - public ErrorResponse Error { get; set; } + private Error _error; + + /// + /// The error field + /// + [JsonIgnore] + public Error Error => _error; + + [JsonProperty("result", NullValueHandling = NullValueHandling.Ignore)] + private T _result; + /// /// The result field for this response, if one is present /// - [JsonProperty("result", NullValueHandling = NullValueHandling.Ignore)] - public T Result { get; set; } + [JsonIgnore] + public T Result + { + get => _result; + set => _result = value; + } /// /// Whether or not this response is an error response @@ -53,6 +69,7 @@ public bool IsError /// /// Create a blank Json rpc response /// + [JsonConstructor] public JsonRpcResponse() { } @@ -64,11 +81,11 @@ public JsonRpcResponse() /// The id of this json response /// The error of this json response, if one is present /// The result of this json response, if one is present - public JsonRpcResponse(long id, ErrorResponse error, T result) + public JsonRpcResponse(long id, Error error, T result) { - Id = id; - Error = error; - Result = result; + _id = id; + _error = error; + _result = result; } } } diff --git a/Core Modules/WalletConnectSharp.Network/Models/RequestArguments.cs b/Core Modules/WalletConnectSharp.Network/Models/RequestArguments.cs index 9682dd5..9316779 100644 --- a/Core Modules/WalletConnectSharp.Network/Models/RequestArguments.cs +++ b/Core Modules/WalletConnectSharp.Network/Models/RequestArguments.cs @@ -1,3 +1,5 @@ +using Newtonsoft.Json; + namespace WalletConnectSharp.Network.Models { /// @@ -8,14 +10,30 @@ namespace WalletConnectSharp.Network.Models /// The type of the parameter in this request public class RequestArguments : IRequestArguments { + [JsonProperty("method")] + private string _method; + + [JsonProperty("params")] + private T _params; + /// /// The method to use /// - public string Method { get; set; } - + [JsonIgnore] + public string Method + { + get => _method; + set => _method = value; + } + /// /// The method for this request /// - public T Params { get; set; } + [JsonIgnore] + public T Params + { + get => _params; + set => _params = value; + } } } diff --git a/Core Modules/WalletConnectSharp.Network/Models/RpcOptionsAttribute.cs b/Core Modules/WalletConnectSharp.Network/Models/RpcOptionsAttribute.cs index 39db21e..5530998 100644 --- a/Core Modules/WalletConnectSharp.Network/Models/RpcOptionsAttribute.cs +++ b/Core Modules/WalletConnectSharp.Network/Models/RpcOptionsAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; namespace WalletConnectSharp.Network.Models { diff --git a/Core Modules/WalletConnectSharp.Network/Models/RpcRequestOptionsAttribute.cs b/Core Modules/WalletConnectSharp.Network/Models/RpcRequestOptionsAttribute.cs index d6bd813..2906461 100644 --- a/Core Modules/WalletConnectSharp.Network/Models/RpcRequestOptionsAttribute.cs +++ b/Core Modules/WalletConnectSharp.Network/Models/RpcRequestOptionsAttribute.cs @@ -10,6 +10,30 @@ namespace WalletConnectSharp.Network.Models [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] public class RpcRequestOptionsAttribute : RpcOptionsAttribute { + /// + /// Returns the first RpcOptionsAttribute found on the given type T. + /// If no attribute is found, then null is returned + /// + /// The type to inspect for RpcOptionsAttribute + /// The first RpcOptionsAttribute found on the given type T + public static RpcRequestOptionsAttribute GetOptionsForType() + { + return GetOptionsForType(typeof(T)); + } + + /// + /// Returns the first RpcOptionsAttribute found on the given type t. + /// If no attribute is found, then null is returned + /// + /// The type to inspect for RpcOptionsAttribute + /// The first RpcOptionsAttribute found on the given type t + public static RpcRequestOptionsAttribute GetOptionsForType(Type t) + { + var attribute = t.GetCustomAttributes(typeof(RpcRequestOptionsAttribute), true).Cast().FirstOrDefault(); + + return attribute; + } + public RpcRequestOptionsAttribute(long ttl, int tag) : base(ttl, tag) { } diff --git a/Core Modules/WalletConnectSharp.Network/Models/RpcResponseOptionsAttribute.cs b/Core Modules/WalletConnectSharp.Network/Models/RpcResponseOptionsAttribute.cs index b5d0af8..03574d1 100644 --- a/Core Modules/WalletConnectSharp.Network/Models/RpcResponseOptionsAttribute.cs +++ b/Core Modules/WalletConnectSharp.Network/Models/RpcResponseOptionsAttribute.cs @@ -10,6 +10,30 @@ namespace WalletConnectSharp.Network.Models [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] public class RpcResponseOptionsAttribute : RpcOptionsAttribute { + /// + /// Returns the first RpcOptionsAttribute found on the given type T. + /// If no attribute is found, then null is returned + /// + /// The type to inspect for RpcOptionsAttribute + /// The first RpcOptionsAttribute found on the given type T + public static RpcResponseOptionsAttribute GetOptionsForType() + { + return GetOptionsForType(typeof(T)); + } + + /// + /// Returns the first RpcOptionsAttribute found on the given type t. + /// If no attribute is found, then null is returned + /// + /// The type to inspect for RpcOptionsAttribute + /// The first RpcOptionsAttribute found on the given type t + public static RpcResponseOptionsAttribute GetOptionsForType(Type t) + { + var attribute = t.GetCustomAttributes(typeof(RpcResponseOptionsAttribute), true).Cast().FirstOrDefault(); + + return attribute; + } + public RpcResponseOptionsAttribute(long ttl, int tag) : base(ttl, tag) { } diff --git a/Core Modules/WalletConnectSharp.Storage/FileSystemStorage.cs b/Core Modules/WalletConnectSharp.Storage/FileSystemStorage.cs index 22685d6..2376614 100644 --- a/Core Modules/WalletConnectSharp.Storage/FileSystemStorage.cs +++ b/Core Modules/WalletConnectSharp.Storage/FileSystemStorage.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; +using WalletConnectSharp.Common.Logging; namespace WalletConnectSharp.Storage { @@ -119,6 +120,10 @@ private async Task Load() { // 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(); } diff --git a/Directory.Build.props b/Directory.Build.props index 1a62c45..642210f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 2.0.6 + 2.1.0 net6.0;netcoreapp3.1;netstandard2.1; diff --git a/Directory.Packages.props b/Directory.Packages.props index 4a1c522..bd294d5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,12 +4,15 @@ + + + \ No newline at end of file diff --git a/Tests/WalletConnectSharp.Auth.Tests/AuthClientTest.cs b/Tests/WalletConnectSharp.Auth.Tests/AuthClientTest.cs new file mode 100644 index 0000000..db6a92f --- /dev/null +++ b/Tests/WalletConnectSharp.Auth.Tests/AuthClientTest.cs @@ -0,0 +1,558 @@ +using System.Text; +using Nethereum.HdWallet; +using WalletConnectSharp.Auth.Interfaces; +using WalletConnectSharp.Auth.Internals; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Models.Pairing; +using WalletConnectSharp.Core.Models.Publisher; +using WalletConnectSharp.Core.Models.Relay; +using WalletConnectSharp.Core.Models.Verify; +using WalletConnectSharp.Events; +using WalletConnectSharp.Storage; +using WalletConnectSharp.Tests.Common; +using Xunit; +using ErrorResponse = WalletConnectSharp.Auth.Models.ErrorResponse; + +namespace WalletConnectSharp.Auth.Tests +{ + public class AuthClientTests : IClassFixture, IAsyncLifetime + { + private static readonly RequestParams DefaultRequestParams = new RequestParams() + { + Aud = "http://localhost:3000/login", + Domain = "localhost:3000", + ChainId = "eip155:1", + Nonce = CryptoUtils.GenerateNonce() + }; + + private readonly CryptoWalletFixture _cryptoWalletFixture; + + private IAuthClient PeerA; + public IAuthClient PeerB; + + public string Iss + { + get + { + return _cryptoWalletFixture.Iss; + } + } + + public Wallet CryptoWallet + { + get + { + return _cryptoWalletFixture.CryptoWallet; + } + } + + public string WalletAddress + { + get + { + return _cryptoWalletFixture.WalletAddress; + } + } + + public AuthClientTests(CryptoWalletFixture cryptoFixture) + { + this._cryptoWalletFixture = cryptoFixture; + } + + [Fact, Trait("Category", "unit")] + public async void TestInit() + { + Assert.NotNull(PeerA); + Assert.NotNull(PeerB); + + Assert.NotNull(PeerA.Core); + Assert.NotNull(PeerA.Events); + Assert.NotNull(PeerA.Core.Expirer); + Assert.NotNull(PeerA.Core.History); + Assert.NotNull(PeerA.Core.Pairing); + + Assert.NotNull(PeerB.Core); + Assert.NotNull(PeerB.Events); + Assert.NotNull(PeerB.Core.Expirer); + Assert.NotNull(PeerB.Core.History); + Assert.NotNull(PeerB.Core.Pairing); + } + + [Fact, Trait("Category", "unit")] + public async void TestPairs() + { + var ogPairSize = PeerA.Core.Pairing.Pairings.Length; + + TaskCompletionSource authRequested = new TaskCompletionSource(); + + void OnPeerBOnAuthRequested(object o, AuthRequest authRequest) => authRequested.SetResult(true); + + PeerB.AuthRequested += OnPeerBOnAuthRequested; + + var uriData = await PeerA.Request(DefaultRequestParams); + var uri = uriData.Uri; + + await PeerB.Core.Pairing.Pair(uri); + + await authRequested.Task; + + Assert.Equal(PeerA.Core.Pairing.Pairings.Select(p => p.Key), PeerB.Core.Pairing.Pairings.Select(p => p.Key)); + Assert.Equal(ogPairSize + 1, PeerA.Core.Pairing.Pairings.Length); + + var peerAHistory = await PeerA.Core.History.JsonRpcHistoryOfType(); + var peerBHistory = await PeerB.Core.History.JsonRpcHistoryOfType(); + + Assert.Equal(peerAHistory.Size, peerBHistory.Size); + + Assert.True(PeerB.Core.Pairing.Pairings[0].Active); + + // Cleanup event listeners + PeerB.AuthRequested -= OnPeerBOnAuthRequested; + } + + [Fact, Trait("Category", "unit")] + public async void TestKnownPairings() + { + var ogSizeA = PeerA.Core.Pairing.Pairings.Length; + var history = await PeerA.AuthHistory(); + var ogHistorySizeA = history.Keys.Length; + + var ogSizeB = PeerB.Core.Pairing.Pairings.Length; + var historyB = await PeerB.AuthHistory(); + var ogHistorySizeB = historyB.Keys.Length; + + List responses = new List(); + TaskCompletionSource responseTask = new TaskCompletionSource(); + + async void OnPeerBOnAuthRequested(object sender, AuthRequest request) + { + var message = PeerB.FormatMessage(request.Parameters.CacaoPayload, this.Iss); + var signature = await CryptoWallet.GetAccount(WalletAddress).AccountSigningService.PersonalSign.SendRequestAsync(Encoding.UTF8.GetBytes(message)); + + await PeerB.Respond(new Cacao() { Id = request.Id, Signature = new Cacao.CacaoSignature.EIP191CacaoSignature(signature) }, this.Iss); + + Assert.Equal(Validation.Unknown, request.VerifyContext?.Validation); + } + + PeerB.AuthRequested += OnPeerBOnAuthRequested; + + void OnPeerAOnAuthResponded(object sender, AuthResponse args) + { + var sessionTopic = args.Topic; + var cacao = args.Response.Result; + var signature = cacao.Signature; + Console.WriteLine($"{sessionTopic}: {signature}"); + responses.Add(args); + responseTask.SetResult(args); + } + + PeerA.AuthResponded += OnPeerAOnAuthResponded; + + void OnPeerAOnAuthError(object sender, AuthErrorResponse args) + { + var sessionTopic = args.Topic; + var error = args.Error; + Console.WriteLine($"{sessionTopic}: {error}"); + responses.Add(args); + responseTask.SetResult(args); + } + + PeerA.AuthError += OnPeerAOnAuthError; + + var requestData = await PeerA.Request(DefaultRequestParams); + + await PeerB.Core.Pairing.Pair(requestData.Uri); + + await responseTask.Task; + + // Reset + responseTask = 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; + + Assert.Null(requestData2.Uri); + + Assert.Equal(ogSizeA + 1, PeerA.Core.Pairing.Pairings.Length); + Assert.Equal(ogHistorySizeA + 2, history.Keys.Length); + Assert.Equal(ogSizeB + 1, PeerB.Core.Pairing.Pairings.Length); + Assert.Equal(ogHistorySizeB + 2, historyB.Keys.Length); + Assert.Equal(responses[0].Topic, responses[1].Topic); + + // Cleanup event listeners + + PeerB.AuthRequested -= OnPeerBOnAuthRequested; + PeerA.AuthResponded -= OnPeerAOnAuthResponded; + PeerA.AuthError -= OnPeerAOnAuthError; + } + + [Fact, Trait("Category", "unit")] + public async void HandlesAuthRequests() + { + var ogSize = PeerB.Requests.Length; + + TaskCompletionSource receivedAuthRequest = new TaskCompletionSource(); + + void OnPeerBOnAuthRequested(object o, AuthRequest authRequest) => receivedAuthRequest.SetResult(true); + + PeerB.AuthRequested += OnPeerBOnAuthRequested; + + var requestData = await PeerA.Request(DefaultRequestParams); + + await PeerB.Core.Pairing.Pair(requestData.Uri); + + await receivedAuthRequest.Task; + + Assert.Equal(ogSize + 1, PeerB.Requests.Length); + + // Cleanup event listeners + PeerB.AuthRequested -= OnPeerBOnAuthRequested; + } + + [Fact, Trait("Category", "unit")] + public async void TestErrorResponses() + { + var ogPSize = PeerA.Core.Pairing.Pairings.Length; + + TaskCompletionSource errorResponse = new TaskCompletionSource(); + + async void OnPeerBOnAuthRequested(object sender, AuthRequest request) + { + await PeerB.Respond(new ErrorResponse() { Error = new Network.Models.Error() { Code = 14001, Message = "Can not login" }, Id = request.Id }, this.Iss); + } + void OnPeerAOnAuthResponded(object sender, AuthResponse response) + { + errorResponse.SetResult(false); + } + + void OnPeerAOnAuthError(object sender, AuthErrorResponse response) => errorResponse.SetResult(true); + + PeerB.AuthRequested += OnPeerBOnAuthRequested; + PeerA.AuthResponded += OnPeerAOnAuthResponded; + PeerA.AuthError += OnPeerAOnAuthError; + + var requestData = await PeerA.Request(DefaultRequestParams); + + Assert.Equal(ogPSize + 1, PeerA.Core.Pairing.Pairings.Length); + Assert.False(PeerA.Core.Pairing.Pairings[^1].Active); + + await PeerB.Core.Pairing.Pair(requestData.Uri); + + await errorResponse.Task; + + Assert.False(PeerA.Core.Pairing.Pairings[^1].Active); + Assert.True(errorResponse.Task.Result); + + PeerB.AuthRequested -= OnPeerBOnAuthRequested; + PeerA.AuthResponded -= OnPeerAOnAuthResponded; + PeerA.AuthError -= OnPeerAOnAuthError; + } + + [Fact, Trait("Category", "unit")] + public async void HandlesSuccessfulResponse() + { + var ogPSize = PeerA.Core.Pairing.Pairings.Length; + + TaskCompletionSource successfulResponse = new TaskCompletionSource(); + + async void OnPeerBOnAuthRequested(object sender, AuthRequest request) + { + var message = PeerB.FormatMessage(request.Parameters.CacaoPayload, this.Iss); + var signature = await CryptoWallet.GetAccount(WalletAddress).AccountSigningService.PersonalSign.SendRequestAsync(Encoding.UTF8.GetBytes(message)); + + await PeerB.Respond(new ResultResponse() { Id = request.Id, Signature = new Cacao.CacaoSignature.EIP191CacaoSignature(signature) }, this.Iss); + + Assert.Equal(Validation.Unknown, request.VerifyContext?.Validation); + } + + PeerB.AuthRequested += OnPeerBOnAuthRequested; + + void OnPeerAOnAuthResponded(object sender, AuthResponse response) => successfulResponse.SetResult(response.Response.Result?.Signature != null); + + PeerA.AuthResponded += OnPeerAOnAuthResponded; + + void OnPeerAOnAuthError(object sender, AuthErrorResponse response) => successfulResponse.SetResult(false); + + PeerA.AuthError += OnPeerAOnAuthError; + + var requestData = await PeerA.Request(DefaultRequestParams); + + Assert.Equal(ogPSize + 1, PeerA.Core.Pairing.Pairings.Length); + Assert.False(PeerA.Core.Pairing.Pairings[^1].Active); + + await PeerB.Core.Pairing.Pair(requestData.Uri); + + await successfulResponse.Task; + + Assert.True(PeerA.Core.Pairing.Pairings[^1].Active); + Assert.True(successfulResponse.Task.Result); + + PeerB.AuthRequested -= OnPeerBOnAuthRequested; + PeerA.AuthResponded -= OnPeerAOnAuthResponded; + PeerA.AuthError -= OnPeerAOnAuthError; + } + + [Fact, Trait("Category", "unit")] + public async void TestCustomRequestExpiry() + { + var uri = ""; + var expiry = 1000; + + TaskCompletionSource resolve1 = new TaskCompletionSource(); + + PeerA.Core.Relayer.Once(RelayerEvents.Publish, (sender, @event) => + { + Assert.Equal(expiry, @event.EventData.Options?.TTL); + resolve1.SetResult(true); + }); + + + await Task.WhenAll(resolve1.Task, Task.Run(async () => + { + var response = await PeerA.Request(new RequestParams(DefaultRequestParams) { Expiry = expiry }); + uri = response.Uri; + })); + + TaskCompletionSource resolve3 = new TaskCompletionSource(); + + async void OnPeerBOnAuthRequested(object sender, AuthRequest request) + { + var message = PeerB.FormatMessage(request.Parameters.CacaoPayload, this.Iss); + var signature = await CryptoWallet.GetAccount(WalletAddress).AccountSigningService.PersonalSign.SendRequestAsync(Encoding.UTF8.GetBytes(message)); + + await PeerB.Respond(new ResultResponse() { Id = request.Id, Signature = new Cacao.CacaoSignature.EIP191CacaoSignature(signature) }, this.Iss); + resolve3.SetResult(true); + } + + PeerB.AuthRequested += OnPeerBOnAuthRequested; + + await Task.WhenAll(resolve3.Task, PeerB.Core.Pairing.Pair(uri)); + + PeerB.AuthRequested -= OnPeerBOnAuthRequested; + } + + [Fact, Trait("Category", "unit")] + public async void TestGetPendingPairings() + { + var ogCount = PeerB.PendingRequests.Count; + + TaskCompletionSource receivedAuthRequest = new TaskCompletionSource(); + var aud = "http://localhost:3000/login"; + + void OnPeerBOnAuthRequested(object sender, AuthRequest request) => receivedAuthRequest.SetResult(true); + + PeerB.AuthRequested += OnPeerBOnAuthRequested; + + var requestData = await PeerA.Request(DefaultRequestParams); + + await PeerB.Core.Pairing.Pair(requestData.Uri); + + await receivedAuthRequest.Task; + + var requests = PeerB.PendingRequests; + + Assert.Equal(ogCount + 1, requests.Count); + Assert.Contains(requests, r => r.Value.CacaoPayload.Aud == aud); + + PeerB.AuthRequested -= OnPeerBOnAuthRequested; + } + + [Fact, Trait("Category", "unit")] + public async void TestGetPairings() + { + var peerAOgSize = PeerA.Core.Pairing.Pairings.Length; + var peerBOgSize = PeerB.Core.Pairing.Pairings.Length; + + TaskCompletionSource receivedAuthRequest = new TaskCompletionSource(); + + void OnPeerBOnAuthRequested(object sender, AuthRequest request) => receivedAuthRequest.SetResult(true); + + PeerB.AuthRequested += OnPeerBOnAuthRequested; + + var requestData = await PeerA.Request(DefaultRequestParams); + + await PeerB.Core.Pairing.Pair(requestData.Uri); + + await receivedAuthRequest.Task; + + var clientPairings = PeerA.Core.Pairing.Pairings; + var peerPairings = PeerB.Core.Pairing.Pairings; + + Assert.Equal(peerAOgSize + 1, clientPairings.Length); + Assert.Equal(peerBOgSize + 1, peerPairings.Length); + Assert.Equal(clientPairings[^1].Topic, peerPairings[^1].Topic); + + PeerB.AuthRequested -= OnPeerBOnAuthRequested; + } + + [Fact, Trait("Category", "unit")] + public async void TestPing() + { + TaskCompletionSource receivedAuthRequest = new TaskCompletionSource(); + TaskCompletionSource receivedClientPing = new TaskCompletionSource(); + TaskCompletionSource receivedPeerPing = new TaskCompletionSource(); + + void OnPeerBOnAuthRequested(object sender, AuthRequest request) => receivedAuthRequest.SetResult(true); + + PeerB.AuthRequested += OnPeerBOnAuthRequested; + + PeerB.Core.Pairing.Once(PairingEvents.PairingPing, (sender, @event) => + { + receivedPeerPing.SetResult(true); + }); + + PeerA.Core.Pairing.Once(PairingEvents.PairingPing, (sender, @event) => + { + receivedClientPing.SetResult(true); + }); + + var requestData = await PeerA.Request(DefaultRequestParams); + + await PeerB.Core.Pairing.Pair(requestData.Uri); + + await receivedAuthRequest.Task; + + var pairing = PeerA.Core.Pairing.Pairings[^1]; + await PeerA.Core.Pairing.Ping(pairing.Topic); + await PeerB.Core.Pairing.Ping(pairing.Topic); + + await Task.WhenAll(receivedClientPing.Task, receivedPeerPing.Task); + + Assert.True(receivedClientPing.Task.Result); + Assert.True(receivedPeerPing.Task.Result); + + PeerB.AuthRequested -= OnPeerBOnAuthRequested; + } + + [Fact, Trait("Category", "unit")] + public async void TestDisconnectedPairing() + { + var peerAOgSize = PeerA.Core.Pairing.Pairings.Length; + var peerBOgSize = PeerB.Core.Pairing.Pairings.Length; + + TaskCompletionSource receivedAuthRequest = new TaskCompletionSource(); + TaskCompletionSource peerDeletedPairing = new TaskCompletionSource(); + + void OnPeerBOnAuthRequested(object sender, AuthRequest request) => receivedAuthRequest.SetResult(true); + + PeerB.AuthRequested += OnPeerBOnAuthRequested; + + PeerB.Core.Pairing.Once(PairingEvents.PairingDelete, (sender, @event) => + { + peerDeletedPairing.SetResult(true); + }); + + var requestData = await PeerA.Request(DefaultRequestParams); + + await PeerB.Core.Pairing.Pair(requestData.Uri); + + await receivedAuthRequest.Task; + + var clientPairings = PeerA.Core.Pairing.Pairings; + var peerPairings = PeerB.Core.Pairing.Pairings; + + Assert.Equal(peerAOgSize + 1, PeerA.Core.Pairing.Pairings.Length); + Assert.Equal(peerBOgSize + 1, PeerB.Core.Pairing.Pairings.Length); + Assert.Equal(clientPairings[^1].Topic, peerPairings[^1].Topic); + + await PeerA.Core.Pairing.Disconnect(clientPairings[^1].Topic); + + await peerDeletedPairing.Task; + Assert.Equal(peerAOgSize, PeerA.Core.Pairing.Pairings.Length); + Assert.Equal(peerBOgSize, PeerB.Core.Pairing.Pairings.Length); + + PeerB.AuthRequested -= OnPeerBOnAuthRequested; + } + + [Fact, Trait("Category", "unit")] + public async void TestReceivesMetadata() + { + var receivedMetadataName = ""; + var ogPairingSize = PeerA.Core.Pairing.Pairings.Length; + + TaskCompletionSource hasResponded = new TaskCompletionSource(); + + async void OnPeerBOnAuthRequested(object sender, AuthRequest request) + { + receivedMetadataName = request.Parameters.Requester?.Metadata?.Name; + var message = PeerB.FormatMessage(request.Parameters.CacaoPayload, this.Iss); + var signature = await CryptoWallet.GetAccount(WalletAddress).AccountSigningService.PersonalSign.SendRequestAsync(Encoding.UTF8.GetBytes(message)); + + await PeerB.Respond(new ResultResponse() { Id = request.Id, Signature = new Cacao.CacaoSignature.EIP191CacaoSignature(signature) }, this.Iss); + + hasResponded.SetResult(true); + Assert.Equal(Validation.Unknown, request.VerifyContext.Validation); + } + + PeerB.AuthRequested += OnPeerBOnAuthRequested; + + var requestData = await PeerA.Request(DefaultRequestParams); + + Assert.Equal(ogPairingSize + 1, PeerA.Core.Pairing.Pairings.Length); + Assert.False(PeerA.Core.Pairing.Pairings[^1].Active); + + await PeerB.Core.Pairing.Pair(requestData.Uri); + + await hasResponded.Task; + + Assert.True(PeerA.Core.Pairing.Pairings[^1].Active); + Assert.True(hasResponded.Task.Result); + Assert.Equal(PeerA.Metadata.Name, receivedMetadataName); + PeerB.AuthRequested -= OnPeerBOnAuthRequested; + } + + public async Task InitializeAsync() + { + var OptionsA = new AuthOptions() + { + ProjectId = TestValues.TestProjectId, + RelayUrl = TestValues.TestRelayUrl, + Metadata = new Metadata() + { + Description = "An example dapp to showcase WalletConnectSharpv2", + Icons = new[] { "https://walletconnect.com/meta/favicon.ico" }, + Name = $"WalletConnectSharpv2 Dapp Example - {Guid.NewGuid().ToString()}", + Url = "https://walletconnect.com" + }, + // Omit if you want persistant storage + Storage = new InMemoryStorage(), + }; + + var OptionsB = new AuthOptions() + { + ProjectId = TestValues.TestProjectId, + RelayUrl = TestValues.TestRelayUrl, + Metadata = new Metadata() + { + Description = "An example wallet to showcase WalletConnectSharpv2", + Icons = new[] { "https://walletconnect.com/meta/favicon.ico" }, + Name = $"WalletConnectSharpv2 Wallet Example - {Guid.NewGuid().ToString()}", + Url = "https://walletconnect.com" + }, + // Omit if you want persistant storage + Storage = new InMemoryStorage() + }; + + PeerA = await WalletConnectAuthClient.Init(OptionsA); + PeerB = await WalletConnectAuthClient.Init(OptionsB); + } + + public async Task DisposeAsync() + { + if (PeerA.Core.Relayer.Connected) + { + await PeerA.Core.Relayer.TransportClose(); + } + + if (PeerB.Core.Relayer.Connected) + { + await PeerB.Core.Relayer.TransportClose(); + } + } + } +} diff --git a/Tests/WalletConnectSharp.Auth.Tests/SignatureTest.cs b/Tests/WalletConnectSharp.Auth.Tests/SignatureTest.cs new file mode 100644 index 0000000..4ba407d --- /dev/null +++ b/Tests/WalletConnectSharp.Auth.Tests/SignatureTest.cs @@ -0,0 +1,47 @@ +using WalletConnectSharp.Auth.Internals; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Tests.Common; +using Xunit; + +namespace WalletConnectSharp.Auth.Tests; + +public class SignatureTest +{ + public string ChainId = "eip155:1"; + public string ProjectId = TestValues.TestProjectId; + public string Address = "0x2faf83c542b68f1b4cdc0e770e8cb9f567b08f71"; + + public string ReconstructedMessage = @"localhost wants you to sign in with your Ethereum account: +0x2faf83c542b68f1b4cdc0e770e8cb9f567b08f71 + +URI: http://localhost:3000/ +Version: 1 +Chain ID: 1 +Nonce: 1665443015700 +Issued At: 2022-10-10T23:03:35.700Z +Expiration Time: 2022-10-11T23:03:35.700Z".Replace("\r", ""); + + [Fact, Trait("Category", "unit")] + public async void TestValidEip1271Signature() + { + var signature = new Cacao.CacaoSignature.EIP1271CacaoSignature( + "0xc1505719b2504095116db01baaf276361efd3a73c28cf8cc28dabefa945b8d536011289ac0a3b048600c1e692ff173ca944246cf7ceb319ac2262d27b395c82b1c"); + + var isValid = + await SignatureUtils.VerifySignature(Address, ReconstructedMessage, signature, ChainId, ProjectId); + + Assert.True(isValid); + } + + [Fact, Trait("Category", "unit")] + public async void TestBadEip1271Signature() + { + var signature = new Cacao.CacaoSignature.EIP1271CacaoSignature( + "0xdead5719b2504095116db01baaf276361efd3a73c28cf8cc28dabefa945b8d536011289ac0a3b048600c1e692ff173ca944246cf7ceb319ac2262d27b395c82b1c"); + + var isValid = + await SignatureUtils.VerifySignature(Address, ReconstructedMessage, signature, ChainId, ProjectId); + + Assert.False(isValid); + } +} diff --git a/Tests/WalletConnectSharp.Auth.Tests/WalletConnectSharp.Auth.Tests.csproj b/Tests/WalletConnectSharp.Auth.Tests/WalletConnectSharp.Auth.Tests.csproj new file mode 100644 index 0000000..782a000 --- /dev/null +++ b/Tests/WalletConnectSharp.Auth.Tests/WalletConnectSharp.Auth.Tests.csproj @@ -0,0 +1,28 @@ + + + + net6.0;netcoreapp3.1 + 2.0.0 + 2.0.0 + 2.0.0 + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/Tests/WalletConnectSharp.Crypto.Tests/Models/TopicData.cs b/Tests/WalletConnectSharp.Crypto.Tests/Models/TopicData.cs index d5fdf86..240e182 100644 --- a/Tests/WalletConnectSharp.Crypto.Tests/Models/TopicData.cs +++ b/Tests/WalletConnectSharp.Crypto.Tests/Models/TopicData.cs @@ -5,6 +5,6 @@ namespace WalletConnectSharp.Crypto.Tests.Models public class TopicData { [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; } -} \ No newline at end of file +} diff --git a/Tests/WalletConnectSharp.Examples/BiDirectional.cs b/Tests/WalletConnectSharp.Examples/BiDirectional.cs index d31f207..2d7e404 100644 --- a/Tests/WalletConnectSharp.Examples/BiDirectional.cs +++ b/Tests/WalletConnectSharp.Examples/BiDirectional.cs @@ -1,5 +1,4 @@ -using WalletConnectSharp.Core.Models; -using WalletConnectSharp.Core.Models.Pairing; +using WalletConnectSharp.Core; using WalletConnectSharp.Sign; using WalletConnectSharp.Sign.Models; using WalletConnectSharp.Sign.Models.Engine; diff --git a/Tests/WalletConnectSharp.Examples/SimpleExample.cs b/Tests/WalletConnectSharp.Examples/SimpleExample.cs index 8d7bfe2..cc12935 100644 --- a/Tests/WalletConnectSharp.Examples/SimpleExample.cs +++ b/Tests/WalletConnectSharp.Examples/SimpleExample.cs @@ -1,7 +1,4 @@ -using System; -using System.Threading.Tasks; -using WalletConnectSharp.Core.Models; -using WalletConnectSharp.Core.Models.Pairing; +using WalletConnectSharp.Core; using WalletConnectSharp.Sign; using WalletConnectSharp.Sign.Models; using WalletConnectSharp.Sign.Models.Engine; @@ -71,4 +68,4 @@ public async Task Execute(string[] args) } } } -} \ No newline at end of file +} diff --git a/Tests/WalletConnectSharp.Network.Tests/Models/TopicData.cs b/Tests/WalletConnectSharp.Network.Tests/Models/TopicData.cs index 3001ddf..7856a61 100644 --- a/Tests/WalletConnectSharp.Network.Tests/Models/TopicData.cs +++ b/Tests/WalletConnectSharp.Network.Tests/Models/TopicData.cs @@ -5,6 +5,6 @@ namespace WalletConnectSharp.Network.Tests.Models public class TopicData { [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; } -} \ No newline at end of file +} diff --git a/Tests/WalletConnectSharp.Network.Tests/RelayTests.cs b/Tests/WalletConnectSharp.Network.Tests/RelayTests.cs index 311234e..6700e29 100644 --- a/Tests/WalletConnectSharp.Network.Tests/RelayTests.cs +++ b/Tests/WalletConnectSharp.Network.Tests/RelayTests.cs @@ -17,12 +17,12 @@ namespace WalletConnectSharp.Network.Tests { public class RelayTests { - private static readonly JsonRpcRequest TEST_WAKU_REQUEST = + private static readonly JsonRpcRequest TEST_IRN_REQUEST = new JsonRpcRequest(RelayProtocols.DefaultProtocol.Subscribe, new TopicData() { Topic = "ca838d59a3a3fe3824dab9ca7882ac9a2227c5d0284c88655b261a2fe85db270" }); - private static readonly JsonRpcRequest TEST_BAD_WAKU_REQUEST = + private static readonly JsonRpcRequest TEST_BAD_IRN_REQUEST = new JsonRpcRequest(RelayProtocols.DefaultProtocol.Subscribe, new TopicData()); private static readonly string DEFAULT_GOOD_WS_URL = "wss://relay.walletconnect.com"; @@ -56,7 +56,7 @@ public async void ConnectAndRequest() var provider = new JsonRpcProvider(connection); await provider.Connect(); - var result = await provider.Request(TEST_WAKU_REQUEST); + var result = await provider.Request(TEST_IRN_REQUEST); Assert.True(result.Length > 0); } @@ -68,7 +68,7 @@ public async void RequestWithoutConnect() var connection = new WebsocketConnection(url); var provider = new JsonRpcProvider(connection); - var result = await provider.Request(TEST_WAKU_REQUEST); + var result = await provider.Request(TEST_IRN_REQUEST); Assert.True(result.Length > 0); } @@ -80,7 +80,7 @@ public async void ThrowOnJsonRpcError() var connection = new WebsocketConnection(url); var provider = new JsonRpcProvider(connection); - await Assert.ThrowsAsync(() => provider.Request(TEST_BAD_WAKU_REQUEST)); + await Assert.ThrowsAsync(() => provider.Request(TEST_BAD_IRN_REQUEST)); } [Fact, Trait("Category", "integration")] @@ -89,7 +89,7 @@ public async void ThrowsOnUnavailableHost() var connection = new WebsocketConnection(BAD_WS_URL); var provider = new JsonRpcProvider(connection); - await Assert.ThrowsAsync(() => provider.Request(TEST_WAKU_REQUEST)); + await Assert.ThrowsAsync(() => provider.Request(TEST_IRN_REQUEST)); } [Fact, Trait("Category", "integration")] @@ -102,7 +102,7 @@ public async void ReconnectsWithNewProvidedHost() await provider.Connect(url); Assert.Equal(url, provider.Connection.Url); - var result = await provider.Request(TEST_WAKU_REQUEST); + var result = await provider.Request(TEST_IRN_REQUEST); Assert.True(result.Length > 0); } diff --git a/Tests/WalletConnectSharp.Sign.Test/Shared/SignTestValues.cs b/Tests/WalletConnectSharp.Sign.Test/Shared/SignTestValues.cs new file mode 100644 index 0000000..87f7b72 --- /dev/null +++ b/Tests/WalletConnectSharp.Sign.Test/Shared/SignTestValues.cs @@ -0,0 +1,77 @@ +using WalletConnectSharp.Sign.Models; + +namespace WalletConnectSharp.Sign.Test.Shared { + + public static class SignTestValues + { + public static readonly string TestPolkadotAddress = "8cGfbK9Q4zbsNzhZsZUtpsQgX5LG2UCPEDuXYV33whktGt7"; + public static readonly string TestEthereumAddress = "0x3c582121909DE92Dc89A36898633C1aE4790382b"; + public static readonly string TestEthereumChain = "eip155:1"; + public static readonly string TestArbitrumChain = "eip155:42161"; + public static readonly string TestAvalancheChain = "eip155:43114"; + public static readonly string TestPolkadotChain = "polkadot:91b171bb158e2d3848fa23a9f1c25182"; + public static readonly string TestPolkadotAccount = $"{TestPolkadotChain}:{TestPolkadotAddress}"; + + public static readonly string[] TestAccounts = new[] + { + $"{TestEthereumChain}:{TestEthereumAddress}", $"{TestArbitrumChain}:{TestEthereumAddress}", + $"{TestAvalancheChain}:{TestEthereumAddress}" + }; + + public static readonly string[] TestMethods = new[] + { + "eth_sendTransaction", "eth_signTransaction", "personal_sign", "eth_signTypedData", + }; + + public static readonly string[] TestPolkadotMethods = new[] + { + "polkadot_signTransaction", + "polkadot_signMessage" + }; + + public static readonly string[] TestChains = new[] { TestEthereumChain, TestArbitrumChain, }; + + public static readonly string[] TestEvents = new[] { "chainChanged", "accountsChanged" }; + + public static readonly RequiredNamespaces TestRequiredNamespacees = new RequiredNamespaces() + { + { + "eip155", new ProposedNamespace() + { + Methods = TestMethods, + Events = TestEvents, + Chains = TestChains + } + } + }; + + public static readonly RequiredNamespaces TestRequiredNamespaceesV2 = new RequiredNamespaces() + { + { + "eip155", new ProposedNamespace() + { + Methods = TestMethods, + Events = TestEvents, + Chains = TestChains + } + }, + { + TestAvalancheChain, new ProposedNamespace() + { + Methods = TestMethods, + Events = TestEvents + } + } + }; + + public static readonly Namespaces TestOptionalNamespaces = new Namespaces() + { + { + "polkadot", new Namespace() + { + + } + } + }; + } +} diff --git a/Tests/WalletConnectSharp.Sign.Test/SignClientConcurrency.cs b/Tests/WalletConnectSharp.Sign.Test/SignClientConcurrency.cs new file mode 100644 index 0000000..86c848a --- /dev/null +++ b/Tests/WalletConnectSharp.Sign.Test/SignClientConcurrency.cs @@ -0,0 +1,285 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Common.Model.Errors; +using WalletConnectSharp.Common.Utils; +using WalletConnectSharp.Events; +using WalletConnectSharp.Events.Model; +using WalletConnectSharp.Network.Models; +using WalletConnectSharp.Sign.Interfaces; +using WalletConnectSharp.Sign.Models; +using WalletConnectSharp.Sign.Models.Engine.Events; +using WalletConnectSharp.Sign.Models.Engine.Methods; +using WalletConnectSharp.Sign.Test.Shared; +using WalletConnectSharp.Tests.Common; +using Xunit; +using Xunit.Abstractions; + +namespace WalletConnectSharp.Sign.Test +{ + + public class SignClientConcurrency + { + public class TestPairings + { + public SignClientFixture clients; + public SessionStruct sessionA; + } + + public class TestResults + { + public long pairingLatencyMs; + public long handshakeLatencyMs; + public bool connected; + } + + public class TestEmitData + { + [JsonProperty("name")] + public string Name; + + [JsonProperty("data")] + public string Data; + } + + private ITestOutputHelper _output; + + public SignClientConcurrency(ITestOutputHelper output) + { + this._output = output; + } + + [Fact, Trait("Category", "concurrency")] + public async void TestConcurrentClients() => await _TestConcurrentClients().WithTimeout(TimeSpan.FromMinutes(20)); + + private int[][] BatchArray(int[] array, int size) + { + List results = new List(); + for (int i = 0; i < array.Length; i += size) + { + var batch = array.Skip(i).Take(size).ToArray(); + results.Add(batch); + } + + return results.ToArray(); + } + + private async Task InitTwoClients() + { + var fixture = new SignClientFixture(false); + await fixture.Init(); + await Task.Delay(500); + return fixture; + } + + private async Task DeleteClients(SignClientFixture clients) + { + await Task.Delay(500); + foreach (var client in new[] { clients.ClientA, clients.ClientB }) + { + if (client == null) + continue; + + // TODO Remove event data + if (client.Core.Relayer.Connected) + { + await client.Core.Relayer.TransportClose(); + } + } + } + + private async Task _TestConcurrentClients() + { + List pairings = new List(); + List> messagesReceived = new List>(); + + CancellationTokenSource heartbeatToken = new CancellationTokenSource(); + +#pragma warning disable CS4014 + Task.Run(async delegate +#pragma warning restore CS4014 + { + while (!heartbeatToken.Token.IsCancellationRequested) + { + Log($"initialized pairs - {pairings.Count}"); + + await Task.Delay(TestValues.HeartbeatInterval); + } + }, heartbeatToken.Token); + + // TODO Do stuff + var testEventParams = new EventData() { Name = SignTestValues.TestEvents[0], Data = "" }; + + Task ProcessMessages(TestPairings data, int clientIndex) + { + var clients = data.clients; + var sessionA = data.sessionA; + + var eventPayload = new SessionEvent() + { + ChainId = SignTestValues.TestEthereumChain, + Event = testEventParams, + Topic = sessionA.Topic, + }; + + messagesReceived.Insert(clientIndex, new List()); + + TaskCompletionSource task = new TaskCompletionSource(); + + ISignClient[] clientsArr = new[] { clients.ClientA, clients.ClientB }; + + var namespacesBefore = sessionA.Namespaces; + var namespacesAfter = new Namespaces(namespacesBefore) + { + { + "eip9001", new Namespace() { + Accounts = new []{ "eip9001:1:0x000000000000000000000000000000000000dead" }, + Methods = new []{ "eth_sendTransaction" }, + Events = new []{ "accountsChanged" } + } + } + }; + + Task Emit(ISignClient client) + { + return client.Emit(sessionA.Topic, testEventParams, SignTestValues.TestEthereumChain); + } + + void CheckAllMessagesProcessed() + { + if (messagesReceived[clientIndex].Count >= TestValues.MessagesPerClient) + { + task.TrySetResult(true); + } + } + + foreach (var client in clientsArr) + { + client.On(EngineEvents.SessionPing, (sender, @event) => + { + Assert.Equal(sessionA.Topic, @event.EventData.Topic); + messagesReceived[clientIndex].Add(@event.EventData); + CheckAllMessagesProcessed(); + }); + + client.On>(EngineEvents.SessionEvent, (sender, @event) => + { + Assert.Equal(testEventParams.Data, @event.EventData.Params.Event.Data); + Assert.Equal(eventPayload.Topic, @event.EventData.Topic); + messagesReceived[clientIndex].Add(@event.EventData); + CheckAllMessagesProcessed(); + }); + + client.On(EngineEvents.SessionUpdate, (sender, @event) => + { + Assert.Equal(client.Session.Get(sessionA.Topic).Namespaces, namespacesAfter); + messagesReceived[clientIndex].Add(@event.EventData); + CheckAllMessagesProcessed(); + }); + } + + async void SendMessages() + { + Random random = new Random(); + for (int i = 0; i < TestValues.MessagesPerClient; i++) + { + var client = (int)Math.Floor(random.NextDouble() * clientsArr.Length); + await Emit(clientsArr[client]); + await Task.Delay(10); + } + } + + SendMessages(); + + return task.Task; + } + + int[] arr = Enumerable.Range(0, TestValues.ClientCount+1).ToArray(); + int[][] batches = BatchArray(arr, 20); + + async Task ConnectClient() + { + var now = Clock.NowMilliseconds(); + var clients = await InitTwoClients(); + var handshakeLatencyMs = Clock.NowMilliseconds() - now; + await Task.Delay(10); + Assert.IsType(clients.ClientA); + Assert.IsType(clients.ClientB); + + var sessionA = await SignTests.TestConnectMethod(clients.ClientA, clients.ClientB); + pairings.Add(new TestPairings() + { + clients = clients, + sessionA = sessionA + }); + var pairingLatencyMs = Clock.NowMilliseconds() - now; + return new TestResults() + { + connected = true, handshakeLatencyMs = handshakeLatencyMs, pairingLatencyMs = pairingLatencyMs + }; + } + + foreach (int[] batch in batches) + { + var connections = (await Task.WhenAll( + batch.Select(async delegate(int i) + { + try + { + return await ConnectClient().WithTimeout(120000); + } + catch (TimeoutException) + { + Log($"Client {i} hung up"); + return new TestResults() + { + connected = false, handshakeLatencyMs = -1, pairingLatencyMs = -1 + }; + } + }) + )).Where(t => t.connected).ToList(); + + var averagePairingLatency = connections.Select(c => c.pairingLatencyMs) + .Aggregate((a, b) => a + b) / connections.Count; + var averageHandhsakeLatency = connections.Select(c => c.handshakeLatencyMs) + .Aggregate((a, b) => a + b) / connections.Count; + var failures = batch.Length - connections.Count; + Log($"{connections.Count} out of {batch.Length} connected ({averagePairingLatency}ms avg pairing latency, {averageHandhsakeLatency}ms avg handshake latency"); + + // TODO uploadLoadTestConnectionDataToCloudWatch + } + + await Task.WhenAll( + pairings.Select(async delegate(TestPairings testPairings, int i) + { + await ProcessMessages(testPairings, i); + }) + ); + + foreach (var data in pairings) + { + var clients = data.clients; + var sessionA = data.sessionA; + + TaskCompletionSource clientBDisconnected = new TaskCompletionSource(); + clients.ClientB.On(EngineEvents.SessionDelete, (sender, @event) => + { + Assert.Equal(sessionA.Topic, @event.EventData.Topic); + clientBDisconnected.TrySetResult(true); + }); + + await Task.WhenAll( + clientBDisconnected.Task, + clients.ClientA.Disconnect(sessionA.Topic, Error.FromErrorType(ErrorType.USER_DISCONNECTED)) + ); + + await DeleteClients(clients); + } + + heartbeatToken.Cancel(); + } + + private void Log(string message) + { + this._output.WriteLine(message); + } + } +} diff --git a/Tests/WalletConnectSharp.Sign.Test/SignClientFixture.cs b/Tests/WalletConnectSharp.Sign.Test/SignClientFixture.cs index bcc9c13..594f397 100644 --- a/Tests/WalletConnectSharp.Sign.Test/SignClientFixture.cs +++ b/Tests/WalletConnectSharp.Sign.Test/SignClientFixture.cs @@ -1,9 +1,53 @@ -namespace WalletConnectSharp.Sign.Test; +using WalletConnectSharp.Core; +using WalletConnectSharp.Sign.Models; +using WalletConnectSharp.Storage; +using WalletConnectSharp.Tests.Common; + +namespace WalletConnectSharp.Sign.Test; public class SignClientFixture : TwoClientsFixture { - protected override async void Init() + public SignClientOptions OptionsA { get; protected set; } + public SignClientOptions OptionsB { get; protected set; } + + public SignClientFixture() : this(true) { } + + internal SignClientFixture(bool initNow) : base(initNow) + { + } + + public override async Task Init() { + OptionsA = new SignClientOptions() + { + ProjectId = TestValues.TestProjectId, + RelayUrl = TestValues.TestRelayUrl, + Metadata = new Metadata() + { + Description = "An example dapp to showcase WalletConnectSharpv2", + Icons = new[] { "https://walletconnect.com/meta/favicon.ico" }, + Name = $"WalletConnectSharpv2 Dapp Example - {Guid.NewGuid().ToString()}", + Url = "https://walletconnect.com" + }, + // Omit if you want persistant storage + Storage = new InMemoryStorage() + }; + + OptionsB = new SignClientOptions() + { + ProjectId = TestValues.TestProjectId, + RelayUrl = TestValues.TestRelayUrl, + Metadata = new Metadata() + { + Description = "An example wallet to showcase WalletConnectSharpv2", + Icons = new[] { "https://walletconnect.com/meta/favicon.ico" }, + Name = $"WalletConnectSharpv2 Wallet Example - {Guid.NewGuid().ToString()}", + Url = "https://walletconnect.com" + }, + // Omit if you want persistant storage + Storage = new InMemoryStorage() + }; + ClientA = await WalletConnectSignClient.Init(OptionsA); ClientB = await WalletConnectSignClient.Init(OptionsB); } diff --git a/Tests/WalletConnectSharp.Sign.Test/SignTests.cs b/Tests/WalletConnectSharp.Sign.Test/SignTests.cs index 61ebedf..f739761 100644 --- a/Tests/WalletConnectSharp.Sign.Test/SignTests.cs +++ b/Tests/WalletConnectSharp.Sign.Test/SignTests.cs @@ -1,8 +1,10 @@ using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Common.Utils; using WalletConnectSharp.Network.Models; +using WalletConnectSharp.Sign.Interfaces; using WalletConnectSharp.Sign.Models; using WalletConnectSharp.Sign.Models.Engine; +using WalletConnectSharp.Tests.Common; using Xunit; namespace WalletConnectSharp.Sign.Test @@ -10,6 +12,7 @@ namespace WalletConnectSharp.Sign.Test public class SignTests : IClassFixture { private SignClientFixture _cryptoFixture; + private const string AllowedChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; [RpcMethod("test_method"), RpcRequestOptions(Clock.ONE_MINUTE, 99998)] public class TestRequest @@ -17,6 +20,15 @@ public class TestRequest public int a; public int b; } + + [RpcMethod("test_method_2"), + RpcRequestOptions(Clock.ONE_MINUTE, 99997), + RpcResponseOptions(Clock.ONE_MINUTE, 99996)] + public class TestRequest2 + { + public string x; + public int y; + } [RpcResponseOptions(Clock.ONE_MINUTE, 99999)] public class TestResponse @@ -45,10 +57,9 @@ public SignTests(SignClientFixture cryptoFixture) this._cryptoFixture = cryptoFixture; } - [Fact, Trait("Category", "integration")] - public async void TestApproveSession() + public static async Task TestConnectMethod(ISignClient clientA, ISignClient clientB) { - await _cryptoFixture.WaitForClientsReady(); + var start = Clock.NowMilliseconds(); var testAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; var dappConnectOptions = new ConnectOptions() @@ -79,15 +90,15 @@ public async void TestApproveSession() } }; - var dappClient = ClientA; + var dappClient = clientA; var connectData = await dappClient.Connect(dappConnectOptions); - var walletClient = ClientB; + var walletClient = clientB; var proposal = await walletClient.Pair(connectData.Uri); Assert.NotNull(proposal.RequiredNamespaces); Assert.NotNull(proposal.OptionalNamespaces); - Assert.NotNull(proposal.SessionProperties); + Assert.True(proposal.SessionProperties == null || proposal.SessionProperties.Count > 0); Assert.NotNull(proposal.Expiry); Assert.NotNull(proposal.Id); Assert.NotNull(proposal.Relays); @@ -98,6 +109,16 @@ public async void TestApproveSession() var sessionData = await connectData.Approval; await approveData.Acknowledged(); + + return sessionData; + } + + [Fact, Trait("Category", "integration")] + public async void TestApproveSession() + { + await _cryptoFixture.WaitForClientsReady(); + + await TestConnectMethod(ClientA, ClientB); } [Fact, Trait("Category", "integration")] @@ -244,5 +265,122 @@ public async void TestSessionRequestResponse() Assert.Equal(eventResult, testData.a * testData.b); Assert.Equal(eventResult, responseReturned.result); } + + [Fact, Trait("Category", "integration")] + public async void TestTwoUniqueSessionRequestResponse() + { + await _cryptoFixture.WaitForClientsReady(); + + 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[] + { + "chainChanged", "accountsChanged" + } + } + } + } + }; + + var dappClient = ClientA; + var connectData = await dappClient.Connect(dappConnectOptions); + + var walletClient = ClientB; + var proposal = await walletClient.Pair(connectData.Uri); + + var approveData = await walletClient.Approve(proposal, testAddress); + + var sessionData = await connectData.Approval; + await approveData.Acknowledged(); + + var rnd = new Random(); + var a = rnd.Next(100); + var b = rnd.Next(100); + var x = rnd.NextStrings(AllowedChars, (Math.Min(a, b), Math.Max(a, b)), 1).First(); + var y = x.Length; + + var testData = new TestRequest() { a = a, b = b, }; + var testData2 = new TestRequest2() { x = x, y = y }; + + var pending = new TaskCompletionSource(); + var pending2 = new TaskCompletionSource(); + + // Step 1. Setup event listener for request + + // The wallet client will listen for the request with the "test_method" rpc method + walletClient.Engine.SessionRequestEvents() + .OnRequest += ( requestData) => + { + var request = requestData.Request; + var data = request.Params; + + requestData.Response = new TestResponse() + { + result = data.a * data.b + }; + + return Task.CompletedTask; + }; + + // The wallet client will listen for the request with the "test_method" rpc method + walletClient.Engine.SessionRequestEvents() + .OnRequest += ( requestData) => + { + var request = requestData.Request; + var data = request.Params; + + requestData.Response = data.x.Length == data.y; + + return Task.CompletedTask; + }; + + // The dapp client will listen for the response + // Normally, we wouldn't do this and just rely on the return value + // 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) + .OnResponse += (responseData) => + { + var response = responseData.Response; + + var data = response.Result; + + pending.TrySetResult(data.result); + + return Task.CompletedTask; + }; + + // 2. Send the request from the dapp client + var responseReturned = await dappClient.Engine.Request(sessionData.Topic, testData); + var responseReturned2 = await dappClient.Engine.Request(sessionData.Topic, testData2); + + // 3. Wait for the response from the event listener + var eventResult = await pending.Task.WithTimeout(TimeSpan.FromSeconds(5)); + + Assert.Equal(eventResult, a * b); + Assert.Equal(eventResult, testData.a * testData.b); + Assert.Equal(eventResult, responseReturned.result); + + Assert.True(responseReturned2); + } } } diff --git a/Tests/WalletConnectSharp.Sign.Test/TwoClientsFixture.cs b/Tests/WalletConnectSharp.Sign.Test/TwoClientsFixture.cs deleted file mode 100644 index 7771824..0000000 --- a/Tests/WalletConnectSharp.Sign.Test/TwoClientsFixture.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Threading.Tasks; -using WalletConnectSharp.Core.Models; -using WalletConnectSharp.Core.Models.Pairing; -using WalletConnectSharp.Sign.Interfaces; -using WalletConnectSharp.Sign.Models; -using WalletConnectSharp.Storage; -using WalletConnectSharp.Tests.Common; - -namespace WalletConnectSharp.Sign.Test -{ - public abstract class TwoClientsFixture where SClient : ISignClient - { - public SClient ClientA { get; protected set; } - public SClient ClientB { get; protected set; } - - public SignClientOptions OptionsA { get; } - public SignClientOptions OptionsB { get; } - - public TwoClientsFixture() - { - OptionsA = new SignClientOptions() - { - ProjectId = TestValues.TestProjectId, - RelayUrl = TestValues.TestRelayUrl, - Metadata = new Metadata() - { - Description = "An example dapp to showcase WalletConnectSharpv2", - Icons = new[] { "https://walletconnect.com/meta/favicon.ico" }, - Name = "WalletConnectSharpv2 Dapp Example", - Url = "https://walletconnect.com" - }, - // Omit if you want persistant storage - Storage = new InMemoryStorage() - }; - - OptionsB = new SignClientOptions() - { - ProjectId = TestValues.TestProjectId, - RelayUrl = TestValues.TestRelayUrl, - Metadata = new Metadata() - { - Description = "An example wallet to showcase WalletConnectSharpv2", - Icons = new[] { "https://walletconnect.com/meta/favicon.ico" }, - Name = "WalletConnectSharpv2 Wallet Example", - Url = "https://walletconnect.com" - }, - // Omit if you want persistant storage - Storage = new InMemoryStorage() - }; - - Init(); - } - - protected abstract void Init(); - - public async Task WaitForClientsReady() - { - while (ClientA == null || ClientB == null) - await Task.Delay(10); - } - } -} diff --git a/Tests/WalletConnectSharp.Sign.Test/WalletConnectSharp.Sign.Test.csproj b/Tests/WalletConnectSharp.Sign.Test/WalletConnectSharp.Sign.Test.csproj index f0f04a4..488bd90 100644 --- a/Tests/WalletConnectSharp.Sign.Test/WalletConnectSharp.Sign.Test.csproj +++ b/Tests/WalletConnectSharp.Sign.Test/WalletConnectSharp.Sign.Test.csproj @@ -21,6 +21,7 @@ + diff --git a/Tests/WalletConnectSharp.Tests.Common/CryptoWalletFixture.cs b/Tests/WalletConnectSharp.Tests.Common/CryptoWalletFixture.cs new file mode 100644 index 0000000..eb581e2 --- /dev/null +++ b/Tests/WalletConnectSharp.Tests.Common/CryptoWalletFixture.cs @@ -0,0 +1,53 @@ +using System.Text; +using NBitcoin; +using Nethereum.HdWallet; + +namespace WalletConnectSharp.Tests.Common +{ + public class CryptoWalletFixture + { + private readonly Wallet _wallet; + private readonly string _iss; + + public string WalletAddress + { + get + { + return _wallet.GetAddresses(1)[0]; + } + } + + public Wallet CryptoWallet + { + get + { + return _wallet; + } + } + + public string Iss + { + get + { + return _iss; + } + } + + public CryptoWalletFixture() + { + this._wallet = new Wallet(Wordlist.English, WordCount.Twelve); + this._iss = $"did:pkh:eip155:1:{this.WalletAddress}"; + } + + public Task SignMessage(string message) + { + return _wallet + .GetAccount(WalletAddress) + .AccountSigningService + .PersonalSign + .SendRequestAsync( + Encoding.UTF8.GetBytes(message) + ); + } + } +} diff --git a/Tests/WalletConnectSharp.Tests.Common/TestValues.cs b/Tests/WalletConnectSharp.Tests.Common/TestValues.cs index 9cd98b5..543aabf 100644 --- a/Tests/WalletConnectSharp.Tests.Common/TestValues.cs +++ b/Tests/WalletConnectSharp.Tests.Common/TestValues.cs @@ -10,7 +10,25 @@ public static class TestValues : DefaultProjectId; private const string DefaultRelayUrl = "wss://relay.walletconnect.com"; + private static readonly string EnvironmentRelayUrl = Environment.GetEnvironmentVariable("RELAY_ENDPOINT"); public static readonly string TestRelayUrl = !string.IsNullOrWhiteSpace(EnvironmentRelayUrl) ? EnvironmentRelayUrl : DefaultRelayUrl; + + private static readonly string EnvironmentClientCount = Environment.GetEnvironmentVariable("CLIENTS"); + public static readonly int ClientCount = !string.IsNullOrWhiteSpace(EnvironmentClientCount) + ? int.Parse(EnvironmentClientCount) + : 200; + + private static readonly string EnvironmentMessageCount = Environment.GetEnvironmentVariable("MESSAGES_PER_CLIENT"); + public static readonly int MessagesPerClient = !string.IsNullOrWhiteSpace(EnvironmentMessageCount) + ? int.Parse(EnvironmentMessageCount) + : 1000; + + private static readonly string EnvironmentHeartbeatInterval = Environment.GetEnvironmentVariable("HEARTBEAT_INTERVAL"); + public static readonly int HeartbeatInterval = !string.IsNullOrWhiteSpace(EnvironmentHeartbeatInterval) + ? int.Parse(EnvironmentHeartbeatInterval) + : 3000; + + } } diff --git a/Tests/WalletConnectSharp.Tests.Common/TwoClientsFixture.cs b/Tests/WalletConnectSharp.Tests.Common/TwoClientsFixture.cs new file mode 100644 index 0000000..e630033 --- /dev/null +++ b/Tests/WalletConnectSharp.Tests.Common/TwoClientsFixture.cs @@ -0,0 +1,22 @@ +namespace WalletConnectSharp.Tests.Common; + +public abstract class TwoClientsFixture +{ + public TClient ClientA { get; protected set; } + public TClient ClientB { get; protected set; } + + + public TwoClientsFixture(bool initNow = true) + { + if (initNow) + Init(); + } + + public abstract Task Init(); + + public async Task WaitForClientsReady() + { + while (ClientA == null || ClientB == null) + await Task.Delay(10); + } +} diff --git a/Tests/WalletConnectSharp.Tests.Common/UtilExtensions.cs b/Tests/WalletConnectSharp.Tests.Common/UtilExtensions.cs new file mode 100644 index 0000000..96a17f1 --- /dev/null +++ b/Tests/WalletConnectSharp.Tests.Common/UtilExtensions.cs @@ -0,0 +1,37 @@ +namespace WalletConnectSharp.Tests.Common; + +public static class UtilExtensions +{ + public static IEnumerable NextStrings( + this Random rnd, + string allowedChars, + (int Min, int Max)length, + int count) + { + ISet usedRandomStrings = new HashSet(); + (int min, int max) = length; + char[] chars = new char[max]; + int setLength = allowedChars.Length; + + while (count-- > 0) + { + int stringLength = rnd.Next(min, max + 1); + + for (int i = 0; i < stringLength; ++i) + { + chars[i] = allowedChars[rnd.Next(setLength)]; + } + + string randomString = new string(chars, 0, stringLength); + + if (usedRandomStrings.Add(randomString)) + { + yield return randomString; + } + else + { + count++; + } + } + } +} diff --git a/Tests/WalletConnectSharp.Tests.Common/WalletConnectSharp.Tests.Common.csproj b/Tests/WalletConnectSharp.Tests.Common/WalletConnectSharp.Tests.Common.csproj index 2db8f9e..eee131d 100644 --- a/Tests/WalletConnectSharp.Tests.Common/WalletConnectSharp.Tests.Common.csproj +++ b/Tests/WalletConnectSharp.Tests.Common/WalletConnectSharp.Tests.Common.csproj @@ -13,4 +13,8 @@ + + + + diff --git a/Tests/WalletConnectSharp.Web3Wallet.Tests/AuthTests.cs b/Tests/WalletConnectSharp.Web3Wallet.Tests/AuthTests.cs new file mode 100644 index 0000000..fb70c84 --- /dev/null +++ b/Tests/WalletConnectSharp.Web3Wallet.Tests/AuthTests.cs @@ -0,0 +1,229 @@ +using WalletConnectSharp.Auth; +using WalletConnectSharp.Auth.Internals; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Models; +using WalletConnectSharp.Network.Models; +using WalletConnectSharp.Storage; +using WalletConnectSharp.Tests.Common; +using Xunit; + +namespace WalletConnectSharp.Web3Wallet.Tests +{ + public class AuthClientTests : IClassFixture, IAsyncLifetime + { + private static readonly RequestParams DefaultRequestParams = new RequestParams() + { + Aud = "http://localhost:3000/login", + Domain = "localhost:3000", + ChainId = "eip155:1", + Nonce = CryptoUtils.GenerateNonce() + }; + + private readonly CryptoWalletFixture _cryptoWalletFixture; + private WalletConnectCore _core; + private WalletConnectAuthClient _dapp; + private Web3WalletClient _wallet; + private string uriString; + + public string WalletAddress + { + get + { + return _cryptoWalletFixture.WalletAddress; + } + } + + public string Iss + { + get + { + return _cryptoWalletFixture.Iss; + } + } + + public AuthClientTests(CryptoWalletFixture cryptoWalletFixture) + { + this._cryptoWalletFixture = cryptoWalletFixture; + } + + public async Task InitializeAsync() + { + _core = new WalletConnectCore(new CoreOptions() + { + ProjectId = TestValues.TestProjectId, RelayUrl = TestValues.TestRelayUrl, + Storage = new InMemoryStorage(), + Name = Guid.NewGuid().ToString(), + }); + _dapp = await WalletConnectAuthClient.Init(new AuthOptions() + { + ProjectId = TestValues.TestProjectId, + RelayUrl = TestValues.TestRelayUrl, + Metadata = new Metadata() + { + Description = "An example dapp to showcase WalletConnectSharpv2", + Icons = new[] { "https://walletconnect.com/meta/favicon.ico" }, + Name = $"WalletConnectSharpv2 Dapp Example - {Guid.NewGuid().ToString()}", + Url = "https://walletconnect.com" + }, + Name = $"dapp-{Guid.NewGuid().ToString()}", + Storage = new InMemoryStorage(), + }); + _wallet = await Web3WalletClient.Init(_core, new Metadata() + { + Description = "An example wallet to showcase WalletConnectSharpv2", + Icons = new[] { "https://walletconnect.com/meta/favicon.ico" }, + Name = $"WalletConnectSharpv2 Wallet Example - {Guid.NewGuid().ToString()}", + Url = "https://walletconnect.com" + }, $"wallet-{Guid.NewGuid().ToString()}"); + + Assert.NotNull(_wallet); + Assert.NotNull(_dapp); + Assert.NotNull(_core); + Assert.Null(_wallet.Metadata.Redirect); + Assert.Null(_dapp.Metadata.Redirect); + } + + public async Task DisposeAsync() + { + if (_core.Relayer.Connected) + { + await _core.Relayer.TransportClose(); + } + } + + [Fact, Trait("Category", "unit")] + public async void TestRespondToAuthRequest() + { + var request = await _dapp.Request(DefaultRequestParams); + uriString = request.Uri; + + TaskCompletionSource task1 = new TaskCompletionSource(); + _wallet.AuthRequested += async (sender, authRequest) => + { + Assert.Equal(DefaultRequestParams.Aud, authRequest.Parameters.CacaoPayload.Aud); + Assert.Equal(DefaultRequestParams.Domain, authRequest.Parameters.CacaoPayload.Domain); + Assert.Equal(DefaultRequestParams.Nonce, authRequest.Parameters.CacaoPayload.Nonce); + + var message = _wallet.FormatMessage(authRequest.Parameters.CacaoPayload, Iss); + var signature = await _cryptoWalletFixture.SignMessage(message); + + await _wallet.RespondAuthRequest(authRequest, signature, Iss); + + task1.TrySetResult(true); + }; + + TaskCompletionSource task2 = new TaskCompletionSource(); + _dapp.AuthResponded += (sender, response) => + { + Assert.NotNull(response); + Assert.NotNull(response.Id); + Assert.NotNull(response.Topic); + Assert.NotNull(response.Topic); + Assert.Equal(Iss, response.Response.Result.Payload.Iss); + task2.TrySetResult(true); + }; + + _dapp.AuthError += (sender, response) => task1.TrySetException(response.Error.ToException()); + + await Task.WhenAll( + task1.Task, + task2.Task, + _wallet.Pair(uriString, true) + ); + } + + [Fact, Trait("Category", "unit")] + public async void TestShouldRejectAuthRequest() + { + var request = await _dapp.Request(DefaultRequestParams); + uriString = request.Uri; + var errorResponse = new Error() + { + Code = 14001, + Message = "Can not login" + }; + + TaskCompletionSource task1 = new TaskCompletionSource(); + + _wallet.AuthRequested += (sender, authRequest) => + { + Assert.Equal(DefaultRequestParams.Aud, authRequest.Parameters.CacaoPayload.Aud); + Assert.Equal(DefaultRequestParams.Domain, authRequest.Parameters.CacaoPayload.Domain); + Assert.Equal(DefaultRequestParams.Nonce, authRequest.Parameters.CacaoPayload.Nonce); + + _wallet.RespondAuthRequest(authRequest, errorResponse, Iss); + + task1.TrySetResult(true); + }; + + TaskCompletionSource task2 = new TaskCompletionSource(); + _dapp.AuthResponded += (sender, response) => + { + task2.SetException(new Exception("Did not get an error response")); + }; + + _dapp.AuthError += (sender, response) => + { + Assert.NotNull(response); + Assert.NotNull(response.Id); + Assert.NotNull(response.Topic); + Assert.Equal(errorResponse, response.Error); + + task2.TrySetResult(true); + }; + + await Task.WhenAll( + task1.Task, + task2.Task, + _wallet.Pair(uriString, true) + ); + } + + [Fact, Trait("Category", "unit")] + public async void TestGetPendingAuthRequest() + { + var request = await _dapp.Request(DefaultRequestParams); + uriString = request.Uri; + + TaskCompletionSource task1 = new TaskCompletionSource(); + _wallet.AuthRequested += async (sender, authRequest) => + { + Assert.NotNull(authRequest.Id); + + var pendingRequest = _wallet.PendingAuthRequests[(long)authRequest.Id]; + + + Assert.Equal(DefaultRequestParams.Aud, pendingRequest.CacaoPayload.Aud); + Assert.Equal(DefaultRequestParams.Domain, pendingRequest.CacaoPayload.Domain); + Assert.Equal(DefaultRequestParams.Nonce, pendingRequest.CacaoPayload.Nonce); + + var message = _wallet.FormatMessage(pendingRequest.CacaoPayload, Iss); + var signature = await _cryptoWalletFixture.SignMessage(message); + + await _wallet.RespondAuthRequest(authRequest, signature, Iss); + + task1.TrySetResult(true); + }; + + TaskCompletionSource task2 = new TaskCompletionSource(); + _dapp.AuthResponded += (sender, response) => + { + Assert.NotNull(response); + Assert.NotNull(response.Id); + Assert.NotNull(response.Topic); + Assert.NotNull(response.Topic); + Assert.Equal(Iss, response.Response.Result.Payload.Iss); + task2.TrySetResult(true); + }; + + _dapp.AuthError += (sender, response) => task1.TrySetException(response.Error.ToException()); + + await Task.WhenAll( + task1.Task, + task2.Task, + _wallet.Pair(uriString, true) + ); + } + } +} diff --git a/Tests/WalletConnectSharp.Web3Wallet.Tests/SignTests.cs b/Tests/WalletConnectSharp.Web3Wallet.Tests/SignTests.cs new file mode 100644 index 0000000..4ffa579 --- /dev/null +++ b/Tests/WalletConnectSharp.Web3Wallet.Tests/SignTests.cs @@ -0,0 +1,696 @@ +using Nethereum.Hex.HexTypes; +using Nethereum.RPC.Eth.DTOs; +using Nethereum.Web3.Accounts; +using Newtonsoft.Json; +using WalletConnectSharp.Auth.Internals; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Common.Model.Errors; +using WalletConnectSharp.Common.Utils; +using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Models; +using WalletConnectSharp.Core.Models.Verify; +using WalletConnectSharp.Events; +using WalletConnectSharp.Network.Models; +using WalletConnectSharp.Sign; +using WalletConnectSharp.Sign.Models; +using WalletConnectSharp.Sign.Models.Engine; +using WalletConnectSharp.Sign.Models.Engine.Events; +using WalletConnectSharp.Storage; +using WalletConnectSharp.Tests.Common; +using Xunit; + +namespace WalletConnectSharp.Web3Wallet.Tests +{ + public class SignClientTests : IClassFixture, IAsyncLifetime + { + [RpcMethod("eth_signTransaction"), + RpcRequestOptions(Clock.ONE_MINUTE, 99997), + RpcResponseOptions(Clock.ONE_MINUTE, 99996) + ] + public class EthSignTransaction : List + { + } + + public class ChainChangedEvent + { + [JsonProperty("test")] + public string Test; + } + + private static readonly string TestEthereumAddress = "0x3c582121909DE92Dc89A36898633C1aE4790382b"; + private static readonly string TestEthereumChain = "eip155:1"; + private static readonly string TestArbitrumChain = "eip155:42161"; + private static readonly string TestAvalancheChain = "eip155:43114"; + + private static readonly string[] TestAccounts = new[] + { + $"{TestEthereumChain}:{TestEthereumAddress}", $"{TestArbitrumChain}:{TestEthereumAddress}", + $"{TestAvalancheChain}:{TestEthereumAddress}" + }; + + private static readonly string[] TestEvents = new[] { "chainChanged", "accountsChanged" }; + + private static readonly RequestParams DefaultRequestParams = new RequestParams() + { + Aud = "http://localhost:3000/login", + Domain = "localhost:3000", + ChainId = "eip155:1", + Nonce = CryptoUtils.GenerateNonce() + }; + + private static readonly RequiredNamespaces TestRequiredNamespaces = new RequiredNamespaces() + { + { + "eip155", new ProposedNamespace() + { + Chains = new []{ "eip155:1" }, + Methods = new[] { "eth_signTransaction" }, + Events = new[] { "chainChanged" } + } + } + }; + + private static readonly Namespaces TestUpdatedNamespaces = new Namespaces() + { + { + "eip155", new Namespace() + { + Methods = new [] + { + "eth_signTransaction", + "eth_sendTransaction", + "personal_sign", + "eth_signTypedData" + }, + Accounts = TestAccounts, + Events = TestEvents + } + } + }; + + private static readonly Namespace TestNamespace = new Namespace() + { + Methods = new[] { "eth_signTransaction", }, + Accounts = new[] { TestAccounts[0] }, + Events = new[] { TestEvents[0] } + }; + + private static readonly Namespaces TestNamespaces = new Namespaces() + { + { + "eip155", TestNamespace + } + }; + + private static readonly ConnectOptions TestConnectOptions = new ConnectOptions() + .UseRequireNamespaces(TestRequiredNamespaces); + + private readonly CryptoWalletFixture _cryptoWalletFixture; + private WalletConnectCore _core; + private WalletConnectSignClient _dapp; + private Web3WalletClient _wallet; + private string uriString; + private Task sessionApproval; + private SessionStruct session; + + + public string WalletAddress + { + get + { + return _cryptoWalletFixture.WalletAddress; + } + } + + public string Iss + { + get + { + return _cryptoWalletFixture.Iss; + } + } + + public SignClientTests(CryptoWalletFixture cryptoWalletFixture) + { + this._cryptoWalletFixture = cryptoWalletFixture; + } + + public async Task InitializeAsync() + { + _core = new WalletConnectCore(new CoreOptions() + { + ProjectId = TestValues.TestProjectId, RelayUrl = TestValues.TestRelayUrl, + Name = $"wallet-csharp-test-{Guid.NewGuid().ToString()}", + Storage = new InMemoryStorage(), + }); + _dapp = await WalletConnectSignClient.Init(new SignClientOptions() + { + ProjectId = TestValues.TestProjectId, + Name = $"dapp-csharp-test-{Guid.NewGuid().ToString()}", + RelayUrl = TestValues.TestRelayUrl, + Metadata = new Metadata() + { + Description = "An example dapp to showcase WalletConnectSharpv2", + Icons = new[] { "https://walletconnect.com/meta/favicon.ico" }, + Name = $"dapp-csharp-test-{Guid.NewGuid().ToString()}", + Url = "https://walletconnect.com" + }, + Storage = new InMemoryStorage(), + }); + var connectData = await _dapp.Connect(TestConnectOptions); + uriString = connectData.Uri ?? ""; + sessionApproval = connectData.Approval; + + _wallet = await Web3WalletClient.Init(_core, new Metadata() + { + Description = "An example wallet to showcase WalletConnectSharpv2", + Icons = new[] { "https://walletconnect.com/meta/favicon.ico" }, + Name = $"wallet-csharp-test-{Guid.NewGuid().ToString()}", + Url = "https://walletconnect.com", + }, $"wallet-csharp-test-{Guid.NewGuid().ToString()}"); + + Assert.NotNull(_wallet); + Assert.NotNull(_dapp); + Assert.NotNull(_core); + Assert.Null(_wallet.Metadata.Redirect); + } + + public async Task DisposeAsync() + { + if (_core.Relayer.Connected) + { + await _core.Relayer.TransportClose(); + } + } + + [Fact, Trait("Category", "unit")] + public async void TestShouldApproveSessionProposal() + { + TaskCompletionSource task1 = new TaskCompletionSource(); + _wallet.On(EngineEvents.SessionProposal, async (sender, @event) => + { + var id = @event.EventData.Id; + var proposal = @event.EventData.Proposal; + var verifyContext = @event.EventData.VerifiedContext; + + Assert.Equal(Validation.Unknown, verifyContext.Validation); + session = await _wallet.ApproveSession(id, TestNamespaces); + + Assert.Equal(proposal.RequiredNamespaces, TestRequiredNamespaces); + task1.TrySetResult(true); + }); + + await Task.WhenAll( + task1.Task, + sessionApproval, + _wallet.Pair(uriString) + ); + } + + [Fact, Trait("Category", "unit")] + public async void TestShouldRejectSessionProposal() + { + var rejectionError = Error.FromErrorType(ErrorType.USER_DISCONNECTED); + + TaskCompletionSource task1 = new TaskCompletionSource(); + _wallet.On(EngineEvents.SessionProposal, async (sender, @event) => + { + var proposal = @event.EventData.Proposal; + + var id = @event.EventData.Id; + Assert.Equal(TestRequiredNamespaces, proposal.RequiredNamespaces); + + await _wallet.RejectSession(id, rejectionError); + task1.TrySetResult(true); + }); + + async Task CheckSessionReject() + { + try + { + await sessionApproval; + } + catch (WalletConnectException e) + { + Assert.Equal(rejectionError.Code, e.Code); + Assert.Equal(rejectionError.Message, e.Message); + return; + } + Assert.Fail("Session approval task did not throw exception, expected rejection"); + } + + await Task.WhenAll( + task1.Task, + _wallet.Pair(uriString), + CheckSessionReject() + ); + } + + [Fact, Trait("Category", "unit")] + public async void TestUpdateSession() + { + TaskCompletionSource task1 = new TaskCompletionSource(); + _wallet.On(EngineEvents.SessionProposal, async (sender, @event) => + { + var id = @event.EventData.Id; + var proposal = @event.EventData.Proposal; + var verifyContext = @event.EventData.VerifiedContext; + + Assert.Equal(Validation.Unknown, verifyContext.Validation); + session = await _wallet.ApproveSession(id, TestNamespaces); + + Assert.Equal(proposal.RequiredNamespaces, TestRequiredNamespaces); + task1.TrySetResult(true); + }); + + await Task.WhenAll( + task1.Task, + sessionApproval, + _wallet.Pair(uriString) + ); + + Assert.NotEqual(TestNamespaces, TestUpdatedNamespaces); + + TaskCompletionSource task2 = new TaskCompletionSource(); + _dapp.On(EngineEvents.SessionUpdate, (sender, @event) => + { + var param = @event.EventData.Params; + Assert.Equal(TestUpdatedNamespaces, param.Namespaces); + task2.TrySetResult(true); + }); + + await Task.WhenAll( + task2.Task, + _wallet.UpdateSession(session.Topic, TestUpdatedNamespaces) + ); + } + + [Fact, Trait("Category", "unit")] + public async void TestExtendSession() + { + TaskCompletionSource task1 = new TaskCompletionSource(); + _wallet.On(EngineEvents.SessionProposal, async (sender, @event) => + { + var id = @event.EventData.Id; + var proposal = @event.EventData.Proposal; + var verifyContext = @event.EventData.VerifiedContext; + + Assert.Equal(Validation.Unknown, verifyContext.Validation); + session = await _wallet.ApproveSession(id, TestNamespaces); + + Assert.Equal(proposal.RequiredNamespaces, TestRequiredNamespaces); + task1.TrySetResult(true); + }); + + await Task.WhenAll( + task1.Task, + sessionApproval, + _wallet.Pair(uriString) + ); + + var prevExpiry = session.Expiry; + var topic = session.Topic; + + // TODO Figure out if we need fake timers? + await Task.Delay(5000); + + await _wallet.ExtendSession(topic); + + var updatedExpiry = _wallet.Engine.SignClient.Session.Get(topic).Expiry; + + Assert.True(updatedExpiry > prevExpiry); + } + + [Fact, Trait("Category", "unit")] + public async void TestRespondToSessionRequest() + { + TaskCompletionSource task1 = new TaskCompletionSource(); + _wallet.On(EngineEvents.SessionProposal, async (sender, @event) => + { + var id = @event.EventData.Id; + var proposal = @event.EventData.Proposal; + var verifyContext = @event.EventData.VerifiedContext; + + session = await _wallet.ApproveSession(id, new Namespaces() + { + { + "eip155", new Namespace() + { + Methods = TestNamespace.Methods, + Events = TestNamespace.Events, + Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" } + } + } + }); + + Assert.Equal(proposal.RequiredNamespaces, TestRequiredNamespaces); + task1.TrySetResult(true); + }); + + await Task.WhenAll( + task1.Task, + sessionApproval, + _wallet.Pair(uriString) + ); + + TaskCompletionSource task2 = new TaskCompletionSource(); + _wallet.Engine.SignClient.Engine.SessionRequestEvents() + .OnRequest += args => + { + var id = args.Request.Id; + var @params = args.Request; + var verifyContext = args.VerifiedContext; + var signTransaction = @params.Params[0]; + + Assert.Equal(Validation.Unknown, verifyContext.Validation); + + var signature = ((AccountSignerTransactionManager)_cryptoWalletFixture.CryptoWallet.GetAccount(0).TransactionManager) + .SignTransaction(signTransaction); + + args.Response = signature; + task2.TrySetResult(true); + + return Task.CompletedTask; + }; + + async Task SendRequest() + { + var result = await _dapp.Request(session.Topic, + new EthSignTransaction() + { + new() + { + From = WalletAddress, + To = WalletAddress, + Data = "0x", + Nonce = new HexBigInteger("0x1"), + GasPrice = new HexBigInteger("0x020a7ac094"), + Gas = new HexBigInteger("0x5208"), + Value = new HexBigInteger("0x00") + } + }, TestEthereumChain); + + Assert.False(string.IsNullOrWhiteSpace(result)); + } + + await Task.WhenAll( + task2.Task, + SendRequest() + ); + } + + [Fact, Trait("Category", "unit")] + public async void TestWalletDisconnectFromSession() + { + TaskCompletionSource task1 = new TaskCompletionSource(); + _wallet.On(EngineEvents.SessionProposal, async (sender, @event) => + { + var id = @event.EventData.Id; + var proposal = @event.EventData.Proposal; + var verifyContext = @event.EventData.VerifiedContext; + + session = await _wallet.ApproveSession(id, new Namespaces() + { + { + "eip155", new Namespace() + { + Methods = TestNamespace.Methods, + Events = TestNamespace.Events, + Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" } + } + } + }); + + Assert.Equal(proposal.RequiredNamespaces, TestRequiredNamespaces); + task1.TrySetResult(true); + }); + + await Task.WhenAll( + task1.Task, + sessionApproval, + _wallet.Pair(uriString) + ); + + var reason = Error.FromErrorType(ErrorType.USER_DISCONNECTED); + + TaskCompletionSource task2 = new TaskCompletionSource(); + _dapp.On(EngineEvents.SessionDelete, (sender, @event) => + { + Assert.Equal(session.Topic, @event.EventData.Topic); + task2.TrySetResult(true); + }); + + await Task.WhenAll( + task2.Task, + _wallet.DisconnectSession(session.Topic, reason) + ); + } + + [Fact, Trait("Category", "unit")] + public async void TestDappDisconnectFromSession() + { + TaskCompletionSource task1 = new TaskCompletionSource(); + _wallet.On(EngineEvents.SessionProposal, async (sender, @event) => + { + var id = @event.EventData.Id; + var proposal = @event.EventData.Proposal; + var verifyContext = @event.EventData.VerifiedContext; + + session = await _wallet.ApproveSession(id, new Namespaces() + { + { + "eip155", new Namespace() + { + Methods = TestNamespace.Methods, + Events = TestNamespace.Events, + Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" } + } + } + }); + + Assert.Equal(proposal.RequiredNamespaces, TestRequiredNamespaces); + task1.TrySetResult(true); + }); + + await Task.WhenAll( + task1.Task, + sessionApproval, + _wallet.Pair(uriString) + ); + + var reason = Error.FromErrorType(ErrorType.USER_DISCONNECTED); + + TaskCompletionSource task2 = new TaskCompletionSource(); + _wallet.On(EngineEvents.SessionDelete, (sender, @event) => + { + Assert.Equal(session.Topic, @event.EventData.Topic); + task2.TrySetResult(true); + }); + + await Task.WhenAll( + task2.Task, + _dapp.Disconnect(session.Topic, reason) + ); + } + + [Fact, Trait("Category", "unit")] + public async void TestEmitSessionEvent() + { + TaskCompletionSource task1 = new TaskCompletionSource(); + _wallet.On(EngineEvents.SessionProposal, async (sender, @event) => + { + var id = @event.EventData.Id; + var proposal = @event.EventData.Proposal; + var verifyContext = @event.EventData.VerifiedContext; + + session = await _wallet.ApproveSession(id, new Namespaces() + { + { + "eip155", new Namespace() + { + Methods = TestNamespace.Methods, + Events = TestNamespace.Events, + Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" } + } + } + }); + + Assert.Equal(proposal.RequiredNamespaces, TestRequiredNamespaces); + task1.TrySetResult(true); + }); + + await Task.WhenAll( + task1.Task, + sessionApproval, + _wallet.Pair(uriString) + ); + + var sentData = new EventData() + { + Name = "chainChanged", + Data = new ChainChangedEvent() + { + Test = "123" + } + }; + + TaskCompletionSource task2 = new TaskCompletionSource(); + _dapp.HandleEventMessageType(async (s, request) => + { + var eventData = request.Params.Event; + var topic = request.Params.Topic; + Assert.Equal(session.Topic, topic); + Assert.Equal(sentData.Name, eventData.Name); + Assert.Equal(sentData.Data.Test, eventData.Data.Test); + task2.TrySetResult(true); + }, null); + + await Task.WhenAll( + task2.Task, + _wallet.EmitSessionEvent(session.Topic, sentData, TestRequiredNamespaces["eip155"].Chains[0]) + ); + } + + [Fact, Trait("Category", "unit")] + public async void TestGetActiveSessions() + { + TaskCompletionSource task1 = new TaskCompletionSource(); + _wallet.On(EngineEvents.SessionProposal, async (sender, @event) => + { + var id = @event.EventData.Id; + var proposal = @event.EventData.Proposal; + var verifyContext = @event.EventData.VerifiedContext; + + session = await _wallet.ApproveSession(id, new Namespaces() + { + { + "eip155", new Namespace() + { + Methods = TestNamespace.Methods, + Events = TestNamespace.Events, + Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" } + } + } + }); + + Assert.Equal(proposal.RequiredNamespaces, TestRequiredNamespaces); + task1.TrySetResult(true); + }); + + await Task.WhenAll( + task1.Task, + sessionApproval, + _wallet.Pair(uriString) + ); + + var sessions = _wallet.ActiveSessions; + Assert.NotNull(sessions); + Assert.Single(sessions); + Assert.Equal(session.Topic, sessions.Values.ToArray()[0].Topic); + } + + [Fact, Trait("Category", "unit")] + public async void TestGetPendingSessionProposals() + { + TaskCompletionSource task1 = new TaskCompletionSource(); + _wallet.On(EngineEvents.SessionProposal, async (sender, @event) => + { + var proposals = _wallet.PendingSessionProposals; + Assert.NotNull(proposals); + Assert.Single(proposals); + Assert.Equal(TestRequiredNamespaces, proposals.Values.ToArray()[0].RequiredNamespaces); + task1.TrySetResult(true); + }); + + await Task.WhenAll( + task1.Task, + _wallet.Pair(uriString) + ); + } + + [Fact, Trait("Category", "unit")] + public async void TestGetPendingSessionRequests() + { + TaskCompletionSource task1 = new TaskCompletionSource(); + _wallet.On(EngineEvents.SessionProposal, async (sender, @event) => + { + var id = @event.EventData.Id; + var proposal = @event.EventData.Proposal; + var verifyContext = @event.EventData.VerifiedContext; + + session = await _wallet.ApproveSession(id, new Namespaces() + { + { + "eip155", new Namespace() + { + Methods = TestNamespace.Methods, + Events = TestNamespace.Events, + Accounts = new []{ $"{TestEthereumChain}:{WalletAddress}" } + } + } + }); + + Assert.Equal(proposal.RequiredNamespaces, TestRequiredNamespaces); + task1.TrySetResult(true); + }); + + await Task.WhenAll( + task1.Task, + sessionApproval, + _wallet.Pair(uriString) + ); + + var requestParams = new EthSignTransaction() + { + new() + { + From = WalletAddress, + To = WalletAddress, + Data = "0x", + Nonce = new HexBigInteger("0x1"), + GasPrice = new HexBigInteger("0x020a7ac094"), + Gas = new HexBigInteger("0x5208"), + Value = new HexBigInteger("0x00") + } + }; + + TaskCompletionSource task2 = new TaskCompletionSource(); + _wallet.Engine.SignClient.Engine.SessionRequestEvents() + .OnRequest += args => + { + // Get the pending session request, since that's what we're testing + var pendingRequests = _wallet.PendingSessionRequests; + var request = pendingRequests[0]; + + var id = request.Id; + var verifyContext = args.VerifiedContext; + + // Perform unsafe cast, all pending requests are stored as object type + var signTransaction = ((EthSignTransaction)request.Parameters.Request.Params)[0]; + + Assert.Equal(args.Request.Id, id); + Assert.Equal(Validation.Unknown, verifyContext.Validation); + + var signature = ((AccountSignerTransactionManager)_cryptoWalletFixture.CryptoWallet.GetAccount(0).TransactionManager) + .SignTransaction(signTransaction); + + args.Response = signature; + task2.TrySetResult(true); + return Task.CompletedTask; + }; + + async Task SendRequest() + { + var result = await _dapp.Request(session.Topic, + requestParams, TestEthereumChain); + + Assert.False(string.IsNullOrWhiteSpace(result)); + } + + await Task.WhenAll( + task2.Task, + SendRequest() + ); + } + } +} diff --git a/Tests/WalletConnectSharp.Web3Wallet.Tests/WalletConnectSharp.Web3Wallet.Tests.csproj b/Tests/WalletConnectSharp.Web3Wallet.Tests/WalletConnectSharp.Web3Wallet.Tests.csproj new file mode 100644 index 0000000..dbf1d2d --- /dev/null +++ b/Tests/WalletConnectSharp.Web3Wallet.Tests/WalletConnectSharp.Web3Wallet.Tests.csproj @@ -0,0 +1,29 @@ + + + + net6.0;netcoreapp3.1 + 2.0.0 + 2.0.0 + 2.0.0 + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/WalletConnectSharp.Auth/Cacao.cs b/WalletConnectSharp.Auth/Cacao.cs new file mode 100644 index 0000000..a739a33 --- /dev/null +++ b/WalletConnectSharp.Auth/Cacao.cs @@ -0,0 +1,191 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Common.Utils; +using WalletConnectSharp.Network.Models; + +namespace WalletConnectSharp.Auth; + +[RpcResponseOptions(Clock.ONE_MINUTE, 3001)] +public class Cacao : Message +{ + public struct CacaoHeader + { + [JsonProperty] + public readonly string t = "eip4361"; + + public CacaoHeader() + { + } + } + + public class CacaoRequestPayload + { + [JsonProperty("domain")] + public string Domain; + + [JsonProperty("aud")] + public string Aud; + + [JsonProperty("version")] + public string Version; + + [JsonProperty("nonce")] + public string Nonce { get; set; } + + [JsonProperty("iat")] + public string Iat { get; set; } + + [JsonProperty("nbf")] + public string Nbf { get; set; } + + [JsonProperty("exp")] + public string Exp { get; set; } + + [JsonProperty("chainId")] + public string ChainId { get; set; } + + [JsonProperty("statement")] + public string Statement { get; set; } + + [JsonProperty("requestId")] + public string RequestId { get; set; } + + [JsonProperty("resources")] + public string[] Resource { get; set; } + } + + public class CacaoPayload : CacaoRequestPayload + { + [JsonProperty("iss")] + public string Iss { get; set; } + + public CacaoPayload() + { + } + + public CacaoPayload(CacaoRequestPayload source) + { + this.Aud = source.Aud; + this.Domain = source.Domain; + this.Exp = source.Exp; + this.Iat = source.Iat; + this.Nbf = source.Nbf; + this.Nonce = source.Nonce; + this.Resource = source.Resource; + this.Statement = source.Statement; + this.Version = source.Version; + this.ChainId = source.ChainId; + this.RequestId = source.RequestId; + } + + public CacaoPayload(CacaoRequestPayload source, string iss) + { + this.Aud = source.Aud; + this.Domain = source.Domain; + this.Exp = source.Exp; + this.Iat = source.Iat; + this.Nbf = source.Nbf; + this.Nonce = source.Nonce; + this.Resource = source.Resource; + this.Statement = source.Statement; + this.Version = source.Version; + this.ChainId = source.ChainId; + this.RequestId = source.RequestId; + + this.Iss = iss; + } + } + + [JsonConverter(typeof(CacaoSignatureConverter))] + public abstract class CacaoSignature + { + [JsonProperty("t")] + public abstract string T { get; } + + public class EIP191CacaoSignature : CacaoSignature + { + public EIP191CacaoSignature(string signature) : base(signature) + { + } + + [JsonProperty("t")] + public override string T + { + get + { + return "eip191"; + } + } + } + + public class EIP1271CacaoSignature : CacaoSignature + { + [JsonProperty("m")] + public string M { get; set; } + + public EIP1271CacaoSignature(string signature) : base(signature) + { + } + + [JsonProperty("t")] + public override string T + { + get + { + return "eip1271"; + } + } + } + + [JsonProperty("s")] + public string S { get; set; } + + protected CacaoSignature(string signature) + { + S = signature; + } + } + + public class CacaoSignatureConverter : JsonConverter + { + public override bool CanWrite + { + get + { + return false; + } + } + + public override void WriteJson(JsonWriter writer, CacaoSignature value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override CacaoSignature ReadJson(JsonReader reader, Type objectType, CacaoSignature existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + JObject jsonObject = JObject.Load(reader); + var type = jsonObject.Value("t"); + var sig = jsonObject.Value("s"); + return type switch + { + "eip191" => new CacaoSignature.EIP191CacaoSignature(sig), + "eip1271" => new CacaoSignature.EIP1271CacaoSignature(sig) + { + M = jsonObject.Value("m") + }, + _ => throw new ArgumentException($"Invalid type {type}, expected eip191 or eip1271") + }; + } + } + + [JsonProperty("h")] + public readonly CacaoHeader Header = new CacaoHeader(); + + [JsonProperty("p")] + public CacaoPayload Payload { get; set; } + + [JsonProperty("s")] + public CacaoSignature Signature { get; set; } +} diff --git a/WalletConnectSharp.Auth/Controllers/AuthEngine.cs b/WalletConnectSharp.Auth/Controllers/AuthEngine.cs new file mode 100644 index 0000000..94efde1 --- /dev/null +++ b/WalletConnectSharp.Auth/Controllers/AuthEngine.cs @@ -0,0 +1,395 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Auth.Interfaces; +using WalletConnectSharp.Auth.Internals; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Common.Model.Errors; +using WalletConnectSharp.Common.Utils; +using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Models.Relay; +using WalletConnectSharp.Core.Models.Verify; +using WalletConnectSharp.Crypto.Models; +using WalletConnectSharp.Events; +using WalletConnectSharp.Events.Model; +using WalletConnectSharp.Network.Models; +using ErrorResponse = WalletConnectSharp.Auth.Models.ErrorResponse; + +namespace WalletConnectSharp.Auth.Controllers; + +public partial class AuthEngine : IAuthEngine +{ + private bool initialized = false; + + + public const string AUTH_CLIENT_PROTOCOL = "wc"; + public const int AUTH_CLIENT_VERSION = 1; + public const string AUTH_CLIENT_CONTEXT = "auth"; + + public static readonly string AUTH_CLIENT_STORAGE_PREFIX = + $"{AUTH_CLIENT_PROTOCOL}@{AUTH_CLIENT_VERSION}:{AUTH_CLIENT_CONTEXT}"; + + public static readonly string AUTH_CLIENT_PUBLIC_KEY_NAME = $"{AUTH_CLIENT_STORAGE_PREFIX}:PUB_KEY"; + + public string Name + { + get + { + return "authEngine"; + } + } + + public string Context + { + get + { + return $"{Name}-context"; + } + } + + public IAuthClient Client { get; } + + public IDictionary PendingRequests + { + get + { + return this.Client.Requests.Values.OfType().Where(obj => obj.Id != null).GroupBy(obj => (long)obj.Id).ToDictionary(x => x.Key, x => x.First()); + } + } + + public AuthEngine(IAuthClient client) + { + Client = client; + } + + public void Init() + { + if (!initialized) + { + RegisterRelayerEvents(); + this.initialized = true; + } + } + + public async Task Request(RequestParams @params, string topic = null) + { + IsInitialized(); + + @params.Type ??= "eip4361"; + + if (!IsValidRequest(@params)) + { + throw new ArgumentException("Invalid request", nameof(@params)); + } + + if (topic != null) + { + return await this.RequestOnKnownPairing(topic, @params); + } + + var data = await this.Client.Core.Pairing.Create(); + var pairingTopic = data.Topic; + var uri = data.Uri; + + // TODO Log + + var publicKey = await this.Client.Core.Crypto.GenerateKeyPair(); + var responseTopic = this.Client.Core.Crypto.HashKey(publicKey); + + await this.Client.AuthKeys.Set(AUTH_CLIENT_PUBLIC_KEY_NAME, + new AuthData() { PublicKey = publicKey, ResponseTopic = responseTopic }); + await this.Client.PairingTopics.Set(responseTopic, + new PairingData() { Topic = responseTopic, PairingTopic = pairingTopic }); + + this.Client.Core.MessageHandler.SetDecodeOptionsForTopic(new DecodeOptions() + { + ReceiverPublicKey = publicKey + }, responseTopic); + + await this.Client.Core.Relayer.Subscribe(responseTopic); + + // TODO Log + + var id = await this.SendRequest(pairingTopic, new WcAuthRequest() + { + Payload = new PayloadParams() + { + Type = @params.Type ?? "eip4361", + ChainId = @params.ChainId, + Statement = @params.Statement, + Aud = @params.Aud, + Domain = @params.Domain, + Version = "1", + Nonce = @params.Nonce, + Iat = DateTime.Now.ToISOString(), + }, + Requester = new Requester() { Metadata = this.Client.Metadata, PublicKey = publicKey } + }, @params.Expiry); + + // TODO Log + + return new RequestUri() { Uri = uri, Id = id }; + } + + private async Task RequestOnKnownPairing(string topic, RequestParams @params) + { + var knownPairing = this.Client.Core.Pairing.Pairings.First(p => p.Active.HasValue && p.Active.Value && p.Topic == topic); + + var publicKey = this.Client.AuthKeys.Get(AUTH_CLIENT_PUBLIC_KEY_NAME); + + var id = await this.SendRequest(knownPairing.Topic, + new WcAuthRequest() + { + Payload = new PayloadParams() + { + Type = @params.Type ?? "eip4361", + ChainId = @params.ChainId, + Statement = @params.Statement, + Aud = @params.Aud, + Domain = @params.Domain, + Version = "1", + Nonce = @params.Nonce, + Iat = DateTime.Now.ToISOString() + }, + Requester = new Requester() { PublicKey = publicKey.PublicKey, Metadata = this.Client.Metadata } + }, @params.Expiry); + + // TODO Log + + return new RequestUri() { Id = id }; + } + + public async Task Respond(Message message, string iss) + { + this.IsInitialized(); + + if (message.Id == null || !IsValidRespond(message, this.Client.Requests)) + { + throw new Exception("Invalid response"); + } + + var pendingRequest = GetPendingRequest(this.Client.Requests, (long)message.Id); + + if (pendingRequest == null || pendingRequest.Id == null) + { + throw new Exception("Invalid pending request stored"); + } + + var id = (long)pendingRequest.Id; + var receiverPublicKey = pendingRequest.Requester.PublicKey; + var senderPublicKey = await this.Client.Core.Crypto.GenerateKeyPair(); + var responseTopic = this.Client.Core.Crypto.HashKey(receiverPublicKey); + var encodeOptions = new EncodeOptions() + { + Type = Crypto.Crypto.TYPE_1, ReceiverPublicKey = receiverPublicKey, SenderPublicKey = senderPublicKey + }; + + Cacao cacao; + switch (message) + { + case AuthErrorResponse errorResponse: + await this.SendError(id, responseTopic, + new ErrorResponse() { Error = errorResponse.Error, Id = errorResponse.Id }, encodeOptions); + return; + case ErrorResponse errorResponse: + await this.SendError(id, responseTopic, errorResponse, encodeOptions); + return; + case Cacao cacao1: + cacao = cacao1; + cacao.Payload ??= new Cacao.CacaoPayload(pendingRequest.CacaoPayload) { Iss = iss }; + break; + case ResultResponse response: + cacao = new Cacao() + { + Payload = new Cacao.CacaoPayload(pendingRequest.CacaoPayload) { Iss = iss }, + Signature = response.Signature + }; + break; + default: + throw new ArgumentException( + $"Unknown message type {message.GetType()}, expected Cacao or ResultResponse"); + } + + await this.SendResult(id, responseTopic, cacao, encodeOptions); + + await this.Client.Requests.Update(id, cacao); + } + + protected Task SendRequest(string topic, WcAuthRequest request, long? expiry = null, EncodeOptions options = null) + { + return this.Client.Core.MessageHandler.SendRequest(topic, request, expiry, options); + } + + protected Task SendError(long id, string topic, ErrorResponse response, EncodeOptions options = null) + { + return this.Client.Core.MessageHandler.SendError(id, topic, response.Error, options); + } + + protected Task SendResult(long id, string topic, Cacao result, EncodeOptions options = null) + { + return this.Client.Core.MessageHandler.SendResult(id, topic, result, options); + } + + protected async Task SetExpiry(string topic, long expiry) + { + if (this.Client.Core.Pairing.Store.Keys.Contains(topic)) + { + await this.Client.Core.Pairing.UpdateExpiry(topic, expiry); + } + this.Client.Core.Expirer.Set(topic, expiry); + } + + private void RegisterRelayerEvents() + { + // MessageHandler will handle all topic tracking + this.Client.Core.MessageHandler.HandleMessageType(OnAuthRequest, OnAuthResponse); + } + + private async Task OnAuthResponse(string topic, JsonRpcResponse response) + { + var id = response.Id; + + if (!response.IsError) + { + var pairing = this.Client.PairingTopics.Get(topic); + await this.Client.Core.Pairing.Activate(pairing.PairingTopic); + + var signature = response.Result.Signature; + var payload = response.Result.Payload; + + await this.Client.Requests.Set(id, response.Result); + var reconstructed = FormatMessage(payload); + + // TODO Log + + var walletAddress = IssDidUtils.DidAddress(payload.Iss); + var chainId = IssDidUtils.DidChainId(payload.Iss); + + if (string.IsNullOrWhiteSpace(walletAddress)) + { + throw new ArgumentException("Could not derive address from iss in payload"); + } + + if (string.IsNullOrWhiteSpace(chainId)) + { + throw new ArgumentException("Could not derive chainId from iss in payload"); + } + + var isValid = await SignatureUtils.VerifySignature(walletAddress, reconstructed, signature, chainId, + this.Client.ProjectId); + + if (!isValid) + { + this.Client.OnAuthResponse(new AuthErrorResponse() + { + Id = id, Topic = topic, Error = Error.FromErrorType(ErrorType.GENERIC, new Dictionary() + { + {"Message", "Invalid signature"} + }) + }); + } + else + { + this.Client.OnAuthResponse(new AuthResponse() { Id = id, Topic = topic, Response = response }); + } + } + else + { + this.Client.OnAuthResponse(new AuthErrorResponse() { Id = id, Topic = topic, Error = response.Error }); + } + } + + private async Task OnAuthRequest(string topic, JsonRpcRequest payload) + { + var payloadParams = payload.Params.Payload; + var cacaoPayload = new Cacao.CacaoRequestPayload() + { + Aud = payloadParams.Aud, + ChainId = payloadParams.ChainId, + Domain = payloadParams.Domain, + Exp = payloadParams.Exp, + Iat = payloadParams.Iat, + Nbf = payloadParams.Nbf, + Nonce = payloadParams.Nonce, + RequestId = payloadParams.RequestId, + Resource = payloadParams.Resources, + Statement = payloadParams.Statement + }; + + await this.Client.Requests.Set(payload.Id, + new PendingRequest() + { + CacaoPayload = cacaoPayload, + Id = payload.Id, + PairingTopic = topic, + Requester = payload.Params.Requester + }); + + var hash = HashUtils.HashMessage(JsonConvert.SerializeObject(payload)); + var verifyContext = await GetVerifyContext(hash, this.Client.Metadata); + + this.Client.OnAuthRequest(new AuthRequest() + { + Id = payload.Id, + Topic = topic, + Parameters = + new AuthRequestData() { CacaoPayload = cacaoPayload, Requester = payload.Params.Requester }, + VerifyContext = verifyContext + }); + } + + private async Task GetVerifyContext(string hash, Metadata metadata) + { + var context = new VerifiedContext() + { + VerifyUrl = metadata.VerifyUrl, Validation = Validation.Unknown, Origin = metadata.Url + }; + try + { + + var origin = await this.Client.Core.Verify.Resolve(hash); + + if (string.IsNullOrWhiteSpace(origin)) + { + return context; + } + + context.Origin = origin; + context.Validation = origin == metadata.Url ? Validation.Valid : Validation.Invalid; + } + catch (Exception e) + { + // TODO Log exception + } + + return context; + } + + public string FormatMessage(Cacao.CacaoPayload cacao) + { + string iss = cacao.Iss; + var header = $"{cacao.Domain} wants you to sign in with your Ethereum account:"; + var walletAddress = IssDidUtils.DidAddress(iss) + "\n" + (cacao.Statement != null ? "" : "\n"); + var statement = cacao.Statement + "\n"; + var uri = $"URI: {cacao.Aud}"; + var version = $"Version: {cacao.Version}"; + var chainId = $"Chain ID: {IssDidUtils.DidChainId(iss)}"; + var nonce = $"Nonce: {cacao.Nonce}"; + var issuedAt = $"Issued At: {cacao.Iat}"; + var resources = cacao.Resource != null && cacao.Resource.Length > 0 + ? $"Resources:\n{string.Join('\n', cacao.Resource.Select((resource) => $"- {resource}"))}" + : null; + + var message = string.Join('\n', + new string[] { header, walletAddress, statement, uri, version, chainId, nonce, issuedAt, resources } + .Where(val => !string.IsNullOrWhiteSpace(val))); + + return message; + } + + private void IsInitialized() + { + if (!this.initialized) + { + throw WalletConnectException.FromType(ErrorType.NOT_INITIALIZED, Name); + } + } +} diff --git a/WalletConnectSharp.Auth/Interfaces/IAuthClient.cs b/WalletConnectSharp.Auth/Interfaces/IAuthClient.cs new file mode 100644 index 0000000..205c697 --- /dev/null +++ b/WalletConnectSharp.Auth/Interfaces/IAuthClient.cs @@ -0,0 +1,42 @@ +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Common; +using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Events.Interfaces; + +namespace WalletConnectSharp.Auth.Interfaces; + +public interface IAuthClient : IModule, IEvents, IAuthClientEvents +{ + string Protocol { get; } + int Version { get; } + + ICore Core { get; set; } + Metadata Metadata { get; set; } + string ProjectId { get; set; } + IStore AuthKeys { get; set; } + IStore PairingTopics { get; set; } + IStore Requests { get; set; } + + IAuthEngine Engine { get; } + + AuthOptions Options { get; } + + IDictionary PendingRequests { get; } + + Task Request(RequestParams @params, string topic = null); + + Task Respond(Message message, string iss); + + Task> AuthHistory(); + + string FormatMessage(Cacao.CacaoPayload cacao); + + string FormatMessage(Cacao.CacaoRequestPayload cacao, string iss); + + internal bool OnAuthRequest(AuthRequest request); + + internal bool OnAuthResponse(AuthErrorResponse errorResponse); + + internal bool OnAuthResponse(AuthResponse response); +} diff --git a/WalletConnectSharp.Auth/Interfaces/IAuthClientEvents.cs b/WalletConnectSharp.Auth/Interfaces/IAuthClientEvents.cs new file mode 100644 index 0000000..fba7210 --- /dev/null +++ b/WalletConnectSharp.Auth/Interfaces/IAuthClientEvents.cs @@ -0,0 +1,10 @@ +using WalletConnectSharp.Auth.Models; + +namespace WalletConnectSharp.Auth.Interfaces; + +public interface IAuthClientEvents +{ + event EventHandler AuthRequested; + event EventHandler AuthResponded; + event EventHandler AuthError; +} diff --git a/WalletConnectSharp.Auth/Interfaces/IAuthEngine.cs b/WalletConnectSharp.Auth/Interfaces/IAuthEngine.cs new file mode 100644 index 0000000..01df8d2 --- /dev/null +++ b/WalletConnectSharp.Auth/Interfaces/IAuthEngine.cs @@ -0,0 +1,19 @@ +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Common; + +namespace WalletConnectSharp.Auth.Interfaces; + +public interface IAuthEngine : IModule +{ + IAuthClient Client { get; } + + IDictionary PendingRequests { get; } + + void Init(); + + Task Request(RequestParams @params, string topic = null); + + Task Respond(Message message, string iss); + + string FormatMessage(Cacao.CacaoPayload cacao); +} diff --git a/WalletConnectSharp.Auth/Internals/AuthEngineValidations.cs b/WalletConnectSharp.Auth/Internals/AuthEngineValidations.cs new file mode 100644 index 0000000..9210c71 --- /dev/null +++ b/WalletConnectSharp.Auth/Internals/AuthEngineValidations.cs @@ -0,0 +1,47 @@ +using WalletConnectSharp.Auth.Interfaces; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Common.Model.Errors; +using WalletConnectSharp.Common.Utils; +using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Interfaces; + +namespace WalletConnectSharp.Auth.Controllers; + +public partial class AuthEngine : IAuthEngine +{ + public const long MinExpiry = Clock.FIVE_MINUTES; + public const long MaxExpiry = Clock.SEVEN_DAYS; + + internal bool IsValidRequest(RequestParams @params) + { + var validAudience = Utils.IsValidUrl(@params.Aud); + // TODO: From typescript + // FIXME: disabling this temporarily since it's failing expected values like `chainId: "1"` + // const validChainId = isValidChainId(params.chainId); + var domainInAud = @params.Aud.Contains(@params.Domain); + var hasNonce = !string.IsNullOrWhiteSpace(@params.Nonce); + var hasValidType = @params.Type == "eip4361"; + var expiry = @params.Expiry; + if (expiry != null && !Utils.IsValidRequestExpiry(expiry.Value, MinExpiry, MaxExpiry)) + { + throw WalletConnectException.FromType(ErrorType.MISSING_OR_INVALID, $"request() expiry: {expiry}. Expiry must be a number (in seconds) between {MinExpiry} and {MaxExpiry}"); + } + + return validAudience && domainInAud && hasNonce && hasValidType; + } + + internal PendingRequest GetPendingRequest(IStore pendingResponses, long id) + { + return pendingResponses.Values.OfType().FirstOrDefault(request => request.Id == id); + } + + internal bool IsValidRespond(Message @params, IStore pendingResponses) + { + if (@params.Id == null) + return false; + + var validId = GetPendingRequest(pendingResponses, (long)@params.Id); + + return validId != null; + } +} diff --git a/WalletConnectSharp.Auth/Internals/CryptoUtils.cs b/WalletConnectSharp.Auth/Internals/CryptoUtils.cs new file mode 100644 index 0000000..1fc755e --- /dev/null +++ b/WalletConnectSharp.Auth/Internals/CryptoUtils.cs @@ -0,0 +1,42 @@ +using System.Security.Cryptography; + +namespace WalletConnectSharp.Auth.Internals +{ + public class CryptoUtils + { + private static readonly char[] ALPHANUMERIC = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".ToCharArray(); + + public static string GenerateNonce() + { + int bits = 96; + int charsetLength = ALPHANUMERIC.Length; + int length = (int)Math.Ceiling(bits / (Math.Log10(charsetLength) / Math.Log(2))); + + string @out = ""; + int maxByte = 256 - (256 % charsetLength); + + using (var rng = RandomNumberGenerator.Create()) + { + while (length > 0) + { + byte[] buf = new byte[(int)Math.Ceiling(length * 256.0 / maxByte)]; + + rng.GetBytes(buf); + + for (int i = 0; i < buf.Length && length > 0; i++) + { + var randomByte = buf[i]; + if (randomByte < maxByte) + { + @out += ALPHANUMERIC[randomByte % charsetLength]; + length--; + } + } + } + } + + return @out; + } + } +} diff --git a/WalletConnectSharp.Auth/Internals/IssDidUtils.cs b/WalletConnectSharp.Auth/Internals/IssDidUtils.cs new file mode 100644 index 0000000..1614574 --- /dev/null +++ b/WalletConnectSharp.Auth/Internals/IssDidUtils.cs @@ -0,0 +1,51 @@ +using System.Globalization; + +namespace WalletConnectSharp.Auth.Internals +{ + public static class IssDidUtils + { + public static string[] ExtractDidAddressSegments(string iss) + { + if (string.IsNullOrWhiteSpace(iss)) + return null; + + return iss.Split(":"); + } + + public static string DidChainId(string iss) + { + if (string.IsNullOrWhiteSpace(iss)) + return null; + + return ExtractDidAddressSegments(iss)[3]; + } + + public static string NamespacedDidChainId(string iss) + { + if (string.IsNullOrWhiteSpace(iss)) + return null; + + var segments = ExtractDidAddressSegments(iss); + + return $"{segments[2]}:{segments[3]}"; + } + + public static string DidAddress(string iss) + { + if (string.IsNullOrWhiteSpace(iss)) + return null; + + var segments = ExtractDidAddressSegments(iss); + + if (segments.Length == 0) + return null; + + return segments[segments.Length - 1]; + } + + public static string ToISOString(this DateTime dateTime) + { + return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", CultureInfo.InvariantCulture); + } + } +} diff --git a/WalletConnectSharp.Auth/Internals/SignatureUtils.cs b/WalletConnectSharp.Auth/Internals/SignatureUtils.cs new file mode 100644 index 0000000..6c5b48c --- /dev/null +++ b/WalletConnectSharp.Auth/Internals/SignatureUtils.cs @@ -0,0 +1,81 @@ +using System.Text; +using Nethereum.Signer; +using Newtonsoft.Json; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Common.Utils; +using WalletConnectSharp.Core.Models.Eth; +using WalletConnectSharp.Network.Models; + +namespace WalletConnectSharp.Auth.Internals; + +public class SignatureUtils +{ + public const string DefaultRpcUrl = "https://rpc.walletconnect.com/v1"; + + public static async Task VerifySignature(string address, string reconstructedMessage, Cacao.CacaoSignature cacaoSignature, + string chainId, string projectId) + { + switch (cacaoSignature.T) + { + case "eip191": + return IsValidEip191Signature(address, reconstructedMessage, cacaoSignature.S); + case "eip1271": + return await IsValidEip1271Signature(address, reconstructedMessage, cacaoSignature.S, chainId, projectId); + default: + throw new ArgumentException( + $"VerifySignature Failed: Attempted to verify CacaoSignature with unknown type {cacaoSignature.T}"); + } + } + + private static async Task IsValidEip1271Signature(string address, string reconstructedMessage, string cacaoSignatureS, string chainId, string projectId) + { + var eip1271MagicValue = "0x1626ba7e"; + var dynamicTypeOffset = "0000000000000000000000000000000000000000000000000000000000000040"; + var dynamicTypeLength = "0000000000000000000000000000000000000000000000000000000000000041"; + var nonPrefixedSignature = cacaoSignatureS.Substring(2); + var signer = new EthereumMessageSigner(); + var nonPrefixedHashedMessage = signer.HashPrefixedMessage(Encoding.UTF8.GetBytes(reconstructedMessage)).ToHex(); + + var data = + eip1271MagicValue + + nonPrefixedHashedMessage + + dynamicTypeOffset + + dynamicTypeLength + + nonPrefixedSignature; + + string result = null; + using (var client = new HttpClient()) + { + var url = $"{DefaultRpcUrl}/?chainId={chainId}&projectId={projectId}"; + + var rpcRequest = new JsonRpcRequest("eth_call", + new object[] { new EthCall() { To = address, Data = data }, "latest" }); + + var httpResponse = await client.PostAsync(url, new StringContent(JsonConvert.SerializeObject(rpcRequest))); + + var jsonResponse = await httpResponse.Content.ReadAsStringAsync(); + + var response = JsonConvert.DeserializeObject>(jsonResponse); + + if (response != null) + result = response.Result; + else + throw new Exception($"Could not deserialize JsonRpcResponse from JSON {jsonResponse}"); + } + + if (string.IsNullOrWhiteSpace(result) || result == "0x") + { + return false; + } + + var recoveredValue = result.Substring(0, eip1271MagicValue.Length); + return recoveredValue.ToLower() == eip1271MagicValue.ToLower(); + } + + private static bool IsValidEip191Signature(string address, string reconstructedMessage, string cacaoSignatureS) + { + var signer = new EthereumMessageSigner(); + var recoveredAddress = signer.EncodeUTF8AndEcRecover(reconstructedMessage, cacaoSignatureS); + return String.Equals(recoveredAddress, address, StringComparison.CurrentCultureIgnoreCase); + } +} diff --git a/WalletConnectSharp.Auth/Models/AuthData.cs b/WalletConnectSharp.Auth/Models/AuthData.cs new file mode 100644 index 0000000..6ae0381 --- /dev/null +++ b/WalletConnectSharp.Auth/Models/AuthData.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Core.Interfaces; + +namespace WalletConnectSharp.Auth.Models; + +public class AuthData : IKeyHolder +{ + [JsonProperty("responseTopic")] + public string ResponseTopic; + + [JsonProperty("publicKey")] + public string PublicKey; + + public string Key + { + get + { + return ResponseTopic; + } + } +} diff --git a/WalletConnectSharp.Auth/Models/AuthErrorResponse.cs b/WalletConnectSharp.Auth/Models/AuthErrorResponse.cs new file mode 100644 index 0000000..b678c45 --- /dev/null +++ b/WalletConnectSharp.Auth/Models/AuthErrorResponse.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Network.Models; + +namespace WalletConnectSharp.Auth.Models; + +public class AuthErrorResponse : TopicMessage +{ + [JsonProperty("params")] + public Error Error; +} diff --git a/WalletConnectSharp.Auth/Models/AuthOptions.cs b/WalletConnectSharp.Auth/Models/AuthOptions.cs new file mode 100644 index 0000000..970b04c --- /dev/null +++ b/WalletConnectSharp.Auth/Models/AuthOptions.cs @@ -0,0 +1,12 @@ +using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Core.Models; + +namespace WalletConnectSharp.Auth.Models; + +public class AuthOptions : CoreOptions +{ + public Metadata Metadata; + + public ICore Core { get; set; } +} diff --git a/WalletConnectSharp.Auth/Models/AuthPayload.cs b/WalletConnectSharp.Auth/Models/AuthPayload.cs new file mode 100644 index 0000000..ad90a89 --- /dev/null +++ b/WalletConnectSharp.Auth/Models/AuthPayload.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; + +namespace WalletConnectSharp.Auth.Models; + +public class AuthPayload +{ + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string? Type; + + [JsonProperty("chainId")] + public string ChainId; + + [JsonProperty("domain")] + public string Domain; + + [JsonProperty("aud")] + public string Aud; + + [JsonProperty("nonce")] + public string Nonce; + + [JsonProperty("nbf", NullValueHandling = NullValueHandling.Ignore)] + public string Nbf; + + [JsonProperty("exp", NullValueHandling = NullValueHandling.Ignore)] + public string Exp; + + [JsonProperty("statement", NullValueHandling = NullValueHandling.Ignore)] + public string Statement; + + [JsonProperty("requestId", NullValueHandling = NullValueHandling.Ignore)] + public string RequestId; + + [JsonProperty("resources", NullValueHandling = NullValueHandling.Ignore)] + public string[] Resources; +} diff --git a/WalletConnectSharp.Auth/Models/AuthRequest.cs b/WalletConnectSharp.Auth/Models/AuthRequest.cs new file mode 100644 index 0000000..f8e573e --- /dev/null +++ b/WalletConnectSharp.Auth/Models/AuthRequest.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Core.Models.Verify; + +namespace WalletConnectSharp.Auth.Models; + +public class AuthRequest : TopicMessage +{ + [JsonProperty("params")] + public AuthRequestData Parameters; + + [JsonProperty("verifyContext")] + public VerifiedContext VerifyContext { get; set; } +} diff --git a/WalletConnectSharp.Auth/Models/AuthRequestData.cs b/WalletConnectSharp.Auth/Models/AuthRequestData.cs new file mode 100644 index 0000000..9f6d8d4 --- /dev/null +++ b/WalletConnectSharp.Auth/Models/AuthRequestData.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Core.Models.Verify; + +namespace WalletConnectSharp.Auth.Models; + +public class AuthRequestData +{ + [JsonProperty("cacaoPayload")] + public Cacao.CacaoRequestPayload CacaoPayload; + + [JsonProperty("requester")] + public Requester Requester { get; set; } +} diff --git a/WalletConnectSharp.Auth/Models/AuthResponse.cs b/WalletConnectSharp.Auth/Models/AuthResponse.cs new file mode 100644 index 0000000..e41d4f9 --- /dev/null +++ b/WalletConnectSharp.Auth/Models/AuthResponse.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Network.Models; + +namespace WalletConnectSharp.Auth.Models; + +public class AuthResponse : TopicMessage +{ + [JsonProperty("params")] + public JsonRpcResponse Response; +} diff --git a/WalletConnectSharp.Auth/Models/ErrorResponse.cs b/WalletConnectSharp.Auth/Models/ErrorResponse.cs new file mode 100644 index 0000000..befb68a --- /dev/null +++ b/WalletConnectSharp.Auth/Models/ErrorResponse.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Network.Models; + +namespace WalletConnectSharp.Auth.Models; + +public class ErrorResponse : Message +{ + [JsonProperty("error")] + public Error Error; +} diff --git a/WalletConnectSharp.Auth/Models/Message.cs b/WalletConnectSharp.Auth/Models/Message.cs new file mode 100644 index 0000000..fe2c759 --- /dev/null +++ b/WalletConnectSharp.Auth/Models/Message.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Core.Interfaces; + +namespace WalletConnectSharp.Auth.Models; + +public class Message : IKeyHolder +{ + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public long? Id; + + public long Key + { + get + { + if (Id != null) + return (long)Id; + throw new KeyNotFoundException("Id Key for message instance is null: " + this.ToString()); + } + } +} diff --git a/WalletConnectSharp.Auth/Models/PairingData.cs b/WalletConnectSharp.Auth/Models/PairingData.cs new file mode 100644 index 0000000..ba8da35 --- /dev/null +++ b/WalletConnectSharp.Auth/Models/PairingData.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Core.Interfaces; + +namespace WalletConnectSharp.Auth.Models; + +public class PairingData : IKeyHolder +{ + [JsonProperty("topic")] + public string Topic; + + [JsonProperty("pairingTopic")] + public string PairingTopic { get; set; } + + public string Key + { + get + { + return PairingTopic; + } + } +} diff --git a/WalletConnectSharp.Auth/Models/PayloadParams.cs b/WalletConnectSharp.Auth/Models/PayloadParams.cs new file mode 100644 index 0000000..7d1c3fd --- /dev/null +++ b/WalletConnectSharp.Auth/Models/PayloadParams.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace WalletConnectSharp.Auth.Models; + +public class PayloadParams : AuthPayload +{ + [JsonProperty("version")] + public string Version; + + [JsonProperty("iat")] + public string Iat { get; set; } +} diff --git a/WalletConnectSharp.Auth/Models/PendingRequest.cs b/WalletConnectSharp.Auth/Models/PendingRequest.cs new file mode 100644 index 0000000..bed653b --- /dev/null +++ b/WalletConnectSharp.Auth/Models/PendingRequest.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace WalletConnectSharp.Auth.Models; + +public class PendingRequest : Message +{ + [JsonProperty("pairingTopic")] + public string PairingTopic; + + [JsonProperty("requester")] + public Requester Requester { get; set; } + + [JsonProperty("cacaoPayload")] + public Cacao.CacaoRequestPayload CacaoPayload { get; set; } +} diff --git a/WalletConnectSharp.Auth/Models/RequestParams.cs b/WalletConnectSharp.Auth/Models/RequestParams.cs new file mode 100644 index 0000000..41ec0a3 --- /dev/null +++ b/WalletConnectSharp.Auth/Models/RequestParams.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +namespace WalletConnectSharp.Auth.Models; + +public class RequestParams : AuthPayload +{ + [JsonProperty("expiry", NullValueHandling = NullValueHandling.Ignore)] + public long? Expiry; + + public RequestParams() { } + + public RequestParams(AuthPayload payload) + { + this.Aud = payload.Aud; + this.Domain = payload.Domain; + this.Exp = payload.Exp; + this.Nbf = payload.Nbf; + this.Nonce = payload.Nonce; + this.Resources = payload.Resources; + this.Statement = payload.Statement; + this.Type = payload.Type; + this.ChainId = payload.ChainId; + this.RequestId = payload.RequestId; + } +} diff --git a/WalletConnectSharp.Auth/Models/RequestUri.cs b/WalletConnectSharp.Auth/Models/RequestUri.cs new file mode 100644 index 0000000..4f5272a --- /dev/null +++ b/WalletConnectSharp.Auth/Models/RequestUri.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace WalletConnectSharp.Auth.Models; + +public class RequestUri +{ + [JsonProperty("id")] + public long Id; + + [JsonProperty("uri")] + public string Uri { get; set; } +} diff --git a/WalletConnectSharp.Auth/Models/Requester.cs b/WalletConnectSharp.Auth/Models/Requester.cs new file mode 100644 index 0000000..6513e10 --- /dev/null +++ b/WalletConnectSharp.Auth/Models/Requester.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Core; + +namespace WalletConnectSharp.Auth.Models; + +public class Requester +{ + [JsonProperty("metadata")] + public Metadata Metadata; + + [JsonProperty("publicKey")] + public string PublicKey; +} diff --git a/WalletConnectSharp.Auth/Models/ResultResponse.cs b/WalletConnectSharp.Auth/Models/ResultResponse.cs new file mode 100644 index 0000000..ed45360 --- /dev/null +++ b/WalletConnectSharp.Auth/Models/ResultResponse.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace WalletConnectSharp.Auth.Models; + +public class ResultResponse : Message +{ + [JsonProperty("signature")] + public Cacao.CacaoSignature Signature; +} diff --git a/WalletConnectSharp.Auth/Models/TopicMessage.cs b/WalletConnectSharp.Auth/Models/TopicMessage.cs new file mode 100644 index 0000000..4c4b826 --- /dev/null +++ b/WalletConnectSharp.Auth/Models/TopicMessage.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace WalletConnectSharp.Auth.Models; + +public class TopicMessage : Message +{ + [JsonProperty("topic")] + public string Topic; +} diff --git a/WalletConnectSharp.Auth/WalletConnectAuthClient.cs b/WalletConnectSharp.Auth/WalletConnectAuthClient.cs new file mode 100644 index 0000000..6ec8a79 --- /dev/null +++ b/WalletConnectSharp.Auth/WalletConnectAuthClient.cs @@ -0,0 +1,165 @@ +using WalletConnectSharp.Auth.Controllers; +using WalletConnectSharp.Auth.Interfaces; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Controllers; +using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Events; + +namespace WalletConnectSharp.Auth; + +public class WalletConnectAuthClient : IAuthClient +{ + public const string AUTH_CLIENT_PROTOCOL = AuthEngine.AUTH_CLIENT_PROTOCOL; + public const int AUTH_CLIENT_VERSION = AuthEngine.AUTH_CLIENT_VERSION; + public const string AUTH_CLIENT_DEFAULT_NAME = "authClient"; + public static readonly string AUTH_CLIENT_STORAGE_PREFIX = AuthEngine.AUTH_CLIENT_STORAGE_PREFIX; + + public string Name + { + get + { + return $"{Metadata.Name}-{AUTH_CLIENT_DEFAULT_NAME}"; + } + } + + public string Context + { + get + { + return $"{Name}-{Version}-context"; + } + } + + public EventDelegator Events { get; } + + public string Protocol + { + get + { + return AUTH_CLIENT_PROTOCOL; + } + } + + public int Version + { + get + { + return AUTH_CLIENT_VERSION; + } + } + + public event EventHandler AuthRequested; + public event EventHandler AuthResponded; + public event EventHandler AuthError; + public ICore Core { get; set; } + public Metadata Metadata { get; set; } + public string ProjectId { get; set; } + public IStore AuthKeys { get; set; } + public IStore PairingTopics { get; set; } + public IStore Requests { get; set; } + + public Task> AuthHistory() + { + return Core.History.JsonRpcHistoryOfType(); + } + + public IAuthEngine Engine { get; } + public AuthOptions Options { get; } + + public IDictionary PendingRequests + { + get + { + return Engine.PendingRequests; + } + } + + public static async Task Init(AuthOptions options) + { + var client = new WalletConnectAuthClient(options); + await client.Initialize(); + return client; + } + + private async Task Initialize() + { + await this.Core.Start(); + await this.AuthKeys.Init(); + await this.Requests.Init(); + await this.PairingTopics.Init(); + this.Engine.Init(); + } + + private WalletConnectAuthClient(AuthOptions options) + { + Options = options; + Metadata = options.Metadata; + ProjectId = options.ProjectId; + + if (string.IsNullOrWhiteSpace(options.Name)) + options.Name = $"{options.Metadata.Name}-{Name}"; + + Core = options.Core ?? new WalletConnectCore(options); + + AuthKeys = new Store(Core, "authKeys", AUTH_CLIENT_STORAGE_PREFIX); + PairingTopics = new Store(Core, "pairingTopics", AUTH_CLIENT_STORAGE_PREFIX); + Requests = new Store(Core, "requests", AUTH_CLIENT_STORAGE_PREFIX); + + Engine = new AuthEngine(this); + Events = new EventDelegator(this); + } + + public Task Request(RequestParams @params, string topic = null) + { + return this.Engine.Request(@params, topic); + } + + public Task Respond(Message message, string iss) + { + return this.Engine.Respond(message, iss); + } + + public string FormatMessage(Cacao.CacaoPayload cacao) + { + return this.Engine.FormatMessage(cacao); + } + + public string FormatMessage(Cacao.CacaoRequestPayload cacao, string iss) + { + return FormatMessage(new Cacao.CacaoPayload(cacao, iss)); + } + + bool IAuthClient.OnAuthRequest(AuthRequest request) + { + if (AuthRequested != null) + { + AuthRequested(this, request); + return true; + } + + return false; + } + + bool IAuthClient.OnAuthResponse(AuthErrorResponse errorResponse) + { + if (AuthError != null) + { + AuthError(this, errorResponse); + return true; + } + + return false; + } + + bool IAuthClient.OnAuthResponse(AuthResponse response) + { + if (AuthResponded != null) + { + AuthResponded(this, response); + return true; + } + + return false; + } +} diff --git a/WalletConnectSharp.Auth/WalletConnectSharp.Auth.csproj b/WalletConnectSharp.Auth/WalletConnectSharp.Auth.csproj new file mode 100644 index 0000000..4d253b9 --- /dev/null +++ b/WalletConnectSharp.Auth/WalletConnectSharp.Auth.csproj @@ -0,0 +1,34 @@ + + + + $(DefaultTargetFrameworks) + $(DefaultVersion) + $(DefaultVersion) + $(DefaultVersion) + WalletConnect.Auth + WalletConnectSharp.Auth + pedrouid, gigajuwels + A port of the TypeScript SDK to C#. A complete implementation of the WalletConnect v2 protocol that can be used to connect to external wallets or connect a wallet to an external Dapp + Copyright (c) WalletConnect 2023 + https://walletconnect.org/ + icon.png + https://github.com/WalletConnect/WalletConnectSharp + git + walletconnect sign wallet web3 ether ethereum blockchain evm + true + Apache-2.0 + + + + + + + + + + + + + + + diff --git a/WalletConnectSharp.Auth/WcAuthRequest.cs b/WalletConnectSharp.Auth/WcAuthRequest.cs new file mode 100644 index 0000000..13efce6 --- /dev/null +++ b/WalletConnectSharp.Auth/WcAuthRequest.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Common.Utils; +using WalletConnectSharp.Network.Models; + +namespace WalletConnectSharp.Auth; + +[RpcMethod("wc_authRequest")] +[RpcRequestOptions(Clock.ONE_DAY, 3000)] +[RpcResponseOptions(Clock.ONE_DAY, 3001)] +public class WcAuthRequest +{ + [JsonProperty("payloadParams")] + public PayloadParams Payload; + + [JsonProperty("requester")] + public Requester Requester { get; set; } +} diff --git a/WalletConnectSharp.Core/Controllers/Expirer.cs b/WalletConnectSharp.Core/Controllers/Expirer.cs index 15ad9b1..5955cf0 100644 --- a/WalletConnectSharp.Core/Controllers/Expirer.cs +++ b/WalletConnectSharp.Core/Controllers/Expirer.cs @@ -332,7 +332,8 @@ private void Expire(string target, Expiration expiration) private void CheckExpirations() { - foreach (var target in _expirations.Keys) + var clonedArray = _expirations.Keys.ToArray(); + foreach (var target in clonedArray) { var expiration = _expirations[target]; CheckExpiry(target, expiration); diff --git a/WalletConnectSharp.Core/Controllers/JsonRpcHistory.cs b/WalletConnectSharp.Core/Controllers/JsonRpcHistory.cs index dbe9a88..9385b64 100644 --- a/WalletConnectSharp.Core/Controllers/JsonRpcHistory.cs +++ b/WalletConnectSharp.Core/Controllers/JsonRpcHistory.cs @@ -32,7 +32,7 @@ public string Name { get { - return $"{_core.Name}-history-of-type-{typeof(T).Name}"; + return $"{_core.Name}-history-of-type-{typeof(T).FullName}-{typeof(TR).FullName}"; } } @@ -179,10 +179,12 @@ public Task> Get(string topic, long id) IsInitialized(); var record = GetRecord(id); - if (topic != record.Topic) + + // TODO Log + /*if (topic != record.Topic) { throw WalletConnectException.FromType(ErrorType.MISMATCHED_TOPIC, $"{Name}: {id}"); - } + }*/ return Task.FromResult>(record); } @@ -235,11 +237,20 @@ public void Delete(string topic, long? id) /// True if the request with the given topic and id exists, false otherwise public Task Exists(string topic, long id) { - IsInitialized(); - if (_records.ContainsKey(id)) return Task.FromResult(false); - var record = GetRecord(id); + try + { + IsInitialized(); + if (_records.ContainsKey(id)) return Task.FromResult(false); + var record = GetRecord(id); - return Task.FromResult(record.Topic == topic); + return Task.FromResult(record.Topic == topic); + } + catch (WalletConnectException e) + { + if (e.CodeType == ErrorType.NO_MATCHING_KEY) + return Task.FromResult(false); + throw; + } } private Task SetJsonRpcRecords(JsonRpcRecord[] records) @@ -261,7 +272,10 @@ private JsonRpcRecord GetRecord(long id) if (!_records.ContainsKey(id)) { - throw WalletConnectException.FromType(ErrorType.NO_MATCHING_KEY, new {Tag = $"{Name}: {id}"}); + throw WalletConnectException.FromType(ErrorType.NO_MATCHING_KEY, new Dictionary() + { + {"Tag", $"{Name}: {id}"} + }); } return _records[id]; diff --git a/WalletConnectSharp.Core/Controllers/JsonRpcHistoryFactory.cs b/WalletConnectSharp.Core/Controllers/JsonRpcHistoryFactory.cs index 3ff0fc2..c644341 100644 --- a/WalletConnectSharp.Core/Controllers/JsonRpcHistoryFactory.cs +++ b/WalletConnectSharp.Core/Controllers/JsonRpcHistoryFactory.cs @@ -23,6 +23,7 @@ public class JsonRpcHistoryFactory : IJsonRpcHistoryFactory /// The response type to store history for public class JsonRpcHistoryHolder { + private static readonly object historyLock = new object(); private static Dictionary> _instance = new Dictionary>(); /// @@ -33,11 +34,16 @@ public class JsonRpcHistoryHolder /// The singleton instance for the given ICore context public static async Task> InstanceForContext(ICore core) { - if (_instance.ContainsKey(core.Context)) - return _instance[core.Context]; + JsonRpcHistoryHolder historyHolder; + lock (historyLock) + { + if (_instance.ContainsKey(core.Context)) + return _instance[core.Context]; - var historyHolder = new JsonRpcHistoryHolder(core); - _instance.Add(core.Context, historyHolder); + historyHolder = new JsonRpcHistoryHolder(core); + _instance.Add(core.Context, historyHolder); + } + await historyHolder.History.Init(); return historyHolder; } diff --git a/WalletConnectSharp.Core/Controllers/Pairing.cs b/WalletConnectSharp.Core/Controllers/Pairing.cs index 2030d15..19da8f0 100644 --- a/WalletConnectSharp.Core/Controllers/Pairing.cs +++ b/WalletConnectSharp.Core/Controllers/Pairing.cs @@ -324,7 +324,7 @@ public async Task Disconnect(string topic) if (Store.Keys.Contains(topic)) { - var error = ErrorResponse.FromErrorType(ErrorType.USER_DISCONNECTED); + var error = Error.FromErrorType(ErrorType.USER_DISCONNECTED); await Core.MessageHandler.SendRequest(topic, new PairingDelete() {Code = error.Code, Message = error.Message}); await DeletePairing(topic); @@ -351,7 +351,7 @@ private async Task DeletePairing(string topic) await this.Core.Relayer.Unsubscribe(topic); await Task.WhenAll( - pairingHasDeleted ? Task.CompletedTask : this.Store.Delete(topic, ErrorResponse.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) ); @@ -430,7 +430,7 @@ private async Task OnPairingPingRequest(string topic, JsonRpcRequest(id, topic, ErrorResponse.FromException(e)); + await Core.MessageHandler.SendError(id, topic, Error.FromException(e)); } } @@ -462,11 +462,11 @@ private async Task OnPairingDeleteRequest(string topic, JsonRpcRequest(id, topic, ErrorResponse.FromException(e)); + await Core.MessageHandler.SendError(id, topic, Error.FromException(e)); } } - private async Task IsValidDisconnect(string topic, ErrorResponse reason) + private async Task IsValidDisconnect(string topic, Error reason) { if (string.IsNullOrWhiteSpace(topic)) { diff --git a/WalletConnectSharp.Core/Controllers/Publisher.cs b/WalletConnectSharp.Core/Controllers/Publisher.cs index 84268fb..8f28a93 100644 --- a/WalletConnectSharp.Core/Controllers/Publisher.cs +++ b/WalletConnectSharp.Core/Controllers/Publisher.cs @@ -147,7 +147,7 @@ public async Task Publish(string topic, string message, PublishOptions opts = nu try { await RpcPublish(topic, message, @params.Options.TTL, @params.Options.Tag, @params.Options.Relay) - .WithTimeout(TimeSpan.FromSeconds(10)); + .WithTimeout(TimeSpan.FromSeconds(45)); this.Relayer.Events.Trigger(RelayerEvents.Publish, @params); OnPublish(hash); } diff --git a/WalletConnectSharp.Core/Controllers/Relayer.cs b/WalletConnectSharp.Core/Controllers/Relayer.cs index 78f8802..e586fa5 100644 --- a/WalletConnectSharp.Core/Controllers/Relayer.cs +++ b/WalletConnectSharp.Core/Controllers/Relayer.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using WalletConnectSharp.Common; +using WalletConnectSharp.Common.Logging; using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Common.Utils; using WalletConnectSharp.Core.Interfaces; @@ -9,7 +10,6 @@ using WalletConnectSharp.Events.Model; using WalletConnectSharp.Network; using WalletConnectSharp.Network.Models; -using WalletConnectSharp.Network.Websocket; namespace WalletConnectSharp.Core.Controllers { @@ -141,12 +141,18 @@ public Relayer(RelayerOptions opts) /// public async Task Init() { + WCLogger.Log("[Relayer] Creating provider"); await CreateProvider(); + WCLogger.Log("[Relayer] Opening transport"); + await TransportOpen(); + + WCLogger.Log("[Relayer] Init MessageHandler and Subscriber"); await Task.WhenAll( - Messages.Init(), TransportOpen(), Subscriber.Init() + Messages.Init(), Subscriber.Init() ); + WCLogger.Log("[Relayer] Registering event listeners"); RegisterEventListeners(); initialized = true; @@ -169,24 +175,30 @@ await Task.WhenAll( protected virtual async Task CreateProvider() { var auth = await this.Core.Crypto.SignJwt(this.relayUrl); - Provider = CreateProvider(auth); + Provider = await CreateProvider(auth); RegisterProviderEventListeners(); } - protected virtual IJsonRpcProvider CreateProvider(string auth) + protected virtual async Task CreateProvider(string auth) { - return new JsonRpcProvider( - new WebsocketConnection( - RelayUrl.FormatRelayRpcUrl( - relayUrl, - IRelayer.Protocol, - IRelayer.Version.ToString(), - SDKConstants.SDK_VERSION, - projectId, - auth - ) + var connection = await BuildConnection( + RelayUrl.FormatRelayRpcUrl( + relayUrl, + IRelayer.Protocol, + IRelayer.Version.ToString(), + SDKConstants.SDK_VERSION, + projectId, + auth ) ); + + return new JsonRpcProvider(connection); + } + + protected virtual Task BuildConnection(string url) + { + return Core.Options.ConnectionBuilder.CreateConnection(url); + //return new WebsocketConnection(url); } protected virtual void RegisterProviderEventListeners() @@ -224,6 +236,9 @@ protected virtual void RegisterEventListeners() { this.Events.ListenFor(RelayerEvents.ConnectionStalled, async (sender, @event) => { + if (this.Provider.Connection.IsPaused) + return; + await this.RestartTransport(); }); } @@ -315,7 +330,7 @@ public async Task Subscribe(string topic, SubscribeOptions opts = null) this.Subscriber.Once(Controllers.Subscriber.SubscriberEvents.Created, (sender, @event) => { if (@event.EventData.Topic == topic) - task1.SetResult(""); + task1.TrySetResult(""); }); return (await Task.WhenAll( @@ -338,7 +353,10 @@ public Task Unsubscribe(string topic, UnsubscribeOptions opts = null) public async Task Request(IRequestArguments request, object context = null) { + WCLogger.Log("[Relayer] Checking for established connection"); await this.ToEstablishConnection(); + + WCLogger.Log("[Relayer] Sending request through provider"); return await this.Provider.Request(request, context); } @@ -442,13 +460,22 @@ protected virtual void IsInitialized() private async Task ToEstablishConnection() { - if (Connected) return; + if (Connected) + { + while (Provider.Connection.IsPaused) + { + WCLogger.Log("[Relayer] Waiting for connection to unpause"); + await Task.Delay(2); + } + return; + } if (Connecting) { // Check for connection while (Connecting) { - await Task.Delay(20); + WCLogger.Log("[Relayer] Waiting for connection to open"); + await Task.Delay(2); } if (!Connected && !Connecting) @@ -457,6 +484,7 @@ private async Task ToEstablishConnection() return; } + WCLogger.Log("[Relayer] Restarting transport"); await this.RestartTransport(); } } diff --git a/WalletConnectSharp.Core/Controllers/Store.cs b/WalletConnectSharp.Core/Controllers/Store.cs index a5e0b90..cb58900 100644 --- a/WalletConnectSharp.Core/Controllers/Store.cs +++ b/WalletConnectSharp.Core/Controllers/Store.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using WalletConnectSharp.Common; @@ -199,7 +200,25 @@ public Task Update(TKey key, TValue update) // If it exists (its not null), then set it if (@value != null) { - prop.SetValue(previousValue, null); + object test = previousValue; + prop.SetValue(test, @value, null); + previousValue = (TValue)test; + } + } + + var fields = t.GetFields(); + + // Loop through all of them + foreach (var prop in fields) + { + // Grab the updated value + var @value = prop.GetValue(update); + // If it exists (its not null), then set it + if (@value != null) + { + object test = previousValue; + prop.SetValue(test, @value); + previousValue = (TValue)test; } } @@ -224,7 +243,7 @@ public Task Update(TKey key, TValue update) /// The key to delete /// The reason this key was deleted using an ErrorResponse /// - public Task Delete(TKey key, ErrorResponse reason) + public Task Delete(TKey key, Error reason) { IsInitialized(); @@ -235,6 +254,13 @@ public Task Delete(TKey key, ErrorResponse reason) return Persist(); } + public IDictionary ToDictionary() + { + IsInitialized(); + + return new ReadOnlyDictionary(map); + } + protected virtual Task SetDataStore(TValue[] data) { return Core.Storage.SetItem(StorageKey, data); diff --git a/WalletConnectSharp.Core/Controllers/Subscriber.cs b/WalletConnectSharp.Core/Controllers/Subscriber.cs index 4970a95..a3d527d 100644 --- a/WalletConnectSharp.Core/Controllers/Subscriber.cs +++ b/WalletConnectSharp.Core/Controllers/Subscriber.cs @@ -1,3 +1,4 @@ +using WalletConnectSharp.Common.Logging; using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Common.Model.Relay; using WalletConnectSharp.Common.Utils; @@ -73,6 +74,8 @@ public string Version private Dictionary pending = new Dictionary(); private TaskCompletionSource restartTask = null; + private event EventHandler onSubscriberReady; + public bool RestartInProgress { get @@ -163,6 +166,7 @@ public string StorageKey private IRelayer _relayer; private bool initialized; private string clientId; + private ILogger logger; private ActiveSubscription[] cached = Array.Empty(); /// @@ -174,6 +178,8 @@ public Subscriber(IRelayer relayer) _relayer = relayer; Events = new EventDelegator(this); + + logger = WCLogger.WithContext(Context); } /// @@ -341,6 +347,9 @@ protected virtual void OnEnabled() { cached = Array.Empty(); initialized = true; + + if (onSubscriberReady != null) + onSubscriberReady(this, EventArgs.Empty); } protected virtual void OnDisconnect() @@ -350,6 +359,7 @@ protected virtual void OnDisconnect() protected virtual void OnDisable() { + logger.Log("OnDisable invoked"); cached = Values; _subscriptions.Clear(); _topicMap.Clear(); @@ -364,11 +374,13 @@ protected virtual async void OnConnect() OnEnabled(); } - private Task RestartToComplete() + private async Task RestartToComplete() { - if (!RestartInProgress) return Task.CompletedTask; + if (!RestartInProgress) return; - return restartTask.Task; + logger.Log("waiting for restart"); + await restartTask.Task; + logger.Log("restart completed"); } protected virtual void OnSubscribe(string id, PendingSubscription @params) @@ -395,7 +407,7 @@ protected virtual void OnResubscribe(string id, PendingSubscription @params) pending.Remove(@params.Topic); } - protected virtual async Task OnUnsubscribe(string topic, string id, ErrorResponse reason) + protected virtual async Task OnUnsubscribe(string topic, string id, Error reason) { // TODO Figure out how to do this //Events.RemoveListener(id); @@ -445,7 +457,7 @@ protected virtual Task UnsubscribeByTopic(string topic, UnsubscribeOptions opts ); } - protected virtual void DeleteSubscription(string id, ErrorResponse reason) + protected virtual void DeleteSubscription(string id, Error reason) { var subscription = GetSubscription(id); _subscriptions.Remove(id); @@ -474,7 +486,7 @@ protected virtual async Task UnsubscribeById(string topic, string id, Unsubscrib } await RpcUnsubscribe(topic, id, opts.Relay); - ErrorResponse reason = null; + Error reason = null; await OnUnsubscribe(topic, id, reason); } @@ -610,7 +622,7 @@ protected virtual async Task RpcBatchSubscribe(string[] topics, Protoc try { return await this._relayer.Request(request) - .WithTimeout(TimeSpan.FromSeconds(10)); + .WithTimeout(TimeSpan.FromSeconds(45)); } catch (Exception e) { diff --git a/WalletConnectSharp.Core/Controllers/TypedMessageHandler.cs b/WalletConnectSharp.Core/Controllers/TypedMessageHandler.cs index 5189304..8f1e350 100644 --- a/WalletConnectSharp.Core/Controllers/TypedMessageHandler.cs +++ b/WalletConnectSharp.Core/Controllers/TypedMessageHandler.cs @@ -1,6 +1,9 @@ -using WalletConnectSharp.Common.Model.Errors; +using Newtonsoft.Json; +using WalletConnectSharp.Common.Logging; +using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Core.Interfaces; using WalletConnectSharp.Core.Models.Relay; +using WalletConnectSharp.Crypto.Models; using WalletConnectSharp.Events; using WalletConnectSharp.Events.Model; using WalletConnectSharp.Network.Models; @@ -10,7 +13,8 @@ namespace WalletConnectSharp.Core.Controllers public class TypedMessageHandler : ITypedMessageHandler { private bool _initialized = false; - + private Dictionary _decodeOptionsMap = new Dictionary(); + public EventDelegator Events { get; } public ICore Core { get; } @@ -59,7 +63,9 @@ async void RelayerMessageCallback(object sender, GenericEvent e) var topic = e.EventData.Topic; var message = e.EventData.Message; - var payload = await this.Core.Crypto.Decode(topic, message); + var options = DecodeOptionForTopic(topic); + + var payload = await this.Core.Crypto.Decode(topic, message, options); if (payload.IsRequest) { Events.Trigger($"request_{payload.Method}", e.EventData); @@ -94,8 +100,10 @@ async void RequestCallback(object sender, GenericEvent e) var topic = e.EventData.Topic; var message = e.EventData.Message; + + var options = DecodeOptionForTopic(topic); - var payload = await this.Core.Crypto.Decode>(topic, message); + var payload = await this.Core.Crypto.Decode>(topic, message, options); (await this.Core.History.JsonRpcHistoryOfType()).Set(topic, payload, null); @@ -108,12 +116,28 @@ async void ResponseCallback(object sender, GenericEvent e) var topic = e.EventData.Topic; var message = e.EventData.Message; + + var options = DecodeOptionForTopic(topic); + + var rawResultPayload = await this.Core.Crypto.Decode(topic, message, options); - var payload = await this.Core.Crypto.Decode>(topic, message); + var history = await this.Core.History.JsonRpcHistoryOfType(); + var expectingResult = await history.Exists(topic, rawResultPayload.Id); - await (await this.Core.History.JsonRpcHistoryOfType()).Resolve(payload); + try + { + var payload = await this.Core.Crypto.Decode>(topic, message, options); + + await history.Resolve(payload); - await responseCallback(topic, payload); + await responseCallback(topic, payload); + } + catch (Exception ex) when (ex is JsonReaderException or JsonSerializationException) + { + if (!expectingResult) + return; + throw; + } } async void InspectResponseRaw(object sender, GenericEvent e) @@ -146,7 +170,7 @@ async void InspectResponseRaw(object sender, GenericEvent e // ignored if we can't find anything in the history } } - + Events.ListenFor($"request_{method}", RequestCallback); Events.ListenFor($"response_{method}", ResponseCallback); @@ -255,7 +279,19 @@ public PublishOptions RpcResponseOptionsForType() TTL = opts.TTL }; } - + + public void SetDecodeOptionsForTopic(DecodeOptions options, string topic) + { + _decodeOptionsMap.Add(topic, options); + } + + public DecodeOptions DecodeOptionForTopic(string topic) + { + if (_decodeOptionsMap.ContainsKey(topic)) + return _decodeOptionsMap[topic]; + return null; + } + /// /// Send a typed request message with the given request / response type pair T, TR to the given topic /// @@ -265,13 +301,15 @@ public PublishOptions RpcResponseOptionsForType() /// The request type /// The response type /// The id of the request sent - public async Task SendRequest(string topic, T parameters, long? expiry = null) + public async Task SendRequest(string topic, T parameters, long? expiry = null, EncodeOptions options = null) { var method = RpcMethodAttribute.MethodForType(); var payload = new JsonRpcRequest(method, parameters); + + WCLogger.Log(JsonConvert.SerializeObject(payload)); - var message = await this.Core.Crypto.Encode(topic, payload); + var message = await this.Core.Crypto.Encode(topic, payload, options); var opts = RpcRequestOptionsFromType(); @@ -299,10 +337,10 @@ public async Task SendRequest(string topic, T parameters, long? exp /// The typed response message to send /// The request type /// The response type - public async Task SendResult(long id, string topic, TR result) + public async Task SendResult(long id, string topic, TR result, EncodeOptions options = null) { var payload = new JsonRpcResponse(id, null, result); - var message = await this.Core.Crypto.Encode(topic, payload); + var message = await this.Core.Crypto.Encode(topic, payload, options); var opts = RpcResponseOptionsFromTypes(); await this.Core.Relayer.Publish(topic, message, opts); await (await this.Core.History.JsonRpcHistoryOfType()).Resolve(payload); @@ -316,10 +354,10 @@ public async Task SendResult(long id, string topic, TR result) /// The error response to send /// The request type /// The response type - public async Task SendError(long id, string topic, ErrorResponse error) + public async Task SendError(long id, string topic, Error error, EncodeOptions options = null) { var payload = new JsonRpcResponse(id, error, default); - var message = await this.Core.Crypto.Encode(topic, payload); + var message = await this.Core.Crypto.Encode(topic, payload, options); var opts = RpcResponseOptionsFromTypes(); await this.Core.Relayer.Publish(topic, message, opts); await (await this.Core.History.JsonRpcHistoryOfType()).Resolve(payload); diff --git a/WalletConnectSharp.Core/Interfaces/ICore.cs b/WalletConnectSharp.Core/Interfaces/ICore.cs index 245c761..f23fb61 100644 --- a/WalletConnectSharp.Core/Interfaces/ICore.cs +++ b/WalletConnectSharp.Core/Interfaces/ICore.cs @@ -1,5 +1,7 @@ using System.Threading.Tasks; using WalletConnectSharp.Common; +using WalletConnectSharp.Core.Models; +using WalletConnectSharp.Core.Models.Verify; using WalletConnectSharp.Crypto.Interfaces; using WalletConnectSharp.Events.Interfaces; using WalletConnectSharp.Storage.Interfaces; @@ -31,6 +33,11 @@ public interface ICore : IModule, IEvents /// public string ProjectId { get; } + /// + /// The CoreOptions this Core module was initialized with + /// + public CoreOptions Options { get; } + //TODO Add logger /// @@ -77,6 +84,8 @@ public interface ICore : IModule, IEvents /// with each other and keeping track of pairing state /// IPairing Pairing { get; } + + Verifier Verify { get; } /// /// Start the Core module, which will initialize all modules the Core module uses diff --git a/WalletConnectSharp.Core/Interfaces/IStore.cs b/WalletConnectSharp.Core/Interfaces/IStore.cs index 4fa4535..0cb0597 100644 --- a/WalletConnectSharp.Core/Interfaces/IStore.cs +++ b/WalletConnectSharp.Core/Interfaces/IStore.cs @@ -64,6 +64,8 @@ public interface IStore : IModule where TValue : IKeyHolder /// The key to delete /// The reason this key was deleted using an ErrorResponse /// - public Task Delete(TKey key, ErrorResponse reason); + public Task Delete(TKey key, Error reason); + + public IDictionary ToDictionary(); } } diff --git a/WalletConnectSharp.Core/Interfaces/ITypedMessageHandler.cs b/WalletConnectSharp.Core/Interfaces/ITypedMessageHandler.cs index e03c9e7..5a7236d 100644 --- a/WalletConnectSharp.Core/Interfaces/ITypedMessageHandler.cs +++ b/WalletConnectSharp.Core/Interfaces/ITypedMessageHandler.cs @@ -1,5 +1,6 @@ using WalletConnectSharp.Common; using WalletConnectSharp.Core.Models.Relay; +using WalletConnectSharp.Crypto.Models; using WalletConnectSharp.Events.Interfaces; using WalletConnectSharp.Network.Models; @@ -74,16 +75,36 @@ void HandleMessageType(Func, Task> requestCallb /// If no is found in the type T PublishOptions RpcResponseOptionsForType(); + /// + /// Set the decode options that should be used whenever a message in the given + /// topic is received. By default, no decode options are used when a message is received. + /// Use this function to set decode options for Type1 messages inside a specific topic + /// + /// The decode options to use for all messages received in a specific topic + /// The topic to set the given decode options for + void SetDecodeOptionsForTopic(DecodeOptions options, string topic); + + /// + /// Get the decode options for the given topic, all messages received in the given topic + /// will be decoded using these decode options. If no decode options are set for the given + /// topic, then null is returned. + /// + /// The topic to get decode options for + /// The decode options set for the given topic. If no decode options + /// are set for the given topic, then null is returned. + DecodeOptions DecodeOptionForTopic(string topic); + /// /// Send a typed request message with the given request / response type pair T, TR to the given topic /// /// The topic to send the request in /// The typed request message to send /// An override to specify how long this request will live for. If null is given, then expiry will be taken from either T or TR attributed options + /// (optional) Crypto Encoding options /// The request type /// The response type /// The id of the request sent - Task SendRequest(string topic, T parameters, long? expiry = null); + Task SendRequest(string topic, T parameters, long? expiry = null, EncodeOptions options = null); /// /// Send a typed response message with the given request / response type pair T, TR to the given topic @@ -91,9 +112,10 @@ void HandleMessageType(Func, Task> requestCallb /// The id of the request to respond to /// The topic to send the response in /// The typed response message to send + /// (optional) Crypto Encoding options /// The request type /// The response type - Task SendResult(long id, string topic, TR result); + Task SendResult(long id, string topic, TR result, EncodeOptions options = null); /// /// Send an error response message with the given request / response type pair T, TR to the given topic @@ -101,8 +123,9 @@ void HandleMessageType(Func, Task> requestCallb /// The id of the request to respond to /// The topic to send the response in /// The error response to send + /// (optional) Crypto Encoding options /// The request type /// The response type - Task SendError(long id, string topic, ErrorResponse error); + Task SendError(long id, string topic, Error error, EncodeOptions options = null); } } diff --git a/WalletConnectSharp.Core/Models/Pairing/Metadata.cs b/WalletConnectSharp.Core/Metadata.cs similarity index 59% rename from WalletConnectSharp.Core/Models/Pairing/Metadata.cs rename to WalletConnectSharp.Core/Metadata.cs index 64f8198..542da7a 100644 --- a/WalletConnectSharp.Core/Models/Pairing/Metadata.cs +++ b/WalletConnectSharp.Core/Metadata.cs @@ -1,35 +1,37 @@ using Newtonsoft.Json; +using WalletConnectSharp.Core.Models; -namespace WalletConnectSharp.Core.Models.Pairing +namespace WalletConnectSharp.Core { /// /// A class that holds Metadata for either peer in a given Session. Includes things such /// as Name of peer, Description, urls and images. /// + [Serializable] public class Metadata { /// /// The name of this peer /// - [JsonProperty("name")] - public string Name { get; set; } - + [JsonProperty("name")] public string Name; + /// /// The description for this peer /// - [JsonProperty("description")] - public string Description { get; set; } - + [JsonProperty("description")] public string Description; + /// /// The URL of the software this peer represents /// - [JsonProperty("url")] - public string Url { get; set; } - + [JsonProperty("url")] public string Url; + /// /// The URL of image icons of the software this peer represents /// - [JsonProperty("icons")] - public string[] Icons { get; set; } + [JsonProperty("icons")] public string[] Icons; + + [JsonProperty("redirect")] public RedirectData Redirect; + + [JsonProperty("verifyUrl")] public string VerifyUrl; } } diff --git a/WalletConnectSharp.Core/Models/CoreOptions.cs b/WalletConnectSharp.Core/Models/CoreOptions.cs index 10e2d92..0d7ebb6 100644 --- a/WalletConnectSharp.Core/Models/CoreOptions.cs +++ b/WalletConnectSharp.Core/Models/CoreOptions.cs @@ -1,5 +1,7 @@ using Newtonsoft.Json; +using WalletConnectSharp.Core.Interfaces; using WalletConnectSharp.Crypto.Interfaces; +using WalletConnectSharp.Network.Interfaces; using WalletConnectSharp.Storage; using WalletConnectSharp.Storage.Interfaces; @@ -14,26 +16,26 @@ public class CoreOptions /// The Project ID to use to authenticate with the relay server /// [JsonProperty("projectId")] - public string ProjectId { get; set; } + public string ProjectId; /// /// The name that this Core module will show itself as /// [JsonProperty("name")] - public string Name { get; set; } + public string Name; /// /// The URL of the relay server to connect to. This should not include any auth info /// [JsonProperty("relayUrl")] - public string RelayUrl { get; set; } + public string RelayUrl; /// /// The base context string to use for module isolation. If null or empty, then the string /// "{Name}-client" will be used /// [JsonProperty("context")] - public string BaseContext { get; set; } + public string BaseContext; /// /// The module to use for storage. This module will be used by most Core modules @@ -41,14 +43,28 @@ public class CoreOptions /// If this is set to null, then the default /// [JsonProperty("storage")] - public IKeyValueStorage Storage { get; set; } + public IKeyValueStorage Storage; /// /// The module to use for the module. /// If set to null, then the default module will be used with the provided Storage module. /// [JsonProperty("keychain")] - public IKeyChain KeyChain { get; set; } + public IKeyChain KeyChain; + + /// + /// The interface to use inside the Relayer to build + /// new websocket connections. + /// + [JsonProperty("connectionBuilder")] + public IConnectionBuilder ConnectionBuilder; + + /// + /// The module to use for crypto operations. This option + /// overrides the KeyChain option. If set to null, then a default Crypto module will be used + /// with either the KeyChain option or a default keychain + /// + public ICrypto CryptoModule; } } diff --git a/WalletConnectSharp.Core/Models/Eth/EthCall.cs b/WalletConnectSharp.Core/Models/Eth/EthCall.cs new file mode 100644 index 0000000..f2d879a --- /dev/null +++ b/WalletConnectSharp.Core/Models/Eth/EthCall.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace WalletConnectSharp.Core.Models.Eth; + +public class EthCall +{ + [JsonProperty("to")] public string To; + + [JsonProperty("data")] public string Data; +} diff --git a/WalletConnectSharp.Core/Models/Expirer/Expiration.cs b/WalletConnectSharp.Core/Models/Expirer/Expiration.cs index 8297c3c..00752ba 100644 --- a/WalletConnectSharp.Core/Models/Expirer/Expiration.cs +++ b/WalletConnectSharp.Core/Models/Expirer/Expiration.cs @@ -13,13 +13,11 @@ public class Expiration /// * id:123 /// * topic:my_topic_string /// - [JsonProperty("target")] - public string Target { get; set; } - + [JsonProperty("target")] public string Target; + /// /// The expiration date, as a unix timestamp (seconds) /// - [JsonProperty("expiry")] - public long Expiry { get; set; } + [JsonProperty("expiry")] public long Expiry; } } diff --git a/WalletConnectSharp.Core/Models/Expirer/ExpirerEventArgs.cs b/WalletConnectSharp.Core/Models/Expirer/ExpirerEventArgs.cs index 1ef6a50..d767922 100644 --- a/WalletConnectSharp.Core/Models/Expirer/ExpirerEventArgs.cs +++ b/WalletConnectSharp.Core/Models/Expirer/ExpirerEventArgs.cs @@ -11,12 +11,12 @@ public class ExpirerEventArgs /// The target this expiration is for /// [JsonProperty("target")] - public string Target { get; set; } + public string Target; /// /// The expiration data for this event /// [JsonProperty("expiration")] - public Expiration Expiration { get; set; } + public Expiration Expiration; } } diff --git a/WalletConnectSharp.Core/Models/Expirer/ExpirerTarget.cs b/WalletConnectSharp.Core/Models/Expirer/ExpirerTarget.cs index 0fed073..feeb36e 100644 --- a/WalletConnectSharp.Core/Models/Expirer/ExpirerTarget.cs +++ b/WalletConnectSharp.Core/Models/Expirer/ExpirerTarget.cs @@ -16,14 +16,14 @@ public class ExpirerTarget /// this field will be null /// [JsonProperty("id")] - public long? Id { get; set; } + public long? Id; /// /// The resulting Topic from the given . If the did not include a Topic, then /// this field will be null /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; /// /// Create a new instance of this class with a given . The given will diff --git a/WalletConnectSharp.Core/Models/History/JsonRpcRecord.cs b/WalletConnectSharp.Core/Models/History/JsonRpcRecord.cs index 17d8e98..ced84e4 100644 --- a/WalletConnectSharp.Core/Models/History/JsonRpcRecord.cs +++ b/WalletConnectSharp.Core/Models/History/JsonRpcRecord.cs @@ -16,19 +16,19 @@ public class JsonRpcRecord /// The id of the JSON RPC request /// [JsonProperty("id")] - public long Id { get; set; } + public long Id; /// /// The topic the request was sent in /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; /// /// The request data for this JSON RPC record /// [JsonProperty("request")] - public IRequestArguments Request { get; set; } + public IRequestArguments Request; /// /// The chainId this request is intended for diff --git a/WalletConnectSharp.Core/Models/History/RequestEvent.cs b/WalletConnectSharp.Core/Models/History/RequestEvent.cs index 39d3c19..89b2bc4 100644 --- a/WalletConnectSharp.Core/Models/History/RequestEvent.cs +++ b/WalletConnectSharp.Core/Models/History/RequestEvent.cs @@ -15,19 +15,19 @@ public class RequestEvent /// The topic the request was sent in /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; /// /// The request parameters sent /// [JsonProperty("request")] - public IRequestArguments Request { get; set; } + public IRequestArguments Request; /// /// The chainId this request is intended for /// [JsonProperty("chainId")] - public string ChainId { get; set; } + public string ChainId; /// /// A helper function to create a new RequestEvent from a diff --git a/WalletConnectSharp.Core/Models/MessageHandler/RequestEventArgs.cs b/WalletConnectSharp.Core/Models/MessageHandler/RequestEventArgs.cs index 3305d74..4e7d6b4 100644 --- a/WalletConnectSharp.Core/Models/MessageHandler/RequestEventArgs.cs +++ b/WalletConnectSharp.Core/Models/MessageHandler/RequestEventArgs.cs @@ -1,4 +1,5 @@ -using WalletConnectSharp.Network.Models; +using WalletConnectSharp.Core.Models.Verify; +using WalletConnectSharp.Network.Models; namespace WalletConnectSharp.Sign.Models { @@ -30,19 +31,22 @@ public class RequestEventArgs /// If the field is non-null, then this field will not be sent and the /// will be sent instead /// - public TR Response { get; set; } + public TR Response; /// /// The current error to send when this event finishes propagating. You can set this value /// to send an Error response when this event completes. /// This value will always override if the value is non-null /// - public ErrorResponse Error { get; set; } + public Error Error; + + public VerifiedContext VerifiedContext; - internal RequestEventArgs(string topic, JsonRpcRequest request) + internal RequestEventArgs(string topic, JsonRpcRequest request, VerifiedContext context) { Topic = topic; Request = request; + VerifiedContext = context; } } } diff --git a/WalletConnectSharp.Core/Models/MessageHandler/TypedEventHandler.cs b/WalletConnectSharp.Core/Models/MessageHandler/TypedEventHandler.cs index 843861f..2f0cb34 100644 --- a/WalletConnectSharp.Core/Models/MessageHandler/TypedEventHandler.cs +++ b/WalletConnectSharp.Core/Models/MessageHandler/TypedEventHandler.cs @@ -1,4 +1,8 @@ -using WalletConnectSharp.Core.Interfaces; +using Newtonsoft.Json; +using WalletConnectSharp.Common.Utils; +using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Core.Models.Verify; using WalletConnectSharp.Network.Models; namespace WalletConnectSharp.Sign.Models @@ -205,7 +209,18 @@ protected virtual Task ResponseCallback(string arg1, JsonRpcResponse arg2) protected virtual async Task RequestCallback(string arg1, JsonRpcRequest arg2) { - var rea = new RequestEventArgs(arg1, arg2); + VerifiedContext verifyContext = new VerifiedContext() { Validation = Validation.Unknown }; + + // Find pairing to get metadata + if (_ref.Pairing.Store.Keys.Contains(arg1)) + { + var pairing = _ref.Pairing.Store.Get(arg1); + + var hash = HashUtils.HashMessage(JsonConvert.SerializeObject(arg2)); + verifyContext = await VerifyContext(hash, pairing.PeerMetadata); + } + + var rea = new RequestEventArgs(arg1, arg2, verifyContext); if (requestPredicate != null && !requestPredicate(rea)) return; if (_onRequest == null) return; @@ -217,5 +232,31 @@ protected virtual async Task RequestCallback(string arg1, JsonRpcRequest arg2 await _ref.MessageHandler.SendResult(arg2.Id, arg1, rea.Response); } } + + async Task VerifyContext(string hash, Metadata metadata) + { + var context = new VerifiedContext() + { + VerifyUrl = metadata.VerifyUrl ?? "", + Validation = Validation.Unknown, + Origin = metadata.Url ?? "" + }; + + try + { + var origin = await _ref.Verify.Resolve(hash); + if (!string.IsNullOrWhiteSpace(origin)) + { + context.Origin = origin; + context.Validation = origin == metadata.Url ? Validation.Valid : Validation.Invalid; + } + } + catch (Exception e) + { + // TODO Log to logger + } + + return context; + } } } diff --git a/WalletConnectSharp.Core/Models/Pairing/CreatePairingData.cs b/WalletConnectSharp.Core/Models/Pairing/CreatePairingData.cs index d84f501..e2726a3 100644 --- a/WalletConnectSharp.Core/Models/Pairing/CreatePairingData.cs +++ b/WalletConnectSharp.Core/Models/Pairing/CreatePairingData.cs @@ -12,12 +12,12 @@ public class CreatePairingData /// The new pairing topic /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; /// /// The URI the wallet should use to pair & retrieve the session proposal /// [JsonProperty("uri")] - public string Uri { get; set; } + public string Uri; } } diff --git a/WalletConnectSharp.Core/Models/Pairing/Methods/PairingDelete.cs b/WalletConnectSharp.Core/Models/Pairing/Methods/PairingDelete.cs index 07f6130..6429657 100644 --- a/WalletConnectSharp.Core/Models/Pairing/Methods/PairingDelete.cs +++ b/WalletConnectSharp.Core/Models/Pairing/Methods/PairingDelete.cs @@ -9,7 +9,7 @@ namespace WalletConnectSharp.Core.Models.Pairing.Methods [RpcMethod("wc_pairingDelete")] [RpcRequestOptions(Clock.ONE_DAY, 1000)] [RpcResponseOptions(Clock.ONE_DAY, 1001)] - public class PairingDelete : ErrorResponse + public class PairingDelete : Error { } } diff --git a/WalletConnectSharp.Core/Models/Pairing/PairingEvent.cs b/WalletConnectSharp.Core/Models/Pairing/PairingEvent.cs index 54fe386..995aefe 100644 --- a/WalletConnectSharp.Core/Models/Pairing/PairingEvent.cs +++ b/WalletConnectSharp.Core/Models/Pairing/PairingEvent.cs @@ -14,12 +14,12 @@ public class PairingEvent /// The ID of the JSON Rpc request that triggered this session event /// [JsonProperty("id")] - public long Id { get; set; } + public long Id; /// /// The topic of the session this event took place in /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; } } diff --git a/WalletConnectSharp.Core/Models/Pairing/PairingStruct.cs b/WalletConnectSharp.Core/Models/Pairing/PairingStruct.cs index aca498b..7301695 100644 --- a/WalletConnectSharp.Core/Models/Pairing/PairingStruct.cs +++ b/WalletConnectSharp.Core/Models/Pairing/PairingStruct.cs @@ -14,7 +14,7 @@ public struct PairingStruct : IKeyHolder /// The topic the pairing took place in /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; /// /// This is the key field, mapped to the Topic. Implemented for @@ -33,24 +33,24 @@ public string Key /// When this pairing expires /// [JsonProperty("expiry")] - public long? Expiry { get; set; } + public long? Expiry; /// /// Relay protocol options for this pairing /// [JsonProperty("relay")] - public ProtocolOptions Relay { get; set; } + public ProtocolOptions Relay; /// /// Whether this pairing is active or not /// [JsonProperty("active")] - public bool? Active { get; set; } + public bool? Active; /// /// The metadata of the peer this pairing is with /// [JsonProperty("peerMetadata")] - public Metadata PeerMetadata { get; set; } + public Metadata PeerMetadata; } } diff --git a/WalletConnectSharp.Core/Models/Pairing/UriParameters.cs b/WalletConnectSharp.Core/Models/Pairing/UriParameters.cs index 1553a26..45885e2 100644 --- a/WalletConnectSharp.Core/Models/Pairing/UriParameters.cs +++ b/WalletConnectSharp.Core/Models/Pairing/UriParameters.cs @@ -13,30 +13,30 @@ public class UriParameters /// The protocol being used for this session (as a protocol string) /// [JsonProperty("protocol")] - public string Protocol { get; set; } + public string Protocol; /// /// The protocol version being used for this session /// [JsonProperty("version")] - public int Version { get; set; } + public int Version; /// /// The pairing topic that should be used to retrieve the session proposal /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; /// /// The sym key used to encrypt the session proposal /// [JsonProperty("symKey")] - public string SymKey { get; set; } + public string SymKey; /// /// Any protocol options that should be used when pairing / approving the session /// [JsonProperty("relay")] - public ProtocolOptions Relay { get; set; } + public ProtocolOptions Relay; } } diff --git a/WalletConnectSharp.Core/Models/Publisher/PublishParams.cs b/WalletConnectSharp.Core/Models/Publisher/PublishParams.cs index e51ee03..b120db9 100644 --- a/WalletConnectSharp.Core/Models/Publisher/PublishParams.cs +++ b/WalletConnectSharp.Core/Models/Publisher/PublishParams.cs @@ -12,18 +12,18 @@ public class PublishParams /// The topic to publish the message to /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; /// /// The message to publish in the set topic /// [JsonProperty("message")] - public string Message { get; set; } + public string Message; /// /// The required PublishOptions to use when publishing /// [JsonProperty("opts")] - public PublishOptions Options { get; set; } + public PublishOptions Options; } } diff --git a/WalletConnectSharp.Core/Models/RedirectData.cs b/WalletConnectSharp.Core/Models/RedirectData.cs new file mode 100644 index 0000000..248abbd --- /dev/null +++ b/WalletConnectSharp.Core/Models/RedirectData.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace WalletConnectSharp.Core.Models +{ + [Serializable] + public class RedirectData + { + [JsonProperty("native")] public string Native; + + [JsonProperty("universal")] public string Universal; + } +} diff --git a/WalletConnectSharp.Core/Models/Relay/DecodedMessageEvent.cs b/WalletConnectSharp.Core/Models/Relay/DecodedMessageEvent.cs index 7389d7d..0fb26f2 100644 --- a/WalletConnectSharp.Core/Models/Relay/DecodedMessageEvent.cs +++ b/WalletConnectSharp.Core/Models/Relay/DecodedMessageEvent.cs @@ -12,6 +12,6 @@ public class DecodedMessageEvent : MessageEvent /// The deserialized payload that was decoded from the Message property /// [JsonProperty("payload")] - public JsonRpcPayload Payload { get; set; } + public JsonRpcPayload Payload; } } diff --git a/WalletConnectSharp.Core/Models/Relay/MessageEvent.cs b/WalletConnectSharp.Core/Models/Relay/MessageEvent.cs index 05e3de0..4b44564 100644 --- a/WalletConnectSharp.Core/Models/Relay/MessageEvent.cs +++ b/WalletConnectSharp.Core/Models/Relay/MessageEvent.cs @@ -12,12 +12,12 @@ public class MessageEvent /// The topic the message was sent / received in /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; /// /// The message that was sent / received /// [JsonProperty("message")] - public string Message { get; set; } + public string Message; } } diff --git a/WalletConnectSharp.Core/Models/Relay/ProtocolOptionHolder.cs b/WalletConnectSharp.Core/Models/Relay/ProtocolOptionHolder.cs index ddd2be1..7e04830 100644 --- a/WalletConnectSharp.Core/Models/Relay/ProtocolOptionHolder.cs +++ b/WalletConnectSharp.Core/Models/Relay/ProtocolOptionHolder.cs @@ -5,12 +5,13 @@ namespace WalletConnectSharp.Core.Models.Relay /// /// An abstract class that simply holds ProtocolOptions under the Relay property /// + [Serializable] public abstract class ProtocolOptionHolder { /// /// The relay protocol options to use for this event /// [JsonProperty("relay")] - public ProtocolOptions Relay { get; set; } + public ProtocolOptions Relay; } } diff --git a/WalletConnectSharp.Core/Models/Relay/ProtocolOptions.cs b/WalletConnectSharp.Core/Models/Relay/ProtocolOptions.cs index ac0d0f5..a1886ee 100644 --- a/WalletConnectSharp.Core/Models/Relay/ProtocolOptions.cs +++ b/WalletConnectSharp.Core/Models/Relay/ProtocolOptions.cs @@ -11,12 +11,12 @@ public class ProtocolOptions /// The protocol to use when communicating with the relay server /// [JsonProperty("protocol")] - public string Protocol { get; set; } + public string Protocol; /// /// Additional protocol data /// - [JsonProperty("data")] - public string Data { get; set; } + [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] + public string Data; } } diff --git a/WalletConnectSharp.Core/Models/Relay/PublishOptions.cs b/WalletConnectSharp.Core/Models/Relay/PublishOptions.cs index 74d692e..51bd649 100644 --- a/WalletConnectSharp.Core/Models/Relay/PublishOptions.cs +++ b/WalletConnectSharp.Core/Models/Relay/PublishOptions.cs @@ -5,18 +5,19 @@ namespace WalletConnectSharp.Core.Models.Relay /// /// A class that represents options when publishing messages /// + [Serializable] public class PublishOptions : ProtocolOptionHolder { /// /// Time To Live value for the message being published. /// [JsonProperty("ttl")] - public long TTL { get; set; } + public long TTL; /// /// A Tag for the message /// [JsonProperty("tag")] - public long Tag { get; set; } + public long Tag; } } diff --git a/WalletConnectSharp.Core/Models/Relay/RelayPublishRequest.cs b/WalletConnectSharp.Core/Models/Relay/RelayPublishRequest.cs index 22f1c56..7b48188 100644 --- a/WalletConnectSharp.Core/Models/Relay/RelayPublishRequest.cs +++ b/WalletConnectSharp.Core/Models/Relay/RelayPublishRequest.cs @@ -6,31 +6,30 @@ namespace WalletConnectSharp.Core.Models.Relay /// /// The parameters for publishing a message to the relay server under a specific topic /// + [Serializable] public class RelayPublishRequest { /// /// The topic to publish the message under /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; /// /// The message to publish to the relay server /// [JsonProperty("message")] - public string Message { get; set; } - + public string Message; + /// /// Time To Live. How long the message will remain on the relay server without being /// consumed (by a subscriber to the topic) before it's deleted /// - [JsonProperty("ttl")] - public long TTL { get; set; } + [JsonProperty("ttl")] public long TTL; /// /// A tag for the message to identify it /// - [JsonProperty("tag")] - public long Tag { get; set; } + [JsonProperty("tag")] public long Tag; } } diff --git a/WalletConnectSharp.Core/Models/Relay/RelayerEvents.cs b/WalletConnectSharp.Core/Models/Relay/RelayerEvents.cs index 77ea37f..fedd92d 100644 --- a/WalletConnectSharp.Core/Models/Relay/RelayerEvents.cs +++ b/WalletConnectSharp.Core/Models/Relay/RelayerEvents.cs @@ -9,6 +9,9 @@ public static class RelayerEvents public static readonly string ConnectionStalled = "relayer_connection_stalled"; + /// + /// The event id for the publish event + /// public static readonly string Publish = "relayer_publish"; /// diff --git a/WalletConnectSharp.Core/Models/Relay/RelayerOptions.cs b/WalletConnectSharp.Core/Models/Relay/RelayerOptions.cs index fc50e75..0a1d645 100644 --- a/WalletConnectSharp.Core/Models/Relay/RelayerOptions.cs +++ b/WalletConnectSharp.Core/Models/Relay/RelayerOptions.cs @@ -14,14 +14,14 @@ public class RelayerOptions /// module requires the core modules to function properly /// [JsonProperty("core")] - public ICore Core { get; set; } + public ICore Core; /// /// The URL of the Relay server to connect to. This should not include any auth information, the Relayer module /// will construct it's own auth token using the project ID specified /// [JsonProperty("relayUrl")] - public string RelayUrl { get; set; } + public string RelayUrl; /// /// The project ID to use for Relay authentication diff --git a/WalletConnectSharp.Core/Models/Relay/UnsubscribeOptions.cs b/WalletConnectSharp.Core/Models/Relay/UnsubscribeOptions.cs index b69a282..4e2c2b1 100644 --- a/WalletConnectSharp.Core/Models/Relay/UnsubscribeOptions.cs +++ b/WalletConnectSharp.Core/Models/Relay/UnsubscribeOptions.cs @@ -11,6 +11,6 @@ public class UnsubscribeOptions : ProtocolOptionHolder /// The id of the subscription to unsubscribe from /// [JsonProperty("id")] - public string Id { get; set; } + public string Id; } } diff --git a/WalletConnectSharp.Core/Models/Subscriber/ActiveSubscription.cs b/WalletConnectSharp.Core/Models/Subscriber/ActiveSubscription.cs index f48ddb6..cad81ca 100644 --- a/WalletConnectSharp.Core/Models/Subscriber/ActiveSubscription.cs +++ b/WalletConnectSharp.Core/Models/Subscriber/ActiveSubscription.cs @@ -11,6 +11,6 @@ public class ActiveSubscription : PendingSubscription /// The id of the subscription /// [JsonProperty("id")] - public string Id { get; set; } + public string Id; } } diff --git a/WalletConnectSharp.Core/Models/Subscriber/BatchSubscribeParams.cs b/WalletConnectSharp.Core/Models/Subscriber/BatchSubscribeParams.cs index 2d2bd17..756a81a 100644 --- a/WalletConnectSharp.Core/Models/Subscriber/BatchSubscribeParams.cs +++ b/WalletConnectSharp.Core/Models/Subscriber/BatchSubscribeParams.cs @@ -5,6 +5,6 @@ namespace WalletConnectSharp.Core.Models.Subscriber public class BatchSubscribeParams { [JsonProperty("topics")] - public string[] Topics { get; set; } + public string[] Topics; } } diff --git a/WalletConnectSharp.Core/Models/Subscriber/DeletedSubscription.cs b/WalletConnectSharp.Core/Models/Subscriber/DeletedSubscription.cs index bfc28d4..9df3e11 100644 --- a/WalletConnectSharp.Core/Models/Subscriber/DeletedSubscription.cs +++ b/WalletConnectSharp.Core/Models/Subscriber/DeletedSubscription.cs @@ -12,6 +12,6 @@ public class DeletedSubscription : ActiveSubscription /// The reason why the subscription was deleted /// [JsonProperty("reason")] - public ErrorResponse Reason { get; set; } + public Error Reason; } } diff --git a/WalletConnectSharp.Core/Models/Subscriber/JsonRpcSubscriberParams.cs b/WalletConnectSharp.Core/Models/Subscriber/JsonRpcSubscriberParams.cs index 2b076f3..80fa01c 100644 --- a/WalletConnectSharp.Core/Models/Subscriber/JsonRpcSubscriberParams.cs +++ b/WalletConnectSharp.Core/Models/Subscriber/JsonRpcSubscriberParams.cs @@ -11,6 +11,6 @@ public class JsonRpcSubscriberParams /// The topic to subscribe to /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; } } diff --git a/WalletConnectSharp.Core/Models/Subscriber/JsonRpcSubscriptionParams.cs b/WalletConnectSharp.Core/Models/Subscriber/JsonRpcSubscriptionParams.cs index d63e9ad..fc005de 100644 --- a/WalletConnectSharp.Core/Models/Subscriber/JsonRpcSubscriptionParams.cs +++ b/WalletConnectSharp.Core/Models/Subscriber/JsonRpcSubscriptionParams.cs @@ -12,12 +12,12 @@ public class JsonRpcSubscriptionParams /// The id of the subscription the message came from /// [JsonProperty("id")] - public string Id { get; set; } + public string Id; /// /// The message data /// [JsonProperty("data")] - public MessageData Data { get; set; } + public MessageData Data; } } diff --git a/WalletConnectSharp.Core/Models/Subscriber/JsonRpcUnsubscribeParams.cs b/WalletConnectSharp.Core/Models/Subscriber/JsonRpcUnsubscribeParams.cs index 43047f5..381aa20 100644 --- a/WalletConnectSharp.Core/Models/Subscriber/JsonRpcUnsubscribeParams.cs +++ b/WalletConnectSharp.Core/Models/Subscriber/JsonRpcUnsubscribeParams.cs @@ -11,12 +11,12 @@ public class JsonRpcUnsubscribeParams /// The subscription id to unsubscribe from /// [JsonProperty("id")] - public string Id { get; set; } + public string Id; /// /// The topic the subscription exists in /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; } } diff --git a/WalletConnectSharp.Core/Models/Subscriber/MessageData.cs b/WalletConnectSharp.Core/Models/Subscriber/MessageData.cs index 89f931d..84d93af 100644 --- a/WalletConnectSharp.Core/Models/Subscriber/MessageData.cs +++ b/WalletConnectSharp.Core/Models/Subscriber/MessageData.cs @@ -12,7 +12,7 @@ public class MessageData /// The topic the message came from /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; /// /// The message as a string diff --git a/WalletConnectSharp.Core/Models/Subscriber/PendingSubscription.cs b/WalletConnectSharp.Core/Models/Subscriber/PendingSubscription.cs index a47d363..840bafc 100644 --- a/WalletConnectSharp.Core/Models/Subscriber/PendingSubscription.cs +++ b/WalletConnectSharp.Core/Models/Subscriber/PendingSubscription.cs @@ -12,6 +12,6 @@ public class PendingSubscription : SubscribeOptions /// The topic that will be subscribed to /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; } } diff --git a/WalletConnectSharp.Core/Models/Verify/Validation.cs b/WalletConnectSharp.Core/Models/Verify/Validation.cs new file mode 100644 index 0000000..cb5fa84 --- /dev/null +++ b/WalletConnectSharp.Core/Models/Verify/Validation.cs @@ -0,0 +1,8 @@ +namespace WalletConnectSharp.Core.Models.Verify; + +public enum Validation +{ + Unknown, + Valid, + Invalid, +} diff --git a/WalletConnectSharp.Core/Models/Verify/VerifiedContext.cs b/WalletConnectSharp.Core/Models/Verify/VerifiedContext.cs new file mode 100644 index 0000000..f130a75 --- /dev/null +++ b/WalletConnectSharp.Core/Models/Verify/VerifiedContext.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; + +namespace WalletConnectSharp.Core.Models.Verify; + +public class VerifiedContext +{ + [JsonProperty("origin")] + public string Origin; + + [JsonProperty("validation")] + private string _validation; + + public string ValidationString => _validation; + + public Validation Validation + { + get + { + return FromString(); + } + set + { + + _validation = AsString(value); + } + } + + [JsonProperty("verifyUrl")] + public string VerifyUrl { get; set; } + + private Validation FromString() + { + switch (ValidationString.ToLowerInvariant()) + { + case "VALID": + return Validation.Valid; + case "INVALID": + return Validation.Invalid; + default: + return Validation.Unknown; + } + } + + private string AsString(Validation str) + { + switch (str) + { + case Validation.Invalid: + return "INVALID"; + case Validation.Valid: + return "VALID"; + default: + return "UNKNOWN"; + } + } +} diff --git a/WalletConnectSharp.Core/Models/Verify/Verifier.cs b/WalletConnectSharp.Core/Models/Verify/Verifier.cs new file mode 100644 index 0000000..18a18b6 --- /dev/null +++ b/WalletConnectSharp.Core/Models/Verify/Verifier.cs @@ -0,0 +1,35 @@ +using System.Net; +using Newtonsoft.Json; +using WalletConnectSharp.Common.Utils; + +namespace WalletConnectSharp.Core.Models.Verify; + +public class Verifier +{ + public const string VerifyServer = "https://verify.walletconnect.com"; + + public CancellationTokenSource CancellationTokenSource { get; } + + public Verifier() + { + this.CancellationTokenSource = new CancellationTokenSource(Clock.AsTimeSpan(Clock.FIVE_SECONDS)); + } + + public async Task Resolve(string attestationId) + { + try + { + using HttpClient client = new HttpClient(); + var url = $"{VerifyServer}/attestation/{attestationId}"; + var results = await client.GetStringAsync(url); + + var verifiedContext = JsonConvert.DeserializeObject(results); + + return verifiedContext != null ? verifiedContext.Origin : ""; + } + catch + { + return ""; + } + } +} diff --git a/WalletConnectSharp.Core/Utils.cs b/WalletConnectSharp.Core/Utils.cs index e156e38..64a0b28 100644 --- a/WalletConnectSharp.Core/Utils.cs +++ b/WalletConnectSharp.Core/Utils.cs @@ -2,6 +2,26 @@ public static class Utils { + public static bool IsValidUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) return false; + + try + { + new Uri(url); + return true; + } + catch (Exception e) + { + return false; + } + } + + public static bool IsValidRequestExpiry(long expiry, long min, long max) + { + return expiry <= max && expiry >= min; + } + public static IEnumerable> Batch(this IEnumerable source, int size) { TSource[] bucket = null; diff --git a/WalletConnectSharp.Core/WalletConnectCore.cs b/WalletConnectSharp.Core/WalletConnectCore.cs index 56d4670..57cf270 100644 --- a/WalletConnectSharp.Core/WalletConnectCore.cs +++ b/WalletConnectSharp.Core/WalletConnectCore.cs @@ -2,11 +2,13 @@ using WalletConnectSharp.Core.Interfaces; using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Relay; +using WalletConnectSharp.Core.Models.Verify; using WalletConnectSharp.Crypto; using WalletConnectSharp.Crypto.Interfaces; using WalletConnectSharp.Events; using WalletConnectSharp.Storage; using WalletConnectSharp.Storage.Interfaces; +using WalletConnectSharp.Network.Websocket; namespace WalletConnectSharp.Core { @@ -109,6 +111,10 @@ public string Context /// public IPairing Pairing { get; } + public Verifier Verify { get; } + + public CoreOptions Options { get; } + /// /// Create a new Core with the given options. /// @@ -132,20 +138,34 @@ public WalletConnectCore(CoreOptions options = null) options.Storage = new FileSystemStorage(); } - if (options.KeyChain == null) - { - options.KeyChain = new KeyChain(options.Storage); - } + options.ConnectionBuilder ??= new WebsocketConnectionBuilder(); + + Options = options; ProjectId = options.ProjectId; RelayUrl = options.RelayUrl; - Crypto = new Crypto.Crypto(options.KeyChain); Storage = options.Storage; + + if (options.CryptoModule != null) + { + Crypto = options.CryptoModule; + } + else + { + if (options.KeyChain == null) + { + options.KeyChain = new KeyChain(options.Storage); + } + + Crypto = new Crypto.Crypto(options.KeyChain); + } + HeartBeat = new HeartBeat(); _optName = options.Name; Events = new EventDelegator(this); Expirer = new Expirer(this); Pairing = new Pairing(this); + Verify = new Verifier(); Relayer = new Relayer(new RelayerOptions() { diff --git a/WalletConnectSharp.Core/WalletConnectSharp.Core.csproj b/WalletConnectSharp.Core/WalletConnectSharp.Core.csproj index b7aee2d..bc8d956 100644 --- a/WalletConnectSharp.Core/WalletConnectSharp.Core.csproj +++ b/WalletConnectSharp.Core/WalletConnectSharp.Core.csproj @@ -7,7 +7,7 @@ $(DefaultVersion) WalletConnect.Core WalletConnectSharp.Core - pedrouid, gigajuwels, edkek + pedrouid, gigajuwels A port of the TypeScript SDK to C#. A complete implementation of the WalletConnect v2 protocol that can be used to connect to external wallets or connect a wallet to an external Dapp Copyright (c) WalletConnect 2023 https://walletconnect.org/ @@ -22,6 +22,7 @@ + diff --git a/WalletConnectSharp.Sign/Controllers/PendingRequests.cs b/WalletConnectSharp.Sign/Controllers/PendingRequests.cs new file mode 100644 index 0000000..3831601 --- /dev/null +++ b/WalletConnectSharp.Sign/Controllers/PendingRequests.cs @@ -0,0 +1,13 @@ +using WalletConnectSharp.Core.Controllers; +using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Sign.Interfaces; +using WalletConnectSharp.Sign.Models; + +namespace WalletConnectSharp.Sign.Controllers; + +public class PendingRequests : Store, IPendingRequests +{ + public PendingRequests(ICore core) : base(core, $"request", WalletConnectSignClient.StoragePrefix) + { + } +} diff --git a/WalletConnectSharp.Sign/Engine.cs b/WalletConnectSharp.Sign/Engine.cs index 67667b3..9dd9fb1 100644 --- a/WalletConnectSharp.Sign/Engine.cs +++ b/WalletConnectSharp.Sign/Engine.cs @@ -1,5 +1,7 @@ using System.Text.RegularExpressions; +using Newtonsoft.Json; using WalletConnectSharp.Common; +using WalletConnectSharp.Common.Logging; using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Common.Model.Relay; using WalletConnectSharp.Common.Utils; @@ -7,6 +9,7 @@ using WalletConnectSharp.Core.Models.Expirer; using WalletConnectSharp.Core.Models.Pairing; using WalletConnectSharp.Core.Models.Relay; +using WalletConnectSharp.Core.Models.Verify; using WalletConnectSharp.Events; using WalletConnectSharp.Events.Interfaces; using WalletConnectSharp.Events.Model; @@ -60,6 +63,8 @@ public string Context } } + private ILogger logger { get; } + /// /// Create a new Engine with the given module /// @@ -68,6 +73,8 @@ public Engine(ISignClient client) { this.Client = client; Events = new EventDelegator(this); + + logger = WCLogger.WithContext(Context); } /// @@ -111,7 +118,7 @@ private void RegisterRelayerEvents() /// The managing events for the given types T, TR public TypedEventHandler SessionRequestEvents() { - return SessionRequestEventHandler.GetInstance(Client.Core); + return SessionRequestEventHandler.GetInstance(Client.Core, PrivateThis); } /// @@ -178,6 +185,18 @@ public UriParameters ParseUri(string uri) return result; } + /// + /// Get all pending session requests + /// + public PendingRequestStruct[] PendingSessionRequests + { + get + { + this.IsInitialized(); + return this.Client.PendingRequests.Values; + } + } + /// /// Connect (a dApp) with the given ConnectOptions. At a minimum, you must specified a RequiredNamespace. /// @@ -201,6 +220,8 @@ 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) @@ -208,6 +229,8 @@ 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(); @@ -231,13 +254,19 @@ public async Task Connect(ConnectOptions options) OptionalNamespaces = optionalNamespaces, SessionProperties = sessionProperties, }; + + WCLogger.Log($"Created public key pair"); TaskCompletionSource approvalTask = new TaskCompletionSource(); this.Events.ListenForOnce("session_connect", async (sender, e) => { + logger.Log("Got session_connect event for session struct"); if (approvalTask.Task.IsCompleted) + { + logger.Log("approval already received though, skipping"); return; - + } + var session = e.EventData; session.Self.PublicKey = publicKey; var completeSession = session with { RequiredNamespaces = requiredNamespaces }; @@ -253,10 +282,16 @@ public async Task Connect(ConnectOptions options) this.Events.ListenForOnce>("session_connect", (sender, e) => { + logger.Log("Got session_connect event for rpc response"); if (approvalTask.Task.IsCompleted) + { + logger.Log("approval already received though, skipping"); return; + } + if (e.EventData.IsError) { + logger.LogError("Got session_connect error " + e.EventData.Error.Message); approvalTask.SetException(e.EventData.Error.ToException()); } }); @@ -266,7 +301,12 @@ public async Task Connect(ConnectOptions options) throw WalletConnectException.FromType(ErrorType.NO_MATCHING_KEY, $"connect() pairing topic: {topic}"); } + 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(Clock.FIVE_MINUTES); await PrivateThis.SetProposal(id, new ProposalStruct() @@ -303,10 +343,16 @@ public async Task Pair(string uri) TaskCompletionSource sessionProposeTask = new TaskCompletionSource(); Client.Once(EngineEvents.SessionProposal, - delegate(object sender, GenericEvent> @event) + delegate(object sender, GenericEvent @event) { - var proposal = @event.EventData.Params; - if (topic == proposal.PairingTopic) + var proposal = @event.EventData.Proposal; + if (topic != proposal.PairingTopic) + return; + + if (@event.EventData.VerifiedContext.Validation == Validation.Invalid) + sessionProposeTask.SetException(new Exception( + $"Could not validate, invalid validation status {@event.EventData.VerifiedContext.Validation} for origin {@event.EventData.VerifiedContext.Origin}")); + else sessionProposeTask.SetResult(proposal); }); @@ -398,7 +444,7 @@ await MessageHandler.SendResult(id, pair }, ResponderPublicKey = selfPublicKey }); - await this.Client.Proposal.Delete(id, ErrorResponse.FromErrorType(ErrorType.USER_DISCONNECTED)); + await this.Client.Proposal.Delete(id, Error.FromErrorType(ErrorType.USER_DISCONNECTED)); await this.Client.Core.Pairing.Activate(pairingTopic); } @@ -408,7 +454,7 @@ await MessageHandler.SendResult(id, pair /// /// 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. - /// Use or + /// Use or /// to generate a object, or use the alias function /// /// The parameters of the rejection @@ -425,7 +471,7 @@ public async Task Reject(RejectParams @params) if (!string.IsNullOrWhiteSpace(pairingTopic)) { await MessageHandler.SendError(id, pairingTopic, reason); - await this.Client.Proposal.Delete(id, ErrorResponse.FromErrorType(ErrorType.USER_DISCONNECTED)); + await this.Client.Proposal.Delete(id, Error.FromErrorType(ErrorType.USER_DISCONNECTED)); } } @@ -435,7 +481,7 @@ public async Task Reject(RejectParams @params) /// The topic to update /// The updated namespaces /// A task that returns an interface that can be used to listen for acknowledgement of the updates - public async Task Update(string topic, Namespaces namespaces) + public async Task UpdateSession(string topic, Namespaces namespaces) { IsInitialized(); await PrivateThis.IsValidUpdate(topic, namespaces); @@ -534,9 +580,9 @@ public async Task Request(string topic, T data, string chainId = null .OnResponse += args => { if (args.Response.IsError) - taskSource.SetException(args.Response.Error.ToException()); + taskSource.TrySetException(args.Response.Error.ToException()); else - taskSource.SetResult(args.Response.Result); + taskSource.TrySetResult(args.Response.Result); return Task.CompletedTask; }; @@ -573,6 +619,8 @@ public async Task Respond(string topic, JsonRpcResponse response) { await MessageHandler.SendResult(id, topic, response.Result); } + + await PrivateThis.DeletePendingSessionRequest(id, new Error() { Code = 0, Message = "fulfilled" }); } /// @@ -589,7 +637,8 @@ public async Task Emit(string topic, EventData @event, string chainId = nu await MessageHandler.SendRequest, object>(topic, new SessionEvent() { ChainId = chainId, - Event = @event + Event = @event, + Topic = topic, }); } @@ -626,10 +675,10 @@ public async Task Ping(string topic) /// /// The topic of the session to disconnect /// An (optional) error reason for the disconnect - public async Task Disconnect(string topic, ErrorResponse reason) + public async Task Disconnect(string topic, Error reason) { IsInitialized(); - var error = reason ?? ErrorResponse.FromErrorType(ErrorType.USER_DISCONNECTED); + var error = reason ?? Error.FromErrorType(ErrorType.USER_DISCONNECTED); await PrivateThis.IsValidDisconnect(topic, error); if (this.Client.Session.Keys.Contains(topic)) @@ -688,7 +737,7 @@ public Task Reject(ProposalStruct proposalStruct, string message = null) /// /// The proposal to reject /// An error explaining the reason for the rejection - public Task Reject(ProposalStruct proposalStruct, ErrorResponse error) + public Task Reject(ProposalStruct proposalStruct, Error error) { return Reject(proposalStruct.RejectProposal(error)); } diff --git a/WalletConnectSharp.Sign/Interfaces/IEngineAPI.cs b/WalletConnectSharp.Sign/Interfaces/IEngineAPI.cs index 557d2ee..21f6746 100644 --- a/WalletConnectSharp.Sign/Interfaces/IEngineAPI.cs +++ b/WalletConnectSharp.Sign/Interfaces/IEngineAPI.cs @@ -3,6 +3,7 @@ using WalletConnectSharp.Sign.Models; using WalletConnectSharp.Sign.Models.Engine; using WalletConnectSharp.Sign.Models.Engine.Events; +using WalletConnectSharp.Sign.Models.Engine.Methods; namespace WalletConnectSharp.Sign.Interfaces { @@ -12,6 +13,11 @@ namespace WalletConnectSharp.Sign.Interfaces /// public interface IEngineAPI { + /// + /// Get all pending session requests as an array + /// + PendingRequestStruct[] PendingSessionRequests { get; } + /// /// Connect (a dApp) with the given ConnectOptions. At a minimum, you must specified a RequiredNamespace. /// @@ -51,7 +57,7 @@ public interface IEngineAPI /// /// 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. - /// Use or + /// Use or /// to generate a object, or use the alias function /// /// The parameters of the rejection @@ -72,7 +78,7 @@ public interface IEngineAPI /// /// The proposal to reject /// An error explaining the reason for the rejection - Task Reject(ProposalStruct proposalStruct, ErrorResponse error); + Task Reject(ProposalStruct proposalStruct, Error error); /// /// Update a session, adding/removing additional namespaces in the given topic. @@ -80,7 +86,7 @@ public interface IEngineAPI /// The topic to update /// The updated namespaces /// A task that returns an interface that can be used to listen for acknowledgement of the updates - Task Update(string topic, Namespaces namespaces); + Task UpdateSession(string topic, Namespaces namespaces); /// /// Extend a session in the given topic. @@ -141,7 +147,7 @@ public interface IEngineAPI /// /// The topic of the session to disconnect /// An (optional) error reason for the disconnect - Task Disconnect(string topic, ErrorResponse reason = null); + Task Disconnect(string topic, Error reason = null); /// /// Find all sessions that have a namespace that match the given @@ -149,5 +155,8 @@ public interface IEngineAPI /// The required namespaces the session must have to be returned /// All sessions that have a namespace that match the given SessionStruct[] Find(RequiredNamespaces requiredNamespaces); + + void HandleEventMessageType(Func>, Task> requestCallback, + Func, Task> responseCallback); } } diff --git a/WalletConnectSharp.Sign/Interfaces/IEnginePrivate.cs b/WalletConnectSharp.Sign/Interfaces/IEnginePrivate.cs index 4477f39..9e0c4bd 100644 --- a/WalletConnectSharp.Sign/Interfaces/IEnginePrivate.cs +++ b/WalletConnectSharp.Sign/Interfaces/IEnginePrivate.cs @@ -7,7 +7,7 @@ namespace WalletConnectSharp.Sign.Interfaces { - internal interface IEnginePrivate + public interface IEnginePrivate { internal Task DeleteSession(string topic); @@ -67,6 +67,10 @@ internal interface IEnginePrivate internal Task IsValidEmit(string topic, EventData request, string chainId); - internal Task IsValidDisconnect(string topic, ErrorResponse reason); + internal Task IsValidDisconnect(string topic, Error reason); + + internal Task DeletePendingSessionRequest(long id, Error reason, bool expirerHasDeleted = false); + + internal Task SetPendingSessionRequest(PendingRequestStruct pendingRequest); } } diff --git a/WalletConnectSharp.Sign/Interfaces/IPendingRequests.cs b/WalletConnectSharp.Sign/Interfaces/IPendingRequests.cs new file mode 100644 index 0000000..abecf65 --- /dev/null +++ b/WalletConnectSharp.Sign/Interfaces/IPendingRequests.cs @@ -0,0 +1,9 @@ +using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Sign.Models; + +namespace WalletConnectSharp.Sign.Interfaces +{ + public interface IPendingRequests : IStore + { + } +} diff --git a/WalletConnectSharp.Sign/Interfaces/ISignClient.cs b/WalletConnectSharp.Sign/Interfaces/ISignClient.cs index edc8b8a..4d64cc5 100644 --- a/WalletConnectSharp.Sign/Interfaces/ISignClient.cs +++ b/WalletConnectSharp.Sign/Interfaces/ISignClient.cs @@ -1,4 +1,5 @@ using WalletConnectSharp.Common; +using WalletConnectSharp.Core; using WalletConnectSharp.Core.Interfaces; using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Pairing; @@ -37,6 +38,8 @@ public interface ISignClient : IModule, IEvents, IEngineAPI /// The module this Sign Client is using to store Proposal data /// IProposal Proposal { get; } + + IPendingRequests PendingRequests { get; } /// /// The options this Sign Client was initialized with diff --git a/WalletConnectSharp.Sign/Internals/EngineHandler.cs b/WalletConnectSharp.Sign/Internals/EngineHandler.cs index 572442c..a8c2cb5 100644 --- a/WalletConnectSharp.Sign/Internals/EngineHandler.cs +++ b/WalletConnectSharp.Sign/Internals/EngineHandler.cs @@ -1,4 +1,6 @@ -using WalletConnectSharp.Common.Model.Errors; +using Newtonsoft.Json; +using WalletConnectSharp.Common.Logging; +using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Common.Utils; using WalletConnectSharp.Core.Models.Expirer; using WalletConnectSharp.Core.Models.Pairing.Methods; @@ -18,6 +20,13 @@ async void ExpiredCallback(object sender, GenericEvent e) { var target = new ExpirerTarget(e.EventData.Target); + if (target.Id != null && this.Client.PendingRequests.Keys.Contains((long)target.Id)) + { + await PrivateThis.DeletePendingSessionRequest((long)target.Id, + Error.FromErrorType(ErrorType.EXPIRED), true); + return; + } + if (!string.IsNullOrWhiteSpace(target.Topic)) { var topic = target.Topic; @@ -54,29 +63,35 @@ async Task IEnginePrivate.OnSessionProposeRequest(string topic, JsonRpcRequest() + var hash = HashUtils.HashMessage(JsonConvert.SerializeObject(payload)); + var verifyContext = await this.VerifyContext(hash, proposal.Proposer.Metadata); + this.Client.Events.Trigger(EngineEvents.SessionProposal, new SessionProposalEvent() { Id = id, - Params = proposal + Proposal = proposal, + VerifiedContext = verifyContext }); } catch (WalletConnectException e) { await MessageHandler.SendError(id, topic, - ErrorResponse.FromException(e)); + Error.FromException(e)); } } async Task IEnginePrivate.OnSessionProposeResponse(string topic, JsonRpcResponse payload) { var id = payload.Id; + logger.Log($"Got session propose response with id {id}"); if (payload.IsError) { - await this.Client.Proposal.Delete(id, ErrorResponse.FromErrorType(ErrorType.USER_DISCONNECTED)); + logger.LogError("response was error"); + await this.Client.Proposal.Delete(id, Error.FromErrorType(ErrorType.USER_DISCONNECTED)); this.Events.Trigger(EngineEvents.SessionConnect, payload); } else { + logger.Log("response was success"); var result = payload.Result; var proposal = this.Client.Proposal.Get(id); var selfPublicKey = proposal.Proposer.PublicKey; @@ -86,8 +101,28 @@ async Task IEnginePrivate.OnSessionProposeResponse(string topic, JsonRpcResponse selfPublicKey, peerPublicKey ); - var subscriptionId = await this.Client.Core.Relayer.Subscribe(sessionTopic); await this.Client.Core.Pairing.Activate(topic); + logger.Log($"pairing activated for topic {topic}"); + + // try to do this a couple of times .. do it until it works? + int attempts = 5; + do + { + try + { + var subscriptionId = await this.Client.Core.Relayer.Subscribe(sessionTopic); + return; + } + catch (Exception e) + { + WCLogger.LogError($"Got error subscribing to topic, attempts left: {attempts}"); + WCLogger.LogError(e); + attempts--; + await Task.Yield(); + } + } while (attempts > 0); + + throw new IOException($"Could not subscribe to session topic {sessionTopic}"); } } @@ -95,6 +130,7 @@ async Task IEnginePrivate.OnSessionSettleRequest(string topic, JsonRpcRequest(id, topic, ErrorResponse.FromException(e)); + logger.LogError("got error while performing session settle"); + logger.LogError(e); + await MessageHandler.SendError(id, topic, Error.FromException(e)); } } @@ -136,7 +174,7 @@ async Task IEnginePrivate.OnSessionSettleResponse(string topic, JsonRpcResponse< var id = payload.Id; if (payload.IsError) { - await this.Client.Session.Delete(topic, ErrorResponse.FromErrorType(ErrorType.USER_DISCONNECTED)); + await this.Client.Session.Delete(topic, Error.FromErrorType(ErrorType.USER_DISCONNECTED)); this.Events.Trigger($"session_approve{id}", payload); } else @@ -172,7 +210,7 @@ async Task IEnginePrivate.OnSessionUpdateRequest(string topic, JsonRpcRequest(id, topic, ErrorResponse.FromException(e)); + await MessageHandler.SendError(id, topic, Error.FromException(e)); } } @@ -198,7 +236,7 @@ async Task IEnginePrivate.OnSessionExtendRequest(string topic, JsonRpcRequest(id, topic, ErrorResponse.FromException(e)); + await MessageHandler.SendError(id, topic, Error.FromException(e)); } } @@ -223,7 +261,7 @@ async Task IEnginePrivate.OnSessionPingRequest(string topic, JsonRpcRequest(id, topic, ErrorResponse.FromException(e)); + await MessageHandler.SendError(id, topic, Error.FromException(e)); } } @@ -255,7 +293,7 @@ async Task IEnginePrivate.OnSessionDeleteRequest(string topic, JsonRpcRequest(id, topic, ErrorResponse.FromException(e)); + await MessageHandler.SendError(id, topic, Error.FromException(e)); } } @@ -276,7 +314,7 @@ async Task IEnginePrivate.OnSessionRequest(string topic, JsonRpcRequest, TR>(id, topic, ErrorResponse.FromException(e)); + await MessageHandler.SendError, TR>(id, topic, Error.FromException(e)); } } @@ -296,7 +334,7 @@ async Task IEnginePrivate.OnSessionEventRequest(string topic, JsonRpcRequest< } catch (WalletConnectException e) { - await MessageHandler.SendError, object>(id, topic, ErrorResponse.FromException(e)); + await MessageHandler.SendError, object>(id, topic, Error.FromException(e)); } } } diff --git a/WalletConnectSharp.Sign/Internals/EngineTasks.cs b/WalletConnectSharp.Sign/Internals/EngineTasks.cs index bd10003..dd3e445 100644 --- a/WalletConnectSharp.Sign/Internals/EngineTasks.cs +++ b/WalletConnectSharp.Sign/Internals/EngineTasks.cs @@ -2,18 +2,42 @@ using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Common.Model.Relay; using WalletConnectSharp.Common.Utils; +using WalletConnectSharp.Core; using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Pairing; using WalletConnectSharp.Core.Models.Relay; +using WalletConnectSharp.Core.Models.Verify; using WalletConnectSharp.Network.Models; using WalletConnectSharp.Sign.Interfaces; using WalletConnectSharp.Sign.Models; using WalletConnectSharp.Sign.Models.Engine; +using WalletConnectSharp.Sign.Models.Engine.Methods; namespace WalletConnectSharp.Sign { public partial class Engine { + async Task IEnginePrivate.DeletePendingSessionRequest(long id, Error reason, bool expirerHasDeleted = false) + { + await Task.WhenAll( + this.Client.PendingRequests.Delete(id, reason), + expirerHasDeleted ? Task.CompletedTask : this.Client.Core.Expirer.Delete(id) + ); + } + + async Task IEnginePrivate.SetPendingSessionRequest(PendingRequestStruct pendingRequest) + { + var options = RpcRequestOptionsAttribute.GetOptionsForType>(); + var expiry = options.TTL; + + await this.Client.PendingRequests.Set(pendingRequest.Id, pendingRequest); + + if (expiry != 0) + { + this.Client.Core.Expirer.Set(pendingRequest.Id, Clock.CalculateExpiry(expiry)); + } + } + async Task IEnginePrivate.DeleteSession(string topic) { var session = this.Client.Session.Get(topic); @@ -26,7 +50,7 @@ async Task IEnginePrivate.DeleteSession(string topic) await this.Client.Core.Relayer.Unsubscribe(topic); await Task.WhenAll( - sessionDeleted ? Task.CompletedTask : this.Client.Session.Delete(topic, ErrorResponse.FromErrorType(ErrorType.USER_DISCONNECTED)), + sessionDeleted ? Task.CompletedTask : this.Client.Session.Delete(topic, Error.FromErrorType(ErrorType.USER_DISCONNECTED)), hasKeypairDeleted ? Task.CompletedTask : this.Client.Core.Crypto.DeleteKeyPair(self.PublicKey), hasSymkeyDeleted ? Task.CompletedTask : this.Client.Core.Crypto.DeleteSymKey(topic), expirerHasDeleted ? Task.CompletedTask : this.Client.Core.Expirer.Delete(topic) @@ -39,7 +63,7 @@ Task IEnginePrivate.DeleteProposal(long id) bool proposalHasDeleted = !this.Client.Proposal.Keys.Contains(id); return Task.WhenAll( - proposalHasDeleted ? Task.CompletedTask : this.Client.Proposal.Delete(id, ErrorResponse.FromErrorType(ErrorType.USER_DISCONNECTED)), + proposalHasDeleted ? Task.CompletedTask : this.Client.Proposal.Delete(id, Error.FromErrorType(ErrorType.USER_DISCONNECTED)), expirerHasDeleted ? Task.CompletedTask : this.Client.Core.Expirer.Delete(id) ); } @@ -66,7 +90,7 @@ async Task IEnginePrivate.SetProposal(long id, ProposalStruct proposal) Task IEnginePrivate.Cleanup() { List sessionTopics = (from session in this.Client.Session.Values.Where(e => e.Expiry != null) where Clock.IsExpired(session.Expiry.Value) select session.Topic).ToList(); - List proposalIds = (from p in this.Client.Proposal.Values.Where(e => e.Expiry != null) where Clock.IsExpired(p.Expiry.Value) select p.Id.Value).ToList(); + List proposalIds = (from p in this.Client.Proposal.Values.Where(e => e.Expiry != null) where Clock.IsExpired(p.Expiry.Value) select p.Id).ToList(); return Task.WhenAll( sessionTopics.Select(t => PrivateThis.DeleteSession(t)).Concat( @@ -74,5 +98,31 @@ Task IEnginePrivate.Cleanup() ) ); } + + async Task VerifyContext(string hash, Metadata metadata) + { + var context = new VerifiedContext() + { + VerifyUrl = metadata.VerifyUrl ?? "", + Validation = Validation.Unknown, + Origin = metadata.Url ?? "" + }; + + try + { + var origin = await this.Client.Core.Verify.Resolve(hash); + if (!string.IsNullOrWhiteSpace(origin)) + { + context.Origin = origin; + context.Validation = origin == metadata.Url ? Validation.Valid : Validation.Invalid; + } + } + catch (Exception e) + { + // TODO Log to logger + } + + return context; + } } } diff --git a/WalletConnectSharp.Sign/Internals/EngineValidation.cs b/WalletConnectSharp.Sign/Internals/EngineValidation.cs index 5e45d14..0a19a81 100644 --- a/WalletConnectSharp.Sign/Internals/EngineValidation.cs +++ b/WalletConnectSharp.Sign/Internals/EngineValidation.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using WalletConnectSharp.Common.Model.Errors; using WalletConnectSharp.Common.Utils; +using WalletConnectSharp.Core; using WalletConnectSharp.Network.Models; using WalletConnectSharp.Sign.Interfaces; using WalletConnectSharp.Sign.Models; @@ -96,24 +97,9 @@ async Task IsValidSessionOrPairingTopic(string topic) } } - private bool IsValidUrl(string url) - { - if (string.IsNullOrWhiteSpace(url)) return false; - - try - { - new Uri(url); - return true; - } - catch (Exception e) - { - return false; - } - } - Task IEnginePrivate.IsValidPair(string uri) { - if (!IsValidUrl(uri)) + if (!Utils.IsValidUrl(uri)) throw WalletConnectException.FromType(ErrorType.MISSING_OR_INVALID, $"pair() uri: {uri}"); return Task.CompletedTask; } @@ -301,7 +287,7 @@ async Task IEnginePrivate.IsValidEmit(string topic, EventData @event, stri } } - async Task IEnginePrivate.IsValidDisconnect(string topic, ErrorResponse reason) + async Task IEnginePrivate.IsValidDisconnect(string topic, Error reason) { if (string.IsNullOrWhiteSpace(topic)) { @@ -325,9 +311,9 @@ private bool IsValidAccountId(string account) return false; } - private ErrorResponse IsValidAccounts(string[] accounts, string context) + private Error IsValidAccounts(string[] accounts, string context) { - ErrorResponse error = null; + Error error = null; foreach (var account in accounts) { if (error != null) @@ -335,16 +321,16 @@ private ErrorResponse IsValidAccounts(string[] accounts, string context) if (!IsValidAccountId(account)) { - error = ErrorResponse.FromErrorType(ErrorType.UNSUPPORTED_ACCOUNTS, $"{context}, account {account} should be a string and conform to 'namespace:chainId:address' format"); + error = Error.FromErrorType(ErrorType.UNSUPPORTED_ACCOUNTS, $"{context}, account {account} should be a string and conform to 'namespace:chainId:address' format"); } } return error; } - private ErrorResponse IsValidNamespaceAccounts(Namespaces namespaces, string method) + private Error IsValidNamespaceAccounts(Namespaces namespaces, string method) { - ErrorResponse error = null; + Error error = null; foreach (var ns in namespaces.Values) { if (error != null) break; @@ -359,9 +345,9 @@ private ErrorResponse IsValidNamespaceAccounts(Namespaces namespaces, string met return error; } - private ErrorResponse IsValidNamespaces(Namespaces namespaces, string method) + private Error IsValidNamespaces(Namespaces namespaces, string method) { - ErrorResponse error = null; + Error error = null; if (namespaces != null) { var validAccountsError = IsValidNamespaceAccounts(namespaces, method); @@ -372,7 +358,7 @@ private ErrorResponse IsValidNamespaces(Namespaces namespaces, string method) } else { - error = ErrorResponse.FromErrorType(ErrorType.MISSING_OR_INVALID, $"{method}, namespaces should be an object with data"); + error = Error.FromErrorType(ErrorType.MISSING_OR_INVALID, $"{method}, namespaces should be an object with data"); } return error; @@ -421,15 +407,15 @@ private bool IsValidNamespacesChainId(Namespaces namespaces, string chainId) return true; } - private ErrorResponse IsConformingNamespaces(RequiredNamespaces requiredNamespaces, Namespaces namespaces, + private Error IsConformingNamespaces(RequiredNamespaces requiredNamespaces, Namespaces namespaces, string context) { - ErrorResponse error = null; + Error error = null; var requiredNamespaceKeys = requiredNamespaces.Keys.ToArray(); var namespaceKeys = namespaces.Keys.ToArray(); if (!HasOverlap(requiredNamespaceKeys, namespaceKeys)) - error = ErrorResponse.FromErrorType(ErrorType.NON_CONFORMING_NAMESPACES, $"{context} namespaces keys don't satisfy requiredNamespaces"); + error = Error.FromErrorType(ErrorType.NON_CONFORMING_NAMESPACES, $"{context} namespaces keys don't satisfy requiredNamespaces"); else { foreach (var key in requiredNamespaceKeys) @@ -442,15 +428,15 @@ private ErrorResponse IsConformingNamespaces(RequiredNamespaces requiredNamespac if (!HasOverlap(requiredNamespaceChains, namespaceChains)) { - error = ErrorResponse.FromErrorType(ErrorType.NON_CONFORMING_NAMESPACES, $"{context} namespaces accounts don't satisfy requiredNamespaces chains for {key}"); + error = Error.FromErrorType(ErrorType.NON_CONFORMING_NAMESPACES, $"{context} namespaces accounts don't satisfy requiredNamespaces chains for {key}"); } else if (!HasOverlap(requiredNamespaces[key].Methods, namespaces[key].Methods)) { - error = ErrorResponse.FromErrorType(ErrorType.NON_CONFORMING_NAMESPACES, $"{context} namespaces methods don't satisfy requiredNamespaces methods for {key}"); + error = Error.FromErrorType(ErrorType.NON_CONFORMING_NAMESPACES, $"{context} namespaces methods don't satisfy requiredNamespaces methods for {key}"); } else if (!HasOverlap(requiredNamespaces[key].Events, namespaces[key].Events)) { - error = ErrorResponse.FromErrorType(ErrorType.NON_CONFORMING_NAMESPACES, $"{context} namespaces events don't satisfy requiredNamespaces events for {key}"); + error = Error.FromErrorType(ErrorType.NON_CONFORMING_NAMESPACES, $"{context} namespaces events don't satisfy requiredNamespaces events for {key}"); } } } diff --git a/WalletConnectSharp.Sign/Models/Engine/ApproveParams.cs b/WalletConnectSharp.Sign/Models/Engine/ApproveParams.cs index 637a537..13e79aa 100644 --- a/WalletConnectSharp.Sign/Models/Engine/ApproveParams.cs +++ b/WalletConnectSharp.Sign/Models/Engine/ApproveParams.cs @@ -12,24 +12,24 @@ public class ApproveParams /// The id of the session proposal to approve /// [JsonProperty("id")] - public long Id { get; set; } + public long Id; /// /// The enabled namespaces in this session /// [JsonProperty("namespaces")] - public Namespaces Namespaces { get; set; } + public Namespaces Namespaces; /// /// The relay protocol that will be used in this session (as a protocol string) /// [JsonProperty("relayProtocol")] - public string RelayProtocol { get; set; } + public string RelayProtocol; /// /// Custom session properties for this approval /// [JsonProperty("sessionProperties")] - public Dictionary SessionProperties { get; set; } + public Dictionary SessionProperties; } } diff --git a/WalletConnectSharp.Sign/Models/Engine/ConnectOptions.cs b/WalletConnectSharp.Sign/Models/Engine/ConnectOptions.cs index f85219e..cd8ad59 100644 --- a/WalletConnectSharp.Sign/Models/Engine/ConnectOptions.cs +++ b/WalletConnectSharp.Sign/Models/Engine/ConnectOptions.cs @@ -13,19 +13,19 @@ public class ConnectOptions /// The required namespaces that will be required for this session /// [JsonProperty("requiredNamespaces")] - public RequiredNamespaces RequiredNamespaces { get; set; } + public RequiredNamespaces RequiredNamespaces; /// /// The optional namespaces for this session /// [JsonProperty("optionalNamespaces")] - public Dictionary OptionalNamespaces { get; set; } + public Dictionary OptionalNamespaces; /// /// Custom session properties for this session /// - [JsonProperty("sessionProperties")] - public Dictionary SessionProperties { get; set; } + [JsonProperty("sessionProperties", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary SessionProperties; /// /// The pairing topic to be used to store the session proposal. By default, this should be left blank so @@ -33,14 +33,14 @@ public class ConnectOptions /// that pairing topic can be used, however the pairing topic MUST exist in storage. /// [JsonProperty("pairingTopic")] - public string PairingTopic { get; set; } + public string PairingTopic; /// /// The protocol options to use for this session. This is optional and defaults to /// value is set. /// [JsonProperty("relays")] - public ProtocolOptions Relays { get; set; } + public ProtocolOptions Relays; /// /// Create blank options with no required namespaces @@ -48,7 +48,7 @@ public class ConnectOptions public ConnectOptions() { RequiredNamespaces = new RequiredNamespaces(); - SessionProperties = new Dictionary(); + SessionProperties = null; OptionalNamespaces = new Dictionary(); } @@ -63,7 +63,7 @@ public ConnectOptions(RequiredNamespaces requiredNamespaces = null, string pairi { RequiredNamespaces = requiredNamespaces ?? new RequiredNamespaces(); OptionalNamespaces = optionalNamespaces ?? new Dictionary(); - SessionProperties = sessionProperties ?? new Dictionary(); + SessionProperties = sessionProperties ?? null; PairingTopic = pairingTopic ?? ""; Relays = relays; } @@ -102,6 +102,8 @@ public ConnectOptions WithOptionalNamespace(string chain, ProposedNamespace prop /// This object, acts a builder function public ConnectOptions AddSessionProperty(string key, string value) { + SessionProperties ??= new Dictionary(); + SessionProperties.Add(key, value); return this; @@ -119,6 +121,19 @@ public ConnectOptions WithSessionProperties(Dictionary propertie return this; } + + /// + /// Require a specific chain and namespace + /// + /// The chain the namespace exists in + /// The required namespace that must be present for this session + /// This object, acts a builder function + public ConnectOptions UseRequireNamespaces(RequiredNamespaces requiredNamespaces) + { + RequiredNamespaces = requiredNamespaces; + + return this; + } /// /// Include a pairing topic with these connect options. The pairing topic MUST exist in storage. diff --git a/WalletConnectSharp.Sign/Models/Engine/ConnectedData.cs b/WalletConnectSharp.Sign/Models/Engine/ConnectedData.cs index ad3aadb..ff4d30a 100644 --- a/WalletConnectSharp.Sign/Models/Engine/ConnectedData.cs +++ b/WalletConnectSharp.Sign/Models/Engine/ConnectedData.cs @@ -12,12 +12,12 @@ public class ConnectedData /// The URI that can be used to retrieve the submitted session proposal. This should be shared /// SECURELY out-of-band to a wallet supporting the SDK /// - public string Uri { get; set; } + public string Uri; /// /// A task that will resolve to an approved session. If the session proposal is rejected, then this /// task will throw an exception. /// - public Task Approval { get; set; } + public Task Approval; } } diff --git a/WalletConnectSharp.Sign/Models/Engine/Events/EmitEvent.cs b/WalletConnectSharp.Sign/Models/Engine/Events/EmitEvent.cs index d094068..9107f5b 100644 --- a/WalletConnectSharp.Sign/Models/Engine/Events/EmitEvent.cs +++ b/WalletConnectSharp.Sign/Models/Engine/Events/EmitEvent.cs @@ -13,6 +13,6 @@ public class EmitEvent : SessionEvent /// The wc_sessionEvent request that triggered this event /// [JsonProperty("params")] - public SessionEvent Params { get; set; } + public SessionEvent Params; } } diff --git a/WalletConnectSharp.Sign/Models/Engine/Events/EventData.cs b/WalletConnectSharp.Sign/Models/Engine/Events/EventData.cs index 91117d8..737a433 100644 --- a/WalletConnectSharp.Sign/Models/Engine/Events/EventData.cs +++ b/WalletConnectSharp.Sign/Models/Engine/Events/EventData.cs @@ -12,12 +12,11 @@ public class EventData /// The name of the event /// [JsonProperty("name")] - public string Name { get; set; } - + public string Name; + /// /// The event data associated with this event /// - [JsonProperty("data")] - public T Data { get; set; } + [JsonProperty("data")] public T Data; } } diff --git a/WalletConnectSharp.Sign/Models/Engine/Events/SessionEvent.cs b/WalletConnectSharp.Sign/Models/Engine/Events/SessionEvent.cs index f9de238..4a10c7e 100644 --- a/WalletConnectSharp.Sign/Models/Engine/Events/SessionEvent.cs +++ b/WalletConnectSharp.Sign/Models/Engine/Events/SessionEvent.cs @@ -19,12 +19,12 @@ public class SessionEvent /// The ID of the JSON Rpc request that triggered this session event /// [JsonProperty("id")] - public long Id { get; set; } + public long Id; /// /// The topic of the session this event took place in /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; } } diff --git a/WalletConnectSharp.Sign/Models/Engine/Events/SessionProposalEvent.cs b/WalletConnectSharp.Sign/Models/Engine/Events/SessionProposalEvent.cs new file mode 100644 index 0000000..fc28ca3 --- /dev/null +++ b/WalletConnectSharp.Sign/Models/Engine/Events/SessionProposalEvent.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Core.Models.Verify; + +namespace WalletConnectSharp.Sign.Models.Engine.Events +{ + public class SessionProposalEvent + { + [JsonProperty("id")] + public long Id; + + [JsonProperty("params")] + public ProposalStruct Proposal; + + [JsonProperty("verifyContext")] + public VerifiedContext VerifiedContext; + } +} diff --git a/WalletConnectSharp.Sign/Models/Engine/Events/SessionRequestEvent.cs b/WalletConnectSharp.Sign/Models/Engine/Events/SessionRequestEvent.cs index 901041f..225ac54 100644 --- a/WalletConnectSharp.Sign/Models/Engine/Events/SessionRequestEvent.cs +++ b/WalletConnectSharp.Sign/Models/Engine/Events/SessionRequestEvent.cs @@ -13,12 +13,12 @@ public class SessionRequestEvent : SessionEvent /// The chainId this request should be performed in /// [JsonProperty("chainId")] - public string ChainId { get; set; } + public string ChainId; /// /// The request arguments of this session request /// [JsonProperty("request")] - public IRequestArguments Request { get; set; } + public IRequestArguments Request; } } diff --git a/WalletConnectSharp.Sign/Models/Engine/Events/SessionUpdateEvent.cs b/WalletConnectSharp.Sign/Models/Engine/Events/SessionUpdateEvent.cs index 887be17..c3d3ca5 100644 --- a/WalletConnectSharp.Sign/Models/Engine/Events/SessionUpdateEvent.cs +++ b/WalletConnectSharp.Sign/Models/Engine/Events/SessionUpdateEvent.cs @@ -12,6 +12,6 @@ public class SessionUpdateEvent : SessionEvent /// The wc_sessionUpdate request that triggered this event /// [JsonProperty("params")] - public SessionUpdate Params { get; set; } + public SessionUpdate Params; } } diff --git a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionDelete.cs b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionDelete.cs index 0158507..d5e5408 100644 --- a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionDelete.cs +++ b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionDelete.cs @@ -12,7 +12,7 @@ namespace WalletConnectSharp.Sign.Models.Engine.Methods [RpcMethod("wc_sessionDelete")] [RpcRequestOptions(Clock.ONE_DAY, 1112)] [RpcResponseOptions(Clock.ONE_DAY, 1113)] - public class SessionDelete : ErrorResponse, IWcMethod + public class SessionDelete : Error, IWcMethod { } } diff --git a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionEvent.cs b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionEvent.cs index 9856cda..fca2666 100644 --- a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionEvent.cs +++ b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionEvent.cs @@ -19,12 +19,15 @@ public class SessionEvent : IWcMethod /// The chainId this event took place in /// [JsonProperty("chainId")] - public string ChainId { get; set; } + public string ChainId; + + [JsonProperty("topic")] + public string Topic; /// /// The event data /// [JsonProperty("event")] - public EventData Event { get; set; } + public EventData Event; } } diff --git a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionPropose.cs b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionPropose.cs index 8f8bdd4..381e6b4 100644 --- a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionPropose.cs +++ b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionPropose.cs @@ -19,30 +19,30 @@ public class SessionPropose : IWcMethod /// Protocol options that should be used during the session /// [JsonProperty("relays")] - public ProtocolOptions[] Relays { get; set; } + public ProtocolOptions[] Relays; /// /// The required namespaces this session will require /// [JsonProperty("requiredNamespaces")] - public RequiredNamespaces RequiredNamespaces { get; set; } + public RequiredNamespaces RequiredNamespaces; /// /// The optional namespaces for this session /// [JsonProperty("optionalNamespaces")] - public Dictionary OptionalNamespaces { get; set; } + public Dictionary OptionalNamespaces; /// /// Custom session properties for this session /// [JsonProperty("sessionProperties")] - public Dictionary SessionProperties { get; set; } + public Dictionary SessionProperties; /// /// The that created this session proposal /// [JsonProperty("proposer")] - public Participant Proposer { get; set; } + public Participant Proposer; } } diff --git a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionProposeResponse.cs b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionProposeResponse.cs index e0ef7c8..0b891c4 100644 --- a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionProposeResponse.cs +++ b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionProposeResponse.cs @@ -15,12 +15,12 @@ public class SessionProposeResponse /// The protocol options that should be used in this session /// [JsonProperty("relay")] - public ProtocolOptions Relay { get; set; } + public ProtocolOptions Relay; /// /// The public key of the responder to this session proposal /// [JsonProperty("responderPublicKey")] - public string ResponderPublicKey { get; set; } + public string ResponderPublicKey; } } diff --git a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionRequest.cs b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionRequest.cs index 70202a2..e317853 100644 --- a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionRequest.cs +++ b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionRequest.cs @@ -18,12 +18,12 @@ public class SessionRequest : IWcMethod /// The chainId this request should be performed in /// [JsonProperty("chainId")] - public string ChainId { get; set; } + public string ChainId; /// /// The JSON RPC request to send to the peer /// [JsonProperty("request")] - public JsonRpcRequest Request { get; set; } + public JsonRpcRequest Request; } } diff --git a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionSettle.cs b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionSettle.cs index af270b4..6b50e75 100644 --- a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionSettle.cs +++ b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionSettle.cs @@ -18,24 +18,24 @@ public class SessionSettle : IWcMethod /// The protocol options that should be used in this session /// [JsonProperty("relay")] - public ProtocolOptions Relay { get; set; } + public ProtocolOptions Relay; /// /// All namespaces that are enabled in this session /// [JsonProperty("namespaces")] - public Namespaces Namespaces { get; set; } + public Namespaces Namespaces; /// /// When this session will expire /// [JsonProperty("expiry")] - public long Expiry { get; set; } + public long Expiry; /// /// The controlling in this session. In most cases, this is the dApp. /// [JsonProperty("controller")] - public Participant Controller { get; set; } + public Participant Controller; } } diff --git a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionUpdate.cs b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionUpdate.cs index 50f6e29..19d7116 100644 --- a/WalletConnectSharp.Sign/Models/Engine/Methods/SessionUpdate.cs +++ b/WalletConnectSharp.Sign/Models/Engine/Methods/SessionUpdate.cs @@ -18,6 +18,6 @@ public class SessionUpdate : IWcMethod /// The updated namespaces that are enabled for this session /// [JsonProperty("namespaces")] - public Namespaces Namespaces { get; set; } + public Namespaces Namespaces; } } diff --git a/WalletConnectSharp.Sign/Models/Engine/RejectParams.cs b/WalletConnectSharp.Sign/Models/Engine/RejectParams.cs index 5765cc2..661f46a 100644 --- a/WalletConnectSharp.Sign/Models/Engine/RejectParams.cs +++ b/WalletConnectSharp.Sign/Models/Engine/RejectParams.cs @@ -5,7 +5,7 @@ namespace WalletConnectSharp.Sign.Models.Engine { /// /// A class that represents parameters for rejecting a session proposal. Contains the id - /// of the session proposal to reject and the reason the session + /// of the session proposal to reject and the reason the session /// proposal was rejected /// public class RejectParams @@ -14,12 +14,12 @@ public class RejectParams /// The id of the session proposal to reject /// [JsonProperty("id")] - public long Id { get; set; } + public long Id; /// - /// The reason the session proposal was rejected, as an + /// The reason the session proposal was rejected, as an /// [JsonProperty("reason")] - public ErrorResponse Reason { get; set; } + public Error Reason; } } diff --git a/WalletConnectSharp.Sign/Models/Namespace.cs b/WalletConnectSharp.Sign/Models/Namespace.cs index 4395efe..1e1a345 100644 --- a/WalletConnectSharp.Sign/Models/Namespace.cs +++ b/WalletConnectSharp.Sign/Models/Namespace.cs @@ -8,22 +8,125 @@ namespace WalletConnectSharp.Sign.Models /// public class Namespace { + public Namespace(ProposedNamespace proposedNamespace) + { + this.Methods = proposedNamespace.Methods; + this.Chains = proposedNamespace.Chains; + this.Events = proposedNamespace.Events; + } + + public Namespace() { } + /// /// An array of all accounts enabled in this namespace /// [JsonProperty("accounts")] - public string[] Accounts { get; set; } + public string[] Accounts; /// /// An array of all methods enabled in this namespace /// [JsonProperty("methods")] - public string[] Methods { get; set; } + public string[] Methods; /// /// An array of all events enabled in this namespace /// [JsonProperty("events")] - public string[] Events { get; set; } + public string[] Events; + + /// + /// An array of all chains enabled in this namespace + /// + [JsonProperty("chains")] public string[] Chains; + + public Namespace WithMethod(string method) + { + Methods = Methods.Append(method).ToArray(); + return this; + } + + public Namespace WithChain(string chain) + { + Chains = Chains.Append(chain).ToArray(); + return this; + } + + public Namespace WithEvent(string @event) + { + Events = Events.Append(@event).ToArray(); + return this; + } + + public Namespace WithAccount(string account) + { + Accounts = Accounts.Append(account).ToArray(); + return this; + } + + protected bool Equals(Namespace other) + { + return Equals(Accounts, other.Accounts) && Equals(Methods, other.Methods) && Equals(Events, other.Events); + } + + 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((Namespace)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Accounts, Methods, Events); + } + + private sealed class NamespaceEqualityComparer : IEqualityComparer + { + public bool Equals(Namespace x, Namespace y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (ReferenceEquals(x, null)) + { + return false; + } + + if (ReferenceEquals(y, null)) + { + return false; + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + return x.Accounts.SequenceEqual(y.Accounts) && x.Methods.SequenceEqual(y.Methods) && x.Events.SequenceEqual(y.Events); + } + + public int GetHashCode(Namespace obj) + { + return HashCode.Combine(obj.Accounts, obj.Methods, obj.Events); + } + } + + public static IEqualityComparer NamespaceComparer { get; } = new NamespaceEqualityComparer(); } } diff --git a/WalletConnectSharp.Sign/Models/Namespaces.cs b/WalletConnectSharp.Sign/Models/Namespaces.cs index 0ec1d41..a154d64 100644 --- a/WalletConnectSharp.Sign/Models/Namespaces.cs +++ b/WalletConnectSharp.Sign/Models/Namespaces.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using WalletConnectSharp.Common.Utils; namespace WalletConnectSharp.Sign.Models { @@ -9,5 +10,77 @@ namespace WalletConnectSharp.Sign.Models /// namespace: [-a-z0-9]{3,8} /// reference: [-_a-zA-Z0-9]{1,32} /// - public class Namespaces : Dictionary { } + public class Namespaces : Dictionary, IEquatable + { + 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); + return this; + } + + public Namespace At(string chainNamespace) + { + return this[chainNamespace]; + } + + public Namespaces WithProposedNamespaces(Dictionary proposedNamespaces) + { + foreach (var pair in proposedNamespaces) + { + var chainNamespace = pair.Key; + var requiredNamespace = pair.Value; + + Add(chainNamespace, new Namespace(requiredNamespace)); + } + + return this; + } + } } diff --git a/WalletConnectSharp.Sign/Models/Participant.cs b/WalletConnectSharp.Sign/Models/Participant.cs index 95a7f63..cc3a49d 100644 --- a/WalletConnectSharp.Sign/Models/Participant.cs +++ b/WalletConnectSharp.Sign/Models/Participant.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using WalletConnectSharp.Core; using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Pairing; @@ -14,12 +15,12 @@ public class Participant /// The public key of this participant, encoded as a hex string /// [JsonProperty("publicKey")] - public string PublicKey { get; set; } + public string PublicKey; /// /// The metadata for this participant /// [JsonProperty("metadata")] - public Metadata Metadata { get; set; } + public Metadata Metadata; } } diff --git a/WalletConnectSharp.Sign/Models/PendingRequestStruct.cs b/WalletConnectSharp.Sign/Models/PendingRequestStruct.cs new file mode 100644 index 0000000..9d81a42 --- /dev/null +++ b/WalletConnectSharp.Sign/Models/PendingRequestStruct.cs @@ -0,0 +1,24 @@ +using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Sign.Models.Engine.Methods; + +namespace WalletConnectSharp.Sign.Models; + +public struct PendingRequestStruct : IKeyHolder +{ + public long Id; + + public string Topic; + + public long Key + { + get + { + return Id; + } + } + + // Specify object here, so we can store any type + // We don't care about type-safety for these pending + // requests + public SessionRequest Parameters; +} diff --git a/WalletConnectSharp.Sign/Models/ProposalStruct.cs b/WalletConnectSharp.Sign/Models/ProposalStruct.cs index b3ba0a4..43d129e 100644 --- a/WalletConnectSharp.Sign/Models/ProposalStruct.cs +++ b/WalletConnectSharp.Sign/Models/ProposalStruct.cs @@ -22,7 +22,7 @@ public struct ProposalStruct : IKeyHolder /// The id of this proposal /// [JsonProperty("id")] - public long? Id { get; set; } + public long Id; /// /// This is the key field, mapped to the Id. Implemented for @@ -41,43 +41,43 @@ public long Key /// When this proposal expires /// [JsonProperty("expiry")] - public long? Expiry { get; set; } + public long? Expiry; /// /// Relay protocol options for this proposal /// [JsonProperty("relays")] - public ProtocolOptions[] Relays { get; set; } + public ProtocolOptions[] Relays; /// /// The participant that created this proposal /// [JsonProperty("proposer")] - public Participant Proposer { get; set; } + public Participant Proposer; /// /// The required namespaces for this proposal requests /// [JsonProperty("requiredNamespaces")] - public RequiredNamespaces RequiredNamespaces { get; set; } + public RequiredNamespaces RequiredNamespaces; /// /// The optional namespaces for this proposal requests /// [JsonProperty("optionalNamespaces")] - public Dictionary OptionalNamespaces { get; set; } + public Dictionary OptionalNamespaces; /// /// Custom session properties for this proposal request /// [JsonProperty("sessionProperties")] - public Dictionary SessionProperties { get; set; } + public Dictionary SessionProperties; /// /// The pairing topic this proposal lives in /// [JsonProperty("pairingTopic")] - public string PairingTopic { get; set; } + public string PairingTopic; /// /// Approve this proposal with a single address and (optional) protocol options. The @@ -143,7 +143,7 @@ public ApproveParams ApproveProposal(string[] approvedAccounts, ProtocolOptions return new ApproveParams() { - Id = Id.Value, + Id = Id, RelayProtocol = relayProtocol, Namespaces = namespaces, SessionProperties = SessionProperties, @@ -151,18 +151,18 @@ public ApproveParams ApproveProposal(string[] approvedAccounts, ProtocolOptions } /// - /// Reject this proposal with the given . This + /// Reject this proposal with the given . This /// will return a which must be used in /// /// The error reason this proposal was rejected /// A new object which must be used in /// If this proposal has no Id - public RejectParams RejectProposal(ErrorResponse error) + public RejectParams RejectProposal(Error error) { if (Id == null) throw new Exception("Proposal has no set Id"); - return new RejectParams() {Id = Id.Value, Reason = error}; + return new RejectParams() {Id = Id, Reason = error}; } /// @@ -174,13 +174,10 @@ public RejectParams RejectProposal(ErrorResponse error) /// If this proposal has no Id public RejectParams RejectProposal(string message = null) { - if (Id == null) - throw new Exception("Proposal has no set Id"); - if (message == null) message = "Proposal denied by remote host"; - return RejectProposal(new ErrorResponse() + return RejectProposal(new Error() { Message = message, Code = (long) ErrorType.USER_DISCONNECTED diff --git a/WalletConnectSharp.Sign/Models/ProposedNamespace.cs b/WalletConnectSharp.Sign/Models/ProposedNamespace.cs index 5ef0c2b..dd1a0f4 100644 --- a/WalletConnectSharp.Sign/Models/ProposedNamespace.cs +++ b/WalletConnectSharp.Sign/Models/ProposedNamespace.cs @@ -13,13 +13,13 @@ public class ProposedNamespace /// A list of all chains that are required to be enabled in this namespace /// [JsonProperty("chains")] - public string[] Chains { get; set; } + public string[] Chains; /// /// A list of all methods that are required to be enabled in this namespace /// [JsonProperty("methods")] - public string[] Methods { get; set; } + public string[] Methods; /// /// A list of all events that are required to be enabled in this namespace @@ -69,5 +69,75 @@ public ProposedNamespace WithEvent(string @event) Events = Events.Append(@event).ToArray(); return this; } + + public Namespace WithAccount(string account) + { + return new Namespace(this).WithAccount(account); + } + + protected bool Equals(ProposedNamespace other) + { + return Equals(Chains, other.Chains) && Equals(Methods, other.Methods) && Equals(Events, other.Events); + } + + 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((ProposedNamespace)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Chains, Methods, Events); + } + + private sealed class RequiredNamespaceEqualityComparer : IEqualityComparer + { + public bool Equals(ProposedNamespace x, ProposedNamespace y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (ReferenceEquals(x, null)) + { + return false; + } + + if (ReferenceEquals(y, null)) + { + return false; + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + return x.Chains.SequenceEqual(y.Chains) && x.Methods.SequenceEqual(y.Methods) && x.Events.SequenceEqual(y.Events); + } + + public int GetHashCode(ProposedNamespace obj) + { + return HashCode.Combine(obj.Chains, obj.Methods, obj.Events); + } + } + + public static IEqualityComparer RequiredNamespaceComparer { get; } = new RequiredNamespaceEqualityComparer(); } } diff --git a/WalletConnectSharp.Sign/Models/RequiredNamespaces.cs b/WalletConnectSharp.Sign/Models/RequiredNamespaces.cs index e1ed091..d093d17 100644 --- a/WalletConnectSharp.Sign/Models/RequiredNamespaces.cs +++ b/WalletConnectSharp.Sign/Models/RequiredNamespaces.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using WalletConnectSharp.Common.Utils; namespace WalletConnectSharp.Sign.Models { @@ -9,5 +10,53 @@ namespace WalletConnectSharp.Sign.Models /// namespace: [-a-z0-9]{3,8} /// reference: [-_a-zA-Z0-9]{1,32} /// - public class RequiredNamespaces : Dictionary { } + public class RequiredNamespaces : Dictionary, IEquatable + { + 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); + return this; + } + } } diff --git a/WalletConnectSharp.Sign/Models/SessionRequestEventHandler.cs b/WalletConnectSharp.Sign/Models/SessionRequestEventHandler.cs index 9806ce2..7fa28fa 100644 --- a/WalletConnectSharp.Sign/Models/SessionRequestEventHandler.cs +++ b/WalletConnectSharp.Sign/Models/SessionRequestEventHandler.cs @@ -1,6 +1,5 @@ -using System; -using System.Threading.Tasks; -using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Network.Models; using WalletConnectSharp.Sign.Interfaces; using WalletConnectSharp.Sign.Models.Engine.Methods; @@ -13,7 +12,9 @@ namespace WalletConnectSharp.Sign.Models /// The type of the session request /// The type of the response for the session request public class SessionRequestEventHandler : TypedEventHandler - { + { + private 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 @@ -23,26 +24,27 @@ public class SessionRequestEventHandler : TypedEventHandler /// The engine this singleton instance is for, and where the context string will /// be read from /// The singleton instance to use for request/response event handlers - public static new TypedEventHandler GetInstance(ICore engine) + public static new TypedEventHandler GetInstance(ICore engine, IEnginePrivate _enginePrivate) { var context = engine.Context; if (_instances.ContainsKey(context)) return _instances[context]; - var _instance = new SessionRequestEventHandler(engine); + var _instance = new SessionRequestEventHandler(engine, _enginePrivate); _instances.Add(context, _instance); return _instance; } - protected SessionRequestEventHandler(ICore engine) : base(engine) + protected SessionRequestEventHandler(ICore engine, IEnginePrivate enginePrivate) : base(engine) { + this._enginePrivate = enginePrivate; } protected override TypedEventHandler BuildNew(ICore _ref, Func, bool> requestPredicate, Func, bool> responsePredicate) { - return new SessionRequestEventHandler(_ref) + return new SessionRequestEventHandler(_ref, _enginePrivate) { requestPredicate = requestPredicate, responsePredicate = responsePredicate @@ -62,12 +64,35 @@ private Task WrappedRefOnOnResponse(ResponseEventArgs e) return base.ResponseCallback(e.Topic, e.Response); } - private Task WrappedRefOnOnRequest(RequestEventArgs, TR> e) + private async Task WrappedRefOnOnRequest(RequestEventArgs, TR> e) { + // Ensure that the request is for us + var method = RpcMethodAttribute.MethodForType(); + + var sessionRequest = e.Request.Params.Request; + + if (sessionRequest.Method != method) return; + //Set inner request id to match outer request id - e.Request.Params.Request.Id = e.Request.Id; + sessionRequest.Id = e.Request.Id; - return base.RequestCallback(e.Topic, e.Request.Params.Request); + //Add to pending requests + //We can't do a simple cast, so we need to copy all the data + await _enginePrivate.SetPendingSessionRequest(new PendingRequestStruct() + { + Id = e.Request.Id, Parameters = new SessionRequest() + { + ChainId = e.Request.Params.ChainId, + Request = new JsonRpcRequest() + { + Id = sessionRequest.Id, + Method = sessionRequest.Method, + Params = sessionRequest.Params + } + }, Topic = e.Topic + }); + + await base.RequestCallback(e.Topic, sessionRequest); } } } diff --git a/WalletConnectSharp.Sign/Models/SessionStruct.cs b/WalletConnectSharp.Sign/Models/SessionStruct.cs index 297b7cc..98165b0 100644 --- a/WalletConnectSharp.Sign/Models/SessionStruct.cs +++ b/WalletConnectSharp.Sign/Models/SessionStruct.cs @@ -14,55 +14,55 @@ public struct SessionStruct : IKeyHolder /// The topic of this session /// [JsonProperty("topic")] - public string Topic { get; set; } + public string Topic; /// /// The relay protocol options this session is using /// [JsonProperty("relay")] - public ProtocolOptions Relay { get; set; } + public ProtocolOptions Relay; /// /// When this session expires /// [JsonProperty("expiry")] - public long? Expiry { get; set; } + public long? Expiry; /// /// Whether this session has been acknowledged or not /// [JsonProperty("acknowledged")] - public bool? Acknowledged { get; set; } + public bool? Acknowledged; /// /// The public key of the current controller for this session /// [JsonProperty("controller")] - public string Controller { get; set; } + public string Controller; /// /// The enabled namespaces this session uses /// [JsonProperty("namespaces")] - public Namespaces Namespaces { get; set; } + public Namespaces Namespaces; /// /// The required enabled namespaces this session uses /// [JsonProperty("requiredNamespaces")] - public RequiredNamespaces RequiredNamespaces { get; set; } + public RequiredNamespaces RequiredNamespaces; /// /// The data that represents ourselves in this session /// [JsonProperty("self")] - public Participant Self { get; set; } + public Participant Self; /// /// The data that represents the peer in this session /// [JsonProperty("peer")] - public Participant Peer { get; set; } + public Participant Peer; /// /// This is the key field, mapped to the Topic. Implemented for diff --git a/WalletConnectSharp.Sign/Models/SignClientOptions.cs b/WalletConnectSharp.Sign/Models/SignClientOptions.cs index d0e8924..3692289 100644 --- a/WalletConnectSharp.Sign/Models/SignClientOptions.cs +++ b/WalletConnectSharp.Sign/Models/SignClientOptions.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using WalletConnectSharp.Core; using WalletConnectSharp.Core.Interfaces; using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Pairing; @@ -6,22 +7,22 @@ namespace WalletConnectSharp.Sign.Models { /// - /// Options for setting up the class. Includes + /// Options for setting up the class. Includes /// options from /// public class SignClientOptions : CoreOptions { /// - /// The instance the should use. If + /// The instance the should use. If /// left null, then a new Core module will be created and initialized /// [JsonProperty("core")] - public ICore Core { get; set; } + public ICore Core; /// - /// The Metadata the should broadcast with + /// The Metadata the should broadcast with /// [JsonProperty("metadata")] - public Metadata Metadata { get; set; } + public Metadata Metadata; } } diff --git a/WalletConnectSharp.Sign/WalletConnectSharp.Sign.csproj b/WalletConnectSharp.Sign/WalletConnectSharp.Sign.csproj index 0cf0890..8983f8c 100644 --- a/WalletConnectSharp.Sign/WalletConnectSharp.Sign.csproj +++ b/WalletConnectSharp.Sign/WalletConnectSharp.Sign.csproj @@ -7,7 +7,7 @@ $(DefaultVersion) WalletConnect.Sign WalletConnectSharp.Sign - pedrouid, gigajuwels, edkek + pedrouid, gigajuwels A port of the TypeScript SDK to C#. A complete implementation of the WalletConnect v2 protocol that can be used to connect to external wallets or connect a wallet to an external Dapp Copyright (c) WalletConnect 2023 https://walletconnect.org/ diff --git a/WalletConnectSharp.Sign/WalletConnectSignClient.cs b/WalletConnectSharp.Sign/WalletConnectSignClient.cs index 64a3ea9..539bb4d 100644 --- a/WalletConnectSharp.Sign/WalletConnectSignClient.cs +++ b/WalletConnectSharp.Sign/WalletConnectSignClient.cs @@ -2,8 +2,6 @@ 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; using WalletConnectSharp.Events; @@ -13,6 +11,7 @@ using WalletConnectSharp.Sign.Models; using WalletConnectSharp.Sign.Models.Engine; using WalletConnectSharp.Sign.Models.Engine.Events; +using WalletConnectSharp.Sign.Models.Engine.Methods; using WalletConnectSharp.Storage; namespace WalletConnectSharp.Sign @@ -91,6 +90,8 @@ public class WalletConnectSignClient : ISignClient /// public IProposal Proposal { get; } + public IPendingRequests PendingRequests { get; } + /// /// The this Sign Client was initialized with. /// @@ -117,6 +118,15 @@ public int Version return VERSION; } } + + + public PendingRequestStruct[] PendingSessionRequests + { + get + { + return Engine.PendingSessionRequests; + } + } /// /// Create a new instance with the given @@ -172,7 +182,8 @@ private WalletConnectSignClient(SignClientOptions options) Core = new WalletConnectCore(options); Events = new EventDelegator(this); - + + PendingRequests = new PendingRequests(Core); PairingStore = new PairingStore(Core); Session = new Session(Core); Proposal = new Proposal(Core); @@ -230,7 +241,7 @@ public Task Approve(ProposalStruct proposalStruct, params string[ /// /// 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. - /// Use or + /// Use or /// to generate a object, or use the alias function /// /// The parameters of the rejection @@ -254,7 +265,7 @@ public Task Reject(ProposalStruct proposalStruct, string message = null) if (message == null) message = "Proposal denied by remote host"; - return Reject(proposalStruct, new ErrorResponse() + return Reject(proposalStruct, new Error() { Message = message, Code = (long) ErrorType.USER_DISCONNECTED, @@ -267,7 +278,7 @@ public Task Reject(ProposalStruct proposalStruct, string message = null) /// /// The proposal to reject /// An error explaining the reason for the rejection - public Task Reject(ProposalStruct proposalStruct, ErrorResponse error) + public Task Reject(ProposalStruct proposalStruct, Error error) { if (proposalStruct.Id == null) throw new ArgumentException("No proposal Id given"); @@ -287,9 +298,9 @@ public Task Reject(ProposalStruct proposalStruct, ErrorResponse error) /// The topic to update /// The updated namespaces /// A task that returns an interface that can be used to listen for acknowledgement of the updates - public Task Update(string topic, Namespaces namespaces) + public Task UpdateSession(string topic, Namespaces namespaces) { - return Engine.Update(topic, namespaces); + return Engine.UpdateSession(topic, namespaces); } /// @@ -365,7 +376,7 @@ public Task Ping(string topic) /// /// The topic of the session to disconnect /// An (optional) error reason for the disconnect - public Task Disconnect(string topic, ErrorResponse reason) + public Task Disconnect(string topic, Error reason) { return Engine.Disconnect(topic, reason); } @@ -380,9 +391,15 @@ public SessionStruct[] Find(RequiredNamespaces requiredNamespaces) return Engine.Find(requiredNamespaces); } + public void HandleEventMessageType(Func>, Task> requestCallback, Func, Task> responseCallback) + { + this.Engine.HandleEventMessageType(requestCallback, responseCallback); + } + private async Task Initialize() { await this.Core.Start(); + await PendingRequests.Init(); await PairingStore.Init(); await Session.Init(); await Proposal.Init(); diff --git a/WalletConnectSharp.Web3Wallet/Controllers/Web3WalletEngine.cs b/WalletConnectSharp.Web3Wallet/Controllers/Web3WalletEngine.cs new file mode 100644 index 0000000..57c65ba --- /dev/null +++ b/WalletConnectSharp.Web3Wallet/Controllers/Web3WalletEngine.cs @@ -0,0 +1,231 @@ +using WalletConnectSharp.Auth; +using WalletConnectSharp.Auth.Interfaces; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Common.Model.Errors; +using WalletConnectSharp.Events; +using WalletConnectSharp.Events.Interfaces; +using WalletConnectSharp.Events.Model; +using WalletConnectSharp.Network.Models; +using WalletConnectSharp.Sign; +using WalletConnectSharp.Sign.Interfaces; +using WalletConnectSharp.Sign.Models; +using WalletConnectSharp.Sign.Models.Engine; +using WalletConnectSharp.Sign.Models.Engine.Events; +using WalletConnectSharp.Web3Wallet.Interfaces; + +namespace WalletConnectSharp.Web3Wallet.Controllers; + +public class Web3WalletEngine : IWeb3WalletEngine +{ + private bool _initialized; + + public IDictionary ActiveSessions + { + get + { + return this.SignClient.Session.ToDictionary(); + } + } + + public IDictionary PendingSessionProposals + { + get + { + return this.SignClient.Proposal.ToDictionary(); + } + } + + public PendingRequestStruct[] PendingSessionRequests + { + get + { + return this.SignClient.PendingSessionRequests; + } + } + + public IDictionary PendingAuthRequests + { + get + { + return this.AuthClient.PendingRequests; + } + } + + public ISignClient SignClient { get; private set; } + public IAuthClient AuthClient { get; private set; } + public IWeb3Wallet Client { get; } + + public Web3WalletEngine(IWeb3Wallet client) + { + Client = client; + } + + public async Task Init() + { + this.SignClient = await WalletConnectSignClient.Init(new SignClientOptions() + { + Core = this.Client.Core, Metadata = this.Client.Metadata + }); + + this.AuthClient = await WalletConnectAuthClient.Init(new AuthOptions() + { + Core = this.Client.Core, ProjectId = this.Client.Core.ProjectId, Metadata = this.Client.Metadata + }); + + InitializeEventListeners(); + + _initialized = true; + } + + public async Task Pair(string uri, bool activatePairing = false) + { + IsInitialized(); + await this.Client.Core.Pairing.Pair(uri, activatePairing); + } + + public async Task ApproveSession(long id, Namespaces namespaces, string relayProtocol = null) + { + var data = await this.SignClient.Approve(new ApproveParams() + { + Id = id, Namespaces = namespaces, RelayProtocol = relayProtocol + }); + + await data.Acknowledged(); + + return this.SignClient.Session.Get(data.Topic); + } + + public Task ApproveSession(ProposalStruct proposal, params string[] approvedAddresses) + { + var param = proposal.ApproveProposal(approvedAddresses); + return ApproveSession(param.Id, param.Namespaces, param.RelayProtocol); + } + + public Task RejectSession(long id, Error reason) + { + return this.SignClient.Reject(new RejectParams() { Id = id, Reason = reason }); + } + + public Task RejectSession(ProposalStruct proposal, Error reason) + { + var parm = proposal.RejectProposal(reason); + return RejectSession(parm.Id, parm.Reason); + } + + public Task RejectSession(ProposalStruct proposal, string reason) + { + var parm = proposal.RejectProposal(reason); + return RejectSession(parm.Id, parm.Reason); + } + + public async Task UpdateSession(string topic, Namespaces namespaces) + { + await (await this.SignClient.UpdateSession(topic, namespaces)).Acknowledged(); + } + + public async Task ExtendSession(string topic) + { + await (await this.SignClient.Extend(topic)).Acknowledged(); + } + + public async Task RespondSessionRequest(string topic, JsonRpcResponse response) + { + await this.SignClient.Respond(topic, response); + } + + public async Task EmitSessionEvent(string topic, EventData eventData, string chainId) + { + await this.SignClient.Emit(topic, eventData, chainId); + } + + public async Task DisconnectSession(string topic, Error reason) + { + await this.SignClient.Disconnect(topic, reason); + } + + public async Task RespondAuthRequest(ResultResponse results, string iss) + { + await this.AuthClient.Respond(results, iss); + } + + public async Task RespondAuthRequest(AuthErrorResponse error, string iss) + { + await this.AuthClient.Respond(error, iss); + } + + public Task RespondAuthRequest(AuthRequest request, Error error, string iss) + { + return RespondAuthRequest(new AuthErrorResponse() { Id = request.Id, Error = error, }, iss); + } + + public Task RespondAuthRequest(AuthRequest request, string signature, string iss, bool eip191 = true) + { + Cacao.CacaoSignature sig = eip191 + ? new Cacao.CacaoSignature.EIP191CacaoSignature(signature) + : new Cacao.CacaoSignature.EIP1271CacaoSignature(signature); + return RespondAuthRequest(new ResultResponse() { Id = request.Id, Signature = sig }, iss); + } + + public string FormatMessage(Cacao.CacaoRequestPayload payload, string iss) + { + return this.AuthClient.FormatMessage(payload, iss); + } + + private void PropagateEventToClient(string eventName, IEvents source) + { + EventHandler> eventHandler = (sender, @event) => + { + this.Client.Events.TriggerType(eventName, @event.EventData, @event.EventData.GetType()); + }; + + source.On(eventName, eventHandler); + } + + private void InitializeEventListeners() + { + PropagateEventToClient("session_proposal", SignClient); + PropagateEventToClient("session_request", SignClient); + PropagateEventToClient("session_delete", SignClient); + + // Propagate auth events + AuthClient.AuthRequested += OnAuthRequest; + AuthClient.AuthResponded += OnAuthResponse; + AuthClient.AuthError += OnAuthResponse; + } + + private void IsInitialized() + { + if (!_initialized) + { + throw WalletConnectException.FromType(ErrorType.NOT_INITIALIZED, "Web3WalletEngine"); + } + } + + public event EventHandler AuthRequested; + public event EventHandler AuthResponded; + public event EventHandler AuthError; + + void OnAuthRequest(object sender, AuthRequest request) + { + if (AuthRequested != null) + { + AuthRequested(sender, request); + } + } + + void OnAuthResponse(object sender, AuthErrorResponse errorResponse) + { + if (AuthError != null) + { + AuthError(sender, errorResponse); + } + } + + void OnAuthResponse(object sender, AuthResponse response) + { + if (AuthResponded != null) + { + AuthResponded(sender, response); + } + } +} diff --git a/WalletConnectSharp.Web3Wallet/Interfaces/IWeb3Wallet.cs b/WalletConnectSharp.Web3Wallet/Interfaces/IWeb3Wallet.cs new file mode 100644 index 0000000..89283b3 --- /dev/null +++ b/WalletConnectSharp.Web3Wallet/Interfaces/IWeb3Wallet.cs @@ -0,0 +1,16 @@ +using WalletConnectSharp.Auth; +using WalletConnectSharp.Common; +using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Events.Interfaces; + +namespace WalletConnectSharp.Web3Wallet.Interfaces; + +public interface IWeb3Wallet : IModule, IEvents, IWeb3WalletApi +{ + IWeb3WalletEngine Engine { get; } + + ICore Core { get; } + + Metadata Metadata { get; } +} diff --git a/WalletConnectSharp.Web3Wallet/Interfaces/IWeb3WalletApi.cs b/WalletConnectSharp.Web3Wallet/Interfaces/IWeb3WalletApi.cs new file mode 100644 index 0000000..f0a796a --- /dev/null +++ b/WalletConnectSharp.Web3Wallet/Interfaces/IWeb3WalletApi.cs @@ -0,0 +1,52 @@ +using WalletConnectSharp.Auth; +using WalletConnectSharp.Auth.Interfaces; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Network.Models; +using WalletConnectSharp.Sign.Models; +using WalletConnectSharp.Sign.Models.Engine.Events; + +namespace WalletConnectSharp.Web3Wallet.Interfaces; + +public interface IWeb3WalletApi : IAuthClientEvents +{ + IDictionary ActiveSessions { get; } + + IDictionary PendingSessionProposals { get; } + + PendingRequestStruct[] PendingSessionRequests { get; } + + IDictionary PendingAuthRequests { get; } + + Task Pair(string uri, bool activatePairing = false); + + Task ApproveSession(long id, Namespaces namespaces, string relayProtocol = null); + + Task ApproveSession(ProposalStruct proposal, params string[] approvedAddresses); + + Task RejectSession(long id, Error reason); + + Task RejectSession(ProposalStruct proposal, Error reason); + + + Task RejectSession(ProposalStruct proposal, string reason); + + Task UpdateSession(string topic, Namespaces namespaces); + + Task ExtendSession(string topic); + + Task RespondSessionRequest(string topic, JsonRpcResponse response); + + Task EmitSessionEvent(string topic, EventData eventData, string chainId); + + Task DisconnectSession(string topic, Error reason); + + Task RespondAuthRequest(ResultResponse results, string iss); + + Task RespondAuthRequest(AuthErrorResponse error, string iss); + + Task RespondAuthRequest(AuthRequest request, Error error, string iss); + + Task RespondAuthRequest(AuthRequest request, string signature, string iss, bool eip191 = true); + + string FormatMessage(Cacao.CacaoRequestPayload payload, string iss); +} diff --git a/WalletConnectSharp.Web3Wallet/Interfaces/IWeb3WalletEngine.cs b/WalletConnectSharp.Web3Wallet/Interfaces/IWeb3WalletEngine.cs new file mode 100644 index 0000000..e80f517 --- /dev/null +++ b/WalletConnectSharp.Web3Wallet/Interfaces/IWeb3WalletEngine.cs @@ -0,0 +1,19 @@ +using WalletConnectSharp.Auth; +using WalletConnectSharp.Auth.Interfaces; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Network.Models; +using WalletConnectSharp.Sign.Interfaces; +using WalletConnectSharp.Sign.Models; + +namespace WalletConnectSharp.Web3Wallet.Interfaces; + +public interface IWeb3WalletEngine : IWeb3WalletApi +{ + ISignClient SignClient { get; } + + IAuthClient AuthClient { get; } + + IWeb3Wallet Client { get; } + + Task Init(); +} diff --git a/WalletConnectSharp.Web3Wallet/Models/BaseEventArgs.cs b/WalletConnectSharp.Web3Wallet/Models/BaseEventArgs.cs new file mode 100644 index 0000000..f05c18d --- /dev/null +++ b/WalletConnectSharp.Web3Wallet/Models/BaseEventArgs.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace WalletConnectSharp.Web3Wallet.Models; + +public class BaseEventArgs : EventArgs +{ + [JsonProperty("id")] + public long Id; + + [JsonProperty("topic")] + public string Topic { get; set; } + + [JsonProperty("params")] + public T Parameters { get; set; } +} diff --git a/WalletConnectSharp.Web3Wallet/Models/SessionRequestEventArgs.cs b/WalletConnectSharp.Web3Wallet/Models/SessionRequestEventArgs.cs new file mode 100644 index 0000000..20623d2 --- /dev/null +++ b/WalletConnectSharp.Web3Wallet/Models/SessionRequestEventArgs.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using WalletConnectSharp.Core.Models.Verify; +using WalletConnectSharp.Sign.Models.Engine.Methods; + +namespace WalletConnectSharp.Web3Wallet.Models; + +public class SessionRequestEventArgs : BaseEventArgs> +{ + [JsonProperty("verifyContext")] + public VerifiedContext VerifyContext; +} diff --git a/WalletConnectSharp.Web3Wallet/WalletConnectSharp.Web3Wallet.csproj b/WalletConnectSharp.Web3Wallet/WalletConnectSharp.Web3Wallet.csproj new file mode 100644 index 0000000..df917a0 --- /dev/null +++ b/WalletConnectSharp.Web3Wallet/WalletConnectSharp.Web3Wallet.csproj @@ -0,0 +1,32 @@ + + + + $(DefaultTargetFrameworks) + $(DefaultVersion) + $(DefaultVersion) + $(DefaultVersion) + WalletConnect.Web3Wallet + WalletConnectSharp.Web3Wallet + pedrouid, gigajuwels + A port of the TypeScript SDK to C#. A complete implementation of the WalletConnect v2 protocol that can be used to connect to external wallets or connect a wallet to an external Dapp + Copyright (c) WalletConnect 2023 + https://walletconnect.org/ + icon.png + https://github.com/WalletConnect/WalletConnectSharp + git + web3wallet walletconnect sign wallet web3 ether ethereum blockchain evm + true + Apache-2.0 + + + + + + + + + + + + + diff --git a/WalletConnectSharp.Web3Wallet/Web3WalletClient.cs b/WalletConnectSharp.Web3Wallet/Web3WalletClient.cs new file mode 100644 index 0000000..d428db5 --- /dev/null +++ b/WalletConnectSharp.Web3Wallet/Web3WalletClient.cs @@ -0,0 +1,196 @@ +using WalletConnectSharp.Auth; +using WalletConnectSharp.Auth.Models; +using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Interfaces; +using WalletConnectSharp.Events; +using WalletConnectSharp.Network.Models; +using WalletConnectSharp.Sign.Models; +using WalletConnectSharp.Sign.Models.Engine.Events; +using WalletConnectSharp.Web3Wallet.Controllers; +using WalletConnectSharp.Web3Wallet.Interfaces; + +namespace WalletConnectSharp.Web3Wallet; + +public class Web3WalletClient : IWeb3Wallet +{ + public string Name { get; } + public string Context { get; } + public EventDelegator Events { get; } + + public IDictionary ActiveSessions + { + get + { + return this.Engine.ActiveSessions; + } + } + + public IDictionary PendingSessionProposals + { + get + { + return this.Engine.PendingSessionProposals; + } + } + + public PendingRequestStruct[] PendingSessionRequests + { + get + { + return this.Engine.PendingSessionRequests; + } + } + + public IDictionary PendingAuthRequests + { + get + { + return this.Engine.PendingAuthRequests; + } + } + + public IWeb3WalletEngine Engine { get; } + public ICore Core { get; } + public Metadata Metadata { get; } + + public static async Task Init(ICore core, Metadata metadata, string name = null) + { + var wallet = new Web3WalletClient(core, metadata, name); + await wallet.Initialize(); + + return wallet; + } + + private Web3WalletClient(ICore core, Metadata metadata, string name = null) + { + this.Metadata = metadata; + if (string.IsNullOrWhiteSpace(this.Metadata.Name)) + this.Metadata.Name = name; + + this.Name = string.IsNullOrWhiteSpace(name) ? "Web3Wallet" : name; + this.Context = $"{Name}-context"; + this.Core = core; + + this.Events = new EventDelegator(this); + this.Engine = new Web3WalletEngine(this); + } + + public Task Pair(string uri, bool activatePairing = false) + { + return this.Engine.Pair(uri, activatePairing); + } + + public Task ApproveSession(long id, Namespaces namespaces, string relayProtocol = null) + { + return this.Engine.ApproveSession(id, namespaces, relayProtocol); + } + + public Task ApproveSession(ProposalStruct proposal, params string[] approvedAddresses) + { + return this.Engine.ApproveSession(proposal, approvedAddresses); + } + + public Task RejectSession(long id, Error reason) + { + return this.Engine.RejectSession(id, reason); + } + + public Task RejectSession(ProposalStruct proposal, Error reason) + { + return this.Engine.RejectSession(proposal, reason); + } + + public Task RejectSession(ProposalStruct proposal, string reason) + { + return this.Engine.RejectSession(proposal, reason); + } + + public Task UpdateSession(string topic, Namespaces namespaces) + { + return this.Engine.UpdateSession(topic, namespaces); + } + + public Task ExtendSession(string topic) + { + return this.Engine.ExtendSession(topic); + } + + public Task RespondSessionRequest(string topic, JsonRpcResponse response) + { + return this.Engine.RespondSessionRequest(topic, response); + } + + public Task EmitSessionEvent(string topic, EventData eventData, string chainId) + { + return this.Engine.EmitSessionEvent(topic, eventData, topic); + } + + public Task DisconnectSession(string topic, Error reason) + { + return this.Engine.DisconnectSession(topic, reason); + } + + public Task RespondAuthRequest(ResultResponse results, string iss) + { + return this.Engine.RespondAuthRequest(results, iss); + } + + public Task RespondAuthRequest(AuthErrorResponse error, string iss) + { + return this.Engine.RespondAuthRequest(error, iss); + } + + public Task RespondAuthRequest(AuthRequest request, Error error, string iss) + { + return this.Engine.RespondAuthRequest(request, error, iss); + } + + public Task RespondAuthRequest(AuthRequest request, string signature, string iss, bool eip191 = true) + { + return this.Engine.RespondAuthRequest(request, signature, iss, eip191); + } + + public string FormatMessage(Cacao.CacaoRequestPayload payload, string iss) + { + return this.Engine.FormatMessage(payload, iss); + } + + private Task Initialize() + { + return this.Engine.Init(); + } + + public event EventHandler AuthRequested + { + add + { + Engine.AuthRequested += value; + } + remove + { + Engine.AuthRequested -= value; + } + } + public event EventHandler AuthResponded + { + add + { + Engine.AuthResponded += value; + } + remove + { + Engine.AuthResponded -= value; + } + } + public event EventHandler AuthError + { + add + { + Engine.AuthError += value; + } + remove + { + Engine.AuthError -= value; + } + } +} diff --git a/WalletConnectSharpV2.sln b/WalletConnectSharpV2.sln index e645502..9a2fbcf 100644 --- a/WalletConnectSharpV2.sln +++ b/WalletConnectSharpV2.sln @@ -34,6 +34,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletConnectSharp.Sign.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletConnectSharp.Tests.Common", "Tests\WalletConnectSharp.Tests.Common\WalletConnectSharp.Tests.Common.csproj", "{C3CC2BC9-CD29-46B8-94DB-B104E427330B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletConnectSharp.Auth", "WalletConnectSharp.Auth\WalletConnectSharp.Auth.csproj", "{5189138F-FAE5-4B2C-912F-CEB429C156AC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletConnectSharp.Auth.Tests", "Tests\WalletConnectSharp.Auth.Tests\WalletConnectSharp.Auth.Tests.csproj", "{7B63C8BD-737F-403F-AF66-8A2E7ADBDBAB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletConnectSharp.Web3Wallet", "WalletConnectSharp.Web3Wallet\WalletConnectSharp.Web3Wallet.csproj", "{53622C78-1162-46A2-9FFE-8E90B018A144}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletConnectSharp.Web3Wallet.Tests", "Tests\WalletConnectSharp.Web3Wallet.Tests\WalletConnectSharp.Web3Wallet.Tests.csproj", "{82C44097-B520-45A9-8228-FF35E6DDDC9D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Packages", "Packages", "{30D2D447-84B3-44B2-8CF8-F7DAE2AD30C5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -100,6 +110,22 @@ Global {C3CC2BC9-CD29-46B8-94DB-B104E427330B}.Debug|Any CPU.Build.0 = Debug|Any CPU {C3CC2BC9-CD29-46B8-94DB-B104E427330B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C3CC2BC9-CD29-46B8-94DB-B104E427330B}.Release|Any CPU.Build.0 = Release|Any CPU + {5189138F-FAE5-4B2C-912F-CEB429C156AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5189138F-FAE5-4B2C-912F-CEB429C156AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5189138F-FAE5-4B2C-912F-CEB429C156AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5189138F-FAE5-4B2C-912F-CEB429C156AC}.Release|Any CPU.Build.0 = Release|Any CPU + {7B63C8BD-737F-403F-AF66-8A2E7ADBDBAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B63C8BD-737F-403F-AF66-8A2E7ADBDBAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B63C8BD-737F-403F-AF66-8A2E7ADBDBAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B63C8BD-737F-403F-AF66-8A2E7ADBDBAB}.Release|Any CPU.Build.0 = Release|Any CPU + {53622C78-1162-46A2-9FFE-8E90B018A144}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53622C78-1162-46A2-9FFE-8E90B018A144}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53622C78-1162-46A2-9FFE-8E90B018A144}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53622C78-1162-46A2-9FFE-8E90B018A144}.Release|Any CPU.Build.0 = Release|Any CPU + {82C44097-B520-45A9-8228-FF35E6DDDC9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82C44097-B520-45A9-8228-FF35E6DDDC9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82C44097-B520-45A9-8228-FF35E6DDDC9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82C44097-B520-45A9-8228-FF35E6DDDC9D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {23897558-4177-425D-B2FA-75AB0B6FEDAE} = {3A55D6C7-0EF8-4EEA-90A5-89F5AFB97BA0} @@ -115,5 +141,11 @@ Global {3B89EF77-B544-4663-9E67-7E0CBF070BB3} = {3A55D6C7-0EF8-4EEA-90A5-89F5AFB97BA0} {5C78A5E4-24ED-499C-91BC-90D16A626EFA} = {3A55D6C7-0EF8-4EEA-90A5-89F5AFB97BA0} {C3CC2BC9-CD29-46B8-94DB-B104E427330B} = {3A55D6C7-0EF8-4EEA-90A5-89F5AFB97BA0} + {7B63C8BD-737F-403F-AF66-8A2E7ADBDBAB} = {3A55D6C7-0EF8-4EEA-90A5-89F5AFB97BA0} + {82C44097-B520-45A9-8228-FF35E6DDDC9D} = {3A55D6C7-0EF8-4EEA-90A5-89F5AFB97BA0} + {5189138F-FAE5-4B2C-912F-CEB429C156AC} = {30D2D447-84B3-44B2-8CF8-F7DAE2AD30C5} + {14F5A680-DC00-4889-B1C1-6E1BA46D2DE6} = {30D2D447-84B3-44B2-8CF8-F7DAE2AD30C5} + {7D5F3009-902E-4AF4-9EA2-992C8D41B6BF} = {30D2D447-84B3-44B2-8CF8-F7DAE2AD30C5} + {53622C78-1162-46A2-9FFE-8E90B018A144} = {30D2D447-84B3-44B2-8CF8-F7DAE2AD30C5} EndGlobalSection EndGlobal