using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Net; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using UnityEngine; using WalletConnectSharp.Core.Events; using WalletConnectSharp.Core.Events.Request; using WalletConnectSharp.Core.Models; using WalletConnectSharp.Core.Models.Ethereum; using WalletConnectSharp.Core.Network; using WalletConnectSharp.Core.Utils; #pragma warning disable 4014 namespace WalletConnectSharp.Core { public class WalletConnectSession : WalletConnectProtocol { private string _handshakeTopic; private long _handshakeId; public event EventHandler OnSessionConnect; public event EventHandler OnSessionCreated; public event EventHandler OnSessionResumed; public event EventHandler OnSessionDisconnect; public event EventHandler OnSend; public event EventHandler SessionUpdate; public int NetworkId { get; private set; } public string[] Accounts { get; private set; } public bool ReadyForUserPrompt { get; private set; } public bool SessionUsed { get; private set; } public int ChainId { get; private set; } private string clientId = ""; public string URI { get { var topicEncode = WebUtility.UrlEncode(_handshakeTopic); var versionEncode = WebUtility.UrlEncode(Version); var bridgeUrlEncode = WebUtility.UrlEncode(_bridgeUrl); var keyEncoded = WebUtility.UrlEncode(_key); return "wc:" + topicEncode + "@" + versionEncode + "?bridge=" + bridgeUrlEncode + "&key=" + keyEncoded; } } public WalletConnectSession(SavedSession savedSession, ITransport transport = null, ICipher cipher = null, EventDelegator eventDelegator = null) : base(savedSession, transport, cipher, eventDelegator) { this.DappMetadata = savedSession.DappMeta; this.WalletMetadata = savedSession.WalletMeta; this.ChainId = savedSession.ChainID; clientId = savedSession.ClientID; this.Accounts = savedSession.Accounts; this.NetworkId = savedSession.NetworkID; this._handshakeId = savedSession.HandshakeID; this.SessionConnected = true; } public WalletConnectSession(ClientMeta clientMeta, string bridgeUrl = null, ITransport transport = null, ICipher cipher = null, int chainId = 1, EventDelegator eventDelegator = null) : base(transport, cipher, eventDelegator) { if (clientMeta == null) { throw new ArgumentException("clientMeta cannot be null!"); } if (string.IsNullOrWhiteSpace(clientMeta.Description)) { throw new ArgumentException("clientMeta must include a valid Description"); } if (string.IsNullOrWhiteSpace(clientMeta.Name)) { throw new ArgumentException("clientMeta must include a valid Name"); } if (string.IsNullOrWhiteSpace(clientMeta.URL)) { throw new ArgumentException("clientMeta must include a valid URL"); } if (clientMeta.Icons == null || clientMeta.Icons.Length == 0) { throw new ArgumentException("clientMeta must include an array of Icons the Wallet app can use. These Icons must be URLs to images. You must include at least one image URL to use"); } if (bridgeUrl == null) { bridgeUrl = DefaultBridge.ChooseRandomBridge(); } bridgeUrl = DefaultBridge.GetBridgeUrl(bridgeUrl); if (bridgeUrl.StartsWith("https")) bridgeUrl = bridgeUrl.Replace("https", "wss"); else if (bridgeUrl.StartsWith("http")) bridgeUrl = bridgeUrl.Replace("http", "ws"); this.DappMetadata = clientMeta; this.ChainId = chainId; this._bridgeUrl = bridgeUrl; this.SessionConnected = false; CreateNewSession(); } private void CreateNewSession() { if (SessionConnected) { throw new IOException("You cannot create a new session after connecting the session. Create a new WalletConnectSession object to create a new session"); } this._bridgeUrl = DefaultBridge.GetBridgeUrl(this._bridgeUrl); var topicGuid = Guid.NewGuid(); _handshakeTopic = topicGuid.ToString(); clientId = Guid.NewGuid().ToString(); GenerateKey(); if (Transport != null) { Transport.ClearSubscriptions(); } SessionUsed = false; ReadyForUserPrompt = false; } private void EnsureNotDisconnected() { if (Disconnected) { throw new IOException( "Session stale! The session has been disconnected. This session cannot be reused."); } } private void GenerateKey() { //Generate a random secret byte[] secret = new byte[32]; RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider(); rngCsp.GetBytes(secret); this._keyRaw = secret; //Convert hex this._key = this._keyRaw.ToHex().ToLower(); } public virtual async Task ConnectSession() { EnsureNotDisconnected(); Connecting = true; try { if (SessionConnected) { //Listen for the _handshakeId response //The response will be of type WCSessionRequestResponse //We do this now before subscribing //This is in case we need to respond to a session disconnect and this is a //resume session Events.ListenForResponse(this._handshakeId, HandleSessionResponse); } if (!base.TransportConnected) { await base.SetupTransport(); } ReadyForUserPrompt = false; await SubscribeAndListenToTopic(this.clientId); ListenToTopic(this._handshakeTopic); WCSessionData result; if (!SessionConnected) { //result = await CreateSession(); //throw timeout exception after two minutes int timeout = 120000; var task = CreateSession(); if (await Task.WhenAny(task, Task.Delay(timeout)) == task) { // task completed within timeout result = task.Result; } else { // timeout logic throw new TimeoutException(); } //Reset this back after we have established a session ReadyForUserPrompt = false; Connecting = false; if (OnSessionCreated != null) OnSessionCreated(this, this); } else { result = new WCSessionData() { accounts = Accounts, approved = true, chainId = ChainId, networkId = NetworkId, peerId = PeerId, peerMeta = WalletMetadata }; Connecting = false; if (OnSessionResumed != null) OnSessionResumed(this, this); } if (OnSessionConnect != null) OnSessionConnect(this, this); return result; } catch (IOException e) { //If the transport is connected, then disconnect that //we tried our best, they can try again if (TransportConnected) { await DisconnectTransport(); } else { throw new IOException("Transport Connection failed", e); } throw new IOException("Session Connection failed", e); } finally { //The session has been made, we are no longer ready for another user prompt ReadyForUserPrompt = false; Connecting = false; } } public override async Task Connect() { EnsureNotDisconnected(); await base.Connect(); await ConnectSession(); } public async Task DisconnectSession(string disconnectMessage = "Session Disconnected", bool createNewSession = true) { EnsureNotDisconnected(); var request = new WCSessionUpdate(new WCSessionData() { approved = false, chainId = 0, accounts = null, networkId = 0 }); await SendRequest(request); await base.Disconnect(); HandleSessionDisconnect(disconnectMessage, "disconnect", createNewSession); } public override async Task Disconnect() { EnsureNotDisconnected(); await DisconnectSession(); } public async Task EthSign(string address, string message) { EnsureNotDisconnected(); if (!message.IsHex()) { var rawMessage = Encoding.UTF8.GetBytes(message); var byteList = new List(); var bytePrefix = "0x19".HexToByteArray(); var textBytePrefix = Encoding.UTF8.GetBytes("Ethereum Signed Message:\n" + rawMessage.Length); byteList.AddRange(bytePrefix); byteList.AddRange(textBytePrefix); byteList.AddRange(rawMessage); var hash = new Sha3Keccack().CalculateHash(byteList.ToArray()); message = "0x" + hash.ToHex(); } var request = new EthSign(address, message); var response = await Send(request); return response.Result; } public async Task EthPersonalSign(string address, string message) { EnsureNotDisconnected(); if (!message.IsHex()) { /*var rawMessage = Encoding.UTF8.GetBytes(message); var byteList = new List(); var bytePrefix = "0x19".HexToByteArray(); var textBytePrefix = Encoding.UTF8.GetBytes("Ethereum Signed Message:\n" + rawMessage.Length); byteList.AddRange(bytePrefix); byteList.AddRange(textBytePrefix); byteList.AddRange(rawMessage); var hash = new Sha3Keccack().CalculateHash(byteList.ToArray());*/ message = "0x" + Encoding.UTF8.GetBytes(message).ToHex(); } var request = new EthPersonalSign(address, message); var response = await Send(request); return response.Result; } public async Task EthSignTypedData(string address, T data, EIP712Domain eip712Domain) { EnsureNotDisconnected(); var request = new EthSignTypedData(address, data, eip712Domain); var response = await Send, EthResponse>(request); return response.Result; } public async Task EthSendTransaction(params TransactionData[] transaction) { EnsureNotDisconnected(); var request = new EthSendTransaction(transaction); var response = await Send(request); return response.Result; } public async Task EthSignTransaction(params TransactionData[] transaction) { EnsureNotDisconnected(); var request = new EthSignTransaction(transaction); var response = await Send(request); return response.Result; } public async Task EthSendRawTransaction(string data, Encoding messageEncoding = null) { EnsureNotDisconnected(); if (!data.IsHex()) { var encoding = messageEncoding; if (encoding == null) { encoding = Encoding.UTF8; } data = "0x" + encoding.GetBytes(data).ToHex(); } var request = new EthGenericRequest("eth_sendRawTransaction", data); var response = await Send, EthResponse>(request); return response.Result; } public async Task Send(T data) where T : JsonRpcRequest where R : JsonRpcResponse { EnsureNotDisconnected(); TaskCompletionSource eventCompleted = new TaskCompletionSource(TaskCreationOptions.None); Events.ListenForResponse(data.ID, (sender, @event) => { var response = @event.Response; if (response.IsError) { eventCompleted.SetException(new IOException(response.Error.Message)); } else { eventCompleted.SetResult(@event.Response); } }); await SendRequest(data); if (OnSend != null) { OnSend(this, this); } return await eventCompleted.Task; } /// /// Create a new WalletConnect session with a Wallet. /// /// private async Task CreateSession() { EnsureNotDisconnected(); var data = new WcSessionRequest(DappMetadata, clientId, ChainId); this._handshakeId = data.ID; //Debug.Log("[WalletConnect] Sending Session to topic " + _handshakeTopic); await SendRequest(data, this._handshakeTopic); SessionUsed = true; TaskCompletionSource eventCompleted = new TaskCompletionSource(TaskCreationOptions.None); //Listen for the _handshakeId response //The response will be of type WCSessionRequestResponse Events.ListenForResponse(this._handshakeId, HandleSessionResponse); //Listen for wc_sessionUpdate requests Events.ListenFor("wc_sessionUpdate", (object sender, GenericEvent @event) => HandleSessionUpdate(@event.Response.parameters[0])); //Listen for the "connect" event triggered by 'HandleSessionResponse' above //This will have the type WCSessionData Events.ListenFor("connect", (sender, @event) => { eventCompleted.TrySetResult(@event.Response); }); //Listen for the "session_failed" event triggered by 'HandleSessionResponse' above //This will have the type failure reason Events.ListenFor("session_failed", delegate(object sender, GenericEvent @event) { if (@event.Response.Message == "Not Approved" || @event.Response.Message == "Session Rejected") { eventCompleted.TrySetCanceled(); } else { eventCompleted.TrySetException( new IOException("WalletConnect: Session Failed: " + @event.Response.Message)); } }); ReadyForUserPrompt = true; //Debug.Log("[WalletConnect] Session Ready for Wallet"); var response = await eventCompleted.Task; ReadyForUserPrompt = false; return response; } private void HandleSessionResponse(object sender, JsonRpcResponseEvent jsonresponse) { var response = jsonresponse.Response.result; if (response != null && response.approved) { HandleSessionUpdate(response); } else if (jsonresponse.Response.IsError) { HandleSessionDisconnect(jsonresponse.Response.Error.Message, "session_failed"); } else { HandleSessionDisconnect("Not Approved", "session_failed"); } } private void HandleSessionUpdate(WCSessionData data) { if (data == null) return; bool wasConnected = SessionConnected; //We are connected if we are approved SessionConnected = data.approved; if (data.chainId != null) ChainId = (int)data.chainId; if (data.networkId != null) NetworkId = (int)data.networkId; Accounts = data.accounts; if (!wasConnected) { PeerId = data.peerId; WalletMetadata = data.peerMeta; Events.Trigger("connect", data); } else if (wasConnected && !SessionConnected) { HandleSessionDisconnect("Wallet Disconnected"); } else { Events.Trigger("session_update", data); } if (SessionUpdate != null) SessionUpdate(this, data); } private void HandleSessionDisconnect(string msg, string topic = "disconnect", bool createNewSession = true) { SessionConnected = false; Disconnected = true; Events.Trigger(topic, new ErrorResponse(msg)); if (TransportConnected) { DisconnectTransport(); } _activeTopics.Clear(); Events.Clear(); if (OnSessionDisconnect != null) OnSessionDisconnect(this, EventArgs.Empty); } /// /// Creates and returns a serializable class that holds all session data required to resume later /// /// public SavedSession SaveSession() { if (!SessionConnected || Disconnected) { return null; } return new SavedSession(clientId, _handshakeId, _bridgeUrl, _key, _keyRaw, PeerId, NetworkId, Accounts, ChainId, DappMetadata, WalletMetadata); } /// /// Save the current session to a Stream. This function will write a GZIP Compressed JSON blob /// of the contents of SaveSession() /// /// The stream to write to /// Whether to leave the stream open /// If there is currently no session active, or if writing to the stream fails public void SaveSession(Stream stream, bool leaveStreamOpen = true) { //We'll save the current session as a GZIP compressed JSON blob var data = SaveSession(); if (data == null) { throw new IOException("No session is active to save"); } var json = JsonConvert.SerializeObject(data); byte[] encodedJson = Encoding.UTF8.GetBytes(json); using (GZipStream gZipStream = new GZipStream(stream, CompressionMode.Compress, leaveStreamOpen)) { byte[] sizeEncoded = BitConverter.GetBytes(encodedJson.Length); gZipStream.Write(sizeEncoded, 0, sizeEncoded.Length); gZipStream.Write(encodedJson, 0, encodedJson.Length); } } /// /// Reads a GZIP Compressed JSON blob of a SavedSession object from a given Stream. This is the reverse of /// SaveSession(Stream) /// /// The stream to write to /// Whether to leave the stream open /// If reading from the stream fails /// A SavedSession object public static SavedSession ReadSession(Stream stream, bool leaveStreamOpen = true) { string json; using (GZipStream gZipStream = new GZipStream(stream, CompressionMode.Decompress, leaveStreamOpen)) { byte[] sizeEncoded = new byte[4]; gZipStream.Read(sizeEncoded, 0, 4); int size = BitConverter.ToInt32(sizeEncoded, 0); byte[] jsonEncoded = new byte[size]; gZipStream.Read(jsonEncoded, 0, size); json = Encoding.UTF8.GetString(jsonEncoded); } return JsonConvert.DeserializeObject(json); } } }