using System; using System.Collections.Generic; using System.Threading; using Cysharp.Threading.Tasks; using MoralisUnity.Platform.Objects; using MoralisUnity.Sdk.Exceptions; using UnityEngine; using UnityEngine.Events; using UnityEngine.EventSystems; using WalletConnectSharp.Core.Models; using WalletConnectSharp.Unity; using WalletConnectSharp.Unity.Models; #pragma warning disable CS1998 namespace MoralisUnity.Kits.AuthenticationKit { /// /// Moralis "kits" each provide drag-and-drop functionality for developers. /// Developers add a kit at edit-time to give additional runtime functionality for users. /// /// The "AuthenticationKit" provides cross-platform /// support for web3 authentication (See ). /// /// Usage: /// Drag-and-drop the into any Unity Scene. /// /// This class is the wrapper for the . /// /// public class AuthenticationKit : MonoBehaviour { // Properties ------------------------------------ public bool WillInitializeOnStart { get { return _willInitializeOnStart; } } [Header("Settings")] [SerializeField] private bool _willInitializeOnStart = true; [SerializeField] private bool _signAndLoginToMoralis = true; // Events ---------------------------------------- /// /// Invoked when State==AuthenticationKitState.MoralisLoggedIn /// [Header("Events")] public UnityEvent OnConnected = new UnityEvent(); /// /// Invoked when State==AuthenticationKitState.Disconnected /// public UnityEvent OnDisconnected = new UnityEvent(); /// /// Invoked upon any change to the /// public AuthenticationKitStateUnityEvent OnStateChanged = new AuthenticationKitStateUnityEvent(); /// /// Get the current /// public AuthenticationKitState State { get { return _stateObservable.Value; } set { _stateObservable.Value = value; } } private AuthenticationKitStateObservable _stateObservable = new AuthenticationKitStateObservable(); // Fields ---------------------------------------- [Header("3rd Party")] [SerializeField] private WalletConnect _walletConnect; // Properties ------------------------------------ /// /// Get the current /// public AuthenticationKitPlatform AuthenticationKitPlatform { get { #if UNITY_ANDROID return AuthenticationKitPlatform.Android; #elif UNITY_IOS return AuthenticationKitPlatform.iOS; #elif UNITY_WEBGL return AuthenticationKitPlatform.WebGL; #else return AuthenticationKitPlatform.WalletConnect; #endif } } // Unity Methods --------------------------------- public AuthenticationKit() { // Any state changes here are likely too 'early' // to be observed externally. That is ok. Just FYI. State = AuthenticationKitState.PreInitialized; _stateObservable.OnValueChanged.AddListener(StateObservable_OnValueChanged); } protected async void Start() { // Add the EventSystem if there is none if (FindObjectOfType() == null) { var eventSystem = new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule)); } if (_willInitializeOnStart) { await InitializeAsync(); } } /// /// Initialize the . /// /// public async UniTask InitializeAsync() { State = AuthenticationKitState.Initializing; // If MoralisClient is disabled we can skip Start if (MoralisSettings.MoralisData.DisableMoralisClient) { State = AuthenticationKitState.Initialized; return; } // Initialize Moralis Moralis.Start(); // Log out any old users so we do a full authentication cycle await Moralis.LogOutAsync(); State = AuthenticationKitState.Initialized; } // Methods --------------------------------------- /// /// Connect to Web3. /// public void Connect() { State = AuthenticationKitState.WalletConnecting; } /// /// User presses the retry button /// public void Retry() { // Based on which state we are one the Retry button does different things switch (State) { // If the Wallet is trying to connect case AuthenticationKitState.WalletConnecting: switch (AuthenticationKitPlatform) { case AuthenticationKitPlatform.WalletConnect: // With the QR code there is no need for a retry button // TODO Add a manual refresh button if the QR is not working break; case AuthenticationKitPlatform.Android: // Retry to open the DeepLink _walletConnect.OpenDeepLink(); break; case AuthenticationKitPlatform.iOS: // Let users go back to the wallet select screen State = AuthenticationKitState.WalletConnecting; break; case AuthenticationKitPlatform.WebGL: // TODO Add a retry on a fail instead of disconnect and start over break; default: SwitchDefaultException.Throw(AuthenticationKitPlatform); break; } break; case AuthenticationKitState.WalletSigning: switch (AuthenticationKitPlatform) { case AuthenticationKitPlatform.WalletConnect: // TODO Add a retry option if the wallet fails. Chance of failure is small. break; case AuthenticationKitPlatform.Android: case AuthenticationKitPlatform.iOS: _walletConnect.OpenMobileWallet(); break; case AuthenticationKitPlatform.WebGL: // TODO Add a retry on a fail instead of disconnect and start over break; default: SwitchDefaultException.Throw(AuthenticationKitPlatform); break; } break; } } /// /// LogIn to Web3 session. /// public async UniTask LoginWithWeb3() { #if !UNITY_WEBGL new PlatformNotSupportedException(); #else string userAddr = ""; // Try to sign and catch the Exception when a user cancels the request try { if (!Web3GL.IsConnected()) { await Moralis.SetupWeb3(); userAddr = Web3GL.Account(); } else { userAddr = Web3GL.Account(); } } catch { // Disconnect and start over if a user cancels the connecting request or there is an error Disconnect(); return; } State = AuthenticationKitState.WalletConnected; if (_signAndLoginToMoralis && !MoralisSettings.MoralisData.DisableMoralisClient) { State = AuthenticationKitState.WalletSigning; if (string.IsNullOrWhiteSpace(userAddr)) { Debug.LogError("Could not login or fetch account from web3."); } else { string address = Web3GL.Account().ToLower(); string appId = Moralis.DappId; long serverTime = 0; // Retrieve server time from Moralis Server for message signature Dictionary serverTimeResponse = await Moralis.Cloud.RunAsync>("getServerTime", new Dictionary()); if (serverTimeResponse == null || !serverTimeResponse.ContainsKey("dateTime") || !long.TryParse(serverTimeResponse["dateTime"].ToString(), out serverTime)) { Debug.LogError("Failed to retrieve server time from Moralis Server!"); } string signMessage = $"Moralis Authentication\n\nId: {appId}:{serverTime}"; string signature = null; // Try to sign and catch the Exception when a user cancels the request try { signature = await Web3GL.Sign(signMessage); } catch (Exception e) { Debug.Log(e); // Disconnect and start over if a user cancels the singing request or there is an error Disconnect(); return; } State = AuthenticationKitState.WalletSigned; State = AuthenticationKitState.MoralisLoggingIn; // Create Moralis auth data from message signing response. Dictionary authData = new Dictionary { { "id", address }, { "signature", signature }, { "data", signMessage } }; // Get chain Id int chainId = Web3GL.ChainId(); // Attempt to login user. MoralisUser user = await Moralis.LogInAsync(authData, chainId); if (user != null) { State = AuthenticationKitState.MoralisLoggedIn; } } } #endif } /// /// Handles WalletConnect connecting and setting up Web3 /// /// private async void WalletConnect_Connect() { // CLear out the session so it is re-establish on sign-in. _walletConnect.CLearSession(); // Enable auto save to remember the session for future use _walletConnect.autoSaveAndResume = true; // Don't start a new session on disconnect automatically _walletConnect.createNewSessionOnSessionDisconnect = false; // Warning the _walletConnect.Connect() won't finish until a user approved Wallet connection has been established await _walletConnect.Connect(); // If WalletConnect is connected set the state to WalletConnected or Disconnect and start over if (_walletConnect.Connected) { State = AuthenticationKitState.WalletConnected; } else { Disconnect(); } } /// /// Handles WalletConnect signing when has a session connected. /// /// /// private async void WalletConnect_SignAndLoginToMoralis(WalletConnectUnitySession session) { // Debug.Log($"WalletConnect_OnConnectedEventSession() wcSessionData = {session}"); // If there is already a Moralis user we can skip the sign and login and go straight to connected if (await Moralis.GetUserAsync() != null) { State = AuthenticationKitState.MoralisLoggedIn; return; } State = AuthenticationKitState.WalletSigning; // Extract wallet address from the Wallet Connect Session data object. string address = session.Accounts[0].ToLower(); string appId = Moralis.DappId; long serverTime = 0; // Retrieve server time from Moralis Server for message signature Dictionary serverTimeResponse = await Moralis.Cloud .RunAsync>("getServerTime", new Dictionary()); if (serverTimeResponse == null || !serverTimeResponse.ContainsKey("dateTime") || !long.TryParse(serverTimeResponse["dateTime"].ToString(), out serverTime)) { Debug.LogError("Failed to retrieve server time from Moralis Server!"); } string signMessage = $"Moralis Authentication\n\nId: {appId}:{serverTime}"; string signature = null; // Try to sign and catch the Exception when a user cancels the request try { signature = await _walletConnect.Session.EthPersonalSign(address, signMessage); } catch { // Disconnect and start over if a user cancels the singing request or there is an error Disconnect(); return; } State = AuthenticationKitState.WalletSigned; State = AuthenticationKitState.MoralisLoggingIn; // Create Moralis auth data from message signing response. Dictionary authData = new Dictionary { { "id", address }, { "signature", signature }, { "data", signMessage } }; // Attempt to login user. MoralisUser user = await Moralis.LogInAsync(authData, session.ChainId); if (user != null) { State = AuthenticationKitState.MoralisLoggedIn; } } // If the user cancels the connect Disconnect and start over public async void WalletConnect_OnDisconnectedEvent(WalletConnectUnitySession session) { // Debug.Log("WalletConnect_OnDisconnectedEvent"); // Only run if we are not already disconnecting if (!AuthenticationKitState.Disconnecting.Equals(State)) { Disconnect(); } } // If something goes wrong Disconnect and start over public async void WalletConnect_OnConnectionFailedEvent(WalletConnectUnitySession session) { // Debug.Log("WalletConnect_OnConnectionFailedEvent"); // Only run if we are not already disconnecting if (!AuthenticationKitState.Disconnecting.Equals(State)) { Disconnect(); } } /// /// Disconnect Moralis and WalletConnect. /// public async void Disconnect() { State = AuthenticationKitState.Disconnecting; try { // Logout the Moralis User. await Moralis.LogOutAsync(); } catch (Exception e) { // Send error to the log but not as an error as this is expected behavior from W.C. Debug.LogError($"Disconnect() failed. Error: {e.Message}"); } #if !UNITY_WEBGL try { // Close the WalletConnect Transport Session await _walletConnect.Session.Transport.Close(); // Disconnect the WalletConnect session await _walletConnect.Session.Disconnect(); } catch (Exception e) { //Reason for Aborted warning is unknown, but expected. if (e.Message != "Aborted") { // Send error to the log but not as an error as this is expected behavior from W.C. Debug.LogWarning($"[WalletConnect] Error = {e.Message}"); } } #endif State = AuthenticationKitState.Disconnected; } // Event Handlers -------------------------------- private async void StateObservable_OnValueChanged(AuthenticationKitState value) { // Debug.Log("StateObservable_OnValueChanged " + value); // Order matters here. // 1. Broadcast OnStateChanged.Invoke(_stateObservable.Value); // 2. Step the state. Rarely. switch (_stateObservable.Value) { case AuthenticationKitState.WalletConnecting: switch (AuthenticationKitPlatform) { case AuthenticationKitPlatform.Android: var cancellationTokenSourceAndroid = new CancellationTokenSource(); cancellationTokenSourceAndroid.CancelAfterSlim(TimeSpan.FromSeconds(15)); try { // Connect to the WalletConnect server WalletConnect_Connect(); // Check if WalletConnect is ready in 15 seconds or else disconnect and start over await UniTask.WaitUntil(() => _walletConnect.Session.ReadyForUserPrompt || _walletConnect.Connected, PlayerLoopTiming.Update, cancellationTokenSourceAndroid.Token); if (_walletConnect.Session.ReadyForUserPrompt) { // Only works if a users has a app installed that handles "wc:" links _walletConnect.OpenDeepLink(); } // TODO check if the app is paused with OnApplicationPause to see if the link working } catch (OperationCanceledException ex) { if (ex.CancellationToken == cancellationTokenSourceAndroid.Token) { // WalletConnect connection timeout so let's start over Disconnect(); } } break; case AuthenticationKitPlatform.iOS: var cancellationTokenSourceIOS = new CancellationTokenSource(); cancellationTokenSourceIOS.CancelAfterSlim(TimeSpan.FromSeconds(15)); try { // Connect to the WalletConnect server WalletConnect_Connect(); // Check if WalletConnect is ready in 15 seconds or else disconnect and start over await UniTask.WaitUntil(() => _walletConnect.Session.ReadyForUserPrompt || _walletConnect.Connected, PlayerLoopTiming.Update, cancellationTokenSourceIOS.Token); // TODO check if the app is paused with OnApplicationPause to see if the link working } catch (OperationCanceledException ex) { if (ex.CancellationToken == cancellationTokenSourceIOS.Token) { // WalletConnect connection timeout so let's start over Disconnect(); } } break; case AuthenticationKitPlatform.WalletConnect: // Connect to the WalletConnect server WalletConnect_Connect(); break; case AuthenticationKitPlatform.WebGL: if (!Application.isEditor) { await LoginWithWeb3(); } break; default: SwitchDefaultException.Throw(AuthenticationKitPlatform); break; } break; case AuthenticationKitState.WalletConnected: switch (AuthenticationKitPlatform) { case AuthenticationKitPlatform.Android: case AuthenticationKitPlatform.iOS: case AuthenticationKitPlatform.WalletConnect: // If the Wallet connection has been accepted first Setup Web3 await Moralis.SetupWeb3(); // If there is a Wallet connected and we got a session // try to Sign and Login to Moralis or else Disconnect and start over if (_walletConnect.Session != null) { if (_signAndLoginToMoralis && !MoralisSettings.MoralisData.DisableMoralisClient) { WalletConnect_SignAndLoginToMoralis(_walletConnect.Session); } } else { Disconnect(); } break; case AuthenticationKitPlatform.WebGL: // TODO Break up the LoginWithWeb3 method to a separate the connecting signing and logging break; default: SwitchDefaultException.Throw(AuthenticationKitPlatform); break; } break; case AuthenticationKitState.MoralisLoggedIn: // Invoke OnConnected event OnConnected.Invoke(); break; case AuthenticationKitState.Disconnected: // Reset the UI so its like BEFORE we ever authenticated await InitializeAsync(); // Invoke OnDisconnected event OnDisconnected.Invoke(); break; default: // Switch default is ok here since not all known conditions are declared above break; } } } }