diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Libraries/ChainSafe.Gaming.Unity.dll b/Packages/io.chainsafe.web3-unity/Runtime/Libraries/ChainSafe.Gaming.Unity.dll index f28ff858f..eeba98149 100644 Binary files a/Packages/io.chainsafe.web3-unity/Runtime/Libraries/ChainSafe.Gaming.Unity.dll and b/Packages/io.chainsafe.web3-unity/Runtime/Libraries/ChainSafe.Gaming.Unity.dll differ diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Libraries/ChainSafe.Gaming.WalletConnect.dll b/Packages/io.chainsafe.web3-unity/Runtime/Libraries/ChainSafe.Gaming.WalletConnect.dll index ea9bea97e..747a354e2 100644 Binary files a/Packages/io.chainsafe.web3-unity/Runtime/Libraries/ChainSafe.Gaming.WalletConnect.dll and b/Packages/io.chainsafe.web3-unity/Runtime/Libraries/ChainSafe.Gaming.WalletConnect.dll differ diff --git a/src/UnitySampleProject/Assets/Plugins.meta b/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket.meta similarity index 77% rename from src/UnitySampleProject/Assets/Plugins.meta rename to Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket.meta index a64f6fd98..f6b669be0 100644 --- a/src/UnitySampleProject/Assets/Plugins.meta +++ b/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 9265622591a5741fca8ccae4087d191a +guid: 3430bfcc4c3b3944b91c5be04ddd9dd2 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket/WebSocket.cs b/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket/WebSocket.cs new file mode 100644 index 000000000..503cf4363 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket/WebSocket.cs @@ -0,0 +1,848 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.WebSockets; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using AOT; +using System.Runtime.InteropServices; +using UnityEngine; +using System.Collections; + +public class MainThreadUtil : MonoBehaviour +{ + public static MainThreadUtil Instance { get; private set; } + public static SynchronizationContext synchronizationContext { get; private set; } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + public static void Setup() + { + Instance = new GameObject("MainThreadUtil") + .AddComponent(); + synchronizationContext = SynchronizationContext.Current; + } + + public static void Run(IEnumerator waitForUpdate) + { + synchronizationContext.Post(_ => Instance.StartCoroutine( + waitForUpdate), null); + } + + void Awake() + { + gameObject.hideFlags = HideFlags.HideAndDontSave; + DontDestroyOnLoad(gameObject); + } +} + +public class WaitForUpdate : CustomYieldInstruction +{ + public override bool keepWaiting + { + get { return false; } + } + + public MainThreadAwaiter GetAwaiter() + { + var awaiter = new MainThreadAwaiter(); + MainThreadUtil.Run(CoroutineWrapper(this, awaiter)); + return awaiter; + } + + public class MainThreadAwaiter : INotifyCompletion + { + Action continuation; + + public bool IsCompleted { get; set; } + + public void GetResult() { } + + public void Complete() + { + IsCompleted = true; + continuation?.Invoke(); + } + + void INotifyCompletion.OnCompleted(Action continuation) + { + this.continuation = continuation; + } + } + + public static IEnumerator CoroutineWrapper(IEnumerator theWorker, MainThreadAwaiter awaiter) + { + yield return theWorker; + awaiter.Complete(); + } +} + +namespace NativeWebSocket +{ + public delegate void WebSocketOpenEventHandler(); + public delegate void WebSocketMessageEventHandler(byte[] data); + public delegate void WebSocketErrorEventHandler(string errorMsg); + public delegate void WebSocketCloseEventHandler(WebSocketCloseCode closeCode); + + public enum WebSocketCloseCode + { + /* Do NOT use NotSet - it's only purpose is to indicate that the close code cannot be parsed. */ + NotSet = 0, + Normal = 1000, + Away = 1001, + ProtocolError = 1002, + UnsupportedData = 1003, + Undefined = 1004, + NoStatus = 1005, + Abnormal = 1006, + InvalidData = 1007, + PolicyViolation = 1008, + TooBig = 1009, + MandatoryExtension = 1010, + ServerError = 1011, + TlsHandshakeFailure = 1015 + } + + public enum WebSocketState + { + Connecting, + Open, + Closing, + Closed + } + + public interface IWebSocket + { + event WebSocketOpenEventHandler OnOpen; + event WebSocketMessageEventHandler OnMessage; + event WebSocketErrorEventHandler OnError; + event WebSocketCloseEventHandler OnClose; + + WebSocketState State { get; } + } + + public static class WebSocketHelpers + { + public static WebSocketCloseCode ParseCloseCodeEnum(int closeCode) + { + + if (WebSocketCloseCode.IsDefined(typeof(WebSocketCloseCode), closeCode)) + { + return (WebSocketCloseCode)closeCode; + } + else + { + return WebSocketCloseCode.Undefined; + } + + } + + public static WebSocketException GetErrorMessageFromCode(int errorCode, Exception inner) + { + switch (errorCode) + { + case -1: + return new WebSocketUnexpectedException("WebSocket instance not found.", inner); + case -2: + return new WebSocketInvalidStateException("WebSocket is already connected or in connecting state.", inner); + case -3: + return new WebSocketInvalidStateException("WebSocket is not connected.", inner); + case -4: + return new WebSocketInvalidStateException("WebSocket is already closing.", inner); + case -5: + return new WebSocketInvalidStateException("WebSocket is already closed.", inner); + case -6: + return new WebSocketInvalidStateException("WebSocket is not in open state.", inner); + case -7: + return new WebSocketInvalidArgumentException("Cannot close WebSocket. An invalid code was specified or reason is too long.", inner); + default: + return new WebSocketUnexpectedException("Unknown error.", inner); + } + } + } + + public class WebSocketException : Exception + { + public WebSocketException() { } + public WebSocketException(string message) : base(message) { } + public WebSocketException(string message, Exception inner) : base(message, inner) { } + } + + public class WebSocketUnexpectedException : WebSocketException + { + public WebSocketUnexpectedException() { } + public WebSocketUnexpectedException(string message) : base(message) { } + public WebSocketUnexpectedException(string message, Exception inner) : base(message, inner) { } + } + + public class WebSocketInvalidArgumentException : WebSocketException + { + public WebSocketInvalidArgumentException() { } + public WebSocketInvalidArgumentException(string message) : base(message) { } + public WebSocketInvalidArgumentException(string message, Exception inner) : base(message, inner) { } + } + + public class WebSocketInvalidStateException : WebSocketException + { + public WebSocketInvalidStateException() { } + public WebSocketInvalidStateException(string message) : base(message) { } + public WebSocketInvalidStateException(string message, Exception inner) : base(message, inner) { } + } + + public class WaitForBackgroundThread + { + public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter() + { + return Task.Run(() => { }).ConfigureAwait(false).GetAwaiter(); + } + } + +#if UNITY_WEBGL && !UNITY_EDITOR + + /// + /// WebSocket class bound to JSLIB. + /// + public class WebSocket : IWebSocket { + + /* WebSocket JSLIB functions */ + [DllImport ("__Internal")] + public static extern int WebSocketConnect (int instanceId); + + [DllImport ("__Internal")] + public static extern int WebSocketClose (int instanceId, int code, string reason); + + [DllImport ("__Internal")] + public static extern int WebSocketSend (int instanceId, byte[] dataPtr, int dataLength); + + [DllImport ("__Internal")] + public static extern int WebSocketSendText (int instanceId, string message); + + [DllImport ("__Internal")] + public static extern int WebSocketGetState (int instanceId); + + protected int instanceId; + + public event WebSocketOpenEventHandler OnOpen; + public event WebSocketMessageEventHandler OnMessage; + public event WebSocketErrorEventHandler OnError; + public event WebSocketCloseEventHandler OnClose; + + public WebSocket (string url, Dictionary headers = null) { + if (!WebSocketFactory.isInitialized) { + WebSocketFactory.Initialize (); + } + + int instanceId = WebSocketFactory.WebSocketAllocate (url); + WebSocketFactory.instances.Add (instanceId, this); + + this.instanceId = instanceId; + } + + public WebSocket (string url, string subprotocol, Dictionary headers = null) { + if (!WebSocketFactory.isInitialized) { + WebSocketFactory.Initialize (); + } + + int instanceId = WebSocketFactory.WebSocketAllocate (url); + WebSocketFactory.instances.Add (instanceId, this); + + WebSocketFactory.WebSocketAddSubProtocol(instanceId, subprotocol); + + this.instanceId = instanceId; + } + + public WebSocket (string url, List subprotocols, Dictionary headers = null) { + if (!WebSocketFactory.isInitialized) { + WebSocketFactory.Initialize (); + } + + int instanceId = WebSocketFactory.WebSocketAllocate (url); + WebSocketFactory.instances.Add (instanceId, this); + + foreach (string subprotocol in subprotocols) { + WebSocketFactory.WebSocketAddSubProtocol(instanceId, subprotocol); + } + + this.instanceId = instanceId; + } + + ~WebSocket () { + WebSocketFactory.HandleInstanceDestroy (this.instanceId); + } + + public int GetInstanceId () { + return this.instanceId; + } + + public Task Connect () { + int ret = WebSocketConnect (this.instanceId); + + if (ret < 0) + throw WebSocketHelpers.GetErrorMessageFromCode (ret, null); + + return Task.CompletedTask; + } + + public void CancelConnection () { + if (State == WebSocketState.Open) + Close (WebSocketCloseCode.Abnormal); + } + + public Task Close (WebSocketCloseCode code = WebSocketCloseCode.Normal, string reason = null) { + int ret = WebSocketClose (this.instanceId, (int) code, reason); + + if (ret < 0) + throw WebSocketHelpers.GetErrorMessageFromCode (ret, null); + + return Task.CompletedTask; + } + + public Task Send (byte[] data) { + int ret = WebSocketSend (this.instanceId, data, data.Length); + + if (ret < 0) + throw WebSocketHelpers.GetErrorMessageFromCode (ret, null); + + return Task.CompletedTask; + } + + public Task SendText (string message) { + int ret = WebSocketSendText (this.instanceId, message); + + if (ret < 0) + throw WebSocketHelpers.GetErrorMessageFromCode (ret, null); + + return Task.CompletedTask; + } + + public WebSocketState State { + get { + int state = WebSocketGetState (this.instanceId); + + if (state < 0) + throw WebSocketHelpers.GetErrorMessageFromCode (state, null); + + switch (state) { + case 0: + return WebSocketState.Connecting; + + case 1: + return WebSocketState.Open; + + case 2: + return WebSocketState.Closing; + + case 3: + return WebSocketState.Closed; + + default: + return WebSocketState.Closed; + } + } + } + + public void DelegateOnOpenEvent () { + this.OnOpen?.Invoke (); + } + + public void DelegateOnMessageEvent (byte[] data) { + this.OnMessage?.Invoke (data); + } + + public void DelegateOnErrorEvent (string errorMsg) { + this.OnError?.Invoke (errorMsg); + } + + public void DelegateOnCloseEvent (int closeCode) { + this.OnClose?.Invoke (WebSocketHelpers.ParseCloseCodeEnum (closeCode)); + } + + } + +#else + + public class WebSocket : IWebSocket + { + public event WebSocketOpenEventHandler OnOpen; + public event WebSocketMessageEventHandler OnMessage; + public event WebSocketErrorEventHandler OnError; + public event WebSocketCloseEventHandler OnClose; + + private Uri uri; + private Dictionary headers; + private List subprotocols; + private ClientWebSocket m_Socket = new ClientWebSocket(); + + private CancellationTokenSource m_TokenSource; + private CancellationToken m_CancellationToken; + + private readonly object OutgoingMessageLock = new object(); + private readonly object IncomingMessageLock = new object(); + + private bool isSending = false; + private List> sendBytesQueue = new List>(); + private List> sendTextQueue = new List>(); + + public WebSocket(string url, Dictionary headers = null) + { + uri = new Uri(url); + + if (headers == null) + { + this.headers = new Dictionary(); + } + else + { + this.headers = headers; + } + + subprotocols = new List(); + + string protocol = uri.Scheme; + if (!protocol.Equals("ws") && !protocol.Equals("wss")) + throw new ArgumentException("Unsupported protocol: " + protocol); + } + + public WebSocket(string url, string subprotocol, Dictionary headers = null) + { + uri = new Uri(url); + + if (headers == null) + { + this.headers = new Dictionary(); + } + else + { + this.headers = headers; + } + + subprotocols = new List {subprotocol}; + + string protocol = uri.Scheme; + if (!protocol.Equals("ws") && !protocol.Equals("wss")) + throw new ArgumentException("Unsupported protocol: " + protocol); + } + + public WebSocket(string url, List subprotocols, Dictionary headers = null) + { + uri = new Uri(url); + + if (headers == null) + { + this.headers = new Dictionary(); + } + else + { + this.headers = headers; + } + + this.subprotocols = subprotocols; + + string protocol = uri.Scheme; + if (!protocol.Equals("ws") && !protocol.Equals("wss")) + throw new ArgumentException("Unsupported protocol: " + protocol); + } + + public void CancelConnection() + { + m_TokenSource?.Cancel(); + } + + public async Task Connect() + { + try + { + m_TokenSource = new CancellationTokenSource(); + m_CancellationToken = m_TokenSource.Token; + + m_Socket = new ClientWebSocket(); + + foreach (var header in headers) + { + m_Socket.Options.SetRequestHeader(header.Key, header.Value); + } + + foreach (string subprotocol in subprotocols) { + m_Socket.Options.AddSubProtocol(subprotocol); + } + + await m_Socket.ConnectAsync(uri, m_CancellationToken); + OnOpen?.Invoke(); + + await Receive(); + } + catch (Exception ex) + { + OnError?.Invoke(ex.Message); + OnClose?.Invoke(WebSocketCloseCode.Abnormal); + } + finally + { + if (m_Socket != null) + { + m_TokenSource.Cancel(); + m_Socket.Dispose(); + } + } + } + + public WebSocketState State + { + get + { + switch (m_Socket.State) + { + case System.Net.WebSockets.WebSocketState.Connecting: + return WebSocketState.Connecting; + + case System.Net.WebSockets.WebSocketState.Open: + return WebSocketState.Open; + + case System.Net.WebSockets.WebSocketState.CloseSent: + case System.Net.WebSockets.WebSocketState.CloseReceived: + return WebSocketState.Closing; + + case System.Net.WebSockets.WebSocketState.Closed: + return WebSocketState.Closed; + + default: + return WebSocketState.Closed; + } + } + } + + public Task Send(byte[] bytes) + { + // return m_Socket.SendAsync(buffer, WebSocketMessageType.Binary, true, CancellationToken.None); + return SendMessage(sendBytesQueue, WebSocketMessageType.Binary, new ArraySegment(bytes)); + } + + public Task SendText(string message) + { + var encoded = Encoding.UTF8.GetBytes(message); + + // m_Socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None); + return SendMessage(sendTextQueue, WebSocketMessageType.Text, new ArraySegment(encoded, 0, encoded.Length)); + } + + private async Task SendMessage(List> queue, WebSocketMessageType messageType, ArraySegment buffer) + { + // Return control to the calling method immediately. + // await Task.Yield (); + + // Make sure we have data. + if (buffer.Count == 0) + { + return; + } + + // The state of the connection is contained in the context Items dictionary. + bool sending; + + lock (OutgoingMessageLock) + { + sending = isSending; + + // If not, we are now. + if (!isSending) + { + isSending = true; + } + } + + if (!sending) + { + // Lock with a timeout, just in case. + if (!Monitor.TryEnter(m_Socket, 1000)) + { + // If we couldn't obtain exclusive access to the socket in one second, something is wrong. + await m_Socket.CloseAsync(WebSocketCloseStatus.InternalServerError, string.Empty, m_CancellationToken); + return; + } + + try + { + // Send the message synchronously. + var t = m_Socket.SendAsync(buffer, messageType, true, m_CancellationToken); + t.Wait(m_CancellationToken); + } + finally + { + Monitor.Exit(m_Socket); + } + + // Note that we've finished sending. + lock (OutgoingMessageLock) + { + isSending = false; + } + + // Handle any queued messages. + await HandleQueue(queue, messageType); + } + else + { + // Add the message to the queue. + lock (OutgoingMessageLock) + { + queue.Add(buffer); + } + } + } + + private async Task HandleQueue(List> queue, WebSocketMessageType messageType) + { + var buffer = new ArraySegment(); + lock (OutgoingMessageLock) + { + // Check for an item in the queue. + if (queue.Count > 0) + { + // Pull it off the top. + buffer = queue[0]; + queue.RemoveAt(0); + } + } + + // Send that message. + if (buffer.Count > 0) + { + await SendMessage(queue, messageType, buffer); + } + } + + private List m_MessageList = new List(); + + // simple dispatcher for queued messages. + public void DispatchMessageQueue() + { + if (m_MessageList.Count == 0) + { + return; + } + + List messageListCopy; + + lock (IncomingMessageLock) + { + messageListCopy = new List(m_MessageList); + m_MessageList.Clear(); + } + + var len = messageListCopy.Count; + for (int i = 0; i < len; i++) + { + OnMessage?.Invoke(messageListCopy[i]); + } + } + + public async Task Receive() + { + WebSocketCloseCode closeCode = WebSocketCloseCode.Abnormal; + await new WaitForBackgroundThread(); + + ArraySegment buffer = new ArraySegment(new byte[8192]); + try + { + while (m_Socket.State == System.Net.WebSockets.WebSocketState.Open) + { + WebSocketReceiveResult result = null; + + using (var ms = new MemoryStream()) + { + do + { + result = await m_Socket.ReceiveAsync(buffer, m_CancellationToken); + ms.Write(buffer.Array, buffer.Offset, result.Count); + } + while (!result.EndOfMessage); + + ms.Seek(0, SeekOrigin.Begin); + + if (result.MessageType == WebSocketMessageType.Text) + { + lock (IncomingMessageLock) + { + m_MessageList.Add(ms.ToArray()); + } + + //using (var reader = new StreamReader(ms, Encoding.UTF8)) + //{ + // string message = reader.ReadToEnd(); + // OnMessage?.Invoke(this, new MessageEventArgs(message)); + //} + } + else if (result.MessageType == WebSocketMessageType.Binary) + { + lock (IncomingMessageLock) + { + m_MessageList.Add(ms.ToArray()); + } + } + else if (result.MessageType == WebSocketMessageType.Close) + { + await Close(); + closeCode = WebSocketHelpers.ParseCloseCodeEnum((int)result.CloseStatus); + break; + } + } + } + } + catch (Exception) + { + m_TokenSource.Cancel(); + } + finally + { + await new WaitForUpdate(); + OnClose?.Invoke(closeCode); + } + } + + public async Task Close() + { + if (State == WebSocketState.Open) + { + await m_Socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, m_CancellationToken); + } + } + } +#endif + + /// + /// Factory + /// + + /// + /// Class providing static access methods to work with JSLIB WebSocket or WebSocketSharp interface + /// + public static class WebSocketFactory + { + +#if UNITY_WEBGL && !UNITY_EDITOR + /* Map of websocket instances */ + public static Dictionary instances = new Dictionary (); + + /* Delegates */ + public delegate void OnOpenCallback (int instanceId); + public delegate void OnMessageCallback (int instanceId, System.IntPtr msgPtr, int msgSize); + public delegate void OnErrorCallback (int instanceId, System.IntPtr errorPtr); + public delegate void OnCloseCallback (int instanceId, int closeCode); + + /* WebSocket JSLIB callback setters and other functions */ + [DllImport ("__Internal")] + public static extern int WebSocketAllocate (string url); + + [DllImport ("__Internal")] + public static extern int WebSocketAddSubProtocol (int instanceId, string subprotocol); + + [DllImport ("__Internal")] + public static extern void WebSocketFree (int instanceId); + + [DllImport ("__Internal")] + public static extern void WebSocketSetOnOpen (OnOpenCallback callback); + + [DllImport ("__Internal")] + public static extern void WebSocketSetOnMessage (OnMessageCallback callback); + + [DllImport ("__Internal")] + public static extern void WebSocketSetOnError (OnErrorCallback callback); + + [DllImport ("__Internal")] + public static extern void WebSocketSetOnClose (OnCloseCallback callback); + + /* If callbacks was initialized and set */ + public static bool isInitialized = false; + + /* + * Initialize WebSocket callbacks to JSLIB + */ + public static void Initialize () { + + WebSocketSetOnOpen (DelegateOnOpenEvent); + WebSocketSetOnMessage (DelegateOnMessageEvent); + WebSocketSetOnError (DelegateOnErrorEvent); + WebSocketSetOnClose (DelegateOnCloseEvent); + + isInitialized = true; + + } + + /// + /// Called when instance is destroyed (by destructor) + /// Method removes instance from map and free it in JSLIB implementation + /// + /// Instance identifier. + public static void HandleInstanceDestroy (int instanceId) { + + instances.Remove (instanceId); + WebSocketFree (instanceId); + + } + + [MonoPInvokeCallback (typeof (OnOpenCallback))] + public static void DelegateOnOpenEvent (int instanceId) { + + WebSocket instanceRef; + + if (instances.TryGetValue (instanceId, out instanceRef)) { + instanceRef.DelegateOnOpenEvent (); + } + + } + + [MonoPInvokeCallback (typeof (OnMessageCallback))] + public static void DelegateOnMessageEvent (int instanceId, System.IntPtr msgPtr, int msgSize) { + + WebSocket instanceRef; + + if (instances.TryGetValue (instanceId, out instanceRef)) { + byte[] msg = new byte[msgSize]; + Marshal.Copy (msgPtr, msg, 0, msgSize); + + instanceRef.DelegateOnMessageEvent (msg); + } + + } + + [MonoPInvokeCallback (typeof (OnErrorCallback))] + public static void DelegateOnErrorEvent (int instanceId, System.IntPtr errorPtr) { + + WebSocket instanceRef; + + if (instances.TryGetValue (instanceId, out instanceRef)) { + + string errorMsg = Marshal.PtrToStringAuto (errorPtr); + instanceRef.DelegateOnErrorEvent (errorMsg); + + } + + } + + [MonoPInvokeCallback (typeof (OnCloseCallback))] + public static void DelegateOnCloseEvent (int instanceId, int closeCode) { + + WebSocket instanceRef; + + if (instances.TryGetValue (instanceId, out instanceRef)) { + instanceRef.DelegateOnCloseEvent (closeCode); + } + + } +#endif + + /// + /// Create WebSocket client instance + /// + /// The WebSocket instance. + /// WebSocket valid URL. + public static WebSocket CreateInstance(string url) + { + return new WebSocket(url); + } + + } + +} diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket/WebSocket.cs.meta b/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket/WebSocket.cs.meta new file mode 100644 index 000000000..708e6f1c3 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket/WebSocket.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: df0e653fbf9005342904aa0f14d46088 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket/WebSocket.jslib b/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket/WebSocket.jslib new file mode 100644 index 000000000..052f2132d --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket/WebSocket.jslib @@ -0,0 +1,333 @@ + +var LibraryWebSocket = { + $webSocketState: { + /* + * Map of instances + * + * Instance structure: + * { + * url: string, + * ws: WebSocket + * } + */ + instances: {}, + + /* Last instance ID */ + lastId: 0, + + /* Event listeners */ + onOpen: null, + onMesssage: null, + onError: null, + onClose: null, + + /* Debug mode */ + debug: false + }, + + /** + * Set onOpen callback + * + * @param callback Reference to C# static function + */ + WebSocketSetOnOpen: function(callback) { + + webSocketState.onOpen = callback; + + }, + + /** + * Set onMessage callback + * + * @param callback Reference to C# static function + */ + WebSocketSetOnMessage: function(callback) { + + webSocketState.onMessage = callback; + + }, + + /** + * Set onError callback + * + * @param callback Reference to C# static function + */ + WebSocketSetOnError: function(callback) { + + webSocketState.onError = callback; + + }, + + /** + * Set onClose callback + * + * @param callback Reference to C# static function + */ + WebSocketSetOnClose: function(callback) { + + webSocketState.onClose = callback; + + }, + + /** + * Allocate new WebSocket instance struct + * + * @param url Server URL + */ + WebSocketAllocate: function(url) { + + var urlStr = UTF8ToString(url); + var id = webSocketState.lastId++; + + webSocketState.instances[id] = { + subprotocols: [], + url: urlStr, + ws: null + }; + + return id; + + }, + + /** + * Add subprotocol to instance + * + * @param instanceId Instance ID + * @param subprotocol Subprotocol name to add to instance + */ + WebSocketAddSubProtocol: function(instanceId, subprotocol) { + + var subprotocolStr = UTF8ToString(subprotocol); + webSocketState.instances[instanceId].subprotocols.push(subprotocolStr); + + }, + + /** + * Remove reference to WebSocket instance + * + * If socket is not closed function will close it but onClose event will not be emitted because + * this function should be invoked by C# WebSocket destructor. + * + * @param instanceId Instance ID + */ + WebSocketFree: function(instanceId) { + + var instance = webSocketState.instances[instanceId]; + + if (!instance) return 0; + + // Close if not closed + if (instance.ws && instance.ws.readyState < 2) + instance.ws.close(); + + // Remove reference + delete webSocketState.instances[instanceId]; + + return 0; + + }, + + /** + * Connect WebSocket to the server + * + * @param instanceId Instance ID + */ + WebSocketConnect: function(instanceId) { + + var instance = webSocketState.instances[instanceId]; + if (!instance) return -1; + + if (instance.ws !== null) + return -2; + + instance.ws = new WebSocket(instance.url, instance.subprotocols); + + instance.ws.binaryType = 'arraybuffer'; + + instance.ws.onopen = function() { + + if (webSocketState.debug) + console.log("[JSLIB WebSocket] Connected."); + + if (webSocketState.onOpen) + Module.dynCall_vi(webSocketState.onOpen, instanceId); + + }; + + instance.ws.onmessage = function(ev) { + + if (webSocketState.debug) + console.log("[JSLIB WebSocket] Received message:", ev.data); + + if (webSocketState.onMessage === null) + return; + + if (ev.data instanceof ArrayBuffer) { + + var dataBuffer = new Uint8Array(ev.data); + + var buffer = _malloc(dataBuffer.length); + HEAPU8.set(dataBuffer, buffer); + + try { + Module.dynCall_viii(webSocketState.onMessage, instanceId, buffer, dataBuffer.length); + } finally { + _free(buffer); + } + + } else { + var dataBuffer = (new TextEncoder()).encode(ev.data); + + var buffer = _malloc(dataBuffer.length); + HEAPU8.set(dataBuffer, buffer); + + try { + Module.dynCall_viii(webSocketState.onMessage, instanceId, buffer, dataBuffer.length); + } finally { + _free(buffer); + } + + } + + }; + + instance.ws.onerror = function(ev) { + + if (webSocketState.debug) + console.log("[JSLIB WebSocket] Error occured."); + + if (webSocketState.onError) { + + var msg = "WebSocket error."; + var length = lengthBytesUTF8(msg) + 1; + var buffer = _malloc(length); + stringToUTF8(msg, buffer, length); + + try { + Module.dynCall_vii(webSocketState.onError, instanceId, buffer); + } finally { + _free(buffer); + } + + } + + }; + + instance.ws.onclose = function(ev) { + + if (webSocketState.debug) + console.log("[JSLIB WebSocket] Closed."); + + if (webSocketState.onClose) + Module.dynCall_vii(webSocketState.onClose, instanceId, ev.code); + + delete instance.ws; + + }; + + return 0; + + }, + + /** + * Close WebSocket connection + * + * @param instanceId Instance ID + * @param code Close status code + * @param reasonPtr Pointer to reason string + */ + WebSocketClose: function(instanceId, code, reasonPtr) { + + var instance = webSocketState.instances[instanceId]; + if (!instance) return -1; + + if (!instance.ws) + return -3; + + if (instance.ws.readyState === 2) + return -4; + + if (instance.ws.readyState === 3) + return -5; + + var reason = ( reasonPtr ? UTF8ToString(reasonPtr) : undefined ); + + try { + instance.ws.close(code, reason); + } catch(err) { + return -7; + } + + return 0; + + }, + + /** + * Send message over WebSocket + * + * @param instanceId Instance ID + * @param bufferPtr Pointer to the message buffer + * @param length Length of the message in the buffer + */ + WebSocketSend: function(instanceId, bufferPtr, length) { + + var instance = webSocketState.instances[instanceId]; + if (!instance) return -1; + + if (!instance.ws) + return -3; + + if (instance.ws.readyState !== 1) + return -6; + + instance.ws.send(HEAPU8.buffer.slice(bufferPtr, bufferPtr + length)); + + return 0; + + }, + + /** + * Send text message over WebSocket + * + * @param instanceId Instance ID + * @param bufferPtr Pointer to the message buffer + * @param length Length of the message in the buffer + */ + WebSocketSendText: function(instanceId, message) { + + var instance = webSocketState.instances[instanceId]; + if (!instance) return -1; + + if (!instance.ws) + return -3; + + if (instance.ws.readyState !== 1) + return -6; + + instance.ws.send(UTF8ToString(message)); + + return 0; + + }, + + /** + * Return WebSocket readyState + * + * @param instanceId Instance ID + */ + WebSocketGetState: function(instanceId) { + + var instance = webSocketState.instances[instanceId]; + if (!instance) return -1; + + if (instance.ws) + return instance.ws.readyState; + else + return 3; + + } + +}; + +autoAddDeps(LibraryWebSocket, '$webSocketState'); +mergeInto(LibraryManager.library, LibraryWebSocket); diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket/WebSocket.jslib.meta b/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket/WebSocket.jslib.meta new file mode 100644 index 000000000..a4f0052dd --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Runtime/Plugins/WebSocket/WebSocket.jslib.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 3b767cc116f846f4b813fe00142b8736 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + WebGL: WebGL + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/UnitySampleProject/Assets/PlayerData.json.meta b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect.meta similarity index 57% rename from src/UnitySampleProject/Assets/PlayerData.json.meta rename to Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect.meta index 7e6a8d520..a2de9aa6e 100644 --- a/src/UnitySampleProject/Assets/PlayerData.json.meta +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect.meta @@ -1,6 +1,7 @@ fileFormatVersion: 2 -guid: f3c322c946d55d34eaadaf3a50312c5c -TextScriptImporter: +guid: 336f7d2de35c9f74191d05fa2158e27b +folderAsset: yes +DefaultImporter: externalObjects: {} userData: assetBundleName: diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectWebSocket.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectWebSocket.cs new file mode 100644 index 000000000..c29c83657 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectWebSocket.cs @@ -0,0 +1,357 @@ +#if !UNITY_2022_1_OR_NEWER +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using NativeWebSocket; +using Newtonsoft.Json; +using UnityEngine; +using WalletConnectSharp.Common; +using WalletConnectSharp.Common.Logging; +using WalletConnectSharp.Common.Utils; +using WalletConnectSharp.Events; +using WalletConnectSharp.Events.Model; +using WalletConnectSharp.Network; +using WalletConnectSharp.Network.Models; +using WalletConnectSharp.Network.Websocket; + +namespace ChainSafe.Gaming.WalletConnect +{ + /// + /// Custom web socket implementation for Wallet Connect, based on NativeWebSocket https://github.com/endel/NativeWebSocket. + /// + public class WalletConnectWebSocket : MonoBehaviour, IJsonRpcConnection, IModule + { + private const string AddressNotFoundError = "getaddrinfo ENOTFOUND"; + private const string ConnectionRefusedError = "connect ECONNREFUSED"; + + private WebSocket webSocket; + + private EventDelegator delegator; + + private string url; + + private bool registering; + + private Guid contextId; + + private TaskCompletionSource beginConnection; + + private bool connectionStarted; + + private TimeSpan connectionTimeout = TimeSpan.FromSeconds(60); + + /// + /// The Url to connect to + /// + public string Url + { + get => url; + set => url = value; + } + + public bool IsPaused { get; private set; } + + /// + /// The name of this websocket connection module + /// + public string Name => "websocket-connection"; + + /// + /// The context string of this Websocket module + /// + public string Context => contextId.ToString(); + + /// + /// The EventDelegator this Websocket connection module is using + /// + public EventDelegator Events => delegator; + + /// + /// Whether this websocket connection is connected + /// + public bool Connected + { + get + { + bool connected = webSocket != null && webSocket.State == WebSocketState.Open; + WCLogger.Log("Websocket Connected State: " + connected); + return connected; + } + } + + /// + /// Whether this websocket connection is currently connecting + /// + public bool Connecting => registering; + + public async void Dispose() + { + if (Connected) + { + WCLogger.Log("Socket is being disposed, cleanup"); + await Close(); + } + } + + /// + /// Open Socket. + /// + public async Task Open() + { + await Register(this.url); + } + + /// + /// Open Socket. + /// + /// Socket options. + /// Options type. + /// + public Task Open(T options) + { + if (typeof(string).IsAssignableFrom(typeof(T))) + { + return Register(options as string); + } + + return Open(); + } + + /// + /// Close socket. + /// + /// Throws if socket is already closed. + public async Task Close() + { + if (webSocket == null) throw new IOException("Connection already closed"); + + await webSocket.Close(); + + WCLogger.Log("Closing websocket due to Close() being called"); + OnClose(WebSocketCloseCode.Normal); + } + + /// + /// Send request using socket. + /// + /// Payload to be sent. + /// Socket context. + /// Payload Type. + public async Task SendRequest(IJsonRpcRequest requestPayload, object context) + { + if (webSocket == null) webSocket = await Register(this.url); + + try + { + Debug.Log($"[WCWebSocket-{contextId}] Sending request {JsonConvert.SerializeObject(requestPayload)}"); + + await webSocket.SendText(JsonConvert.SerializeObject(requestPayload)); + } + catch (Exception e) + { + Debug.LogError(e); + + OnError(requestPayload, e); + } + } + + /// + /// Send result using socket. + /// + /// Payload to be sent. + /// Socket context. + /// Payload Type. + public async Task SendResult(IJsonRpcResult responsePayload, object context) + { + if (webSocket == null) webSocket = await Register(this.url); + + try + { + await webSocket.SendText(JsonConvert.SerializeObject(responsePayload)); + } + catch (Exception e) + { + OnError(responsePayload, e); + } + } + + /// + /// Send error using socket. + /// + /// Payload to be sent. + /// Socket context. + public async Task SendError(IJsonRpcError errorPayload, object context) + { + if (webSocket == null) webSocket = await Register(this.url); + + try + { + await webSocket.SendText(JsonConvert.SerializeObject(errorPayload)); + } + catch (Exception e) + { + OnError(errorPayload, e); + } + } + + private void Awake() + { + contextId = Guid.NewGuid(); + delegator = new EventDelegator(this); + } + + private async Task Register(string newUrl) + { + if (!Validation.IsWsUrl(newUrl)) + { + throw new ArgumentException("Provided URL is not compatible with WebSocket connection: " + newUrl); + } + + if (registering) + { + TaskCompletionSource registeringTask = + new TaskCompletionSource(TaskCreationOptions.None); + + Events.ListenForOnce(WebsocketConnectionEvents.RegisterError, + delegate(object sender, GenericEvent @event) + { + registeringTask.SetException(@event.EventData); + }); + + Events.ListenForOnce(WebsocketConnectionEvents.Open, + delegate(object sender, GenericEvent @event) + { + registeringTask.SetResult(@event.EventData); + }); + + await registeringTask.Task; + + return registeringTask.Task.Result; + } + + this.url = newUrl; + this.registering = true; + + try + { + webSocket = new WebSocket(this.url); + //_socket = new WebsocketClient(new Uri(_url)); + + await StartWebsocket(webSocket).WithTimeout(connectionTimeout, "Unavailable WS RPC url at " + this.url); + OnOpen(webSocket); + return webSocket; + } + catch (Exception e) + { + Events.Trigger(WebsocketConnectionEvents.RegisterError, e); + + WCLogger.Log($"Calling close due to exception {e}"); + OnClose(WebSocketCloseCode.ServerError); + + throw; + } + } + + private Task StartWebsocket(WebSocket socket) + { + this.webSocket = socket; + connectionStarted = false; + beginConnection = new TaskCompletionSource(); + + return beginConnection.Task; + } + + private void OnOpen(WebSocket socket) + { + socket.OnMessage += OnPayload; + socket.OnClose += OnDisconnect; + + this.registering = false; + Events.Trigger(WebsocketConnectionEvents.Open, this.webSocket); + } + + private void OnDisconnect(WebSocketCloseCode code) + { + if (code != WebSocketCloseCode.Normal) Events.Trigger(WebsocketConnectionEvents.Error, code); + + WCLogger.Log("Socket closing due to Disconnect event from socket"); + OnClose(code); + } + + private void OnClose(WebSocketCloseCode code) + { + if (this.webSocket == null) return; + + //_socket.Dispose(); + this.webSocket = null; + this.registering = false; + Events.Trigger(WebsocketConnectionEvents.Close, code); + } + + private void OnPayload(byte[] data) + { + string json = Encoding.UTF8.GetString(data); + + Debug.Log($"[WCWebSocket-{contextId}] Got payload {json}"); + + if (string.IsNullOrWhiteSpace(json)) return; + + Debug.Log($"[WCWebsocket-{contextId}] Triggering payload event with JSON {json}"); + + Events.Trigger(WebsocketConnectionEvents.Payload, json); + } + + void Update() + { +#if !UNITY_WEBGL || UNITY_EDITOR + if (webSocket != null) webSocket.DispatchMessageQueue(); +#endif + } + + private async void LateUpdate() + { + if (connectionStarted || beginConnection == null || beginConnection.Task.IsCompleted) return; + if (webSocket == null) return; + + connectionStarted = true; + this.webSocket.OnOpen += SocketOnOnOpen; + try + { + await this.webSocket.Connect(); + } + catch (Exception e) + { + this.beginConnection.TrySetException(e); + } + } + + private void SocketOnOnOpen() + { + beginConnection.TrySetResult(true); + } + + private void OnError(IJsonRpcPayload payload, Exception e) + { + var exception = e.Message.Contains(AddressNotFoundError) || e.Message.Contains(ConnectionRefusedError) + ? new IOException("Unavailable WS RPC url at " + this.url) + : e; + + string message = exception.Message; + + var response = new JsonRpcResponse(payload.Id, + new Error() { Code = exception.HResult, Data = null, Message = message }, default(T)); + + //Trigger the payload event, converting the new JsonRpcResponse object to JSON string + Events.Trigger(WebsocketConnectionEvents.Payload, JsonConvert.SerializeObject(response)); + + Debug.LogError(e); + } + + private void OnApplicationPause(bool isPaused) + { + IsPaused = isPaused; + } + } +} +#endif \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectWebSocket.cs.meta b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectWebSocket.cs.meta new file mode 100644 index 000000000..bbbb0ff6d --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectWebSocket.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9d74bd135ec0b7948af61a6966dfcb40 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectWebSocketBuilder.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectWebSocketBuilder.cs new file mode 100644 index 000000000..e0b1f1f41 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectWebSocketBuilder.cs @@ -0,0 +1,42 @@ +#if !UNITY_2022_1_OR_NEWER +using System.Threading.Tasks; +using ChainSafe.Gaming.Evm.Unity; +using UnityEngine; +using WalletConnectSharp.Network; +using WalletConnectSharp.Network.Interfaces; + +namespace ChainSafe.Gaming.WalletConnect +{ + /// + /// This is a custom connection builder for Wallet Connect. + /// We need this because of Unity's IL2CPP build code stripping and reachability issue. + /// For version 2022 and above this issue has been fixed by Unity as stated here https://blog.unity.com/engine-platform/il2cpp-full-generic-sharing-in-unity-2022-1-beta so this custom implementation isn't needed. + /// + public class WalletConnectWebSocketBuilder : MonoBehaviour, IConnectionBuilder + { + /// + /// Create WebSocket connection for Wallet Connect. + /// + /// + /// Created connection. + public Task CreateConnection(string url) + { + TaskCompletionSource taskCompletionSource = + new TaskCompletionSource(); + + Dispatcher.Instance().Enqueue(() => + { + Debug.Log("Building websocket with URL " + url); + var websocket = gameObject.AddComponent(); + websocket.Url = url; + + taskCompletionSource.TrySetResult(websocket); + }); + + + return taskCompletionSource.Task; + } + } +} + +#endif \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectWebSocketBuilder.cs.meta b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectWebSocketBuilder.cs.meta new file mode 100644 index 000000000..f250cf2cb --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectWebSocketBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6dc71ee83d1afff469d88de475a57994 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/io.chainsafe.web3-unity/Samples~/Web3.Unity/Scripts/Scenes/ExistingWalletLogin.cs b/Packages/io.chainsafe.web3-unity/Samples~/Web3.Unity/Scripts/Scenes/ExistingWalletLogin.cs index 0c9ed5b43..08dcf0b0c 100644 --- a/Packages/io.chainsafe.web3-unity/Samples~/Web3.Unity/Scripts/Scenes/ExistingWalletLogin.cs +++ b/Packages/io.chainsafe.web3-unity/Samples~/Web3.Unity/Scripts/Scenes/ExistingWalletLogin.cs @@ -14,11 +14,15 @@ using UnityEngine; using UnityEngine.Assertions; using UnityEngine.Networking; -using UnityEngine.Serialization; +using UnityEngine.Scripting; using UnityEngine.UI; using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Controllers; +using WalletConnectSharp.Events; +using WalletConnectSharp.Events.Model; using WalletConnectSharp.Sign.Models; using WalletConnectSharp.Sign.Models.Engine; +using WalletConnectSharp.Sign.Models.Engine.Methods; /// /// Login using an existing wallet using Wallet Connect. @@ -33,6 +37,11 @@ public class ExistingWalletLogin : Login [SerializeField] private Toggle rememberMeToggle; +#if !UNITY_2022_1_OR_NEWER + // Use a custom connection builder due to an issue fixed in version 2022 and above https://blog.unity.com/engine-platform/il2cpp-full-generic-sharing-in-unity-2022-1-beta. + private WalletConnectWebSocketBuilder connectionBuilder; +#endif + [Header("Wallet Connect")] [SerializeField] private string projectId; @@ -71,6 +80,23 @@ protected override IEnumerator Initialize() Assert.IsNotNull(loginButton); Assert.IsNotNull(rememberMeToggle); +#if !UNITY_2022_1_OR_NEWER + + connectionBuilder = FindObjectOfType(); + + // Initialize custom web socket if it's not already. + if (connectionBuilder == null) + { + GameObject webSocketBuilderObj = + new GameObject(nameof(WalletConnectWebSocketBuilder), typeof(WalletConnectWebSocketBuilder)); + + connectionBuilder = webSocketBuilderObj.GetComponent(); + + // keep web socket during scene unload + DontDestroyOnLoad(webSocketBuilderObj); + } +#endif + #if UNITY_ANDROID if (!Application.isEditor) @@ -180,6 +206,10 @@ private void BuildWalletConnectConfig() ProjectId = projectId, ProjectName = projectName, BaseContext = baseContext, +#if !UNITY_2022_1_OR_NEWER + // Assign custom connection builder/web socket. + ConnectionBuilder = connectionBuilder, +#endif Chain = chain, Metadata = metadata, // try and get saved value @@ -266,4 +296,22 @@ private void SessionApproved(SessionStruct session) Debug.Log($"{session.Topic} Approved"); } + +#if !UNITY_MONO + [Preserve] + void SetupAOT() + { + // Reference all required models + // This is required so AOT code is generated for these generic functions + var historyFactory = new JsonRpcHistoryFactory(null); + Debug.Log(historyFactory.JsonRpcHistoryOfType().GetType().FullName); + Debug.Log(historyFactory.JsonRpcHistoryOfType().GetType().FullName); + Debug.Log(historyFactory.JsonRpcHistoryOfType().GetType().FullName); + Debug.Log(historyFactory.JsonRpcHistoryOfType().GetType().FullName); + Debug.Log(historyFactory.JsonRpcHistoryOfType().GetType().FullName); + Debug.Log(historyFactory.JsonRpcHistoryOfType().GetType().FullName); + EventManager>.InstanceOf(null).PropagateEvent(null, null); + throw new InvalidOperationException("This method is only for AOT code generation."); + } +#endif } \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Tests/Runtime/EvmTests.cs b/Packages/io.chainsafe.web3-unity/Tests/Runtime/EvmTests.cs index 55b954dc8..8764b25f0 100644 --- a/Packages/io.chainsafe.web3-unity/Tests/Runtime/EvmTests.cs +++ b/Packages/io.chainsafe.web3-unity/Tests/Runtime/EvmTests.cs @@ -333,7 +333,7 @@ public IEnumerator TestTransferErc721() [UnityTest] public IEnumerator TestTransferErc1155() { - config.TestResponse = "0x5de8fb6c522d4ba85f09961f03bbdc1ee8d9c283ddd1dea0f3ccdfa27cc189af"; + config.TestResponse = "0x390b47d378e9a6de830e2cc6d624de0920efc44d7b40fb61f75d983545c987fc"; var transferErc1155 = Erc1155.TransferErc1155(web3, Contracts.Erc1155, Transfer1155Id, Transfer1155Amount, SendToAddress); yield return new WaitUntil(() => transferErc1155.IsCompleted); if (transferErc1155.Exception != null) throw transferErc1155.Exception; diff --git a/src/ChainSafe.Gaming.WalletConnect/WalletConnectConfig.cs b/src/ChainSafe.Gaming.WalletConnect/WalletConnectConfig.cs index 1993aceb3..f36b1e067 100644 --- a/src/ChainSafe.Gaming.WalletConnect/WalletConnectConfig.cs +++ b/src/ChainSafe.Gaming.WalletConnect/WalletConnectConfig.cs @@ -3,6 +3,7 @@ using ChainSafe.Gaming.WalletConnect.Models; using Newtonsoft.Json; using WalletConnectSharp.Core; +using WalletConnectSharp.Network.Interfaces; using WalletConnectSharp.Sign.Models; using WalletConnectSharp.Sign.Models.Engine; @@ -65,6 +66,12 @@ public class WalletConnectConfig /// public string BaseContext { get; set; } + /// + /// Custom Connection Builder. + /// This can be useful if the default web socket implementation is unreachable. + /// + public IConnectionBuilder ConnectionBuilder { get; set; } + /// /// Chain of wallet to connect to, eg - Goerli. /// Used for specifying required namespaces for connecting to wallet. diff --git a/src/ChainSafe.Gaming.WalletConnect/WalletConnectCustomProvider.cs b/src/ChainSafe.Gaming.WalletConnect/WalletConnectCustomProvider.cs index cb7c72d1e..37071674e 100644 --- a/src/ChainSafe.Gaming.WalletConnect/WalletConnectCustomProvider.cs +++ b/src/ChainSafe.Gaming.WalletConnect/WalletConnectCustomProvider.cs @@ -98,6 +98,7 @@ private async Task Initialize() ProjectId = config.ProjectId, Storage = BuildStorage(), BaseContext = config.BaseContext, + ConnectionBuilder = config.ConnectionBuilder, }); await Core.Start(); diff --git a/src/UnitySampleProject/.gitignore b/src/UnitySampleProject/.gitignore index 727c26cbe..fde00b818 100644 --- a/src/UnitySampleProject/.gitignore +++ b/src/UnitySampleProject/.gitignore @@ -80,6 +80,13 @@ crashlytics-build.properties /Assets/WebGLTemplates /Assets/WebGLTemplates.meta +# Ignore generated data files +/Assets/PayerData.json +/Assets/PayerData.json.meta + +/Assets/walletconnect.json +/Assets/walletconnect.json.meta + # Ignore the project config since it can always be rebuilt /Assets/Resources.meta /Assets/Resources/ProjectConfigData.asset diff --git a/src/UnitySampleProject/Assets/PlayerData.json b/src/UnitySampleProject/Assets/PlayerData.json deleted file mode 100644 index 3aa8c0f3f..000000000 --- a/src/UnitySampleProject/Assets/PlayerData.json +++ /dev/null @@ -1 +0,0 @@ -{"WalletConnectConfig":null} diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.5.0/Web3.Unity Samples/Scripts/Scenes/ExistingWalletLogin.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.5.0/Web3.Unity Samples/Scripts/Scenes/ExistingWalletLogin.cs index 0c9ed5b43..08dcf0b0c 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.5.0/Web3.Unity Samples/Scripts/Scenes/ExistingWalletLogin.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.5.0/Web3.Unity Samples/Scripts/Scenes/ExistingWalletLogin.cs @@ -14,11 +14,15 @@ using UnityEngine; using UnityEngine.Assertions; using UnityEngine.Networking; -using UnityEngine.Serialization; +using UnityEngine.Scripting; using UnityEngine.UI; using WalletConnectSharp.Core; +using WalletConnectSharp.Core.Controllers; +using WalletConnectSharp.Events; +using WalletConnectSharp.Events.Model; using WalletConnectSharp.Sign.Models; using WalletConnectSharp.Sign.Models.Engine; +using WalletConnectSharp.Sign.Models.Engine.Methods; /// /// Login using an existing wallet using Wallet Connect. @@ -33,6 +37,11 @@ public class ExistingWalletLogin : Login [SerializeField] private Toggle rememberMeToggle; +#if !UNITY_2022_1_OR_NEWER + // Use a custom connection builder due to an issue fixed in version 2022 and above https://blog.unity.com/engine-platform/il2cpp-full-generic-sharing-in-unity-2022-1-beta. + private WalletConnectWebSocketBuilder connectionBuilder; +#endif + [Header("Wallet Connect")] [SerializeField] private string projectId; @@ -71,6 +80,23 @@ protected override IEnumerator Initialize() Assert.IsNotNull(loginButton); Assert.IsNotNull(rememberMeToggle); +#if !UNITY_2022_1_OR_NEWER + + connectionBuilder = FindObjectOfType(); + + // Initialize custom web socket if it's not already. + if (connectionBuilder == null) + { + GameObject webSocketBuilderObj = + new GameObject(nameof(WalletConnectWebSocketBuilder), typeof(WalletConnectWebSocketBuilder)); + + connectionBuilder = webSocketBuilderObj.GetComponent(); + + // keep web socket during scene unload + DontDestroyOnLoad(webSocketBuilderObj); + } +#endif + #if UNITY_ANDROID if (!Application.isEditor) @@ -180,6 +206,10 @@ private void BuildWalletConnectConfig() ProjectId = projectId, ProjectName = projectName, BaseContext = baseContext, +#if !UNITY_2022_1_OR_NEWER + // Assign custom connection builder/web socket. + ConnectionBuilder = connectionBuilder, +#endif Chain = chain, Metadata = metadata, // try and get saved value @@ -266,4 +296,22 @@ private void SessionApproved(SessionStruct session) Debug.Log($"{session.Topic} Approved"); } + +#if !UNITY_MONO + [Preserve] + void SetupAOT() + { + // Reference all required models + // This is required so AOT code is generated for these generic functions + var historyFactory = new JsonRpcHistoryFactory(null); + Debug.Log(historyFactory.JsonRpcHistoryOfType().GetType().FullName); + Debug.Log(historyFactory.JsonRpcHistoryOfType().GetType().FullName); + Debug.Log(historyFactory.JsonRpcHistoryOfType().GetType().FullName); + Debug.Log(historyFactory.JsonRpcHistoryOfType().GetType().FullName); + Debug.Log(historyFactory.JsonRpcHistoryOfType().GetType().FullName); + Debug.Log(historyFactory.JsonRpcHistoryOfType().GetType().FullName); + EventManager>.InstanceOf(null).PropagateEvent(null, null); + throw new InvalidOperationException("This method is only for AOT code generation."); + } +#endif } \ No newline at end of file diff --git a/src/UnitySampleProject/Assets/link.xml b/src/UnitySampleProject/Assets/link.xml index 6bdac4ded..269e4a760 100644 --- a/src/UnitySampleProject/Assets/link.xml +++ b/src/UnitySampleProject/Assets/link.xml @@ -1,4 +1,13 @@ + + + + + + + + + \ No newline at end of file