From 3d3e1137b47e25147546e044c08b47ed2a2f4385 Mon Sep 17 00:00:00 2001 From: creeppak Date: Wed, 4 Sep 2024 16:15:56 +0100 Subject: [PATCH 01/24] Introduced IChainManager & IChainSwitchHandler --- .../Web3/Core/Chains/ChainManager.cs | 84 +++++++++++++++++++ .../Core/Chains/ChainManagerChainConfig.cs | 30 +++++++ .../Web3/Core/Chains/IChainConfigSet.cs | 8 ++ .../Web3/Core/Chains/IChainManager.cs | 14 ++++ .../Web3/Core/Chains/IChainSwitchHandler.cs | 9 ++ 5 files changed, 145 insertions(+) create mode 100644 src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs create mode 100644 src/ChainSafe.Gaming/Web3/Core/Chains/ChainManagerChainConfig.cs create mode 100644 src/ChainSafe.Gaming/Web3/Core/Chains/IChainConfigSet.cs create mode 100644 src/ChainSafe.Gaming/Web3/Core/Chains/IChainManager.cs create mode 100644 src/ChainSafe.Gaming/Web3/Core/Chains/IChainSwitchHandler.cs diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs new file mode 100644 index 000000000..3ad4f9cc3 --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ChainSafe.Gaming.Web3.Environment; + +namespace ChainSafe.Gaming.Web3.Core.Chains +{ + public class ChainManager : IChainManager + { + private readonly Dictionary configs; + private readonly IList switchHandlers; + private readonly ILogWriter logWriter; + + public ChainManager(IChainConfigSet configs, IList switchHandlers, ILogWriter logWriter) + { + this.logWriter = logWriter; + this.configs = configs.ToDictionary(config => config.ChainId, config => config); + this.switchHandlers = switchHandlers; + Current = configs.First(); + } + + public event Action ChainSwitched; + + public IChainConfig Current { get; private set; } + + public async Task SwitchChain(string newChainId) + { + if (!configs.TryGetValue(newChainId, out var newChainConfig)) + { + throw new Web3Exception($"No {nameof(IChainConfig)} was registered with id '{newChainId}'. Make sure to configure settings for the chain before switching to it."); + } + + Current = newChainConfig; + var previousChainId = Current.ChainId; + var succeededHandlers = new Stack(); + + try + { + foreach (var switchHandler in switchHandlers) + { + await switchHandler.HandleChainSwitch(Current); + succeededHandlers.Push(switchHandler); + } + } + catch (Exception switchException) + { + // revert everything + Current = configs[previousChainId]; + + while (succeededHandlers.Count != 0) + { + var handlerToRevert = succeededHandlers.Pop(); + + try + { + await handlerToRevert.HandleChainSwitch(Current); + } + catch (Exception revertException) + { + logWriter.LogError( + $"Error occured while reverting handler {handlerToRevert.GetType().Name}. " + + $"Proceeding with revert.\n{revertException}"); + } + } + + throw new Web3Exception( + $"One of the handlers thrown an exception. Reverted {nameof(ChainManager)} to the previous chain configuration.", + switchException); + } + + logWriter.Log($"Successfully switched to the chain with id '{newChainId}'."); + + try + { + ChainSwitched?.Invoke(Current); + } + catch (Exception e) + { + logWriter.LogError(e.ToString()); + } + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManagerChainConfig.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManagerChainConfig.cs new file mode 100644 index 000000000..a709743c8 --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManagerChainConfig.cs @@ -0,0 +1,30 @@ +namespace ChainSafe.Gaming.Web3.Core.Chains +{ + public class ChainManagerChainConfig : IChainConfig + { + private readonly IChainManager chainManager; + + public ChainManagerChainConfig(IChainManager chainManager) + { + this.chainManager = chainManager; + } + + private IChainConfig CurrentConfig => chainManager.Current; + + public string ChainId => CurrentConfig.ChainId; + + public string Symbol => CurrentConfig.Symbol; + + public string Chain => CurrentConfig.Chain; + + public string Network => CurrentConfig.Network; + + public string Rpc => CurrentConfig.Rpc; + + public string Ipc => CurrentConfig.Ipc; + + public string Ws => CurrentConfig.Ws; + + public string BlockExplorerUrl => CurrentConfig.BlockExplorerUrl; + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/IChainConfigSet.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainConfigSet.cs new file mode 100644 index 000000000..d74957872 --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainConfigSet.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace ChainSafe.Gaming.Web3.Core.Chains +{ + public interface IChainConfigSet : IList + { + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/IChainManager.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainManager.cs new file mode 100644 index 000000000..c25b94812 --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainManager.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading.Tasks; + +namespace ChainSafe.Gaming.Web3.Core.Chains +{ + public interface IChainManager + { + event Action ChainSwitched; + + IChainConfig Current { get; } + + Task SwitchChain(string newChainId); + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/IChainSwitchHandler.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainSwitchHandler.cs new file mode 100644 index 000000000..5e916799b --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainSwitchHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace ChainSafe.Gaming.Web3.Core.Chains +{ + public interface IChainSwitchHandler + { + public Task HandleChainSwitch(IChainConfig newChainConfig); + } +} \ No newline at end of file From 914769e4ff07a750819d6c2ecccf51e0971ea1e5 Mon Sep 17 00:00:00 2001 From: creeppak Date: Wed, 4 Sep 2024 16:31:30 +0100 Subject: [PATCH 02/24] Added an overload of Web3Builder constructor that takes IChainConfigSet as an argument --- .../Web3/Core/Build/Web3Builder.cs | 28 +++++++++++++------ .../Web3/Core/Chains/ChainConfigSet.cs | 12 ++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigSet.cs diff --git a/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs b/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs index 04b5e3b2b..d66a63e60 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs @@ -4,11 +4,10 @@ using ChainSafe.Gaming.Evm.Contracts; using ChainSafe.Gaming.Evm.Contracts.BuiltIn; using ChainSafe.Gaming.LocalStorage; -using ChainSafe.Gaming.RPC.Events; using ChainSafe.Gaming.Web3.Core; +using ChainSafe.Gaming.Web3.Core.Chains; using ChainSafe.Gaming.Web3.Core.Evm.EventPoller; using ChainSafe.Gaming.Web3.Core.Logout; -using ChainSafe.Gaming.Web3.Core.Nethereum; using ChainSafe.Gaming.Web3.Environment; using Microsoft.Extensions.DependencyInjection; @@ -27,14 +26,16 @@ private Web3Builder() // Bind default services serviceCollection - .UseEventPoller() // todo: remove in favor of EventManager which supports WebSocket connection + .UseEventPoller() // todo: remove, make a WebGL IEventManager implementation that utilizes Event Polling .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton(); } /// @@ -43,7 +44,7 @@ private Web3Builder() /// Project config to use with the resulting Web3 instance. /// Chain config to use with the resulting Web3 instance. /// One of the arguments is null. - public Web3Builder(IProjectConfig projectConfig, IChainConfig chainConfig) + public Web3Builder(IProjectConfig projectConfig, IChainConfigSet chainConfigSet) : this() { if (projectConfig == null) @@ -51,13 +52,24 @@ public Web3Builder(IProjectConfig projectConfig, IChainConfig chainConfig) throw new ArgumentNullException(nameof(projectConfig), $"{nameof(IProjectConfig)} is required for Web3 to work."); } - if (chainConfig == null) + if (chainConfigSet == null) { - throw new ArgumentNullException(nameof(chainConfig), $"{nameof(IChainConfig)} is required for Web3 to work."); + throw new ArgumentNullException(nameof(chainConfigSet), $"{nameof(IChainConfig)} is required for Web3 to work."); } serviceCollection.AddSingleton(projectConfig); - serviceCollection.AddSingleton(chainConfig); + serviceCollection.AddSingleton(chainConfigSet); + } + + /// + /// Initializes a new instance of the class. + /// + /// Project config to use with the resulting Web3 instance. + /// Chain config to use with the resulting Web3 instance. + /// One of the arguments is null. + public Web3Builder(IProjectConfig projectConfig, params IChainConfig[] chainConfigs) + : this(projectConfig, new ChainConfigSet(chainConfigs)) + { } /// diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigSet.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigSet.cs new file mode 100644 index 000000000..e5c1880c1 --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigSet.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace ChainSafe.Gaming.Web3.Core.Chains +{ + public class ChainConfigSet : List, IChainConfigSet + { + public ChainConfigSet(params IChainConfig[] chainConfigs) + : base(chainConfigs) + { + } + } +} \ No newline at end of file From 89bc2eccf849203e1ce05c22d1978f4c1b3a056f Mon Sep 17 00:00:00 2001 From: creeppak Date: Mon, 9 Sep 2024 13:29:10 +0100 Subject: [PATCH 03/24] ICompleteProjectConfig implement IChainConfigSet instead of IChainConfig Reworked ServerSettings to support Chain Config Sets --- .../Editor/ServerSettings.cs | 445 ------------------ .../Editor/ServerSettings.cs.meta | 11 - .../io.chainsafe.web3-unity/Editor/Startup.cs | 4 +- .../Editor/StringListSearchProvider.cs | 3 +- .../Editor/Textures/ChainSafeLogo2.png | Bin 0 -> 184929 bytes .../Editor/Textures/ChainSafeLogo2.png.meta | 127 +++++ .../Web3SettingsEditor.ChainSettings.cs | 223 +++++++++ .../Web3SettingsEditor.ChainSettings.cs.meta | 3 + .../Editor/Web3SettingsEditor.cs | 354 ++++++++++++++ .../Editor/Web3SettingsEditor.cs.meta | 3 + .../Editor/WebGLTemplateSync.cs | 2 +- .../Runtime/Scripts/ChainConfigEntry.cs | 48 ++ .../Runtime/Scripts/ChainConfigEntry.cs.meta | 3 + .../Runtime/Scripts/Model/ChainInfoModel.cs | 1 + .../Runtime/Scripts/ProjectConfigAsset.cs | 17 + .../Scripts/ProjectConfigAsset.cs.meta | 3 + ...ojectConfigScriptableObject_Deprecated.cs} | 4 +- ...ConfigScriptableObject_Deprecated.cs.meta} | 0 .../Runtime/Scripts/ProjectConfigUtilities.cs | 51 +- .../Web3/Core/Chains/ChainConfigSet.cs | 5 + .../Web3/Core/Chains/ChainManager.cs | 6 +- .../Core/Chains/ChainManagerChainConfig.cs | 2 - .../Web3/Core/Chains/IChainConfigSet.cs | 3 +- .../Web3/Core/IChainConfig.cs | 5 - .../Web3/Core/ICompleteProjectConfig.cs | 4 +- .../Scripts/ChainlinkLootboxSampleLauncher.cs | 25 +- 26 files changed, 845 insertions(+), 507 deletions(-) delete mode 100644 Packages/io.chainsafe.web3-unity/Editor/ServerSettings.cs delete mode 100644 Packages/io.chainsafe.web3-unity/Editor/ServerSettings.cs.meta create mode 100644 Packages/io.chainsafe.web3-unity/Editor/Textures/ChainSafeLogo2.png create mode 100644 Packages/io.chainsafe.web3-unity/Editor/Textures/ChainSafeLogo2.png.meta create mode 100644 Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs create mode 100644 Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs.meta create mode 100644 Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs create mode 100644 Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs.meta create mode 100644 Packages/io.chainsafe.web3-unity/Runtime/Scripts/ChainConfigEntry.cs create mode 100644 Packages/io.chainsafe.web3-unity/Runtime/Scripts/ChainConfigEntry.cs.meta create mode 100644 Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs create mode 100644 Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs.meta rename Packages/io.chainsafe.web3-unity/Runtime/Scripts/{ProjectConfigScriptableObject.cs => ProjectConfigScriptableObject_Deprecated.cs} (87%) rename Packages/io.chainsafe.web3-unity/Runtime/Scripts/{ProjectConfigScriptableObject.cs.meta => ProjectConfigScriptableObject_Deprecated.cs.meta} (100%) diff --git a/Packages/io.chainsafe.web3-unity/Editor/ServerSettings.cs b/Packages/io.chainsafe.web3-unity/Editor/ServerSettings.cs deleted file mode 100644 index 77f1c7956..000000000 --- a/Packages/io.chainsafe.web3-unity/Editor/ServerSettings.cs +++ /dev/null @@ -1,445 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using ChainSafe.Gaming.UnityPackage; -using Newtonsoft.Json; -using UnityEditor; -using UnityEditor.Experimental.GraphView; -using UnityEngine; -using UnityEngine.Events; -using UnityEngine.Networking; -using ChainInfo = ChainSafe.Gaming.UnityPackage.Model; - -/// -/// Allows the developer to alter chain configuration via GUI -/// -public class ChainSafeServerSettings : EditorWindow -{ - #region Fields - - // Default values - private const string ProjectIdPrompt = "Please enter your project ID"; - private const string ChainIdDefault = "11155111"; - private const string ChainDefault = "Ethereum"; - private const string NetworkDefault = "Sepolia"; - private const string SymbolDefault = "Seth"; - private const string RpcDefault = "https://rpc.sepolia.org"; - private const string BlockExplorerUrlDefault = "https://sepolia.etherscan.io"; - private const string EnableAnalyticsScriptingDefineSymbol = "ENABLE_ANALYTICS"; - - // Chain values - private string projectID; - private string chainID; - private string chain; - private string network; - private string symbol; - private string rpc; - private string ws; - private string newRpc; - private string blockExplorerUrl; - private bool enableAnalytics; - public string previousProjectId; - - private Texture2D logo; - - // Search window - private StringListSearchProvider searchProvider; - private ISearchWindowProvider _searchWindowProviderImplementation; - private int previousNetworkDropdownIndex; - private List chainList; - private int selectedChainIndex; - private int selectedRpcIndex; - private int selectedWebHookIndex; - private FetchingStatus fetchingStatus = FetchingStatus.NotFetching; - private bool _changedRpcOrWs; - - private enum FetchingStatus - { - NotFetching, - Fetching, - Fetched - } - - #endregion - - #region Methods - - /// - /// Checks if data is already entered, sets default values if not - /// - private void Awake() - { - // Get saved settings or revert to default - var projectConfig = ProjectConfigUtilities.CreateOrLoad(); - projectID = string.IsNullOrEmpty(projectConfig?.ProjectId) ? ProjectIdPrompt : projectConfig.ProjectId; - chainID = string.IsNullOrEmpty(projectConfig?.ChainId) ? ChainIdDefault : projectConfig.ChainId; - chain = string.IsNullOrEmpty(projectConfig?.Chain) ? ChainDefault : projectConfig.Chain; - network = string.IsNullOrEmpty(projectConfig?.Network) ? NetworkDefault : projectConfig.Network; - symbol = string.IsNullOrEmpty(projectConfig?.Symbol) ? SymbolDefault : projectConfig.Symbol; - rpc = string.IsNullOrEmpty(projectConfig?.Rpc) ? RpcDefault : projectConfig.Rpc; - Debug.Log("PROJECT CONFIG"); - blockExplorerUrl = string.IsNullOrEmpty(projectConfig?.BlockExplorerUrl) - ? BlockExplorerUrlDefault - : projectConfig.BlockExplorerUrl; - enableAnalytics = projectConfig.EnableAnalytics; - ws = projectConfig.Ws; - } - - /// - /// Updates the values in the server settings area when an item is selected - /// - public void UpdateServerMenuInfo(bool chainSwitched = false) - { - // Get the selected chain index - selectedChainIndex = Array.FindIndex(chainList.ToArray(), x => x.name == chain); - // Check if the selectedChainIndex is valid - if (selectedChainIndex >= 0 && selectedChainIndex < chainList.Count) - { - // Set chain values - network = chainList[selectedChainIndex].chain; - chainID = chainList[selectedChainIndex].chainId.ToString(); - symbol = chainList[selectedChainIndex].nativeCurrency.symbol; - // Ensure that the selectedRpcIndex is within bounds - selectedRpcIndex = Mathf.Clamp(selectedRpcIndex, 0, chainList[selectedChainIndex].rpc.Count - 1); - // Set the rpc - if(chainSwitched || string.IsNullOrEmpty(rpc)) - rpc = chainList[selectedChainIndex].rpc[selectedRpcIndex]; - blockExplorerUrl = chainList[selectedChainIndex].explorers[0].url; - - if (chainSwitched) - { - ws = chainList[selectedChainIndex].rpc.FirstOrDefault(x => x.StartsWith("wss")); - selectedWebHookIndex = chainList[selectedChainIndex].rpc.IndexOf(ws); - _changedRpcOrWs = true; - } - else - { - selectedWebHookIndex = chainList[selectedChainIndex].rpc.IndexOf(ws) == -1 - ? chainList[selectedChainIndex].rpc - .IndexOf(chainList[selectedChainIndex].rpc.FirstOrDefault(x => x.StartsWith("wss"))) - : chainList[selectedChainIndex].rpc.IndexOf(ws); - } - } - else - { - // Handle the case where the selected chain is not found - Debug.LogError("Selected chain not found in the chainList."); - } - } - - /// - /// Fetches the supported EVM chains list from Chainlist's github json - /// - private async void FetchSupportedChains() - { - using var webRequest = UnityWebRequest.Get("https://chainid.network/chains.json"); - await EditorUtilities.SendAndWait(webRequest); - if (webRequest.result != UnityWebRequest.Result.Success) - { - Debug.LogError("Error Getting Supported Chains: " + webRequest.error); - return; - } - - var json = webRequest.downloadHandler.text; - chainList = JsonConvert.DeserializeObject>(json); - chainList = chainList.OrderBy(x => x.name).ToList(); - fetchingStatus = FetchingStatus.Fetched; - UpdateServerMenuInfo(); - } - - // Initializes window - [MenuItem("ChainSafe SDK/Server Settings", false, 1)] - public static void ShowWindow() - { - // Show existing window instance. If one doesn't exist, make one. - GetWindow(typeof(ChainSafeServerSettings)); - } - - /// - /// Called when menu is opened, loads Chainsafe Logo - /// - private void OnEnable() - { - if (!logo) - logo = AssetDatabase.LoadAssetAtPath( - "Packages/io.chainsafe.web3-unity/Editor/Textures/ChainSafeLogo.png"); - } - - private Vector2 scrollPosition; - /// - /// Displayed content - /// - private void OnGUI() - { - // Image - EditorGUILayout.BeginVertical("box"); - GUILayout.Label(logo, GUILayout.MaxWidth(250f), GUILayout.MaxHeight(250f)); - EditorGUILayout.EndVertical(); - - EditorGUI.BeginChangeCheck(); - // Text - GUILayout.Label("Welcome To The ChainSafe SDK!", EditorStyles.boldLabel); - GUILayout.Label("Here you can enter all the information needed to get your game started on the blockchain!", - EditorStyles.label); - // Inputs - projectID = EditorGUILayout.TextField("Project ID", projectID); - // Search menu - // Null check to stop the recursive loop before the web request has completed - if (chainList == null) - { - if (fetchingStatus == FetchingStatus.Fetching) return; - fetchingStatus = FetchingStatus.Fetching; - FetchSupportedChains(); - - return; - } - scrollPosition = GUILayout.BeginScrollView(scrollPosition); - - // Set string array from chainList to pass into the menu - var chainOptions = chainList.Select(x => x.name).ToArray(); - // Display the dynamically updating Popup - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.PrefixLabel("Select Chain"); - // Show the network drop down menu - if (GUILayout.Button(chain, EditorStyles.popup)) - { - searchProvider = CreateInstance(); - searchProvider.Initialize(chainOptions, x => - { - chain = x; - UpdateServerMenuInfo(true); - }); - SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(Event.current.mousePosition)), - searchProvider); - } - - EditorGUILayout.EndHorizontal(); - network = EditorGUILayout.TextField("Network: ", network); - chainID = EditorGUILayout.TextField("Chain ID: ", chainID); - symbol = EditorGUILayout.TextField("Symbol: ", symbol); - blockExplorerUrl = EditorGUILayout.TextField("Block Explorer: ", blockExplorerUrl); - enableAnalytics = - EditorGUILayout.Toggle( - new GUIContent("Collect Data for Analytics:", - "Consent to collecting data for analytics purposes. This will help improve our product."), - enableAnalytics); - - if (enableAnalytics) - ScriptingDefineSymbols.TryAddDefineSymbol(EnableAnalyticsScriptingDefineSymbol); - else - ScriptingDefineSymbols.TryRemoveDefineSymbol(EnableAnalyticsScriptingDefineSymbol); - - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.PrefixLabel("Select RPC"); - // Remove "https://" so the user doesn't have to click through 2 levels for the rpc options - var rpcOptions = chainList[selectedChainIndex].rpc.Where(x => x.StartsWith("https")) - .Select(x => x.Replace("/", "\u2215")).ToArray(); - var selectedRpc = chainList[selectedChainIndex].rpc[selectedRpcIndex]; - // Show the rpc drop down menu - if (GUILayout.Button(selectedRpc, EditorStyles.popup)) - { - searchProvider = CreateInstance(); - searchProvider.Initialize(rpcOptions, x => - { - var str = x.Replace("\u2215", "/"); - selectedRpcIndex = chainList[selectedChainIndex].rpc.IndexOf(str); - // Add "https://" back - rpc = str; - _changedRpcOrWs = true; - }); - SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(Event.current.mousePosition)), - searchProvider); - } - - EditorGUILayout.EndHorizontal(); - // Allows for a custom rpc - rpc = EditorGUILayout.TextField("Custom RPC: ", rpc); - GUILayout.Label("If you're using a custom RPC it will override the selection above", EditorStyles.boldLabel); - - - // Remove "https://" so the user doesn't have to click through 2 levels for the rpc options - var webHookOptions = chainList[selectedChainIndex].rpc.Where(x => x.StartsWith("w")) - .Select(x => x.Replace("/", "\u2215")).ToArray(); - if (webHookOptions.Length > 0) - { - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.PrefixLabel("Select WebHook"); - selectedWebHookIndex = Mathf.Clamp(selectedWebHookIndex, 0, chainList[selectedChainIndex].rpc.Count - 1); - var webhookIndex = chainList[selectedChainIndex].rpc.IndexOf(ws); - var selectedWebHook = webhookIndex == -1 ? chainList[selectedChainIndex].rpc[selectedWebHookIndex] : ws; - if (GUILayout.Button(selectedWebHook, EditorStyles.popup)) - { - searchProvider = CreateInstance(); - searchProvider.Initialize(webHookOptions, - x => - { - var str = x.Replace("\u2215", "/"); - - selectedWebHookIndex = chainList[selectedChainIndex].rpc.IndexOf(str); - ws = str; - _changedRpcOrWs = true; - }); - SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(Event.current.mousePosition)), - searchProvider); - } - - EditorGUILayout.EndHorizontal(); - ws = EditorGUILayout.TextField("Custom Webhook: ", ws); - GUILayout.Label("If you're using a custom Webhook it will override the selection above", - EditorStyles.boldLabel); - } - else - { - ws = string.Empty; - } - - // Buttons - // Register - if (GUILayout.Button("Need To Register?")) Application.OpenURL("https://dashboard.gaming.chainsafe.io/"); - // Docs - if (GUILayout.Button("Check Out Our Docs!")) Application.OpenURL("https://docs.gaming.chainsafe.io/"); - // Save button - if (EditorGUI.EndChangeCheck() || _changedRpcOrWs) - { - _changedRpcOrWs = false; - var projectConfig = ProjectConfigUtilities.CreateOrLoad(); - projectConfig.ProjectId = projectID; - projectConfig.ChainId = chainID; - projectConfig.Chain = chain; - projectConfig.Network = network; - projectConfig.Symbol = symbol; - projectConfig.Rpc = rpc; - projectConfig.Ws = ws; - projectConfig.BlockExplorerUrl = blockExplorerUrl; - projectConfig.EnableAnalytics = enableAnalytics; - ProjectConfigUtilities.Save(projectConfig); - if (projectID != previousProjectId) - ValidateProjectID(projectID); - previousProjectId = projectConfig.ProjectId; - } - - GUILayout.EndScrollView(); - - GUILayout.Label( - "Reminder: Your ProjectID Must Be Valid To Save & Build With Our SDK. You Can Register For One On Our Website At Dashboard.Gaming.Chainsafe.io", - EditorStyles.label); - } - - /// - /// Validates the project ID via ChainSafe's backend & writes values to the network js file, static so it can be called externally - /// - /// - private static async void ValidateProjectID(string projectID) - { - try - { - if (await ValidateProjectIDAsync(projectID)) - { -#if UNITY_WEBGL - WriteNetworkFile(); -#endif - } - } - catch (Exception e) - { - Debug.LogError("Failed to validate project ID"); - Debug.LogException(e); - } - } - - /// - /// Validates the project ID via ChainSafe's backend - /// - private static async Task ValidateProjectIDAsync(string projectID) - { - var form = new WWWForm(); - form.AddField("projectId", projectID); - Debug.Log("Checking Project ID!"); - using var www = UnityWebRequest.Post("https://api.gaming.chainsafe.io/project/checkId", form); - await EditorUtilities.SendAndWait(www); - const string dbgProjectIDMessage = - "Project ID is not valid! Please go to https://dashboard.daming.chainsafe.io to get a new Project ID"; - - if (www.result != UnityWebRequest.Result.Success) - { - Debug.Log(www.error); - Debug.Log("Error Checking Project ID!"); - Debug.LogError(dbgProjectIDMessage); - return false; - } - - var response = JsonConvert.DeserializeObject(www.downloadHandler.text); - if (response.Response.ToString().Equals("True", StringComparison.InvariantCultureIgnoreCase)) - { - Debug.Log("ProjectID is valid, you may now build with The SDK!"); - return true; - } - - Debug.LogError(dbgProjectIDMessage); - return false; - } - - /// - /// Writes values to the network js file - /// - public static void WriteNetworkFile() - { - Debug.Log("Updating network.js..."); - - var projectConfig = ProjectConfigUtilities.CreateOrLoad(); - - // declares paths to write our javascript files to - var path1 = "Assets/WebGLTemplates/Web3GL-2020x/network.js"; - var path2 = "Assets/WebGLTemplates/Web3GL-MetaMask/network.js"; - - if (AssetDatabase.IsValidFolder(Path.GetDirectoryName(path1))) - { - // write data to the webgl default network file - var sb = new StringBuilder(); - sb.AppendLine("//You can see a list of compatible EVM chains at https://chainlist.org/"); - sb.AppendLine("window.networks = ["); - sb.AppendLine(" {"); - sb.AppendLine(" id: " + projectConfig.ChainId + ","); - sb.AppendLine(" label: " + '"' + projectConfig.Chain + " " + projectConfig.Network + '"' + ","); - sb.AppendLine(" token: " + '"' + projectConfig.Symbol + '"' + ","); - sb.AppendLine(" rpcUrl: " + "'" + projectConfig.Rpc + "'" + ","); - sb.AppendLine(" }"); - sb.AppendLine("]"); - File.WriteAllText(path1, sb.ToString()); - } - else - { - Debug.LogWarning( - $"{Path.GetDirectoryName(path1)} is missing, network.js file will not be updated for this template"); - } - - if (AssetDatabase.IsValidFolder(Path.GetDirectoryName(path2))) - { - // writes data to the webgl metamask network file - var sb = new StringBuilder(); - sb.AppendLine("//You can see a list of compatible EVM chains at https://chainlist.org/"); - sb.AppendLine("window.web3ChainId = " + projectConfig.ChainId + ";"); - File.WriteAllText(path2, sb.ToString()); - } - else - { - Debug.LogWarning( - $"{Path.GetDirectoryName(path2)} is missing, network.js file will not be updated for this template"); - } - - AssetDatabase.Refresh(); - - Debug.Log("Done"); - } - - private class ValidateProjectIDResponse - { - [JsonProperty("response")] public bool Response { get; set; } - } - - #endregion -} \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Editor/ServerSettings.cs.meta b/Packages/io.chainsafe.web3-unity/Editor/ServerSettings.cs.meta deleted file mode 100644 index 0fdee00e0..000000000 --- a/Packages/io.chainsafe.web3-unity/Editor/ServerSettings.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 137e7c488f5f7df438703b4ad74e92b0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/io.chainsafe.web3-unity/Editor/Startup.cs b/Packages/io.chainsafe.web3-unity/Editor/Startup.cs index 7f5b21b4c..35e4eb156 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/Startup.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/Startup.cs @@ -54,9 +54,9 @@ static void ValidateProjectID() try { var projectID = ProjectConfigUtilities.Load()?.ProjectId; - if (string.IsNullOrEmpty(projectID)) + if (string.IsNullOrWhiteSpace(projectID)) { - ChainSafeServerSettings.ShowWindow(); + Web3SettingsEditor.ShowWindow(); } } catch (Exception e) diff --git a/Packages/io.chainsafe.web3-unity/Editor/StringListSearchProvider.cs b/Packages/io.chainsafe.web3-unity/Editor/StringListSearchProvider.cs index 4c29a5d4a..10176330a 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/StringListSearchProvider.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/StringListSearchProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using ChainSafe.GamingSdk.Editor; using UnityEditor; using UnityEditor.Experimental.GraphView; using UnityEngine; @@ -94,8 +95,6 @@ public List CreateSearchTree(SearchWindowContext context) public bool OnSelectEntry(SearchTreeEntry SearchTreeEntry, SearchWindowContext context) { onSetIndexCallback?.Invoke((string)SearchTreeEntry.userData); - ChainSafeServerSettings instance = EditorWindow.GetWindow(); - instance.UpdateServerMenuInfo(); return true; } diff --git a/Packages/io.chainsafe.web3-unity/Editor/Textures/ChainSafeLogo2.png b/Packages/io.chainsafe.web3-unity/Editor/Textures/ChainSafeLogo2.png new file mode 100644 index 0000000000000000000000000000000000000000..f29ec02d8de807e33cc91bfe91cb40ee6e64097f GIT binary patch literal 184929 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%0Agn)= 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

on(gpCm@JuKaG$`bPjKr>pe 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 SDfxg3k-F*;mGu2)fBZj4q1!0{ literal 0 HcmV?d00001 diff --git a/Packages/io.chainsafe.web3-unity/Editor/Textures/ChainSafeLogo2.png.meta b/Packages/io.chainsafe.web3-unity/Editor/Textures/ChainSafeLogo2.png.meta new file mode 100644 index 000000000..9b3cf2754 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Editor/Textures/ChainSafeLogo2.png.meta @@ -0,0 +1,127 @@ +fileFormatVersion: 2 +guid: c56296e2d11bb0c41ab45b3ae647a00d +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: WebGL + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs new file mode 100644 index 000000000..c2dbe995a --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs @@ -0,0 +1,223 @@ +using System; +using System.Linq; +using ChainSafe.Gaming; +using UnityEditor; +using UnityEditor.Experimental.GraphView; +using UnityEngine; + +namespace ChainSafe.GamingSdk.Editor +{ + public partial class Web3SettingsEditor + { + private class ChainSettingsPanel + { + private readonly Web3SettingsEditor window; + private readonly ProjectConfigAsset configAsset; + private readonly ChainConfigEntry chainConfig; + + private int selectedChainIndex; + private int selectedRpcIndex; + private StringListSearchProvider searchProvider; + private ISearchWindowProvider _searchWindowProviderImplementation; + private bool changedRpcOrWs; + private int selectedWebHookIndex; + + public ChainSettingsPanel(Web3SettingsEditor window, ChainConfigEntry chainConfigEntry) + { + this.window = window; + this.configAsset = window.projectConfig; + this.chainConfig = chainConfigEntry; + + UpdateServerMenuInfo(); + } + + public string ChainId => chainConfig.ChainId; + + public void OnGUI() + { + var title = $"{chainConfig.Chain} ({chainConfig.ChainId})"; + + EditorGUILayout.BeginHorizontal(); + GUILayout.Label(title, EditorStyles.boldLabel); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Remove")) + { + OnRemoveClick(); + } + EditorGUILayout.EndHorizontal(); + + EditorGUI.indentLevel++; + EditorGUI.BeginChangeCheck(); + + // Set string array from chainList to pass into the menu + var chainOptions = window.chainList.Select(x => x.name).ToArray(); + // Display the dynamically updating Popup + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.PrefixLabel("Select Chain"); + // Show the network drop down menu + if (GUILayout.Button(chainConfig.Chain, EditorStyles.popup)) + { + searchProvider = CreateInstance(); + searchProvider.Initialize(chainOptions, x => + { + chainConfig.Chain = x; + UpdateServerMenuInfo(true); + }); + SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(Event.current.mousePosition)), + searchProvider); + } + + EditorGUILayout.EndHorizontal(); + + var usingCustomSettings = selectedChainIndex == 0; + + GUI.enabled = usingCustomSettings; + + chainConfig.Network = EditorGUILayout.TextField("Network", chainConfig.Network); + chainConfig.ChainId = EditorGUILayout.TextField("Chain ID", chainConfig.ChainId); + chainConfig.Symbol = EditorGUILayout.TextField("Symbol", chainConfig.Symbol); + chainConfig.BlockExplorerUrl = EditorGUILayout.TextField("Block Explorer", chainConfig.BlockExplorerUrl); + + GUI.enabled = true; + + // Remove "https://" so the user doesn't have to click through 2 levels for the rpc options + var rpcOptions = window.chainList[selectedChainIndex].rpc.Where(x => x.StartsWith("https")) + .Select(x => x.Replace("/", "\u2215")).ToArray(); + if (rpcOptions.Length > 0) + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.PrefixLabel("Select RPC"); + var selectedRpc = window.chainList[selectedChainIndex].rpc[selectedRpcIndex]; + // Show the rpc drop down menu + if (GUILayout.Button(selectedRpc, EditorStyles.popup)) + { + searchProvider = CreateInstance(); + searchProvider.Initialize(rpcOptions, x => + { + var str = x.Replace("\u2215", "/"); + selectedRpcIndex = window.chainList[selectedChainIndex].rpc.IndexOf(str); + // Add "https://" back + chainConfig.Rpc = str; + changedRpcOrWs = true; + UpdateServerMenuInfo(); + }); + SearchWindow.Open( + new SearchWindowContext(GUIUtility.GUIToScreenPoint(Event.current.mousePosition)), + searchProvider); + } + EditorGUILayout.EndHorizontal(); + EditorGUILayout.HelpBox("If you set your custom RPC URI it will override the selection above.", MessageType.Info); + } + + // Allows for a custom rpc + chainConfig.Rpc = EditorGUILayout.TextField("Custom RPC", chainConfig.Rpc); + + + // Remove "https://" so the user doesn't have to click through 2 levels for the rpc options + var webHookOptions = window.chainList[selectedChainIndex].rpc.Where(x => x.StartsWith("w")) + .Select(x => x.Replace("/", "\u2215")).ToArray(); + if (webHookOptions.Length > 0) + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.PrefixLabel("Select WebHook"); + selectedWebHookIndex = + Mathf.Clamp(selectedWebHookIndex, 0, window.chainList[selectedChainIndex].rpc.Count - 1); + var webhookIndex = window.chainList[selectedChainIndex].rpc.IndexOf(chainConfig.Ws); + var selectedWebHook = + webhookIndex == -1 ? window.chainList[selectedChainIndex].rpc[selectedWebHookIndex] : chainConfig.Ws; + if (GUILayout.Button(selectedWebHook, EditorStyles.popup)) + { + searchProvider = CreateInstance(); + searchProvider.Initialize(webHookOptions, + x => + { + var str = x.Replace("\u2215", "/"); + + selectedWebHookIndex = window.chainList[selectedChainIndex].rpc.IndexOf(str); + chainConfig.Ws = str; + changedRpcOrWs = true; + UpdateServerMenuInfo(); + }); + SearchWindow.Open( + new SearchWindowContext(GUIUtility.GUIToScreenPoint(Event.current.mousePosition)), + searchProvider); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.HelpBox("If you set your custom WebHook URI it will override the selection above.", MessageType.Info); + } + + chainConfig.Ws = EditorGUILayout.TextField("Custom WebHook", chainConfig.Ws); + + EditorGUI.indentLevel--; + if (EditorGUI.EndChangeCheck() || changedRpcOrWs) + { + Debug.Log("Change detected."); + changedRpcOrWs = false; + EditorUtility.SetDirty(configAsset); + } + } + + private void OnRemoveClick() + { + if (EditorUtility.DisplayDialog( + "Remove Chain Config", + "Do you want to remove the chain config?\nThis action can't be undone.", + "Remove", + "Cancel")) + { + window.RemoveChainConfigEntry(ChainId); + } + } + + private void UpdateServerMenuInfo(bool chainSwitched = false) + { + // Get the selected chain index + selectedChainIndex = Array.FindIndex(window.chainList.ToArray(), x => x.name == chainConfig.Chain); + // Check if the selectedChainIndex is valid + if (selectedChainIndex >= 0 && selectedChainIndex < window.chainList.Count) + { + // Set chain values + var chainPrototype = window.chainList[selectedChainIndex]; + + var overwriteValues = !chainPrototype.allowCustomValues; + + if (overwriteValues) + { + chainConfig.Network = chainPrototype.chain; + chainConfig.ChainId = chainPrototype.chainId.ToString(); + chainConfig.Symbol = chainPrototype.nativeCurrency.symbol; + if (chainPrototype.explorers != null) + { + chainConfig.BlockExplorerUrl = chainPrototype.explorers[0].url; + } + } + // Ensure that the selectedRpcIndex is within bounds + selectedRpcIndex = Mathf.Clamp(selectedRpcIndex, 0, chainPrototype.rpc.Count - 1); + // Set the rpc + if(chainSwitched || string.IsNullOrEmpty(chainConfig.Rpc)) + chainConfig.Rpc = chainPrototype.rpc[selectedRpcIndex]; + + if (chainSwitched) + { + chainConfig.Ws = chainPrototype.rpc.FirstOrDefault(x => x.StartsWith("wss")); + selectedWebHookIndex = chainPrototype.rpc.IndexOf(chainConfig.Ws); + changedRpcOrWs = true; + } + else + { + selectedWebHookIndex = chainPrototype.rpc.IndexOf(chainConfig.Ws) == -1 + ? chainPrototype.rpc + .IndexOf(chainPrototype.rpc.FirstOrDefault(x => x.StartsWith("wss"))) + : chainPrototype.rpc.IndexOf(chainConfig.Ws); + } + } + else + { + // Handle the case where the selected chain is not found + Debug.LogError("Selected chain not found in the chainList."); + } + } + } + } +} \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs.meta b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs.meta new file mode 100644 index 000000000..bcc486f10 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 027c787444f042f8a13c53838f94c33c +timeCreated: 1725881329 \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs new file mode 100644 index 000000000..c94001221 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs @@ -0,0 +1,354 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ChainSafe.Gaming; +using ChainSafe.Gaming.UnityPackage; +using Newtonsoft.Json; +using UnityEditor; +using UnityEngine; +using UnityEngine.Networking; +using ChainInfo = ChainSafe.Gaming.UnityPackage.Model; + +namespace ChainSafe.GamingSdk.Editor +{ + public partial class Web3SettingsEditor : EditorWindow + { + // Default values + private const string EnableAnalyticsScriptingDefineSymbol = "ENABLE_ANALYTICS"; + + // Initializes window + [MenuItem("ChainSafe SDK/Project Settings", false, 1)] + public static void ShowWindow() + { + // Show existing window instance. If one doesn't exist, make one. + var window = GetWindow(typeof(Web3SettingsEditor)); + window.titleContent = new GUIContent("Web3 Settings"); + window.minSize = new Vector2(450, 300); + } + + public static void WriteNetworkFile() + { + throw new NotImplementedException(); + } + + private static GUIStyle centeredLabelStyle; + private static GUIStyle wrappedGreyMiniLabel; + + // Chain values + public string previousProjectId; + + private ProjectConfigAsset projectConfig; + private List chainSettingPanels; + private List chainList; + private FetchingStatus fetchingStatus = FetchingStatus.NotFetching; + + private Texture2D logo; + private Vector2 chainsScrollPosition; + + private Tabs ActiveTab + { + get => (Tabs)EditorPrefs.GetInt("Web3SdkSettingsEditor.Tab", (int)Tabs.Project); + set => EditorPrefs.SetInt("Web3SdkSettingsEditor.Tab", (int)value); + } + + private void Awake() + { + projectConfig = ProjectConfigUtilities.CreateOrLoad(); + previousProjectId = projectConfig.ProjectId; + } + + private void OnEnable() + { + if (!logo) + logo = AssetDatabase.LoadAssetAtPath( + "Packages/io.chainsafe.web3-unity/Editor/Textures/ChainSafeLogo2.png"); + + TryFetchSupportedChains(); + } + + private void OnGUI() + { + InitStyles(); + + using (new EditorGUILayout.VerticalScope()) + { + DrawHeader(); + DrawBody(); + GUILayout.FlexibleSpace(); + DrawFooter(); + } + } + + private void InitStyles() + { + centeredLabelStyle ??= new GUIStyle(EditorStyles.label) + { + alignment = TextAnchor.MiddleCenter + }; + + wrappedGreyMiniLabel ??= new GUIStyle(EditorStyles.miniLabel) + { + wordWrap = true, + // alignment = TextAnchor.MiddleCenter, + }; + } + + private void DrawHeader() + { + using (new GUILayout.VerticalScope(GUILayout.Height(240))) + { + GUILayout.FlexibleSpace(); + + // logo layout + using (new EditorGUILayout.HorizontalScope()) + { + // GUILayout.FlexibleSpace(); + GUILayout.Label(logo, centeredLabelStyle, GUILayout.MaxHeight(160)); + // GUILayout.FlexibleSpace(); + } + + GUILayout.Space(15); + + GUILayout.Label("Welcome to web3.unity, the ChainSafe Gaming SDK!", centeredLabelStyle); + + GUILayout.FlexibleSpace(); + } + } + + private void DrawBody() + { + // tabs layout + using (new EditorGUILayout.HorizontalScope()) + { + GUILayout.FlexibleSpace(); + ActiveTab = (Tabs)GUILayout.Toolbar((int)ActiveTab, new[] { "Project Settings", "Chain Settings" }); + GUILayout.FlexibleSpace(); + } + + // EditorGUILayout.Separator(); + GUILayout.Space(15); + + // DrawHorizontalLine(); + + switch (ActiveTab) + { + case Tabs.Project: + DrawProjectTabContent(); + break; + case Tabs.Chains: + DrawChainsTabContent(); + break; + } + } + + private void DrawProjectTabContent() + { + EditorGUI.BeginChangeCheck(); + + projectConfig.ProjectId = EditorGUILayout.TextField("Project ID", projectConfig.ProjectId); + if (string.IsNullOrWhiteSpace(projectConfig.ProjectId)) + { + EditorGUILayout.HelpBox( + "Please enter your Project ID to start using the ChainSafe Gaming SDK.", + MessageType.Warning); + if (GUILayout.Button("Get a Project ID")) + { + Application.OpenURL("https://dashboard.gaming.chainsafe.io/"); + } + } + EditorGUILayout.Space(); + projectConfig.EnableAnalytics = + EditorGUILayout.Toggle( + new GUIContent("Allow Analytics:", + "Consent to collecting data for analytics purposes. This will help improve our product."), + projectConfig.EnableAnalytics); + GUILayout.Label( + "We will collect data for analytics to help improve your experience with our SDK. This data allows us to optimize performance, introduce new features, and ensure seamless integration. You can opt out at any time, but we encourage keeping analytics enabled for the best results!", + wrappedGreyMiniLabel); + + if (EditorGUI.EndChangeCheck()) + { + EditorUtility.SetDirty(projectConfig); + + if (projectConfig.ProjectId != previousProjectId) + { + ValidateProjectID(projectConfig.ProjectId); + previousProjectId = projectConfig.ProjectId; + } + + if (projectConfig.EnableAnalytics) + ScriptingDefineSymbols.TryAddDefineSymbol(EnableAnalyticsScriptingDefineSymbol); + else + ScriptingDefineSymbols.TryRemoveDefineSymbol(EnableAnalyticsScriptingDefineSymbol); + } + } + + private void DrawChainsTabContent() + { + if (fetchingStatus != FetchingStatus.Fetched) + { + EditorGUILayout.LabelField(new GUIContent("Fetching supported wallets.."), centeredLabelStyle); + return; + } + + chainsScrollPosition = GUILayout.BeginScrollView(chainsScrollPosition); + + for (int i = 0; i < chainSettingPanels.Count; i++) + { + var panel = chainSettingPanels[i]; + panel.OnGUI(); + EditorGUILayout.Space(25); + } + + if (GUILayout.Button("+")) + { + var newChainConfig = ChainConfigEntry.Empty; + projectConfig.ChainConfigs.Add(newChainConfig); + chainSettingPanels.Add(new ChainSettingsPanel(this, newChainConfig)); + EditorUtility.SetDirty(projectConfig); + } + + GUILayout.EndScrollView(); + } + + private void DrawFooter() + { + GUILayout.Label("ChainSafe Gaming", EditorStyles.centeredGreyMiniLabel); + } + + private void RemoveChainConfigEntry(string chainId) + { + var index = projectConfig.ChainConfigs.FindIndex(entry => entry.ChainId == chainId); + + if (index < 0) + { + Debug.LogError($"Can't remove. Chain config with id {chainId} was not found."); + return; + } + + projectConfig.ChainConfigs.RemoveAt(index); + EditorUtility.SetDirty(projectConfig); + chainSettingPanels.RemoveAt(chainSettingPanels.FindIndex(panel => panel.ChainId == chainId)); + } + + ///

+ /// Validates the project ID via ChainSafe's backend & writes values to the network js file, static so it can be called externally + /// + /// + private static async void ValidateProjectID(string projectID) + { + try + { + if (await ValidateProjectIDAsync(projectID)) + { +#if UNITY_WEBGL + WriteNetworkFile(); +#endif + } + } + catch (Exception e) + { + Debug.LogError("Failed to validate project ID"); + Debug.LogException(e); + } + + static async Task ValidateProjectIDAsync(string projectID) + { + var form = new WWWForm(); + form.AddField("projectId", projectID); + Debug.Log("Checking Project ID.."); + using var www = UnityWebRequest.Post("https://api.gaming.chainsafe.io/project/checkId", form); + await EditorUtilities.SendAndWait(www); + const string dbgProjectIDMessage = + "Project ID is not valid! Please go to https://dashboard.daming.chainsafe.io to get a new Project ID"; + + if (www.result != UnityWebRequest.Result.Success) + { + Debug.Log(www.error); + Debug.Log("Error checking Project ID!"); + Debug.LogError(dbgProjectIDMessage); + return false; + } + + var response = JsonConvert.DeserializeObject(www.downloadHandler.text); + if (response.Response.ToString().Equals("True", StringComparison.InvariantCultureIgnoreCase)) + { + Debug.Log("Project ID is valid. You can now proceed with building using the SDK!"); + return true; + } + + Debug.LogError(dbgProjectIDMessage); + return false; + } + } + + /// + /// Fetches the supported EVM chains list from Chainlist's github json + /// + private async void TryFetchSupportedChains() + { + if (fetchingStatus == FetchingStatus.Fetching) return; + fetchingStatus = FetchingStatus.Fetching; + using var webRequest = UnityWebRequest.Get("https://chainid.network/chains.json"); + await EditorUtilities.SendAndWait(webRequest); + if (webRequest.result != UnityWebRequest.Result.Success) + { + Debug.LogError("Error Getting Supported Chains: " + webRequest.error); + return; + } + + var json = webRequest.downloadHandler.text; + chainList = JsonConvert.DeserializeObject>(json); + chainList = chainList.OrderBy(x => x.name).ToList(); + chainList.Insert(0, new ChainInfo.Root + { + name = "Custom", + allowCustomValues = true, + rpc = new List { string.Empty } + }); + + OnChainListFetched(); + fetchingStatus = FetchingStatus.Fetched; + } + + private void OnChainListFetched() + { + if (projectConfig.ChainConfigs.Count != 0) + { + chainSettingPanels = projectConfig.ChainConfigs + .Select((chainConfig) => new ChainSettingsPanel(this, chainConfig)) + .ToList(); + } + else + { + var newChainConfig = ChainConfigEntry.Default; + projectConfig.ChainConfigs.Add(newChainConfig); + EditorUtility.SetDirty(projectConfig); + + chainSettingPanels = new List + { + new(this, newChainConfig) + }; + } + } + + private class ValidateProjectIDResponse + { + [JsonProperty("response")] public bool Response { get; set; } + } + + private enum Tabs + { + Project = 0, + Chains = 1 + } + + private enum FetchingStatus + { + NotFetching, + Fetching, + Fetched + } + } +} \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs.meta b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs.meta new file mode 100644 index 000000000..ce59c97a6 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7059d30ff27146adbb23919420280d25 +timeCreated: 1725476414 \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Editor/WebGLTemplateSync.cs b/Packages/io.chainsafe.web3-unity/Editor/WebGLTemplateSync.cs index e7612d839..f492c3615 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/WebGLTemplateSync.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/WebGLTemplateSync.cs @@ -73,7 +73,7 @@ public static void Syncronize() AssetDatabase.AllowAutoRefresh(); AssetDatabase.Refresh(); // Update template values to chain config - ChainSafeServerSettings.WriteNetworkFile(); + Web3SettingsEditor.WriteNetworkFile(); } } diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ChainConfigEntry.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ChainConfigEntry.cs new file mode 100644 index 000000000..72e21bd25 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ChainConfigEntry.cs @@ -0,0 +1,48 @@ +using System; +using ChainSafe.Gaming.Web3; +using UnityEngine; + +namespace ChainSafe.Gaming +{ + [Serializable] + public class ChainConfigEntry : IChainConfig + { + private const string ChainIdDefault = "11155111"; + private const string ChainDefault = "Sepolia"; + private const string NetworkDefault = "Sepolia"; + private const string SymbolDefault = "Seth"; + private const string RpcDefault = "https://rpc.sepolia.org"; + private const string BlockExplorerUrlDefault = "https://sepolia.etherscan.io"; + private const string WsDefault = ""; + + [field: SerializeField] public string ChainId { get; set; } + [field: SerializeField] public string Symbol { get; set; } + [field: SerializeField] public string Chain { get; set; } + [field: SerializeField] public string Network { get; set; } + [field: SerializeField] public string Rpc { get; set; } + [field: SerializeField] public string Ws { get; set; } + [field: SerializeField] public string BlockExplorerUrl { get; set; } + + public static ChainConfigEntry Default => new() + { + ChainId = ChainIdDefault, + Symbol = SymbolDefault, + Chain = ChainDefault, + Network = NetworkDefault, + Rpc = RpcDefault, + BlockExplorerUrl = BlockExplorerUrlDefault, + Ws = WsDefault + }; + + public static ChainConfigEntry Empty => new() + { + ChainId = string.Empty, + Symbol = string.Empty, + Chain = "Custom", + Network = string.Empty, + Rpc = string.Empty, + BlockExplorerUrl = string.Empty, + Ws = string.Empty + }; + } +} \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ChainConfigEntry.cs.meta b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ChainConfigEntry.cs.meta new file mode 100644 index 000000000..8bc881dff --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ChainConfigEntry.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4ba84f4ff342416c8c01d1a5c70a2a31 +timeCreated: 1725471671 \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Model/ChainInfoModel.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Model/ChainInfoModel.cs index 787994c27..63afdd884 100644 --- a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Model/ChainInfoModel.cs +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Model/ChainInfoModel.cs @@ -22,5 +22,6 @@ public struct Root public NativeCurrency nativeCurrency { get; set; } public object chainId { get; set; } public List explorers { get; set; } + public bool allowCustomValues { get; set; } } } \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs new file mode 100644 index 000000000..9a48f1914 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using ChainSafe.Gaming.Web3; +using ChainSafe.Gaming.Web3.Core.Chains; +using UnityEngine; + +namespace ChainSafe.Gaming +{ + [CreateAssetMenu(menuName = "ChainSafe/Project Configuration", fileName = "ProjectConfig", order = 0)] + public class ProjectConfigAsset : ScriptableObject, ICompleteProjectConfig + { + [field: SerializeField] public string ProjectId { get; set; } + [field: SerializeField] public List ChainConfigs { get; set; } = new(); + [field: SerializeField] public bool EnableAnalytics { get; set; } + + IEnumerable IChainConfigSet.Configs => ChainConfigs; + } +} \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs.meta b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs.meta new file mode 100644 index 000000000..a3a0b764a --- /dev/null +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 512a0f920c4549c6b8fa15ad0baa6cca +timeCreated: 1725464581 \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigScriptableObject.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigScriptableObject_Deprecated.cs similarity index 87% rename from Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigScriptableObject.cs rename to Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigScriptableObject_Deprecated.cs index 792aa6e9d..fc5541f92 100644 --- a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigScriptableObject.cs +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigScriptableObject_Deprecated.cs @@ -3,9 +3,7 @@ namespace ChainSafe.Gaming.UnityPackage { - [CreateAssetMenu(fileName = "ProjectConfigScriptableObject", menuName = "ScriptableObjects/ProjectConfigScriptableObject", - order = 1)] - public class ProjectConfigScriptableObject : ScriptableObject, ICompleteProjectConfig + public class ProjectConfigScriptableObject_Deprecated : ScriptableObject { [SerializeField] private string projectID; [SerializeField] private string chainID; diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigScriptableObject.cs.meta b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigScriptableObject_Deprecated.cs.meta similarity index 100% rename from Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigScriptableObject.cs.meta rename to Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigScriptableObject_Deprecated.cs.meta diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs index 504f7ac82..583514c4b 100644 --- a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs @@ -8,40 +8,40 @@ namespace ChainSafe.Gaming.UnityPackage { public static class ProjectConfigUtilities { - private const string AssetName = "ProjectConfigData"; + private const string AssetName = "ProjectConfig"; - public static ProjectConfigScriptableObject Load() + public static ProjectConfigAsset Load() { - var projectConfig = Resources.Load(AssetName); + var projectConfig = Resources.Load(AssetName); return projectConfig ? projectConfig : null; } - public static ProjectConfigScriptableObject Create(string projectId, string chainId, string chain, string network, + public static ProjectConfigAsset Create(string projectId, string chainId, string chain, string network, string symbol, string rpc, string blockExplorerUrl, bool enableAnalytics, string ws = "") { - var projectConfig = ScriptableObject.CreateInstance(); + var projectConfig = ScriptableObject.CreateInstance(); projectConfig.ProjectId = projectId; - projectConfig.ChainId = chainId; - projectConfig.Chain = chain; - projectConfig.Network = network; - projectConfig.Symbol = symbol; - projectConfig.Rpc = rpc; - projectConfig.Ws = ws; - projectConfig.BlockExplorerUrl = blockExplorerUrl; projectConfig.EnableAnalytics = enableAnalytics; - + projectConfig.ChainConfigs = new List + { + new() + { + ChainId = chainId, + Chain = chain, + Network = network, + Symbol = symbol, + Rpc = rpc, + Ws = ws, + BlockExplorerUrl = blockExplorerUrl, + } + }; + return projectConfig; } - public static IChainConfig BuildLocalhostConfig(string port = "8545", string chainId = "31337", - string chain = "Anvil", string symbol = "ETH", string network = "GoChain Testnet") - { - return new LocalhostChainConfig(chainId, symbol, chain, network, port); - } - #if UNITY_EDITOR - public static ProjectConfigScriptableObject CreateOrLoad() + public static ProjectConfigAsset CreateOrLoad() { var projectConfig = Load(); @@ -54,7 +54,7 @@ public static ProjectConfigScriptableObject CreateOrLoad() Directory.CreateDirectory(assetDirectory); } - projectConfig = ScriptableObject.CreateInstance(); + projectConfig = ScriptableObject.CreateInstance(); UnityEditor.AssetDatabase.CreateAsset(projectConfig, Path.Combine("Assets", nameof(Resources), $"{AssetName}.asset")); } @@ -62,12 +62,19 @@ public static ProjectConfigScriptableObject CreateOrLoad() return projectConfig; } - public static void Save(ProjectConfigScriptableObject projectConfig) + public static void Save(ProjectConfigAsset projectConfig) { UnityEditor.EditorUtility.SetDirty(projectConfig); UnityEditor.AssetDatabase.SaveAssets(); } #endif + + public static IChainConfig BuildLocalhostConfig(string port = "8545", string chainId = "31337", + string chain = "Anvil", string symbol = "ETH", string network = "GoChain Testnet") + { + return new LocalhostChainConfig(chainId, symbol, chain, network, port); + } + private class LocalhostChainConfig : IChainConfig { public LocalhostChainConfig(string chainId, string symbol, string chain, string network, string port) diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigSet.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigSet.cs index e5c1880c1..5d9d5489a 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigSet.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigSet.cs @@ -4,9 +4,14 @@ namespace ChainSafe.Gaming.Web3.Core.Chains { public class ChainConfigSet : List, IChainConfigSet { + private readonly IChainConfig[] chainConfigs; + public ChainConfigSet(params IChainConfig[] chainConfigs) : base(chainConfigs) { + this.chainConfigs = chainConfigs; } + + public IEnumerable Configs => chainConfigs; } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs index 3ad4f9cc3..6be05d21f 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs @@ -12,12 +12,12 @@ public class ChainManager : IChainManager private readonly IList switchHandlers; private readonly ILogWriter logWriter; - public ChainManager(IChainConfigSet configs, IList switchHandlers, ILogWriter logWriter) + public ChainManager(IChainConfigSet configSet, IList switchHandlers, ILogWriter logWriter) { this.logWriter = logWriter; - this.configs = configs.ToDictionary(config => config.ChainId, config => config); + this.configs = configSet.Configs.ToDictionary(config => config.ChainId, config => config); this.switchHandlers = switchHandlers; - Current = configs.First(); + Current = configSet.Configs.First(); } public event Action ChainSwitched; diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManagerChainConfig.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManagerChainConfig.cs index a709743c8..d46c5b385 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManagerChainConfig.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManagerChainConfig.cs @@ -21,8 +21,6 @@ public ChainManagerChainConfig(IChainManager chainManager) public string Rpc => CurrentConfig.Rpc; - public string Ipc => CurrentConfig.Ipc; - public string Ws => CurrentConfig.Ws; public string BlockExplorerUrl => CurrentConfig.BlockExplorerUrl; diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/IChainConfigSet.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainConfigSet.cs index d74957872..eda855e02 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/IChainConfigSet.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainConfigSet.cs @@ -2,7 +2,8 @@ namespace ChainSafe.Gaming.Web3.Core.Chains { - public interface IChainConfigSet : IList + public interface IChainConfigSet { + IEnumerable Configs { get; } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/IChainConfig.cs b/src/ChainSafe.Gaming/Web3/Core/IChainConfig.cs index 7d732ed8e..82edf0631 100644 --- a/src/ChainSafe.Gaming/Web3/Core/IChainConfig.cs +++ b/src/ChainSafe.Gaming/Web3/Core/IChainConfig.cs @@ -30,11 +30,6 @@ public interface IChainConfig // TODO: double check these xml docs pls ///
public string Rpc { get; } - /// - /// The path to the IPC file to be used by the IPC provider by default. - /// - public string Ipc { get; } - /// /// TODO. /// diff --git a/src/ChainSafe.Gaming/Web3/Core/ICompleteProjectConfig.cs b/src/ChainSafe.Gaming/Web3/Core/ICompleteProjectConfig.cs index e94f74bb3..f5b04a490 100644 --- a/src/ChainSafe.Gaming/Web3/Core/ICompleteProjectConfig.cs +++ b/src/ChainSafe.Gaming/Web3/Core/ICompleteProjectConfig.cs @@ -1,9 +1,11 @@ +using ChainSafe.Gaming.Web3.Core.Chains; + namespace ChainSafe.Gaming.Web3 { /// /// merged with . /// - public interface ICompleteProjectConfig : IProjectConfig, IChainConfig + public interface ICompleteProjectConfig : IProjectConfig, IChainConfigSet { } } \ No newline at end of file diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK Lootboxes/2.6.0/Lootboxes Samples/Scripts/ChainlinkLootboxSampleLauncher.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK Lootboxes/2.6.0/Lootboxes Samples/Scripts/ChainlinkLootboxSampleLauncher.cs index 605641dd7..4f3da6d5e 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK Lootboxes/2.6.0/Lootboxes Samples/Scripts/ChainlinkLootboxSampleLauncher.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK Lootboxes/2.6.0/Lootboxes Samples/Scripts/ChainlinkLootboxSampleLauncher.cs @@ -1,4 +1,6 @@ -using LootBoxes.Chainlink.Scene; +using System.Collections.Generic; +using ChainSafe.Gaming; +using LootBoxes.Chainlink.Scene; using Chainsafe.Gaming.Chainlink; using ChainSafe.Gaming.Debugging; using ChainSafe.Gaming.Evm.JsonRpc; @@ -27,15 +29,20 @@ public class ChainlinkLootboxSampleLauncher : MonoBehaviour private class Web3Config : ICompleteProjectConfig { public string ProjectId => string.Empty; - public string ChainId => "31337"; - public string Chain => "Anvil"; - public string Network => "GoChain Testnet"; - public string Symbol => "GO"; - public string Rpc => $"http://127.0.0.1:8545"; - public string BlockExplorerUrl => "https://explorer.gochain.io/"; - public string Ipc { get; } - public string Ws { get; } public bool EnableAnalytics => true; + + public IEnumerable Configs { get; } = new[] + { + new ChainConfigEntry + { + ChainId = "31337", + Chain = "Anvil", + Network = "GoChain Testnet", + Symbol = "GO", + Rpc = $"http://127.0.0.1:8545", + BlockExplorerUrl = "https://explorer.gochain.io/", + } + }; } private async void Awake() From 6950863798b5d1d641c5f9b0fcdbc4800eb91700 Mon Sep 17 00:00:00 2001 From: creeppak Date: Mon, 9 Sep 2024 19:17:57 +0100 Subject: [PATCH 04/24] Added AddChainConfig to IChainManager to add new chain configs in runtime; Improved ChainManager Added a shortcut to Web3 that points to IChainManager --- .../Web3/Core/Chains}/ChainConfig.cs | 8 ++--- .../Web3/Core/Chains/ChainConfigExtensions.cs | 21 ++++++++++++ .../Web3/Core/Chains/ChainManager.cs | 34 +++++++++++++++++-- .../Web3/Core/{ => Chains}/IChainConfig.cs | 0 .../Web3/Core/Chains/IChainManager.cs | 2 ++ src/ChainSafe.Gaming/Web3/Core/Web3.cs | 9 ++++- 6 files changed, 65 insertions(+), 9 deletions(-) rename src/{ChainSafe.Gaming.NetCore => ChainSafe.Gaming/Web3/Core/Chains}/ChainConfig.cs (90%) create mode 100644 src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigExtensions.cs rename src/ChainSafe.Gaming/Web3/Core/{ => Chains}/IChainConfig.cs (100%) diff --git a/src/ChainSafe.Gaming.NetCore/ChainConfig.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfig.cs similarity index 90% rename from src/ChainSafe.Gaming.NetCore/ChainConfig.cs rename to src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfig.cs index 66061438b..1db3033ce 100644 --- a/src/ChainSafe.Gaming.NetCore/ChainConfig.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfig.cs @@ -1,3 +1,4 @@ +using System; using ChainSafe.Gaming.Web3; namespace ChainSafe.Gaming.NetCore @@ -6,6 +7,7 @@ namespace ChainSafe.Gaming.NetCore /// Concrete Implementation of . /// Holds all config files related to chain and network. ///
+ [Serializable] public class ChainConfig : IChainConfig { /// @@ -38,12 +40,6 @@ public class ChainConfig : IChainConfig /// public string Rpc { get; set; } - /// - /// Implementation of - /// IPC link. - /// - public string Ipc { get; set; } - /// /// Implementation of /// WebSocket link. diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigExtensions.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigExtensions.cs new file mode 100644 index 000000000..7eccd7f00 --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigExtensions.cs @@ -0,0 +1,21 @@ +using ChainSafe.Gaming.Web3; + +namespace ChainSafe.Gaming.NetCore +{ + public static class ChainConfigExtensions + { + public static IChainConfig Clone(this IChainConfig chainConfig) + { + return new ChainConfig + { + Chain = chainConfig.Chain, + ChainId = chainConfig.ChainId, + Network = chainConfig.Network, + Rpc = chainConfig.Rpc, + Symbol = chainConfig.Symbol, + BlockExplorerUrl = chainConfig.BlockExplorerUrl, + Ws = chainConfig.Ws, + }; + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs index 6be05d21f..8169bcdc0 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using ChainSafe.Gaming.NetCore; using ChainSafe.Gaming.Web3.Environment; namespace ChainSafe.Gaming.Web3.Core.Chains @@ -15,9 +16,26 @@ public class ChainManager : IChainManager public ChainManager(IChainConfigSet configSet, IList switchHandlers, ILogWriter logWriter) { this.logWriter = logWriter; - this.configs = configSet.Configs.ToDictionary(config => config.ChainId, config => config); this.switchHandlers = switchHandlers; Current = configSet.Configs.First(); + + // build configs map + try + { + configs = configSet.Configs.ToDictionary(config => config.ChainId, config => config.Clone()); // cloning all the configs + } + catch (ArgumentException) + { + logWriter.LogError("There are 2 or more Chain Configs with the same Chain ID. " + + "Please make sure to remove the duplicates that you don't need. " + + "Using first occurrences for now.."); + + var usedIds = configSet.Configs.Select(config => config.ChainId).Distinct().ToArray(); + var filteredConfigs = + usedIds.Select(chainId => configSet.Configs.First(config => config.ChainId == chainId)); + + configs = filteredConfigs.ToDictionary(config => config.ChainId, config => config.Clone()); // cloning all the configs + } } public event Action ChainSwitched; @@ -28,7 +46,8 @@ public async Task SwitchChain(string newChainId) { if (!configs.TryGetValue(newChainId, out var newChainConfig)) { - throw new Web3Exception($"No {nameof(IChainConfig)} was registered with id '{newChainId}'. Make sure to configure settings for the chain before switching to it."); + throw new Web3Exception($"No {nameof(IChainConfig)} was registered with id '{newChainId}'. " + + "Make sure to configure settings for the provided chain before switching to it."); } Current = newChainConfig; @@ -80,5 +99,16 @@ public async Task SwitchChain(string newChainId) logWriter.LogError(e.ToString()); } } + + public void AddChainConfig(IChainConfig newConfig) + { + if (configs.ContainsKey(newConfig.ChainId)) + { + throw new Web3Exception( + "Couldn't add Chain Config. A Chain Config with the same Chain Id is already present in the map."); + } + + configs[newConfig.ChainId] = newConfig; + } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/IChainConfig.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainConfig.cs similarity index 100% rename from src/ChainSafe.Gaming/Web3/Core/IChainConfig.cs rename to src/ChainSafe.Gaming/Web3/Core/Chains/IChainConfig.cs diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/IChainManager.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainManager.cs index c25b94812..fd6557190 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/IChainManager.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainManager.cs @@ -10,5 +10,7 @@ public interface IChainManager IChainConfig Current { get; } Task SwitchChain(string newChainId); + + void AddChainConfig(IChainConfig newConfig); } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Web3.cs b/src/ChainSafe.Gaming/Web3/Core/Web3.cs index 53e6d90fd..a729544fd 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Web3.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Web3.cs @@ -7,6 +7,7 @@ using ChainSafe.Gaming.Evm.Signers; using ChainSafe.Gaming.LocalStorage; using ChainSafe.Gaming.Web3.Core; +using ChainSafe.Gaming.Web3.Core.Chains; using ChainSafe.Gaming.Web3.Core.Evm; using ChainSafe.Gaming.Web3.Core.Logout; using Microsoft.Extensions.DependencyInjection; @@ -37,6 +38,7 @@ internal Web3(ServiceProvider serviceProvider) signer = serviceProvider.GetService(); transactionExecutor = serviceProvider.GetService(); events = serviceProvider.GetRequiredService(); + Chains = serviceProvider.GetRequiredService(); ContractBuilder = serviceProvider.GetRequiredService(); ProjectConfig = serviceProvider.GetRequiredService(); ChainConfig = serviceProvider.GetRequiredService(); @@ -73,10 +75,15 @@ internal Web3(ServiceProvider serviceProvider) public ITransactionExecutor TransactionExecutor => AssertComponentAccessible(transactionExecutor, nameof(TransactionExecutor)); /// - /// Access the event service of the Web3 instance, allowing you to subscribe to blockchain events. + /// Access the Event Service of the Web3 instance, allowing you to subscribe to blockchain events. /// public IEvmEvents Events => AssertComponentAccessible(events, nameof(Events)); + /// + /// Access the Chain Manager of the Web3 instance to switch chains in runtime. + /// + public IChainManager Chains { get; } + /// /// Access the factory for creating Ethereum smart contract wrappers. /// From 42cccef050a39979c1ef7bcc778abbd304494789 Mon Sep 17 00:00:00 2001 From: creeppak Date: Tue, 10 Sep 2024 13:58:28 +0100 Subject: [PATCH 05/24] Removed unnecessary dependencies from LootboxService --- .../LootboxService.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/ChainSafe.Gaming.Lootboxes.Chainlink/LootboxService.cs b/src/ChainSafe.Gaming.Lootboxes.Chainlink/LootboxService.cs index fe637f17c..e4ceb118c 100644 --- a/src/ChainSafe.Gaming.Lootboxes.Chainlink/LootboxService.cs +++ b/src/ChainSafe.Gaming.Lootboxes.Chainlink/LootboxService.cs @@ -28,8 +28,6 @@ public class LootboxService : ILootboxService, ILifecycleParticipant private readonly LootboxServiceConfig config; private readonly ISigner signer; private readonly IRpcProvider rpcProvider; - private readonly IProjectConfig projectConfig; - private readonly IChainConfig chainConfig; private readonly IAnalyticsClient analyticsClient; private Contract contract; @@ -39,13 +37,9 @@ public LootboxService( LootboxServiceConfig config, IContractBuilder contractBuilder, IRpcProvider rpcProvider, - IProjectConfig projectConfig, - IChainConfig chainConfig, IAnalyticsClient analyticsClient) { this.rpcProvider = rpcProvider; - this.projectConfig = projectConfig; - this.chainConfig = chainConfig; this.analyticsClient = analyticsClient; this.config = config; this.contractBuilder = contractBuilder; @@ -56,10 +50,8 @@ public LootboxService( IContractBuilder contractBuilder, IRpcProvider rpcProvider, ISigner signer, - IProjectConfig projectConfig, - IChainConfig chainConfig, IAnalyticsClient analyticsClient) - : this(config, contractBuilder, rpcProvider, projectConfig, chainConfig, analyticsClient) + : this(config, contractBuilder, rpcProvider, analyticsClient) { this.signer = signer; } From e5c88b96fcff12e1f03ea524530b299f754f486b Mon Sep 17 00:00:00 2001 From: creeppak Date: Wed, 11 Sep 2024 11:53:57 +0100 Subject: [PATCH 06/24] Implemented custom chain switching logic for all the components that required it Removed LastKnownNetwork property from IRpcProvider --- src/ChainSafe.Gaming.Gelato/Gelato.cs | 34 ++++++-- .../GelatoExtensions.cs | 3 +- .../HyperPlayProvider.cs | 4 +- .../SygmaClient.cs | 2 +- .../MetaMaskProvider.cs | 4 +- .../WalletConnectProvider.cs | 31 +------ src/ChainSafe.Gaming/MultiCall/IMultiCall.cs | 3 +- src/ChainSafe.Gaming/MultiCall/MultiCall.cs | 83 ++++++++++++------- .../MultiCall/MultiCallExtensions.cs | 5 +- .../RPC/Events/EventExtensions.cs | 3 +- .../RPC/Events/EventManager.cs | 49 ++++++++++- .../Web3/Core/Chains/ChainManager.cs | 4 +- .../Web3/Core/Chains/IChainSwitchHandler.cs | 2 +- .../Web3/Core/Evm/IEvmEvents.cs | 11 --- .../Web3/Core/Evm/IRpcProvider.cs | 14 ---- .../Nethereum/INethereumAccountAdapter.cs | 8 ++ .../Core/Nethereum/NethereumAccountAdapter.cs | 17 +++- .../Core/Nethereum/NethereumExtensions.cs | 3 +- .../NethereumTransactionManagerAdapter.cs | 5 ++ .../Core/Nethereum/NethereumWeb3Adapter.cs | 2 +- .../Web3/Evm/EventPoller/EventPoller.cs | 35 +------- .../Web3/Evm/JsonRpc/RpcClientExtensions.cs | 5 +- .../Web3/Evm/JsonRpc/RpcClientProvider.cs | 65 ++------------- .../Web3/Evm/Wallet/WalletProvider.cs | 42 ++++++++-- .../Evm/Wallet/WalletProviderExtensions.cs | 3 +- 25 files changed, 221 insertions(+), 216 deletions(-) create mode 100644 src/ChainSafe.Gaming/Web3/Core/Nethereum/INethereumAccountAdapter.cs diff --git a/src/ChainSafe.Gaming.Gelato/Gelato.cs b/src/ChainSafe.Gaming.Gelato/Gelato.cs index 9eebd1383..bb11f6f4e 100644 --- a/src/ChainSafe.Gaming.Gelato/Gelato.cs +++ b/src/ChainSafe.Gaming.Gelato/Gelato.cs @@ -7,6 +7,7 @@ using ChainSafe.Gaming.Web3; using ChainSafe.Gaming.Web3.Analytics; using ChainSafe.Gaming.Web3.Core; +using ChainSafe.Gaming.Web3.Core.Chains; using ChainSafe.Gaming.Web3.Core.Evm; using ChainSafe.Gaming.Web3.Environment; using ChainSafe.GamingSdk.Gelato.Dto; @@ -15,7 +16,7 @@ namespace ChainSafe.GamingSdk.Gelato { - public class Gelato : IGelato, ILifecycleParticipant + public class Gelato : IGelato, ILifecycleParticipant, IChainSwitchHandler { private readonly GelatoClient gelatoClient; private readonly IContractBuilder contractBuilder; @@ -23,7 +24,6 @@ public class Gelato : IGelato, ILifecycleParticipant private readonly GelatoConfig config; private readonly IChainConfig chainConfig; private readonly IAnalyticsClient analyticsClient; - private readonly IProjectConfig projectConfig; private bool gelatoDisabled; @@ -35,7 +35,6 @@ public Gelato(IHttpClient httpClient, IChainConfig chainConfig, GelatoConfig con this.chainConfig = chainConfig; this.contractBuilder = contractBuilder; this.analyticsClient = analyticsClient; - this.projectConfig = projectConfig; } public Gelato(IHttpClient httpClient, IChainConfig chainConfig, IProjectConfig projectConfig, GelatoConfig config, IContractBuilder contractBuilder, IAnalyticsClient analyticsClient) @@ -44,28 +43,40 @@ public Gelato(IHttpClient httpClient, IChainConfig chainConfig, IProjectConfig p this.config = config; this.chainConfig = chainConfig; this.contractBuilder = contractBuilder; - this.projectConfig = projectConfig; } public async ValueTask WillStartAsync() { - if (!await IsNetworkSupported(chainConfig.ChainId)) + if (await FetchGelatoDisabled()) { - gelatoDisabled = true; return; } analyticsClient.CaptureEvent(new AnalyticsEvent() { - EventName = $"Gelato initialized", + EventName = "Gelato initialized", PackageName = "io.chainsafe.web3-unity", }); } - public bool GetGelatoDisabled() => gelatoDisabled; - public ValueTask WillStopAsync() => new(Task.CompletedTask); + public async Task HandleChainSwitching() + { + if (await FetchGelatoDisabled()) + { + return; + } + + analyticsClient.CaptureEvent(new AnalyticsEvent + { + EventName = "Gelato reinitialized during chain switching", + PackageName = "io.chainsafe.web3-unity", + }); + } + + public bool GetGelatoDisabled() => gelatoDisabled; + public async Task CallWithSyncFee(CallWithSyncFeeRequest request) { try @@ -262,5 +273,10 @@ public async Task GetPaymentTokens() { return await gelatoClient.GetPaymentTokens(chainConfig.ChainId); } + + private async Task FetchGelatoDisabled() + { + return gelatoDisabled = !await IsNetworkSupported(chainConfig.ChainId); + } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Gelato/GelatoExtensions.cs b/src/ChainSafe.Gaming.Gelato/GelatoExtensions.cs index c07a048fb..e994d4dfb 100644 --- a/src/ChainSafe.Gaming.Gelato/GelatoExtensions.cs +++ b/src/ChainSafe.Gaming.Gelato/GelatoExtensions.cs @@ -1,6 +1,7 @@ using ChainSafe.Gaming.Web3; using ChainSafe.Gaming.Web3.Build; using ChainSafe.Gaming.Web3.Core; +using ChainSafe.Gaming.Web3.Core.Chains; using ChainSafe.GamingSdk.Gelato.Types; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -61,7 +62,7 @@ public static IWeb3ServiceCollection UseGelato(this IWeb3ServiceCollection colle var config = DefaultConfig(); collection.TryAddSingleton(config); - collection.AddSingleton(); + collection.AddSingleton(); return collection; } diff --git a/src/ChainSafe.Gaming.HyperPlay/HyperPlayProvider.cs b/src/ChainSafe.Gaming.HyperPlay/HyperPlayProvider.cs index 5a6e57bce..3d376d57e 100644 --- a/src/ChainSafe.Gaming.HyperPlay/HyperPlayProvider.cs +++ b/src/ChainSafe.Gaming.HyperPlay/HyperPlayProvider.cs @@ -36,8 +36,8 @@ public class HyperPlayProvider : WalletProvider /// Injected . /// ChainConfig to fetch chain data. /// Injected . - public HyperPlayProvider(IHyperPlayConfig config, IHyperPlayData data, ILocalStorage localStorage, Web3Environment environment, IChainConfig chainConfig, ChainRegistryProvider chainRegistryProvider) - : base(environment, chainRegistryProvider, chainConfig) + public HyperPlayProvider(IHyperPlayConfig config, IHyperPlayData data, ILocalStorage localStorage, Web3Environment environment, IChainConfig chainConfig) + : base(environment, chainConfig) { this.config = config; this.data = data; diff --git a/src/ChainSafe.Gaming.SygmaClient/SygmaClient.cs b/src/ChainSafe.Gaming.SygmaClient/SygmaClient.cs index 65186fb63..69b864f5a 100644 --- a/src/ChainSafe.Gaming.SygmaClient/SygmaClient.cs +++ b/src/ChainSafe.Gaming.SygmaClient/SygmaClient.cs @@ -25,7 +25,7 @@ namespace ChainSafe.Gaming.SygmaClient { - public class SygmaClient : ISygmaClient, ILifecycleParticipant + public class SygmaClient : ISygmaClient, ILifecycleParticipant // todo handle chain switching { private readonly IContractBuilder contractBuilder; private readonly ISigner signer; diff --git a/src/ChainSafe.Gaming.Unity.MetaMask/MetaMaskProvider.cs b/src/ChainSafe.Gaming.Unity.MetaMask/MetaMaskProvider.cs index cfc9f901c..1cc25eacb 100644 --- a/src/ChainSafe.Gaming.Unity.MetaMask/MetaMaskProvider.cs +++ b/src/ChainSafe.Gaming.Unity.MetaMask/MetaMaskProvider.cs @@ -28,7 +28,7 @@ public class MetaMaskProvider : WalletProvider /// Injected . /// Injected . public MetaMaskProvider(Web3Environment environment, IChainConfig chainConfig, ChainRegistryProvider chainRegistryProvider) - : base(environment, chainRegistryProvider, chainConfig) + : base(environment, chainConfig) { logWriter = environment.LogWriter; this.chainConfig = chainConfig; @@ -80,7 +80,7 @@ public override async Task Connect() analyticsClient.CaptureEvent(new AnalyticsEvent() { - EventName = "Metamask WebGL Initialized", + EventName = "MetaMask WebGL Initialized", PackageName = "io.chainsafe.web3-unity", }); diff --git a/src/ChainSafe.Gaming.WalletConnect/WalletConnectProvider.cs b/src/ChainSafe.Gaming.WalletConnect/WalletConnectProvider.cs index 4a5289257..5f2074498 100644 --- a/src/ChainSafe.Gaming.WalletConnect/WalletConnectProvider.cs +++ b/src/ChainSafe.Gaming.WalletConnect/WalletConnectProvider.cs @@ -45,6 +45,7 @@ public class WalletConnectProvider : WalletProvider, ILifecycleParticipant, ICon private readonly IWalletRegistry walletRegistry; private readonly RedirectionHandler redirection; private readonly IHttpClient httpClient; + private readonly IAnalyticsClient analyticsClient; private WalletConnectCore core; private WalletConnectSignClient signClient; @@ -53,7 +54,6 @@ public class WalletConnectProvider : WalletProvider, ILifecycleParticipant, ICon private SessionStruct session; private bool connected; private bool initialized; - private IAnalyticsClient analyticsClient; private ConnectionHandlerConfig connectionHandlerConfig; public WalletConnectProvider( @@ -62,9 +62,8 @@ public WalletConnectProvider( IChainConfig chainConfig, IWalletRegistry walletRegistry, RedirectionHandler redirection, - Web3Environment environment, - ChainRegistryProvider chainRegistryProvider) - : base(environment, chainRegistryProvider, chainConfig) + Web3Environment environment) + : base(environment, chainConfig) { analyticsClient = environment.AnalyticsClient; this.redirection = redirection; @@ -245,7 +244,7 @@ private async Task CheckAndSwitchNetwork() var chainId = GetChainId(); if (chainId != $"{ChainModel.EvmNamespace}:{chainConfig.ChainId}") { - await PromptNetworkSwitch(); + await SwitchChain(chainConfig.ChainId); UpdateSessionChainId(); } } @@ -275,28 +274,6 @@ private List ConvertArrayToListAndRemoveFirst(T[] array) return list; } - private async Task PromptNetworkSwitch() - { - var str = $"{BigInteger.Parse(chainConfig.ChainId):X}"; - str = str.TrimStart('0'); - var networkSwitchParams = new - { - chainId = $"0x{str}", // Convert the Chain ID to hex format - }; - - try - { - await Request("wallet_switchEthereumChain", networkSwitchParams); - WCLogger.Log("Network switch requested."); - } - catch (Exception ex) - { - WCLogger.LogError($"Network switch failed: {ex.Message}"); - - // Optionally, you can prompt the user with a UI dialog here. - } - } - public override async Task Disconnect() { if (!connected) diff --git a/src/ChainSafe.Gaming/MultiCall/IMultiCall.cs b/src/ChainSafe.Gaming/MultiCall/IMultiCall.cs index cddce0c9d..e2d180d6b 100644 --- a/src/ChainSafe.Gaming/MultiCall/IMultiCall.cs +++ b/src/ChainSafe.Gaming/MultiCall/IMultiCall.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Threading.Tasks; -using ChainSafe.Gaming.Web3.Core; using Nethereum.Contracts.QueryHandlers.MultiCall; namespace ChainSafe.Gaming.MultiCall @@ -8,7 +7,7 @@ namespace ChainSafe.Gaming.MultiCall /// /// Represents an interface for making batched Ethereum function calls using the MultiCall service. /// - public interface IMultiCall : ILifecycleParticipant + public interface IMultiCall { /// /// Executes a batch of Ethereum function calls using the MultiCall service asynchronously. diff --git a/src/ChainSafe.Gaming/MultiCall/MultiCall.cs b/src/ChainSafe.Gaming/MultiCall/MultiCall.cs index 039b17a3b..9f9ed36d3 100644 --- a/src/ChainSafe.Gaming/MultiCall/MultiCall.cs +++ b/src/ChainSafe.Gaming/MultiCall/MultiCall.cs @@ -1,15 +1,18 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ChainSafe.Gaming.Evm.Contracts; using ChainSafe.Gaming.Web3; +using ChainSafe.Gaming.Web3.Core; +using ChainSafe.Gaming.Web3.Core.Chains; using Nethereum.ABI.FunctionEncoding; using Nethereum.Contracts.QueryHandlers.MultiCall; using Nethereum.Util; namespace ChainSafe.Gaming.MultiCall { - public class MultiCall : IMultiCall + public class MultiCall : IMultiCall, IChainSwitchHandler, ILifecycleParticipant { private const int DefaultCallsPerRequest = 3000; @@ -17,7 +20,12 @@ public class MultiCall : IMultiCall "[{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes[]\",\"name\":\"returnData\",\"type\":\"bytes[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"allowFailure\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Call3[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate3\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"allowFailure\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Call3Value[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate3Value\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"blockAndAggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getBasefee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"basefee\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"name\":\"getBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getBlockNumber\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getChainId\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"chainid\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockCoinbase\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"coinbase\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockDifficulty\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"difficulty\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockGasLimit\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"gaslimit\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockTimestamp\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"getEthBalance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getLastBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bool\",\"name\":\"requireSuccess\",\"type\":\"bool\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"tryAggregate\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bool\",\"name\":\"requireSuccess\",\"type\":\"bool\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"tryBlockAndAggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"}]"; private const string DefaultDeploymentAddress = "0xcA11bde05977b3631167028862bE2a173976CA11"; - private readonly Contract multiCallContract; + + private readonly IContractBuilder builder; + private readonly IChainConfig chainConfig; + private readonly MultiCallConfig config; + + private Contract multiCallContract; /// /// Initializes a new instance of the class. @@ -27,17 +35,24 @@ public class MultiCall : IMultiCall /// The configuration settings for MultiCall. public MultiCall(IContractBuilder builder, IChainConfig chainConfig, MultiCallConfig config) { - if (chainConfig.ChainId != null && MultiCallDefaults.DeployedNetworks.Contains(chainConfig.ChainId)) - { - multiCallContract = builder.Build(ContractAbi, DefaultDeploymentAddress); - } - else - { - if (chainConfig.ChainId != null && config.CustomNetworks.TryGetValue(chainConfig.ChainId, out var address)) - { - multiCallContract = builder.Build(ContractAbi, address); - } - } + this.config = config; + this.chainConfig = chainConfig; + this.builder = builder; + } + + public ValueTask WillStartAsync() + { + BuildMulticallContract(); + return default; + } + + public ValueTask WillStopAsync() => default; + + public Task HandleChainSwitching() + { + // build a new instance of the contract client + BuildMulticallContract(); + return default; } /// @@ -99,6 +114,31 @@ public async Task> MultiCallAsync(Call3Value[] multiCalls, int page } } + private void BuildMulticallContract() + { + try + { + if (MultiCallDefaults.DeployedNetworks.Contains(chainConfig.ChainId)) + { + multiCallContract = builder.Build(ContractAbi, DefaultDeploymentAddress); + return; + } + + if (config.CustomNetworks.TryGetValue(chainConfig.ChainId, out var address)) + { + multiCallContract = builder.Build(ContractAbi, address); + } + } + catch (Exception e) + { + throw new Web3Exception("Error occured while building Multicall contract client.", e); + } + + throw new Web3Exception( + "Couldn't build Multicall contract. " + + $"No configuration for chain id \"{chainConfig.ChainId}\" were found."); + } + /// /// Extracts and formats the results of Multicall function calls into a list of objects. /// @@ -116,20 +156,5 @@ private List ExtractResults(IReadOnlyList results) return parsed; } - - /// - /// Asynchronously initializes the Multicall service. Does nothing in this implementation. - /// - /// A representing the asynchronous operation. - public ValueTask WillStartAsync() - { - return default; - } - - /// - /// Asynchronously cleans up and stops the Multicall service. Does nothing in this implementation. - /// - /// A representing the asynchronous operation. - public ValueTask WillStopAsync() => new(Task.CompletedTask); } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/MultiCall/MultiCallExtensions.cs b/src/ChainSafe.Gaming/MultiCall/MultiCallExtensions.cs index b294ac362..2379d4517 100644 --- a/src/ChainSafe.Gaming/MultiCall/MultiCallExtensions.cs +++ b/src/ChainSafe.Gaming/MultiCall/MultiCallExtensions.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using ChainSafe.Gaming.Web3.Build; using ChainSafe.Gaming.Web3.Core; +using ChainSafe.Gaming.Web3.Core.Chains; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -8,7 +9,7 @@ namespace ChainSafe.Gaming.MultiCall { public static class MultiCallExtensions { - private static readonly MultiCallConfig DefaultConfig = new MultiCallConfig(new Dictionary()); + private static readonly MultiCallConfig DefaultConfig = new(new Dictionary()); public static IMultiCall MultiCall(this Web3.Web3 web3) => web3.ServiceProvider.GetRequiredService(); @@ -22,7 +23,7 @@ public static IWeb3ServiceCollection UseMultiCall(this IWeb3ServiceCollection co collection.TryAddSingleton(configuration ?? DefaultConfig); - collection.AddSingleton(); + collection.AddSingleton(); return collection; } diff --git a/src/ChainSafe.Gaming/RPC/Events/EventExtensions.cs b/src/ChainSafe.Gaming/RPC/Events/EventExtensions.cs index 739b57061..63d42c495 100644 --- a/src/ChainSafe.Gaming/RPC/Events/EventExtensions.cs +++ b/src/ChainSafe.Gaming/RPC/Events/EventExtensions.cs @@ -1,5 +1,6 @@ using ChainSafe.Gaming.Web3.Build; using ChainSafe.Gaming.Web3.Core; +using ChainSafe.Gaming.Web3.Core.Chains; namespace ChainSafe.Gaming.RPC.Events { @@ -9,7 +10,7 @@ public static IWeb3ServiceCollection UseEvents(this IWeb3ServiceCollection servi { // todo bind EventPoller implementation of IEventManager when running in WebGL build return services - .AddSingleton(); + .AddSingleton(); } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/RPC/Events/EventManager.cs b/src/ChainSafe.Gaming/RPC/Events/EventManager.cs index 9a6bb7e83..1294e3eda 100644 --- a/src/ChainSafe.Gaming/RPC/Events/EventManager.cs +++ b/src/ChainSafe.Gaming/RPC/Events/EventManager.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using ChainSafe.Gaming.Web3; using ChainSafe.Gaming.Web3.Core; +using ChainSafe.Gaming.Web3.Core.Chains; using ChainSafe.Gaming.Web3.Environment; using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.Contracts; @@ -13,7 +14,7 @@ namespace ChainSafe.Gaming.RPC.Events { - public class EventManager : IEventManager, ILifecycleParticipant + public class EventManager : IEventManager, ILifecycleParticipant, IChainSwitchHandler { private readonly IChainConfig chainConfig; private readonly Dictionary subscriptions = new(); @@ -78,17 +79,53 @@ public async Task Unsubscribe(Action handler) } } + async Task IChainSwitchHandler.HandleChainSwitching() + { + // unsubscribe + foreach (var subscription in subscriptions.Values) + { + if (subscription.NethSubscription is not null) + { + await subscription.NethSubscription.UnsubscribeAsync(); + subscription.NethSubscription = null; + } + } + + // dispose web socket client + if (webSocketClient is not null) + { + webSocketClient?.Dispose(); + webSocketClient = null; + } + + // initialize a new web socket client + webSocketClient = new StreamingWebSocketClient(chainConfig.Ws); + + // subscribe with a new web socket client + foreach (var subscription in subscriptions.Values) + { + subscription.NethSubscription = new EthLogsObservableSubscription(webSocketClient); + subscription.NethSubscription + .GetSubscriptionDataResponsesAsObservable() + .Subscribe(new FilterLogObserver(subscription.LogHandleAction)); + + await subscription.NethSubscription.SubscribeAsync(subscription.EventFilter); + } + } + private async Task InitializeSubscriptionForType() where TEvent : IEventDTO, new() { Subscription rawSubscription = new Subscription(webSocketClient); - var eventFilter = Event.GetEventABI().CreateFilterInput(); + rawSubscription.EventFilter = Event.GetEventABI().CreateFilterInput(); + + rawSubscription.LogHandleAction = HandleLog; rawSubscription .NethSubscription .GetSubscriptionDataResponsesAsObservable() - .Subscribe(new FilterLogObserver(HandleLog)); + .Subscribe(new FilterLogObserver(rawSubscription.LogHandleAction)); - await rawSubscription.NethSubscription.SubscribeAsync(eventFilter); + await rawSubscription.NethSubscription.SubscribeAsync(rawSubscription.EventFilter); subscriptions[typeof(TEvent)] = rawSubscription; return rawSubscription; @@ -136,6 +173,10 @@ private Task TerminateSubscriptionForType(Type type) private abstract class Subscription { public EthLogsObservableSubscription NethSubscription { get; set; } + + public Action LogHandleAction { get; set; } + + public NewFilterInput EventFilter { get; set; } } private class Subscription : Subscription diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs index 8169bcdc0..91edb11ec 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs @@ -58,7 +58,7 @@ public async Task SwitchChain(string newChainId) { foreach (var switchHandler in switchHandlers) { - await switchHandler.HandleChainSwitch(Current); + await switchHandler.HandleChainSwitching(); succeededHandlers.Push(switchHandler); } } @@ -73,7 +73,7 @@ public async Task SwitchChain(string newChainId) try { - await handlerToRevert.HandleChainSwitch(Current); + await handlerToRevert.HandleChainSwitching(); } catch (Exception revertException) { diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/IChainSwitchHandler.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainSwitchHandler.cs index 5e916799b..2d8fae44d 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/IChainSwitchHandler.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainSwitchHandler.cs @@ -4,6 +4,6 @@ namespace ChainSafe.Gaming.Web3.Core.Chains { public interface IChainSwitchHandler { - public Task HandleChainSwitch(IChainConfig newChainConfig); + public Task HandleChainSwitching(); } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Evm/IEvmEvents.cs b/src/ChainSafe.Gaming/Web3/Core/Evm/IEvmEvents.cs index 10174b643..d63b1196b 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Evm/IEvmEvents.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Evm/IEvmEvents.cs @@ -6,12 +6,6 @@ namespace ChainSafe.Gaming.Web3.Core.Evm { public interface IEvmEvents { - /// - /// Represents a delegate for handling chain change events. - /// - /// The chain ID of the new chain. - public delegate void ChainChangedDelegate(ulong chainID); - /// /// Represents a delegate for handling poll error events. /// @@ -31,11 +25,6 @@ public interface IEvmEvents /// The block number of the new block. public delegate void NewBlockDelegate(ulong blockNumber); - /// - /// Occurs when the chain is changed. - /// - public event ChainChangedDelegate ChainChanged; - /// /// Occurs when an error is encountered during polling. /// diff --git a/src/ChainSafe.Gaming/Web3/Core/Evm/IRpcProvider.cs b/src/ChainSafe.Gaming/Web3/Core/Evm/IRpcProvider.cs index 7064311e2..776c289d8 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Evm/IRpcProvider.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Evm/IRpcProvider.cs @@ -12,20 +12,6 @@ namespace ChainSafe.Gaming.Evm.Providers /// public interface IRpcProvider { - /// - /// Gets the last known blockchain network information. - /// - Network.Network LastKnownNetwork { get; } - - /// - /// Asynchronously refreshes and retrieves the current blockchain network information. - /// - /// - /// A representing the asynchronous operation. The task result contains - /// the updated object representing the current network. - /// - Task RefreshNetwork(); - /// /// Asynchronously performs a specific RPC method on the blockchain. /// diff --git a/src/ChainSafe.Gaming/Web3/Core/Nethereum/INethereumAccountAdapter.cs b/src/ChainSafe.Gaming/Web3/Core/Nethereum/INethereumAccountAdapter.cs new file mode 100644 index 000000000..d57ebf136 --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Core/Nethereum/INethereumAccountAdapter.cs @@ -0,0 +1,8 @@ +using Nethereum.RPC.Accounts; + +namespace ChainSafe.Gaming.Web3.Core.Nethereum +{ + public interface INethereumAccountAdapter : IAccount + { + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumAccountAdapter.cs b/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumAccountAdapter.cs index cd6725693..30dabdb31 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumAccountAdapter.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumAccountAdapter.cs @@ -1,30 +1,39 @@ using System; using System.Threading.Tasks; using ChainSafe.Gaming.Evm.Signers; +using ChainSafe.Gaming.Web3.Core.Chains; using ChainSafe.Gaming.Web3.Core.Evm; -using Nethereum.RPC.Accounts; using Nethereum.RPC.AccountSigning; using Nethereum.RPC.NonceServices; using Nethereum.RPC.TransactionManagers; namespace ChainSafe.Gaming.Web3.Core.Nethereum { - public class NethereumAccountAdapter : IAccount + public class NethereumAccountAdapter : INethereumAccountAdapter, IChainSwitchHandler { private readonly ISigner signer; + private readonly NethereumTransactionManagerAdapter transactionManager; + private readonly IChainConfig chainConfig; public NethereumAccountAdapter(IChainConfig chainConfig, ISigner signer, ITransactionExecutor transactionExecutor) { + this.chainConfig = chainConfig; this.signer = signer; - TransactionManager = new NethereumTransactionManagerAdapter(this, chainConfig, transactionExecutor); + transactionManager = new NethereumTransactionManagerAdapter(this, chainConfig, transactionExecutor); } public string Address => signer.PublicAddress; - public ITransactionManager TransactionManager { get; } + public ITransactionManager TransactionManager => transactionManager; public INonceService NonceService { get; set; } public IAccountSigningService AccountSigningService => null; + + public Task HandleChainSwitching() + { + transactionManager.SetChainConfig(chainConfig); + return default; + } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumExtensions.cs b/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumExtensions.cs index 265519f56..8a72b1e58 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumExtensions.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumExtensions.cs @@ -1,5 +1,6 @@ using ChainSafe.Gaming.Evm.Signers; using ChainSafe.Gaming.Web3.Build; +using ChainSafe.Gaming.Web3.Core.Chains; using ChainSafe.Gaming.Web3.Core.Evm; using Microsoft.Extensions.DependencyInjection; @@ -14,7 +15,7 @@ public static IWeb3ServiceCollection UseNethereumAdapters(this IWeb3ServiceColle // build Adapters for Writing too if we can if (services.IsBound() && services.IsBound()) { - services.AddSingleton(); + services.AddSingleton(); } return services; diff --git a/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumTransactionManagerAdapter.cs b/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumTransactionManagerAdapter.cs index 3c3f6364b..c79b82416 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumTransactionManagerAdapter.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumTransactionManagerAdapter.cs @@ -32,5 +32,10 @@ public override Task SignTransactionAsync(TransactionInput transaction) { throw new NotImplementedException($"Signing transaction is not implemented for {nameof(NethereumTransactionManagerAdapter)}."); } + + public void SetChainConfig(IChainConfig chainConfig) + { + ChainId = BigInteger.Parse(chainConfig.ChainId); + } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumWeb3Adapter.cs b/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumWeb3Adapter.cs index fc116f1c3..03269ad41 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumWeb3Adapter.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumWeb3Adapter.cs @@ -19,7 +19,7 @@ public NethereumWeb3Adapter(IClient nethClient) } // build Writing adapter - public NethereumWeb3Adapter(IClient nethClient, NethereumAccountAdapter accountAdapter) + public NethereumWeb3Adapter(IClient nethClient, INethereumAccountAdapter accountAdapter) { original = new global::Nethereum.Web3.Web3(accountAdapter, nethClient); } diff --git a/src/ChainSafe.Gaming/Web3/Evm/EventPoller/EventPoller.cs b/src/ChainSafe.Gaming/Web3/Evm/EventPoller/EventPoller.cs index b0adcc12a..68b4e6200 100644 --- a/src/ChainSafe.Gaming/Web3/Evm/EventPoller/EventPoller.cs +++ b/src/ChainSafe.Gaming/Web3/Evm/EventPoller/EventPoller.cs @@ -10,13 +10,12 @@ namespace ChainSafe.Gaming.Web3.Core.Evm.EventPoller { - internal class EventPoller : IEvmEvents + internal class EventPoller : IEvmEvents // todo: transform into IEventManager and parse logs for events { private readonly EventPollerConfiguration config; private readonly IRpcProvider rpcProvider; private readonly Web3Environment environment; - private IEvmEvents.ChainChangedDelegate chainChanged; private IEvmEvents.PollDelegate poll; private IEvmEvents.NewBlockDelegate newBlock; @@ -34,21 +33,6 @@ public EventPoller(EventPollerConfiguration config, IRpcProvider rpcProvider, We this.environment = environment; } - public event IEvmEvents.ChainChangedDelegate ChainChanged - { - add - { - chainChanged += value; - PollableHandlerAdded(); - } - - remove - { - chainChanged -= value; - PollableHandlerRemoved(); - } - } - public event IEvmEvents.PollErrorDelegate PollError; public event IEvmEvents.PollDelegate Poll @@ -84,7 +68,6 @@ public event IEvmEvents.NewBlockDelegate NewBlock private MulticastDelegate[] AllPollableDelegates() => new MulticastDelegate[] { - chainChanged, poll, newBlock, }; @@ -176,18 +159,9 @@ private async Task DoPoll() { var pollId = nextPollId++; - ulong blockNumber; - Network newNetwork = null; try { blockNumber = await GetBlockNumber(config.PollInterval / 2); - - var lastNetwork = rpcProvider.LastKnownNetwork; - var currentNetwork = await rpcProvider.RefreshNetwork(); - if (lastNetwork.ChainId != currentNetwork.ChainId) - { - newNetwork = currentNetwork; - } } catch (Exception e) { @@ -196,15 +170,8 @@ private async Task DoPoll() return; } - this.blockNumber = blockNumber; - poll?.Invoke(pollId, blockNumber); - if (newNetwork != null) - { - chainChanged?.Invoke(newNetwork.ChainId); - } - if (reportedBlock == 0) { reportedBlock = blockNumber; diff --git a/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientExtensions.cs b/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientExtensions.cs index ff409c4a5..7e4dd620a 100644 --- a/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientExtensions.cs +++ b/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientExtensions.cs @@ -1,8 +1,5 @@ using ChainSafe.Gaming.Evm.Providers; using ChainSafe.Gaming.Web3.Build; -using ChainSafe.Gaming.Web3.Core; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Nethereum.JsonRpc.Client; namespace ChainSafe.Gaming.Evm.JsonRpc @@ -18,7 +15,7 @@ public static IWeb3ServiceCollection UseRpcProvider(this IWeb3ServiceCollection collection.AssertServiceNotBound(); collection.AssertServiceNotBound(); - collection.AddSingleton(); + collection.AddSingleton(); return collection; } diff --git a/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientProvider.cs b/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientProvider.cs index 55c4d33c2..8356c6f6d 100644 --- a/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientProvider.cs +++ b/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientProvider.cs @@ -2,9 +2,7 @@ using System.Threading.Tasks; using ChainSafe.Gaming.Web3; using ChainSafe.Gaming.Web3.Analytics; -using ChainSafe.Gaming.Web3.Core; using ChainSafe.Gaming.Web3.Environment; -using Nethereum.Hex.HexTypes; using Nethereum.JsonRpc.Client; using Nethereum.JsonRpc.Client.RpcMessages; using Newtonsoft.Json; @@ -12,66 +10,17 @@ namespace ChainSafe.Gaming.Evm.Providers { - public class RpcClientProvider : ClientBase, IRpcProvider, ILifecycleParticipant + public class RpcClientProvider : ClientBase, IRpcProvider { - private readonly string rpcNodeUrl; private readonly Web3Environment environment; - private readonly ChainRegistryProvider chainRegistryProvider; private readonly IChainConfig chainConfig; public RpcClientProvider( Web3Environment environment, - ChainRegistryProvider chainRegistryProvider, IChainConfig chainConfig) { - this.chainRegistryProvider = chainRegistryProvider; this.environment = environment; this.chainConfig = chainConfig; - rpcNodeUrl = chainConfig.Rpc; - } - - public Network.Network LastKnownNetwork { get; private set; } - - public async ValueTask WillStartAsync() - { - if (LastKnownNetwork is null || LastKnownNetwork.ChainId == 0) - { - if (ulong.TryParse(chainConfig.ChainId, out var chainId)) - { - var chain = await chainRegistryProvider.GetChain(chainId); - LastKnownNetwork = new Network.Network() - { - ChainId = chainId, - Name = chain?.Name, - }; - } - - LastKnownNetwork = await RefreshNetwork(); - } - } - - public ValueTask WillStopAsync() => new(Task.CompletedTask); - - public async Task RefreshNetwork() - { - // TODO: cache - var chainIdHexString = await Perform("eth_chainId"); - var chainId = new HexBigInteger(chainIdHexString).ToUlong(); - - if (chainId <= 0) - { - throw new Web3Exception("Couldn't detect network"); - } - - if (chainId == LastKnownNetwork.ChainId) - { - return LastKnownNetwork; - } - - var chain = await chainRegistryProvider.GetChain(chainId); - return chain != null - ? new Network.Network { Name = chain.Name, ChainId = chainId } - : new Network.Network { Name = "Unknown", ChainId = chainId }; } public async Task Perform(string method, params object[] parameters) @@ -82,15 +31,11 @@ public async Task Perform(string method, params object[] parameters) try { var request = new RpcRequestMessage(Guid.NewGuid().ToString(), method, parameters); - response = await SendAsync(request); - - var serializer = JsonSerializer.Create(); - return serializer.Deserialize(new JTokenReader(response.Result))!; } catch (Exception ex) { - throw new Web3Exception($"{method} threw an exception:" + response?.Error.Message, ex); + throw new Web3Exception($"RPC method \"{method}\" threw an exception:" + response?.Error.Message, ex); } finally { @@ -104,6 +49,10 @@ public async Task Perform(string method, params object[] parameters) PackageName = "io.chainsafe.web3-unity", }); } + + return JsonSerializer + .Create() + .Deserialize(new JTokenReader(response.Result))!; } protected override async Task SendAsync(RpcRequestMessage request, string route = null) @@ -122,7 +71,7 @@ protected override async Task SendAsync(RpcRequestMessage[ private async Task SendAsyncInternally(string body) { - var result = await environment.HttpClient.PostRaw(rpcNodeUrl, body, "application/json"); + var result = await environment.HttpClient.PostRaw(chainConfig.Rpc, body, "application/json"); return JsonConvert.DeserializeObject(result.Response); } diff --git a/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProvider.cs b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProvider.cs index b3f62237c..0ee4aaf70 100644 --- a/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProvider.cs +++ b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProvider.cs @@ -1,26 +1,32 @@ +using System; +using System.Numerics; using System.Threading.Tasks; using ChainSafe.Gaming.Evm; -using ChainSafe.Gaming.Evm.Network; using ChainSafe.Gaming.Evm.Providers; +using ChainSafe.Gaming.Web3.Core.Chains; using ChainSafe.Gaming.Web3.Environment; -using Nethereum.Hex.HexTypes; namespace ChainSafe.Gaming.Web3.Evm.Wallet { /// /// Concrete implementation of . /// - public abstract class WalletProvider : RpcClientProvider, IWalletProvider + public abstract class WalletProvider : RpcClientProvider, IWalletProvider, IChainSwitchHandler // todo make sure chain id is in sync for wallet and sdk { + private readonly ILogWriter logWriter; + private readonly IChainConfig chainConfig; + /// /// Initializes a new instance of the class. /// /// Injected . /// Injected . /// Injected . - protected WalletProvider(Web3Environment environment, ChainRegistryProvider chainRegistryProvider, IChainConfig chainConfig) - : base(environment, chainRegistryProvider, chainConfig) + protected WalletProvider(Web3Environment environment, IChainConfig chainConfig) + : base(environment, chainConfig) { + this.chainConfig = chainConfig; + this.logWriter = environment.LogWriter; } public abstract Task Connect(); @@ -28,5 +34,31 @@ protected WalletProvider(Web3Environment environment, ChainRegistryProvider chai public abstract Task Disconnect(); public abstract Task Request(string method, params object[] parameters); + + public Task HandleChainSwitching() + { + return SwitchChain(chainConfig.ChainId); + } + + protected async Task SwitchChain(string chainId) + { + var str = $"{BigInteger.Parse(chainId):X}"; + str = str.TrimStart('0'); + var networkSwitchParams = new + { + chainId = $"0x{str}", // Convert the Chain ID to hex format + }; + + try + { + await Request("wallet_switchEthereumChain", networkSwitchParams); + } + catch (Exception ex) + { + throw new Web3Exception($"Error occured while trying to switch wallet chain.", ex); + } + + logWriter.Log("Wallet chain switch complete."); + } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProviderExtensions.cs b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProviderExtensions.cs index 4f5656a97..003c18e03 100644 --- a/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProviderExtensions.cs +++ b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProviderExtensions.cs @@ -1,6 +1,7 @@ using ChainSafe.Gaming.Evm.Signers; using ChainSafe.Gaming.Web3.Build; using ChainSafe.Gaming.Web3.Core; +using ChainSafe.Gaming.Web3.Core.Chains; using ChainSafe.Gaming.Web3.Core.Evm; using ChainSafe.Gaming.Web3.Core.Logout; using Microsoft.Extensions.DependencyInjection; @@ -22,7 +23,7 @@ public static IWeb3ServiceCollection UseWalletProvider(this IWeb3Serv { collection.AssertServiceNotBound(); - collection.AddSingleton(); + collection.AddSingleton(); collection.Replace(ServiceDescriptor.Singleton(typeof(IWalletProviderConfig), config)); From 68c9ef2c577955c90d6219263da1258908899608 Mon Sep 17 00:00:00 2001 From: creeppak Date: Wed, 11 Sep 2024 13:18:04 +0100 Subject: [PATCH 07/24] Applied changes lost during the last merge --- .../Editor/Web3SettingsEditor.ChainSettings.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs index c2dbe995a..db270d940 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs @@ -173,7 +173,7 @@ private void OnRemoveClick() private void UpdateServerMenuInfo(bool chainSwitched = false) { // Get the selected chain index - selectedChainIndex = Array.FindIndex(window.chainList.ToArray(), x => x.name == chainConfig.Chain); + selectedChainIndex = window.chainList.FindIndex(x => x.name == chainConfig.Chain); // Check if the selectedChainIndex is valid if (selectedChainIndex >= 0 && selectedChainIndex < window.chainList.Count) { @@ -215,7 +215,8 @@ private void UpdateServerMenuInfo(bool chainSwitched = false) else { // Handle the case where the selected chain is not found - Debug.LogError("Selected chain not found in the chainList."); + Debug.LogError("Couldn't find the chain, switching to default chain."); + selectedChainIndex = window.chainList.FindIndex(x => x.name == ChainConfigEntry.Default.Chain); } } } From 659989f98db87652472c00b8c3679da06a105149 Mon Sep 17 00:00:00 2001 From: creeppak Date: Wed, 11 Sep 2024 13:44:43 +0100 Subject: [PATCH 08/24] Fixed DI issue with ChainManager --- src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs index 8169bcdc0..111f96573 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs @@ -10,10 +10,10 @@ namespace ChainSafe.Gaming.Web3.Core.Chains public class ChainManager : IChainManager { private readonly Dictionary configs; - private readonly IList switchHandlers; + private readonly IEnumerable switchHandlers; private readonly ILogWriter logWriter; - public ChainManager(IChainConfigSet configSet, IList switchHandlers, ILogWriter logWriter) + public ChainManager(IChainConfigSet configSet, IEnumerable switchHandlers, ILogWriter logWriter) { this.logWriter = logWriter; this.switchHandlers = switchHandlers; From cf902beecbba649396d76d131acc1916ad176dba Mon Sep 17 00:00:00 2001 From: creeppak Date: Thu, 12 Sep 2024 14:10:08 +0100 Subject: [PATCH 09/24] Fixed WebGL build after the Chain Switching functionality was introduced --- .../Runtime/Web3AuthConnectionProvider.cs | 13 +++- .../Editor/Web3SettingsEditor.cs | 78 +++++++++++++++++-- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Web3AuthConnectionProvider.cs b/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Web3AuthConnectionProvider.cs index 19efc21ef..5f5e8ae5e 100644 --- a/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Web3AuthConnectionProvider.cs +++ b/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Web3AuthConnectionProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using System.Threading.Tasks; @@ -75,10 +76,18 @@ public override async Task Initialize() _initializeTcs = new TaskCompletionSource(); var projectConfig = ProjectConfigUtilities.Load(); + + var chainConfig = projectConfig.ChainConfigs.FirstOrDefault(); + + if (chainConfig is null) + { + Debug.LogError($"Couldn't initialize {nameof(Web3AuthConnectionProvider)}. No Chain Config were found in Project Config."); + return; + } //1155 is a decimal number, we need to convert it to an integer - InitWeb3Auth(clientId, new HexBigInteger(BigInteger.Parse(projectConfig.ChainId)).HexValue, - projectConfig.Rpc, projectConfig.Network, "", projectConfig.Symbol, "", network.ToString().ToLower(), Initialized, InitializeError); + InitWeb3Auth(clientId, new HexBigInteger(BigInteger.Parse(chainConfig.ChainId)).HexValue, + chainConfig.Rpc, chainConfig.Network, "", chainConfig.Symbol, "", network.ToString().ToLower(), Initialized, InitializeError); await _initializeTcs.Task; } diff --git a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs index c94001221..fbc597d8d 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; using ChainSafe.Gaming; using ChainSafe.Gaming.UnityPackage; @@ -21,7 +23,7 @@ public partial class Web3SettingsEditor : EditorWindow [MenuItem("ChainSafe SDK/Project Settings", false, 1)] public static void ShowWindow() { - // Show existing window instance. If one doesn't exist, make one. + // Show existing window instance. If it doesn't exist, make one. var window = GetWindow(typeof(Web3SettingsEditor)); window.titleContent = new GUIContent("Web3 Settings"); window.minSize = new Vector2(450, 300); @@ -29,7 +31,63 @@ public static void ShowWindow() public static void WriteNetworkFile() { - throw new NotImplementedException(); + Debug.Log("Updating network.js..."); + + var projectConfig = ProjectConfigUtilities.CreateOrLoad(); + + if (!projectConfig.ChainConfigs.Any()) + { + Debug.LogError("Can not generate network.js files for WebGL. Please add at least one Chain Config to continue."); + return; + } + + // declares paths to write our javascript files to + var path1 = "Assets/WebGLTemplates/Web3GL-2020x/network.js"; + var path2 = "Assets/WebGLTemplates/Web3GL-MetaMask/network.js"; + + if (AssetDatabase.IsValidFolder(Path.GetDirectoryName(path1))) + { + // write data to the webgl default network file + var sb = new StringBuilder(); + sb.AppendLine("//You can see a list of compatible EVM chains at https://chainlist.org/"); + sb.AppendLine("window.networks = ["); + for (var i = 0; i < projectConfig.ChainConfigs.Count; i++) + { + var chainConfig = projectConfig.ChainConfigs[i]; + var isLast = i == projectConfig.ChainConfigs.Count - 1; + sb.AppendLine(" {"); + sb.AppendLine(" id: " + chainConfig.ChainId + ","); + sb.AppendLine(" label: " + '"' + chainConfig.Chain + " " + chainConfig.Network + '"' + ","); + sb.AppendLine(" token: " + '"' + chainConfig.Symbol + '"' + ","); + sb.AppendLine(" rpcUrl: " + "'" + chainConfig.Rpc + "'" + ","); + sb.AppendLine(!isLast ? " }," : " }"); + } + sb.AppendLine("]"); + File.WriteAllText(path1, sb.ToString()); + } + else + { + Debug.LogWarning( + $"{Path.GetDirectoryName(path1)} is missing, network.js file will not be updated for this template"); + } + + if (AssetDatabase.IsValidFolder(Path.GetDirectoryName(path2))) + { + // writes data to the webgl metamask network file + var sb = new StringBuilder(); + sb.AppendLine("//You can see a list of compatible EVM chains at https://chainlist.org/"); + sb.AppendLine("window.web3ChainId = " + projectConfig.ChainConfigs.First().ChainId + ";"); + File.WriteAllText(path2, sb.ToString()); + } + else + { + Debug.LogWarning( + $"{Path.GetDirectoryName(path2)} is missing, network.js file will not be updated for this template"); + } + + AssetDatabase.Refresh(); + + Debug.Log("Done"); } private static GUIStyle centeredLabelStyle; @@ -238,19 +296,23 @@ private void RemoveChainConfigEntry(string chainId) /// private static async void ValidateProjectID(string projectID) { + bool projectIdValid; try { - if (await ValidateProjectIDAsync(projectID)) - { -#if UNITY_WEBGL - WriteNetworkFile(); -#endif - } + projectIdValid = await ValidateProjectIDAsync(projectID); } catch (Exception e) { Debug.LogError("Failed to validate project ID"); Debug.LogException(e); + return; + } + + if (projectIdValid) + { +#if UNITY_WEBGL + WriteNetworkFile(); +#endif } static async Task ValidateProjectIDAsync(string projectID) From c591f13b2765a4c1e943c417bfc0355872d8b8b2 Mon Sep 17 00:00:00 2001 From: creeppak Date: Thu, 12 Sep 2024 14:20:24 +0100 Subject: [PATCH 10/24] Made Setting editor open up with the right tab when prompting to enter the Project ID --- .../io.chainsafe.web3-unity/Editor/Startup.cs | 2 +- .../Editor/Web3SettingsEditor.cs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Packages/io.chainsafe.web3-unity/Editor/Startup.cs b/Packages/io.chainsafe.web3-unity/Editor/Startup.cs index 35e4eb156..1a04da5ea 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/Startup.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/Startup.cs @@ -56,7 +56,7 @@ static void ValidateProjectID() var projectID = ProjectConfigUtilities.Load()?.ProjectId; if (string.IsNullOrWhiteSpace(projectID)) { - Web3SettingsEditor.ShowWindow(); + Web3SettingsEditor.ShowWindow(Web3SettingsEditor.Tabs.Project); } } catch (Exception e) diff --git a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs index c94001221..6a31f69b1 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs @@ -19,12 +19,19 @@ public partial class Web3SettingsEditor : EditorWindow // Initializes window [MenuItem("ChainSafe SDK/Project Settings", false, 1)] - public static void ShowWindow() + public static void ShowWindow() => ShowWindow(null); + + public static void ShowWindow(Tabs? tabOverride = null) { // Show existing window instance. If one doesn't exist, make one. - var window = GetWindow(typeof(Web3SettingsEditor)); + var window = (Web3SettingsEditor) GetWindow(typeof(Web3SettingsEditor)); window.titleContent = new GUIContent("Web3 Settings"); window.minSize = new Vector2(450, 300); + + if (tabOverride.HasValue) + { + window.ActiveTab = tabOverride.Value; + } } public static void WriteNetworkFile() @@ -337,8 +344,8 @@ private class ValidateProjectIDResponse { [JsonProperty("response")] public bool Response { get; set; } } - - private enum Tabs + + public enum Tabs { Project = 0, Chains = 1 From f299a5d7b1eaa1ba0562c78257b1fb1bde5148b0 Mon Sep 17 00:00:00 2001 From: creeppak Date: Thu, 12 Sep 2024 14:20:48 +0100 Subject: [PATCH 11/24] Compile errors fixes --- .../Runtime/Web3AuthProvider.cs | 2 +- .../Runtime/Web3AuthWebGLProvider.cs | 2 +- .../Tests/Runtime/StubWalletConnectProvider.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Web3AuthProvider.cs b/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Web3AuthProvider.cs index b39ddbc9e..781088d6b 100644 --- a/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Web3AuthProvider.cs +++ b/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Web3AuthProvider.cs @@ -20,7 +20,7 @@ public class Web3AuthProvider : WalletProvider, IAccountProvider private TaskCompletionSource _connectTcs; private TaskCompletionSource _disconnectTcs; - public Web3AuthProvider(Web3AuthWalletConfig config, Web3Environment environment, IChainConfig chainConfig, ChainRegistryProvider chainRegistryProvider) : base(environment, chainRegistryProvider, chainConfig) + public Web3AuthProvider(Web3AuthWalletConfig config, Web3Environment environment, IChainConfig chainConfig) : base(environment, chainConfig) { _config = config; } diff --git a/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Web3AuthWebGLProvider.cs b/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Web3AuthWebGLProvider.cs index fcb1103ef..bddc8dff2 100644 --- a/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Web3AuthWebGLProvider.cs +++ b/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Web3AuthWebGLProvider.cs @@ -11,7 +11,7 @@ public class Web3AuthWebGLProvider : Web3AuthProvider { private readonly Web3AuthWalletConfig _config; - public Web3AuthWebGLProvider(Web3AuthWalletConfig config, Web3Environment environment, IChainConfig chainConfig, ChainRegistryProvider chainRegistryProvider) : base(config, environment, chainConfig, chainRegistryProvider) + public Web3AuthWebGLProvider(Web3AuthWalletConfig config, Web3Environment environment, IChainConfig chainConfig) : base(config, environment, chainConfig) { _config = config; } diff --git a/Packages/io.chainsafe.web3-unity/Tests/Runtime/StubWalletConnectProvider.cs b/Packages/io.chainsafe.web3-unity/Tests/Runtime/StubWalletConnectProvider.cs index f400e5675..728de38ee 100644 --- a/Packages/io.chainsafe.web3-unity/Tests/Runtime/StubWalletConnectProvider.cs +++ b/Packages/io.chainsafe.web3-unity/Tests/Runtime/StubWalletConnectProvider.cs @@ -15,7 +15,7 @@ public class StubWalletConnectProvider : WalletProvider private readonly IChainConfig chainConfig; private readonly IHttpClient httpClient; - public StubWalletConnectProvider(StubWalletConnectProviderConfig config, Web3Environment environment, IChainConfig chainConfig, ChainRegistryProvider chainRegistryProvider) : base(environment, chainRegistryProvider, chainConfig) + public StubWalletConnectProvider(StubWalletConnectProviderConfig config, Web3Environment environment, IChainConfig chainConfig) : base(environment, chainConfig) { this.config = config; this.chainConfig = chainConfig; From 558ce89813474361ada11b9a0fcee3f71334a3e8 Mon Sep 17 00:00:00 2001 From: creeppak Date: Fri, 13 Sep 2024 12:41:36 +0100 Subject: [PATCH 12/24] Removed `WriteNetworkFile` method --- .../Editor/Web3SettingsEditor.cs | 61 ------------------- .../Editor/WebGLTemplateSync.cs | 2 - 2 files changed, 63 deletions(-) diff --git a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs index 194699417..119f17d8e 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs @@ -35,67 +35,6 @@ public static void ShowWindow(Tabs? tabOverride = null) window.ActiveTab = tabOverride.Value; } } - - public static void WriteNetworkFile() - { - Debug.Log("Updating network.js..."); - - var projectConfig = ProjectConfigUtilities.CreateOrLoad(); - - if (!projectConfig.ChainConfigs.Any()) - { - Debug.LogError("Can not generate network.js files for WebGL. Please add at least one Chain Config to continue."); - return; - } - - // declares paths to write our javascript files to - var path1 = "Assets/WebGLTemplates/Web3GL-2020x/network.js"; - var path2 = "Assets/WebGLTemplates/Web3GL-MetaMask/network.js"; - - if (AssetDatabase.IsValidFolder(Path.GetDirectoryName(path1))) - { - // write data to the webgl default network file - var sb = new StringBuilder(); - sb.AppendLine("//You can see a list of compatible EVM chains at https://chainlist.org/"); - sb.AppendLine("window.networks = ["); - for (var i = 0; i < projectConfig.ChainConfigs.Count; i++) - { - var chainConfig = projectConfig.ChainConfigs[i]; - var isLast = i == projectConfig.ChainConfigs.Count - 1; - sb.AppendLine(" {"); - sb.AppendLine(" id: " + chainConfig.ChainId + ","); - sb.AppendLine(" label: " + '"' + chainConfig.Chain + " " + chainConfig.Network + '"' + ","); - sb.AppendLine(" token: " + '"' + chainConfig.Symbol + '"' + ","); - sb.AppendLine(" rpcUrl: " + "'" + chainConfig.Rpc + "'" + ","); - sb.AppendLine(!isLast ? " }," : " }"); - } - sb.AppendLine("]"); - File.WriteAllText(path1, sb.ToString()); - } - else - { - Debug.LogWarning( - $"{Path.GetDirectoryName(path1)} is missing, network.js file will not be updated for this template"); - } - - if (AssetDatabase.IsValidFolder(Path.GetDirectoryName(path2))) - { - // writes data to the webgl metamask network file - var sb = new StringBuilder(); - sb.AppendLine("//You can see a list of compatible EVM chains at https://chainlist.org/"); - sb.AppendLine("window.web3ChainId = " + projectConfig.ChainConfigs.First().ChainId + ";"); - File.WriteAllText(path2, sb.ToString()); - } - else - { - Debug.LogWarning( - $"{Path.GetDirectoryName(path2)} is missing, network.js file will not be updated for this template"); - } - - AssetDatabase.Refresh(); - - Debug.Log("Done"); - } private static GUIStyle centeredLabelStyle; private static GUIStyle wrappedGreyMiniLabel; diff --git a/Packages/io.chainsafe.web3-unity/Editor/WebGLTemplateSync.cs b/Packages/io.chainsafe.web3-unity/Editor/WebGLTemplateSync.cs index f492c3615..9f7c9329a 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/WebGLTemplateSync.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/WebGLTemplateSync.cs @@ -72,8 +72,6 @@ public static void Syncronize() { AssetDatabase.AllowAutoRefresh(); AssetDatabase.Refresh(); - // Update template values to chain config - Web3SettingsEditor.WriteNetworkFile(); } } From 822d55b8f617988a57ae399e6340554390771175 Mon Sep 17 00:00:00 2001 From: creeppak Date: Fri, 13 Sep 2024 12:56:59 +0100 Subject: [PATCH 13/24] Rolled back eventName update --- src/ChainSafe.Gaming.Unity.MetaMask/MetaMaskProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ChainSafe.Gaming.Unity.MetaMask/MetaMaskProvider.cs b/src/ChainSafe.Gaming.Unity.MetaMask/MetaMaskProvider.cs index 1cc25eacb..37a5ecbe2 100644 --- a/src/ChainSafe.Gaming.Unity.MetaMask/MetaMaskProvider.cs +++ b/src/ChainSafe.Gaming.Unity.MetaMask/MetaMaskProvider.cs @@ -80,7 +80,7 @@ public override async Task Connect() analyticsClient.CaptureEvent(new AnalyticsEvent() { - EventName = "MetaMask WebGL Initialized", + EventName = "Metamask WebGL Initialized", PackageName = "io.chainsafe.web3-unity", }); From dfe6682327aedc127bf2b6f22cf07752b296e909 Mon Sep 17 00:00:00 2001 From: creeppak Date: Fri, 13 Sep 2024 12:57:59 +0100 Subject: [PATCH 14/24] Added SwitchChainCalls to SampleMain --- .../Scenes/SampleMain.unity | 713 +++++++++++++++++- .../Scenes/SampleMain/SwitchChain.meta | 3 + .../SwitchChain/EchoChainContract.cs | 139 ++++ .../SwitchChain/EchoChainContract.cs.meta | 11 + .../SwitchChain/SwitchChainCalls.cs | 44 ++ .../SwitchChain/SwitchChainCalls.cs.meta | 3 + 6 files changed, 911 insertions(+), 2 deletions(-) create mode 100644 src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain.meta create mode 100644 src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/EchoChainContract.cs create mode 100644 src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/EchoChainContract.cs.meta create mode 100644 src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs create mode 100644 src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs.meta diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity index a81b8930e..e7f93dd04 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity @@ -38,7 +38,6 @@ RenderSettings: m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 0} - m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: @@ -1161,6 +1160,7 @@ Transform: - {fileID: 626054679} - {fileID: 496534497} - {fileID: 2077011803} + - {fileID: 1665307234} - {fileID: 1993709007} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} @@ -3152,7 +3152,7 @@ MonoBehaviour: m_HandleRect: {fileID: 37375165} m_Direction: 0 m_Value: 0 - m_Size: 0.23710066 + m_Size: 0.2485437 m_NumberOfSteps: 0 m_OnValueChanged: m_PersistentCalls: @@ -3955,9 +3955,13 @@ MonoBehaviour: marketplaceToDelete: Set marketplace to delete collectionContract721: Set 721 collection to mint to uri721: Set metadata uri with full path i.e. https://ipfs.chainsafe.io/ipfs/bafyjvzacdj4apx52hvbyjkwyf7i6a7t3pcqd4kw4xxfc67hgvn3a + name721: Set Nft name + description721: Set Nft description collectionContract1155: Set 1155 collection to mint to uri1155: Set metadata uri with full path i.e. https://ipfs.chainsafe.io/ipfs/bafyjvzacdj4apx52hvbyjkwyf7i6a7t3pcqd4kw4xxfc67hgvn3a amount1155: Set amount of Nfts to mint i.e 1 + name1155: Set Nft name + description1155: Set Nft description marketplaceName: Set marketplace name marketplaceDescription: Set marketplace description marketplaceWhitelisting: 0 @@ -9944,6 +9948,212 @@ RectTransform: m_CorrespondingSourceObject: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} m_PrefabInstance: {fileID: 1133733956} m_PrefabAsset: {fileID: 0} +--- !u!1001 &1165839531 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 2011699642} + m_Modifications: + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_RootOrder + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalScale.x + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalScale.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalScale.z + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.size + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Mode + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Mode + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Target + value: + objectReference: {fileID: 1665307235} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Target + value: + objectReference: {fileID: 7978786053810957042} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_CallState + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_CallState + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_MethodName + value: CallSmartContract + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_MethodName + value: Execute + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_TargetAssemblyTypeName + value: Samples.Behaviours.SwitchChain.SwitchChainCalls, Samples + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_TargetAssemblyTypeName + value: Samples.Behaviours.SampleBehaviour, Samples + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Arguments.m_ObjectArgumentAssemblyTypeName + value: UnityEngine.Object, UnityEngine + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Arguments.m_ObjectArgumentAssemblyTypeName + value: UnityEngine.Object, UnityEngine + objectReference: {fileID: 0} + - target: {fileID: 8775736491206355084, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_Name + value: Button - Echo + objectReference: {fileID: 0} + - target: {fileID: 8781309615174179339, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_text + value: Call Test Smart-Contract + objectReference: {fileID: 0} + - target: {fileID: 8781309615174179339, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_fontSize + value: 18.6 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 8775736491206355084, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + insertIndex: -1 + addedObject: {fileID: 1165839534} + m_SourcePrefab: {fileID: 100100000, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} +--- !u!224 &1165839532 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + m_PrefabInstance: {fileID: 1165839531} + m_PrefabAsset: {fileID: 0} +--- !u!1 &1165839533 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 8775736491206355084, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + m_PrefabInstance: {fileID: 1165839531} + m_PrefabAsset: {fileID: 0} +--- !u!114 &1165839534 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1165839533} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 0 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: 50 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 --- !u!1 &1170271242 GameObject: m_ObjectHideFlags: 0 @@ -12533,6 +12743,7 @@ RectTransform: - {fileID: 876509800} - {fileID: 1300152324561942831} - {fileID: 199502201} + - {fileID: 2011699641} - {fileID: 1019211773} - {fileID: 743188457} - {fileID: 1432457964} @@ -12995,6 +13206,55 @@ RectTransform: m_CorrespondingSourceObject: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} m_PrefabInstance: {fileID: 1633924247} m_PrefabAsset: {fileID: 0} +--- !u!1 &1665307233 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1665307234} + - component: {fileID: 1665307235} + m_Layer: 0 + m_Name: SwitchChainCalls + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1665307234 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1665307233} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 67264514} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1665307235 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1665307233} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: ccc3f4b11c1e441b8c9f7b998173cc54, type: 3} + m_Name: + m_EditorClassIdentifier: + chainSetups: + - chainId: 11155111 + contractAddress: 0x3ea3542463ed9464CB7B48618d4c1627038bb8D8 + - chainId: 11155420 + contractAddress: 0x78E755e5b3D65aeF313f57421ae737Dd3ab689Fc --- !u!1001 &1670752445 PrefabInstance: m_ObjectHideFlags: 0 @@ -15397,6 +15657,455 @@ RectTransform: m_CorrespondingSourceObject: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} m_PrefabInstance: {fileID: 2011409375} m_PrefabAsset: {fileID: 0} +--- !u!1001 &2011699640 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 1547239394} + m_Modifications: + - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.size + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Mode + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Mode + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Target + value: + objectReference: {fileID: 1665307235} + - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Target + value: + objectReference: {fileID: 7978786053810957042} + - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_CallState + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_CallState + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_MethodName + value: ToggleChain + objectReference: {fileID: 0} + - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_MethodName + value: Execute + objectReference: {fileID: 0} + - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_TargetAssemblyTypeName + value: Samples.Behaviours.SwitchChain.SwitchChainCalls, Samples + objectReference: {fileID: 0} + - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_TargetAssemblyTypeName + value: Samples.Behaviours.SampleBehaviour, Samples + objectReference: {fileID: 0} + - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Arguments.m_ObjectArgumentAssemblyTypeName + value: UnityEngine.Object, UnityEngine + objectReference: {fileID: 0} + - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Arguments.m_ObjectArgumentAssemblyTypeName + value: UnityEngine.Object, UnityEngine + objectReference: {fileID: 0} + - target: {fileID: 531827906274991465, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: Target + value: + objectReference: {fileID: 1665307233} + - target: {fileID: 1196625356310129404, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.x + value: 134 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_RootOrder + value: 5 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_SizeDelta.y + value: 574 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1438947642840759288, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.x + value: -135 + objectReference: {fileID: 0} + - target: {fileID: 2068160274303812336, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.x + value: -131 + objectReference: {fileID: 0} + - target: {fileID: 2290226172473003671, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_text + value: Toggle Chain + objectReference: {fileID: 0} + - target: {fileID: 2290226172473003671, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_fontSize + value: 18.85 + objectReference: {fileID: 0} + - target: {fileID: 2295802096196926480, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_Name + value: Button - Toggle Chains + objectReference: {fileID: 0} + - target: {fileID: 2295802096196926480, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_IsActive + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2597086131278812474, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2597086131278812474, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2597086131278812474, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2597086131278812474, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2597086131278812474, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2597086131278812474, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2966402704676899854, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.size + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2966402704676899854, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Mode + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2966402704676899854, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Target + value: + objectReference: {fileID: 2011699643} + - target: {fileID: 2966402704676899854, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_CallState + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 2966402704676899854, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_MethodName + value: Select + objectReference: {fileID: 0} + - target: {fileID: 2966402704676899854, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_TargetAssemblyTypeName + value: SelectAssetInEditor, Samples + objectReference: {fileID: 0} + - target: {fileID: 2966402704676899854, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Arguments.m_ObjectArgumentAssemblyTypeName + value: UnityEngine.Object, UnityEngine + objectReference: {fileID: 0} + - target: {fileID: 3719940706716398937, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3719940706716398937, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.y + value: -0.00036621094 + objectReference: {fileID: 0} + - target: {fileID: 3777435202905068912, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_IgnoreLayout + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3842012009874044970, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_fontSize + value: 22.45 + objectReference: {fileID: 0} + - target: {fileID: 3987575268489723521, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_RootOrder + value: 4 + objectReference: {fileID: 0} + - target: {fileID: 3987575268489723521, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3987575268489723521, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3987575268489723521, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3987575268489723521, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3987575268489723521, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5508192653135270670, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_RootOrder + value: 4 + objectReference: {fileID: 0} + - target: {fileID: 5508192653135270670, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5508192653135270670, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5508192653135270670, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5508192653135270670, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5508192653135270670, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5575651454046172251, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_text + value: + objectReference: {fileID: 0} + - target: {fileID: 5575651454046172251, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_fontSize + value: 12.95 + objectReference: {fileID: 0} + - target: {fileID: 5575651454046172251, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_margin.w + value: 52.193153 + objectReference: {fileID: 0} + - target: {fileID: 5575651454046172251, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_margin.z + value: -2.6824493 + objectReference: {fileID: 0} + - target: {fileID: 5575651454046172251, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_VerticalAlignment + value: 512 + objectReference: {fileID: 0} + - target: {fileID: 5575651454046172251, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_HorizontalAlignment + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 5855924980481563309, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_Name + value: Category - Switch Chain + objectReference: {fileID: 0} + - target: {fileID: 5880704299188231306, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_text + value: Switch Chains + objectReference: {fileID: 0} + - target: {fileID: 5880704299188231306, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_VerticalAlignment + value: 256 + objectReference: {fileID: 0} + - target: {fileID: 5880704299188231306, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_HorizontalAlignment + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7607616083152500082, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7607616083152500082, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.y + value: -2 + objectReference: {fileID: 0} + - target: {fileID: 8309710637256427594, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8309710637256427594, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8309710637256427594, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8309710637256427594, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8309710637256427594, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8349020101940296388, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.x + value: -129 + objectReference: {fileID: 0} + - target: {fileID: 8349020101940296388, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.y + value: -2 + objectReference: {fileID: 0} + - target: {fileID: 8632572386326910067, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8632572386326910067, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8632572386326910067, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8632572386326910067, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8632572386326910067, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8632572386326910067, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: + - targetCorrespondingSourceObject: {fileID: 3719940706716398937, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + insertIndex: -1 + addedObject: {fileID: 1165839532} + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 5855924980481563309, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + insertIndex: -1 + addedObject: {fileID: 2011699645} + m_SourcePrefab: {fileID: 100100000, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} +--- !u!224 &2011699641 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 1300152325659962740, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + m_PrefabInstance: {fileID: 2011699640} + m_PrefabAsset: {fileID: 0} +--- !u!224 &2011699642 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 3719940706716398937, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + m_PrefabInstance: {fileID: 2011699640} + m_PrefabAsset: {fileID: 0} +--- !u!114 &2011699643 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 531827906274991465, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + m_PrefabInstance: {fileID: 2011699640} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1d92ec8189c0cc84a95f0cfc8bc95bd2, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &2011699644 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 5855924980481563309, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + m_PrefabInstance: {fileID: 2011699640} + m_PrefabAsset: {fileID: 0} +--- !u!114 &2011699645 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2011699644} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 0 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: 550 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 --- !u!224 &2013807919 stripped RectTransform: m_CorrespondingSourceObject: {fileID: 3719940706716398937, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain.meta b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain.meta new file mode 100644 index 000000000..8a3ac6455 --- /dev/null +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5da89fd878984e46bdabca097fd91365 +timeCreated: 1726060625 \ No newline at end of file diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/EchoChainContract.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/EchoChainContract.cs new file mode 100644 index 000000000..edcfe4de9 --- /dev/null +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/EchoChainContract.cs @@ -0,0 +1,139 @@ +using System.Threading.Tasks; +using System; +using ChainSafe.Gaming.Evm.Transactions; +using Nethereum.Hex.HexTypes; +using ChainSafe.Gaming.Evm.Contracts; +using System.Numerics; +using Nethereum.RPC.Reactive.Eth.Subscriptions; +using Nethereum.JsonRpc.WebSocketStreamingClient; +using Nethereum.ABI.FunctionEncoding.Attributes; +using Nethereum.Contracts; +using UnityEngine; + + + +namespace ChainSafe.Gaming.Evm.Contracts.Custom +{ + public class EchoChainContract : ICustomContract + { + public string Address => OriginalContract.Address; + + public string ABI => "[ { \"inputs\": [], \"name\": \"echoChain\", \"outputs\": [ { \"internalType\": \"string\", \"name\": \"\", \"type\": \"string\" } ], \"stateMutability\": \"pure\", \"type\": \"function\" } ]"; + + public string ContractAddress { get; set; } + + public IContractBuilder ContractBuilder { get; set; } + + public Contract OriginalContract { get; set; } + + public string WebSocketUrl { get; set; } + + public bool Subscribed { get; set; } + + private StreamingWebSocketClient _webSocketClient; + + #region Methods + + public async Task EchoChain() + { + var response = await OriginalContract.Call("echoChain", new object [] { + + }); + + return response; + } + + + + #endregion + + + #region Event Classes + + + #endregion + + #region Interface Implemented Methods + + public async ValueTask DisposeAsync() + { + if(string.IsNullOrEmpty(WebSocketUrl)) + return; + if(!Subscribed) + return; + Subscribed = false; + + + _webSocketClient?.Dispose(); + } + + public async ValueTask Init() + { + if(Subscribed) + return; + + if(string.IsNullOrEmpty(WebSocketUrl)) + { + Debug.LogWarning($"WebSocketUrl is not set for this class. Event Subscriptions will not work."); + return; + } + + _webSocketClient ??= new StreamingWebSocketClient(WebSocketUrl); + await _webSocketClient.StartAsync(); + Subscribed = true; + + + } + + [Obsolete("It's not advisable to use this method. Use the pre-generated methods instead.")] + public IContract Attach(string address) + { + return OriginalContract.Attach(address); + } + + [Obsolete("It's not advisable to use this method. Use the pre-generated methods instead.")] + public Task Call(string method, object[] parameters = null, TransactionRequest overwrite = null) + { + return OriginalContract.Call(method, parameters, overwrite); + } + + [Obsolete("It's not advisable to use this method. Use the pre-generated methods instead.")] + public object[] Decode(string method, string output) + { + return OriginalContract.Decode(method, output); + } + + [Obsolete("It's not advisable to use this method. Use the pre-generated methods instead.")] + public Task Send(string method, object[] parameters = null, TransactionRequest overwrite = null) + { + return OriginalContract.Send(method, parameters, overwrite); + } + + [Obsolete("It's not advisable to use this method. Use the pre-generated methods instead.")] + public Task<(object[] response, TransactionReceipt receipt)> SendWithReceipt(string method, object[] parameters = null, TransactionRequest overwrite = null) + { + return OriginalContract.SendWithReceipt(method, parameters, overwrite); + } + + [Obsolete("It's not advisable to use this method. Use the pre-generated methods instead.")] + public Task EstimateGas(string method, object[] parameters, TransactionRequest overwrite = null) + { + return OriginalContract.EstimateGas(method, parameters, overwrite); + } + + [Obsolete("It's not advisable to use this method. Use the pre-generated methods instead.")] + public string Calldata(string method, object[] parameters = null) + { + return OriginalContract.Calldata(method, parameters); + } + + [Obsolete("It's not advisable to use this method. Use the pre-generated methods instead.")] + public Task PrepareTransactionRequest(string method, object[] parameters, bool isReadCall = false, TransactionRequest overwrite = null) + { + return OriginalContract.PrepareTransactionRequest(method, parameters, isReadCall, overwrite); + } + #endregion + } + + +} diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/EchoChainContract.cs.meta b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/EchoChainContract.cs.meta new file mode 100644 index 000000000..75f7a83bd --- /dev/null +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/EchoChainContract.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f43b68b4b41681a45b9c37f8f444b89b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs new file mode 100644 index 000000000..77a9229d0 --- /dev/null +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs @@ -0,0 +1,44 @@ +using System; +using ChainSafe.Gaming.Evm.Contracts.Custom; +using ChainSafe.Gaming.UnityPackage; +using UnityEngine; + +namespace Samples.Behaviours.SwitchChain +{ + public class SwitchChainCalls : MonoBehaviour + { + public ChainSetup[] chainSetups; + + private int currentChainIndex; + + public async void ToggleChain() + { + // get next chain id + currentChainIndex = (currentChainIndex + 1) % chainSetups.Length; + var chainId = chainSetups[currentChainIndex].chainId; + + // switch chains + await Web3Accessor.Web3.Chains.SwitchChain(chainId); + } + + public async void CallSmartContract() + { + // get contract address for the current chain + var contractAddress = chainSetups[currentChainIndex].contractAddress; + + // build contract client instance + var contract = await Web3Accessor.Web3.ContractBuilder.Build(contractAddress); + + // call the EchoChain function + var echoMessage = await contract.EchoChain(); + Debug.Log(echoMessage); + } + + [Serializable] + public class ChainSetup + { + public string chainId; + public string contractAddress; + } + } +} \ No newline at end of file diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs.meta b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs.meta new file mode 100644 index 000000000..58506c764 --- /dev/null +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ccc3f4b11c1e441b8c9f7b998173cc54 +timeCreated: 1726060668 \ No newline at end of file From 9b3d4df377e753df4bf6dd609eedf62a9fe66ca8 Mon Sep 17 00:00:00 2001 From: creeppak Date: Fri, 13 Sep 2024 14:45:23 +0100 Subject: [PATCH 15/24] Finalized Chain Switching --- .../Scripts/Connection/IConnectionHandler.cs | 2 +- .../Runtime/Scripts/ProjectConfigAsset.cs | 2 +- .../Runtime/Scripts/ProjectConfigUtilities.cs | 2 +- .../Web3/Core/Build/Web3Builder.cs | 3 +- .../Web3/Core/Chains/ChainConfigSet.cs | 3 +- .../Web3/Core/Chains/ChainManager.cs | 12 +- .../Core/Chains/ChainManagerExtensions.cs | 16 + .../Chains/SwitchChainHandlersProvider.cs | 19 + .../Scenes/SampleMain.unity | 424 +++++++++++++++++- .../SwitchChain/SwitchChainCalls.cs | 7 + 10 files changed, 473 insertions(+), 17 deletions(-) create mode 100644 src/ChainSafe.Gaming/Web3/Core/Chains/ChainManagerExtensions.cs create mode 100644 src/ChainSafe.Gaming/Web3/Core/Chains/SwitchChainHandlersProvider.cs diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Connection/IConnectionHandler.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Connection/IConnectionHandler.cs index c8d009d37..8b54fe109 100644 --- a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Connection/IConnectionHandler.cs +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Connection/IConnectionHandler.cs @@ -62,7 +62,7 @@ private void ConfigureCommonServices(IWeb3ServiceCollection services) services .UseUnityEnvironment() .UseGelato(GelatoApiKey) - .UseMultiCall() + // .UseMultiCall() .UseRpcProvider() .UseMarketplace(); diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs index 9a48f1914..d8a736cec 100644 --- a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs @@ -9,8 +9,8 @@ namespace ChainSafe.Gaming public class ProjectConfigAsset : ScriptableObject, ICompleteProjectConfig { [field: SerializeField] public string ProjectId { get; set; } - [field: SerializeField] public List ChainConfigs { get; set; } = new(); [field: SerializeField] public bool EnableAnalytics { get; set; } + [field: SerializeField] public List ChainConfigs { get; set; } = new(); IEnumerable IChainConfigSet.Configs => ChainConfigs; } diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs index 583514c4b..eaa3ffec5 100644 --- a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs @@ -8,7 +8,7 @@ namespace ChainSafe.Gaming.UnityPackage { public static class ProjectConfigUtilities { - private const string AssetName = "ProjectConfig"; + private const string AssetName = "Web3Config"; public static ProjectConfigAsset Load() { diff --git a/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs b/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs index d66a63e60..fbaa69868 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs @@ -34,8 +34,7 @@ private Web3Builder() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddChainManager(); } /// diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigSet.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigSet.cs index 5d9d5489a..becce8ffd 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigSet.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainConfigSet.cs @@ -2,12 +2,11 @@ namespace ChainSafe.Gaming.Web3.Core.Chains { - public class ChainConfigSet : List, IChainConfigSet + public class ChainConfigSet : IChainConfigSet { private readonly IChainConfig[] chainConfigs; public ChainConfigSet(params IChainConfig[] chainConfigs) - : base(chainConfigs) { this.chainConfigs = chainConfigs; } diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs index 9eef66f1a..d67252c26 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs @@ -10,13 +10,14 @@ namespace ChainSafe.Gaming.Web3.Core.Chains public class ChainManager : IChainManager { private readonly Dictionary configs; - private readonly IEnumerable switchHandlers; private readonly ILogWriter logWriter; + private readonly SwitchChainHandlersProvider switchHandlersProvider; - public ChainManager(IChainConfigSet configSet, IEnumerable switchHandlers, ILogWriter logWriter) + public ChainManager(IChainConfigSet configSet, SwitchChainHandlersProvider switchHandlersProvider, ILogWriter logWriter) { + // Referencing IEnumerable in ChainManager causes a deadlock, so we use SwitchChainHandlersProvider + this.switchHandlersProvider = switchHandlersProvider; this.logWriter = logWriter; - this.switchHandlers = switchHandlers; Current = configSet.Configs.First(); // build configs map @@ -44,6 +45,8 @@ public ChainManager(IChainConfigSet configSet, IEnumerable public async Task SwitchChain(string newChainId) { + var previousChainId = Current.ChainId; + if (!configs.TryGetValue(newChainId, out var newChainConfig)) { throw new Web3Exception($"No {nameof(IChainConfig)} was registered with id '{newChainId}'. " + @@ -51,12 +54,11 @@ public async Task SwitchChain(string newChainId) } Current = newChainConfig; - var previousChainId = Current.ChainId; var succeededHandlers = new Stack(); try { - foreach (var switchHandler in switchHandlers) + foreach (var switchHandler in switchHandlersProvider.Handlers) { await switchHandler.HandleChainSwitching(); succeededHandlers.Push(switchHandler); diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManagerExtensions.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManagerExtensions.cs new file mode 100644 index 000000000..cc7bbf944 --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManagerExtensions.cs @@ -0,0 +1,16 @@ +using ChainSafe.Gaming.Web3.Build; +using Microsoft.Extensions.DependencyInjection; + +namespace ChainSafe.Gaming.Web3.Core.Chains +{ + public static class ChainManagerExtensions + { + internal static IServiceCollection AddChainManager(this IServiceCollection services) + { + return services + .AddSingleton() + .AddSingleton() + .AddSingleton(); + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/SwitchChainHandlersProvider.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/SwitchChainHandlersProvider.cs new file mode 100644 index 000000000..3d2cdfe4b --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/SwitchChainHandlersProvider.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; + +namespace ChainSafe.Gaming.Web3.Core.Chains +{ + // We're using this class to lazily obtain an enumeration of IChainSwitchHandlers + public class SwitchChainHandlersProvider + { + private readonly IServiceProvider serviceProvider; + + public SwitchChainHandlersProvider(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + public IEnumerable Handlers => serviceProvider.GetServices(); + } +} \ No newline at end of file diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity index e7f93dd04..1c9147d27 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity @@ -7961,6 +7961,208 @@ MonoBehaviour: m_FlexibleWidth: -1 m_FlexibleHeight: -1 m_LayoutPriority: 1 +--- !u!1001 &1005010517 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 2011699642} + m_Modifications: + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_RootOrder + value: 3 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalScale.x + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalScale.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalScale.z + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.size + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Mode + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Mode + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Target + value: + objectReference: {fileID: 21975179} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Target + value: + objectReference: {fileID: 7978786053810957042} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_CallState + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_CallState + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_MethodName + value: NativeBalanceOf + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_MethodName + value: Execute + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_TargetAssemblyTypeName + value: Erc20Calls, Samples + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_TargetAssemblyTypeName + value: Samples.Behaviours.SampleBehaviour, Samples + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Arguments.m_ObjectArgumentAssemblyTypeName + value: UnityEngine.Object, UnityEngine + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Arguments.m_ObjectArgumentAssemblyTypeName + value: UnityEngine.Object, UnityEngine + objectReference: {fileID: 0} + - target: {fileID: 8775736491206355084, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_Name + value: Button - Native Balance Of + objectReference: {fileID: 0} + - target: {fileID: 8781309615174179339, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_text + value: Native Balance Of + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 8775736491206355084, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + insertIndex: -1 + addedObject: {fileID: 1005010520} + m_SourcePrefab: {fileID: 100100000, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} +--- !u!224 &1005010518 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + m_PrefabInstance: {fileID: 1005010517} + m_PrefabAsset: {fileID: 0} +--- !u!1 &1005010519 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 8775736491206355084, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + m_PrefabInstance: {fileID: 1005010517} + m_PrefabAsset: {fileID: 0} +--- !u!114 &1005010520 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1005010519} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 0 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: 50 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 --- !u!1001 &1019211772 PrefabInstance: m_ObjectHideFlags: 0 @@ -10106,15 +10308,15 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 8775736491206355084, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} propertyPath: m_Name - value: Button - Echo + value: Button - Contract Echo objectReference: {fileID: 0} - target: {fileID: 8781309615174179339, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} propertyPath: m_text - value: Call Test Smart-Contract + value: Call Test Smart Contract objectReference: {fileID: 0} - target: {fileID: 8781309615174179339, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} propertyPath: m_fontSize - value: 18.6 + value: 18.8 objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] @@ -13252,9 +13454,9 @@ MonoBehaviour: m_EditorClassIdentifier: chainSetups: - chainId: 11155111 - contractAddress: 0x3ea3542463ed9464CB7B48618d4c1627038bb8D8 + contractAddress: 0x3A7b0A1ef50D4072960E10d22b04583f99fd8EfB - chainId: 11155420 - contractAddress: 0x78E755e5b3D65aeF313f57421ae737Dd3ab689Fc + contractAddress: 0x83e37dA2B64dc0487f58B0cAf954DC50D65B51fd --- !u!1001 &1670752445 PrefabInstance: m_ObjectHideFlags: 0 @@ -13667,6 +13869,212 @@ RectTransform: m_CorrespondingSourceObject: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} m_PrefabInstance: {fileID: 1732360733} m_PrefabAsset: {fileID: 0} +--- !u!1001 &1743414114 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 2011699642} + m_Modifications: + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_RootOrder + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalScale.x + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalScale.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalScale.z + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.size + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Mode + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Mode + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Target + value: + objectReference: {fileID: 1665307235} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Target + value: + objectReference: {fileID: 7978786053810957042} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_CallState + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_CallState + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_MethodName + value: PrintChainId + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_MethodName + value: Execute + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_TargetAssemblyTypeName + value: Samples.Behaviours.SwitchChain.SwitchChainCalls, Samples + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_TargetAssemblyTypeName + value: Samples.Behaviours.SampleBehaviour, Samples + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Arguments.m_ObjectArgumentAssemblyTypeName + value: UnityEngine.Object, UnityEngine + objectReference: {fileID: 0} + - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_Arguments.m_ObjectArgumentAssemblyTypeName + value: UnityEngine.Object, UnityEngine + objectReference: {fileID: 0} + - target: {fileID: 8775736491206355084, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_Name + value: Button - Print Chain ID + objectReference: {fileID: 0} + - target: {fileID: 8781309615174179339, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_text + value: Print Current Chain ID + objectReference: {fileID: 0} + - target: {fileID: 8781309615174179339, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + propertyPath: m_fontSize + value: 18.85 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 8775736491206355084, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + insertIndex: -1 + addedObject: {fileID: 1743414117} + m_SourcePrefab: {fileID: 100100000, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} +--- !u!224 &1743414115 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 4764608378852082086, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + m_PrefabInstance: {fileID: 1743414114} + m_PrefabAsset: {fileID: 0} +--- !u!1 &1743414116 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 8775736491206355084, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} + m_PrefabInstance: {fileID: 1743414114} + m_PrefabAsset: {fileID: 0} +--- !u!114 &1743414117 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1743414116} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 0 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: 50 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 --- !u!1001 &1762398783 PrefabInstance: m_ObjectHideFlags: 0 @@ -16055,6 +16463,12 @@ PrefabInstance: - targetCorrespondingSourceObject: {fileID: 3719940706716398937, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} insertIndex: -1 addedObject: {fileID: 1165839532} + - targetCorrespondingSourceObject: {fileID: 3719940706716398937, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + insertIndex: -1 + addedObject: {fileID: 1743414115} + - targetCorrespondingSourceObject: {fileID: 3719940706716398937, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} + insertIndex: -1 + addedObject: {fileID: 1005010518} m_AddedComponents: - targetCorrespondingSourceObject: {fileID: 5855924980481563309, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} insertIndex: -1 diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs index 77a9229d0..5285b62d0 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs @@ -19,6 +19,8 @@ public async void ToggleChain() // switch chains await Web3Accessor.Web3.Chains.SwitchChain(chainId); + + Debug.Log($"Successfully switched to the chain {chainId}"); } public async void CallSmartContract() @@ -33,6 +35,11 @@ public async void CallSmartContract() var echoMessage = await contract.EchoChain(); Debug.Log(echoMessage); } + + public void PrintChainId() + { + Debug.Log($"Running the SDK with Chain ID: {Web3Accessor.Web3.ChainConfig.ChainId}"); + } [Serializable] public class ChainSetup From b0f3424caa9c3133baf559d8333a9801a0b4d102 Mon Sep 17 00:00:00 2001 From: creeppak Date: Fri, 13 Sep 2024 17:08:36 +0100 Subject: [PATCH 16/24] Fixed 'GetNetworkTest' Added 'GetChainId' extensions method to IRpcProvider Added Web3Config.asset to gitignore --- src/ChainSafe.Gaming.Tests/ChainsafeRPCTests.cs | 5 ++--- .../Web3/Core/Evm/RpcProviderExtensions.cs | 9 +++++++++ src/UnitySampleProject/.gitignore | 4 ++-- .../Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs | 6 ++++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/ChainSafe.Gaming.Tests/ChainsafeRPCTests.cs b/src/ChainSafe.Gaming.Tests/ChainsafeRPCTests.cs index f910be67f..4315931c0 100644 --- a/src/ChainSafe.Gaming.Tests/ChainsafeRPCTests.cs +++ b/src/ChainSafe.Gaming.Tests/ChainsafeRPCTests.cs @@ -80,9 +80,8 @@ public void Cleanup() [Test] public void GetNetworkTest() { - var network = firstAccount.RpcProvider.LastKnownNetwork; - Assert.AreEqual("GoChain Testnet", network.Name); - Assert.AreEqual(31337, network.ChainId); + var chainId = firstAccount.RpcProvider.GetChainId().Result; + Assert.AreEqual("31337", chainId); } /// diff --git a/src/ChainSafe.Gaming/Web3/Core/Evm/RpcProviderExtensions.cs b/src/ChainSafe.Gaming/Web3/Core/Evm/RpcProviderExtensions.cs index 8af9faa80..62c0af448 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Evm/RpcProviderExtensions.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Evm/RpcProviderExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Numerics; using System.Threading.Tasks; using ChainSafe.Gaming.Evm.Transactions; @@ -16,6 +17,14 @@ namespace ChainSafe.Gaming.Evm.Providers { public static class RpcProviderExtensions { + public static async Task GetChainId(this IRpcProvider provider) + { + var rawHexChainId = await provider.Perform("eth_chainId"); + var chainId = new HexBigInteger(rawHexChainId).ToUlong(); + + return chainId.ToString(CultureInfo.InvariantCulture); + } + /// /// eth_getBalance
Asynchronously retrieves the native balance (ETH for Ethereum) of a specified wallet address. ///
diff --git a/src/UnitySampleProject/.gitignore b/src/UnitySampleProject/.gitignore index 09eff11ff..ba3c842dd 100644 --- a/src/UnitySampleProject/.gitignore +++ b/src/UnitySampleProject/.gitignore @@ -83,5 +83,5 @@ crashlytics-build.properties # Ignore the project config since it can always be rebuilt /Assets/Resources.meta -/Assets/Resources/ProjectConfigData.asset -/Assets/Resources/ProjectConfigData.asset.meta \ No newline at end of file +/Assets/Resources/Web3Config.asset +/Assets/Resources/Web3Config.asset.asset.meta \ No newline at end of file diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs index 5285b62d0..8f1fa8a00 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs @@ -1,5 +1,6 @@ using System; using ChainSafe.Gaming.Evm.Contracts.Custom; +using ChainSafe.Gaming.Evm.Providers; using ChainSafe.Gaming.UnityPackage; using UnityEngine; @@ -36,9 +37,10 @@ public async void CallSmartContract() Debug.Log(echoMessage); } - public void PrintChainId() + public async void PrintChainId() { - Debug.Log($"Running the SDK with Chain ID: {Web3Accessor.Web3.ChainConfig.ChainId}"); + var chainId = await Web3Accessor.Web3.RpcProvider.GetChainId(); + Debug.Log($"Running the SDK with Chain ID: {chainId}"); } [Serializable] From a6522057ea7e7d5ca6e16fb4aae9dff506f65f0d Mon Sep 17 00:00:00 2001 From: creeppak Date: Fri, 13 Sep 2024 17:19:53 +0100 Subject: [PATCH 17/24] Fixed "The name 'WriteNetworkFile' does not exist in the current context" compile error --- .../Editor/Web3SettingsEditor.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs index 119f17d8e..cdff5ee8f 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs @@ -242,23 +242,14 @@ private void RemoveChainConfigEntry(string chainId) /// private static async void ValidateProjectID(string projectID) { - bool projectIdValid; try { - projectIdValid = await ValidateProjectIDAsync(projectID); + await ValidateProjectIDAsync(projectID); } catch (Exception e) { Debug.LogError("Failed to validate project ID"); Debug.LogException(e); - return; - } - - if (projectIdValid) - { -#if UNITY_WEBGL - WriteNetworkFile(); -#endif } static async Task ValidateProjectIDAsync(string projectID) From d0a9e8aae1ff7b96dfd7f8676567cbb9037974b9 Mon Sep 17 00:00:00 2001 From: creeppak Date: Fri, 13 Sep 2024 17:55:09 +0100 Subject: [PATCH 18/24] Added Semaphore to ChainManager.SwitchChain() method Other small changes --- .../Web3/Core/Chains/ChainManager.cs | 89 +++++++++++-------- .../Web3/Evm/Wallet/WalletProvider.cs | 2 +- .../Scenes/SampleMain.unity | 10 +-- .../Scenes/SampleMain/SampleBehaviour.cs | 10 +++ .../SwitchChain/SwitchChainCalls.cs | 4 +- 5 files changed, 68 insertions(+), 47 deletions(-) diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs index d67252c26..d277de09f 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using ChainSafe.Gaming.NetCore; using ChainSafe.Gaming.Web3.Environment; @@ -12,6 +13,7 @@ public class ChainManager : IChainManager private readonly Dictionary configs; private readonly ILogWriter logWriter; private readonly SwitchChainHandlersProvider switchHandlersProvider; + private readonly SemaphoreSlim switchChainSemaphore = new(1); public ChainManager(IChainConfigSet configSet, SwitchChainHandlersProvider switchHandlersProvider, ILogWriter logWriter) { @@ -43,62 +45,71 @@ public ChainManager(IChainConfigSet configSet, SwitchChainHandlersProvider switc public IChainConfig Current { get; private set; } - public async Task SwitchChain(string newChainId) + public async Task SwitchChain(string newChainId) // todo add timeout mechanism or just take cancellation token as an argument { - var previousChainId = Current.ChainId; - - if (!configs.TryGetValue(newChainId, out var newChainConfig)) - { - throw new Web3Exception($"No {nameof(IChainConfig)} was registered with id '{newChainId}'. " + - "Make sure to configure settings for the provided chain before switching to it."); - } - - Current = newChainConfig; - var succeededHandlers = new Stack(); + await switchChainSemaphore.WaitAsync(); // wait till previous switch chain process completes try { - foreach (var switchHandler in switchHandlersProvider.Handlers) + var previousChainId = Current.ChainId; + + if (!configs.TryGetValue(newChainId, out var newChainConfig)) { - await switchHandler.HandleChainSwitching(); - succeededHandlers.Push(switchHandler); + throw new ArgumentException($"No {nameof(IChainConfig)} was registered with id '{newChainId}'. " + + "Make sure to configure settings for the provided chain before switching to it."); } - } - catch (Exception switchException) - { - // revert everything - Current = configs[previousChainId]; - while (succeededHandlers.Count != 0) - { - var handlerToRevert = succeededHandlers.Pop(); + Current = newChainConfig; + var succeededHandlers = new Stack(); - try + try + { + foreach (var switchHandler in switchHandlersProvider.Handlers) { - await handlerToRevert.HandleChainSwitching(); + await switchHandler.HandleChainSwitching(); + succeededHandlers.Push(switchHandler); } - catch (Exception revertException) + } + catch (Exception switchException) + { + // revert everything + Current = configs[previousChainId]; + + while (succeededHandlers.Count != 0) { - logWriter.LogError( - $"Error occured while reverting handler {handlerToRevert.GetType().Name}. " + - $"Proceeding with revert.\n{revertException}"); + var handlerToRevert = succeededHandlers.Pop(); + + try + { + await handlerToRevert.HandleChainSwitching(); + } + catch (Exception revertException) + { + logWriter.LogError( + $"Error occured while reverting handler {handlerToRevert.GetType().Name}. " + + $"Proceeding with revert.\n{revertException}"); + } } - } - throw new Web3Exception( - $"One of the handlers thrown an exception. Reverted {nameof(ChainManager)} to the previous chain configuration.", - switchException); - } + throw new Web3Exception( + $"One of the handlers thrown an exception. Reverted {nameof(ChainManager)} to the previous chain configuration.", + switchException); + } - logWriter.Log($"Successfully switched to the chain with id '{newChainId}'."); + logWriter.Log($"Successfully switched to the chain with id '{newChainId}'."); - try - { - ChainSwitched?.Invoke(Current); + try + { + ChainSwitched?.Invoke(Current); + } + catch (Exception e) + { + logWriter.LogError(e.ToString()); + } } - catch (Exception e) + finally { - logWriter.LogError(e.ToString()); + switchChainSemaphore.Release(); } } diff --git a/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProvider.cs b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProvider.cs index 0ee4aaf70..02a344003 100644 --- a/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProvider.cs +++ b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProvider.cs @@ -33,7 +33,7 @@ protected WalletProvider(Web3Environment environment, IChainConfig chainConfig) public abstract Task Disconnect(); - public abstract Task Request(string method, params object[] parameters); + public abstract Task Request(string method, params object[] parameters); // todo sync wallet chain id before sending any other request public Task HandleChainSwitching() { diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity index 1c9147d27..f3eb6735e 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity @@ -8099,7 +8099,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_MethodName - value: Execute + value: ExecuteNoChainCheck objectReference: {fileID: 0} - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_TargetAssemblyTypeName @@ -10288,7 +10288,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_MethodName - value: Execute + value: ExecuteNoChainCheck objectReference: {fileID: 0} - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_TargetAssemblyTypeName @@ -13975,7 +13975,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.size - value: 1 + value: 2 objectReference: {fileID: 0} - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Mode @@ -14007,7 +14007,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_MethodName - value: Execute + value: ExecuteNoChainCheck objectReference: {fileID: 0} - target: {fileID: 7324590823460843055, guid: 50ad8ea555027414b8ddfc03fc7d41ab, type: 3} propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_TargetAssemblyTypeName @@ -16107,7 +16107,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[1].m_MethodName - value: Execute + value: ExecuteNoChainCheck objectReference: {fileID: 0} - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_TargetAssemblyTypeName diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SampleBehaviour.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SampleBehaviour.cs index 213f93a01..6f0d1e253 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SampleBehaviour.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SampleBehaviour.cs @@ -26,5 +26,15 @@ public async void Execute() await new WaitForSeconds(2); SampleFeedback.Instance?.Deactivate(); } + + public async void ExecuteNoChainCheck() + { + // Activates the loading pop up to stop duplicate calls + SampleFeedback.Instance?.Activate(); + + // Deactivates the loading pop up after a few seconds + await new WaitForSeconds(2); + SampleFeedback.Instance?.Deactivate(); + } } } \ No newline at end of file diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs index 8f1fa8a00..16675fdc7 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs @@ -18,10 +18,10 @@ public async void ToggleChain() currentChainIndex = (currentChainIndex + 1) % chainSetups.Length; var chainId = chainSetups[currentChainIndex].chainId; + Debug.Log($"Switching the chain... Make sure you confirm the chain change in your wallet."); + // switch chains await Web3Accessor.Web3.Chains.SwitchChain(chainId); - - Debug.Log($"Successfully switched to the chain {chainId}"); } public async void CallSmartContract() From 6dcecf2459ff8488ca3e39e7978f503db59dcbe0 Mon Sep 17 00:00:00 2001 From: creeppak Date: Fri, 13 Sep 2024 19:03:23 +0100 Subject: [PATCH 19/24] HyperPlayWebGLProvider compile error fix --- .../Runtime/Scripts/HyperPlayWebGLProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/io.chainsafe.web3-unity.hyperplay/Runtime/Scripts/HyperPlayWebGLProvider.cs b/Packages/io.chainsafe.web3-unity.hyperplay/Runtime/Scripts/HyperPlayWebGLProvider.cs index 35801f882..d522fd776 100644 --- a/Packages/io.chainsafe.web3-unity.hyperplay/Runtime/Scripts/HyperPlayWebGLProvider.cs +++ b/Packages/io.chainsafe.web3-unity.hyperplay/Runtime/Scripts/HyperPlayWebGLProvider.cs @@ -31,7 +31,7 @@ public class HyperPlayWebGLProvider : HyperPlayProvider /// Injected . /// ChainConfig to fetch chain data. /// Injected . - public HyperPlayWebGLProvider(IHyperPlayConfig config, IHyperPlayData data, ILocalStorage localStorage, Web3Environment environment, IChainConfig chainConfig, ChainRegistryProvider chainRegistryProvider) : base(config, data, localStorage, environment, chainConfig, chainRegistryProvider) + public HyperPlayWebGLProvider(IHyperPlayConfig config, IHyperPlayData data, ILocalStorage localStorage, Web3Environment environment, IChainConfig chainConfig, ChainRegistryProvider chainRegistryProvider) : base(config, data, localStorage, environment, chainConfig) { _config = config; _data = data; From c8bb847a7fc66100f5ac0a22178081850c5690f2 Mon Sep 17 00:00:00 2001 From: creeppak Date: Fri, 13 Sep 2024 19:54:22 +0100 Subject: [PATCH 20/24] Replaced ChainManager's semaphore with a simple flag. Throwing exception now when the last chain switching operation is not completed. --- .../Web3/Core/Chains/ChainManager.cs | 15 +++++++++++---- .../Web3/Core/Chains/IChainManager.cs | 2 ++ .../Web3/Evm/Wallet/WalletProvider.cs | 2 -- .../SampleMain/SwitchChain/SwitchChainCalls.cs | 8 +++++++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs index d277de09f..65be7b8bf 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs @@ -13,7 +13,6 @@ public class ChainManager : IChainManager private readonly Dictionary configs; private readonly ILogWriter logWriter; private readonly SwitchChainHandlersProvider switchHandlersProvider; - private readonly SemaphoreSlim switchChainSemaphore = new(1); public ChainManager(IChainConfigSet configSet, SwitchChainHandlersProvider switchHandlersProvider, ILogWriter logWriter) { @@ -45,9 +44,17 @@ public ChainManager(IChainConfigSet configSet, SwitchChainHandlersProvider switc public IChainConfig Current { get; private set; } - public async Task SwitchChain(string newChainId) // todo add timeout mechanism or just take cancellation token as an argument + public bool IsSwitching { get; private set; } + + public async Task SwitchChain(string newChainId) // todo add cancellation token as an argument and use it { - await switchChainSemaphore.WaitAsync(); // wait till previous switch chain process completes + if (IsSwitching) + { + throw new InvalidOperationException( + "Can't switch chain. The last chain switching procedure has not yet finished."); + } + + IsSwitching = true; try { @@ -109,7 +116,7 @@ public async Task SwitchChain(string newChainId) // todo add timeout mechanism o } finally { - switchChainSemaphore.Release(); + IsSwitching = false; } } diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/IChainManager.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainManager.cs index fd6557190..312940687 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/IChainManager.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/IChainManager.cs @@ -9,6 +9,8 @@ public interface IChainManager IChainConfig Current { get; } + bool IsSwitching { get; } + Task SwitchChain(string newChainId); void AddChainConfig(IChainConfig newConfig); diff --git a/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProvider.cs b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProvider.cs index 02a344003..11750491c 100644 --- a/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProvider.cs +++ b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProvider.cs @@ -57,8 +57,6 @@ protected async Task SwitchChain(string chainId) { throw new Web3Exception($"Error occured while trying to switch wallet chain.", ex); } - - logWriter.Log("Wallet chain switch complete."); } } } \ No newline at end of file diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs index 16675fdc7..68e167333 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs @@ -14,11 +14,17 @@ public class SwitchChainCalls : MonoBehaviour public async void ToggleChain() { + if (Web3Accessor.Web3.Chains.IsSwitching) + { + Debug.LogError("The last chain switching operation has not yet been completed."); + return; + } + // get next chain id currentChainIndex = (currentChainIndex + 1) % chainSetups.Length; var chainId = chainSetups[currentChainIndex].chainId; - Debug.Log($"Switching the chain... Make sure you confirm the chain change in your wallet."); + Debug.Log("Switching the chain... Make sure you confirm the chain change in your wallet."); // switch chains await Web3Accessor.Web3.Chains.SwitchChain(chainId); From d0083b1de04fb8426621b2a4a1c18cc84c52af65 Mon Sep 17 00:00:00 2001 From: creeppak Date: Sat, 14 Sep 2024 11:17:38 +0100 Subject: [PATCH 21/24] Editor scripts & Menue items polish --- .../StageItems/LootBoxesFrontEndDataSet.cs | 2 +- .../Runtime/MudConfigAsset.cs | 4 +- .../Web3AuthSDK/Editor/CreateDeepLink.cs | 2 +- .../Web3AuthSDK/Debug/Web3AuthDebug.cs | 2 +- .../Editor/ABICSharpConverter.cs | 2 +- .../Editor/ABIConverter.cs | 2 +- .../Editor/ReportBug.cs | 44 ++++++++++++++++--- .../Web3SettingsEditor.ChainSettings.cs | 6 +-- .../Editor/Web3SettingsEditor.cs | 44 +++++++++---------- .../Editor/WebGLTemplateSync.cs | 2 +- .../Editor/WebGLThreadPatcherInstaller.cs | 2 +- .../Runtime/Scripts/ProjectConfigUtilities.cs | 16 +++---- ...ojectConfigAsset.cs => Web3ConfigAsset.cs} | 4 +- ...gAsset.cs.meta => Web3ConfigAsset.cs.meta} | 0 .../2.6.0/Lootboxes Samples/Scripts/Menues.cs | 2 +- .../StageItems/LootBoxesFrontEndDataSet.cs | 2 +- 16 files changed, 83 insertions(+), 53 deletions(-) rename Packages/io.chainsafe.web3-unity/Runtime/Scripts/{ProjectConfigAsset.cs => Web3ConfigAsset.cs} (81%) rename Packages/io.chainsafe.web3-unity/Runtime/Scripts/{ProjectConfigAsset.cs.meta => Web3ConfigAsset.cs.meta} (100%) diff --git a/Packages/io.chainsafe.web3-unity.lootboxes/Samples~/Web3.Unity Chainlink Lootboxes/Scripts/Scene/StageItems/LootBoxesFrontEndDataSet.cs b/Packages/io.chainsafe.web3-unity.lootboxes/Samples~/Web3.Unity Chainlink Lootboxes/Scripts/Scene/StageItems/LootBoxesFrontEndDataSet.cs index 91cfc6fac..df0903357 100644 --- a/Packages/io.chainsafe.web3-unity.lootboxes/Samples~/Web3.Unity Chainlink Lootboxes/Scripts/Scene/StageItems/LootBoxesFrontEndDataSet.cs +++ b/Packages/io.chainsafe.web3-unity.lootboxes/Samples~/Web3.Unity Chainlink Lootboxes/Scripts/Scene/StageItems/LootBoxesFrontEndDataSet.cs @@ -5,7 +5,7 @@ namespace LootBoxes.Chainlink.Scene.StageItems { - [CreateAssetMenu(menuName = Menues.Root + "LootBoxPrefabSet", fileName = "LootBoxPrefabSet", order = 0)] + [CreateAssetMenu(menuName = Menues.Root + "Lootbox Prefab Set", fileName = "LootboxPrefabSet", order = 0)] public class LootBoxesFrontEndDataSet : ScriptableObject { [Serializable] diff --git a/Packages/io.chainsafe.web3-unity.mud/Runtime/MudConfigAsset.cs b/Packages/io.chainsafe.web3-unity.mud/Runtime/MudConfigAsset.cs index 471bd210d..6b0ad879e 100644 --- a/Packages/io.chainsafe.web3-unity.mud/Runtime/MudConfigAsset.cs +++ b/Packages/io.chainsafe.web3-unity.mud/Runtime/MudConfigAsset.cs @@ -7,9 +7,9 @@ namespace ChainSafe.Gaming.Mud.Unity { /// - /// Represents a configuration asset for MUD module. + /// Represents a configuration asset for the MUD module. /// - [CreateAssetMenu(menuName = "ChainSafe/Mud Config Asset", fileName = "MudConfigAsset", order = 0)] + [CreateAssetMenu(menuName = "ChainSafe/MUD Config", fileName = "MudConfig", order = 0)] public class MudConfigAsset : ScriptableObject, IMudConfig { public MudStorageType StorageType; diff --git a/Packages/io.chainsafe.web3-unity.web3auth/Editor/Web3AuthSDK/Editor/CreateDeepLink.cs b/Packages/io.chainsafe.web3-unity.web3auth/Editor/Web3AuthSDK/Editor/CreateDeepLink.cs index 3246060b4..e8ebad794 100644 --- a/Packages/io.chainsafe.web3-unity.web3auth/Editor/Web3AuthSDK/Editor/CreateDeepLink.cs +++ b/Packages/io.chainsafe.web3-unity.web3auth/Editor/Web3AuthSDK/Editor/CreateDeepLink.cs @@ -9,7 +9,7 @@ public class CreateDeepLink : EditorWindow TextField uri; - [MenuItem("ChainSafe SDK/Web3Auth/Generate Deep Link")] + [MenuItem("ChainSafe SDK/Web3Auth/Generate Deep Link", priority = -100)] public static void ShowExample() { CreateDeepLink wnd = GetWindow(); diff --git a/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Plugins/Web3AuthSDK/Debug/Web3AuthDebug.cs b/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Plugins/Web3AuthSDK/Debug/Web3AuthDebug.cs index e7e899761..86268091e 100644 --- a/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Plugins/Web3AuthSDK/Debug/Web3AuthDebug.cs +++ b/Packages/io.chainsafe.web3-unity.web3auth/Runtime/Plugins/Web3AuthSDK/Debug/Web3AuthDebug.cs @@ -15,7 +15,7 @@ public class Web3AuthDebug : EditorWindow [SerializeField] public int index; - [MenuItem("ChainSafe SDK/Web3Auth/Deep Linking Debug")] + [MenuItem("ChainSafe SDK/Web3Auth/Deep Linking Debug", priority = -100)] public static void ShowExample() { Web3AuthDebug wnd = GetWindow(); diff --git a/Packages/io.chainsafe.web3-unity/Editor/ABICSharpConverter.cs b/Packages/io.chainsafe.web3-unity/Editor/ABICSharpConverter.cs index e77586295..e3867d733 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/ABICSharpConverter.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/ABICSharpConverter.cs @@ -82,7 +82,7 @@ private void OnGUI() } } - [MenuItem("ChainSafe SDK/ABI to C# Contract Converter")] + [MenuItem("ChainSafe SDK/Convert ABI to C# Contract", priority = 0)] public static void ShowWindow() { Instance = GetWindow("ABI to C# Contract Converter"); diff --git a/Packages/io.chainsafe.web3-unity/Editor/ABIConverter.cs b/Packages/io.chainsafe.web3-unity/Editor/ABIConverter.cs index a14b6dc0d..a9707f8db 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/ABIConverter.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/ABIConverter.cs @@ -11,7 +11,7 @@ public class ABIWindow : EditorWindow Vector2 TextArea; // Where is the window menu located - [MenuItem("ChainSafe SDK/Generate ABI wrapper")] + [MenuItem("ChainSafe SDK/Generate ABI wrapper", priority = 0)] // Show our window public static void ShowWindow() diff --git a/Packages/io.chainsafe.web3-unity/Editor/ReportBug.cs b/Packages/io.chainsafe.web3-unity/Editor/ReportBug.cs index 443d7a049..4b93a3c7f 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/ReportBug.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/ReportBug.cs @@ -14,14 +14,16 @@ ///
public class ReportBug : EditorWindow { - Texture2D logo = null; + private static GUIStyle centeredLabelStyle; + private Texture2D logo; // Initializes window - [MenuItem("ChainSafe SDK/Report Bug")] + [MenuItem("ChainSafe SDK/Report Bug", priority = 100)] public static void ShowWindow() { // show existing window instance. If one doesn't exist, make one. - GetWindow(typeof(ReportBug)); + var window = GetWindow(typeof(ReportBug)); + window.minSize = new Vector2(450, 300); } // Called when menu is opened, loads Chainsafe Logo @@ -29,17 +31,18 @@ void OnEnable() { if (!logo) { - logo = AssetDatabase.LoadAssetAtPath("Packages/io.chainsafe.web3-unity/Editor/Textures/ChainSafeLogo.png"); + logo = AssetDatabase.LoadAssetAtPath( + "Packages/io.chainsafe.web3-unity/Editor/Textures/ChainSafeLogo2.png"); } } // Displayed content void OnGUI() { + InitStyles(); + // Image - EditorGUILayout.BeginVertical("box"); - GUILayout.Label(logo, GUILayout.MaxWidth(250f), GUILayout.MaxHeight(250f)); - EditorGUILayout.EndVertical(); + DrawHeader(); // Text GUILayout.Label("Found an issue with the SDK?", EditorStyles.boldLabel); GUILayout.Label("Here you can report a bug and someone from the team will attend to it ASAP", EditorStyles.label); @@ -49,6 +52,7 @@ void OnGUI() { Application.OpenURL("https://github.com/ChainSafe/web3.unity/issues/new?assignees=&labels=Type%3A+Bug&projects=&template=bug_report.md&title="); } + GUILayout.Space(10); // Discord GUILayout.Label("You can also join our discord if you're looking for additional help", EditorStyles.label); if (GUILayout.Button("Join Discord")) @@ -56,4 +60,30 @@ void OnGUI() Application.OpenURL("http://discord.gg/n2U6x9c"); } } + + private void InitStyles() + { + centeredLabelStyle ??= new GUIStyle(EditorStyles.label) + { + alignment = TextAnchor.MiddleCenter + }; + } + + private void DrawHeader() + { + using (new GUILayout.VerticalScope(GUILayout.Height(200))) + { + GUILayout.FlexibleSpace(); + + // logo layout + using (new EditorGUILayout.HorizontalScope()) + { + // GUILayout.FlexibleSpace(); + GUILayout.Label(logo, centeredLabelStyle, GUILayout.MaxHeight(160)); + // GUILayout.FlexibleSpace(); + } + + GUILayout.FlexibleSpace(); + } + } } \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs index db270d940..2c495d90f 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.ChainSettings.cs @@ -12,20 +12,20 @@ public partial class Web3SettingsEditor private class ChainSettingsPanel { private readonly Web3SettingsEditor window; - private readonly ProjectConfigAsset configAsset; + private readonly Web3ConfigAsset configAsset; private readonly ChainConfigEntry chainConfig; private int selectedChainIndex; private int selectedRpcIndex; private StringListSearchProvider searchProvider; - private ISearchWindowProvider _searchWindowProviderImplementation; + private ISearchWindowProvider searchWindowProviderImplementation; private bool changedRpcOrWs; private int selectedWebHookIndex; public ChainSettingsPanel(Web3SettingsEditor window, ChainConfigEntry chainConfigEntry) { this.window = window; - this.configAsset = window.projectConfig; + this.configAsset = window.web3Config; this.chainConfig = chainConfigEntry; UpdateServerMenuInfo(); diff --git a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs index cdff5ee8f..3702dc3d2 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/Web3SettingsEditor.cs @@ -20,7 +20,7 @@ public partial class Web3SettingsEditor : EditorWindow private const string EnableAnalyticsScriptingDefineSymbol = "ENABLE_ANALYTICS"; // Initializes window - [MenuItem("ChainSafe SDK/Project Settings", false, 1)] + [MenuItem("ChainSafe SDK/Project Settings", false, -200)] public static void ShowWindow() => ShowWindow(null); public static void ShowWindow(Tabs? tabOverride = null) @@ -42,7 +42,7 @@ public static void ShowWindow(Tabs? tabOverride = null) // Chain values public string previousProjectId; - private ProjectConfigAsset projectConfig; + private Web3ConfigAsset web3Config; private List chainSettingPanels; private List chainList; private FetchingStatus fetchingStatus = FetchingStatus.NotFetching; @@ -58,8 +58,8 @@ private Tabs ActiveTab private void Awake() { - projectConfig = ProjectConfigUtilities.CreateOrLoad(); - previousProjectId = projectConfig.ProjectId; + web3Config = ProjectConfigUtilities.CreateOrLoad(); + previousProjectId = web3Config.ProjectId; } private void OnEnable() @@ -150,8 +150,8 @@ private void DrawProjectTabContent() { EditorGUI.BeginChangeCheck(); - projectConfig.ProjectId = EditorGUILayout.TextField("Project ID", projectConfig.ProjectId); - if (string.IsNullOrWhiteSpace(projectConfig.ProjectId)) + web3Config.ProjectId = EditorGUILayout.TextField("Project ID", web3Config.ProjectId); + if (string.IsNullOrWhiteSpace(web3Config.ProjectId)) { EditorGUILayout.HelpBox( "Please enter your Project ID to start using the ChainSafe Gaming SDK.", @@ -162,26 +162,26 @@ private void DrawProjectTabContent() } } EditorGUILayout.Space(); - projectConfig.EnableAnalytics = + web3Config.EnableAnalytics = EditorGUILayout.Toggle( new GUIContent("Allow Analytics:", "Consent to collecting data for analytics purposes. This will help improve our product."), - projectConfig.EnableAnalytics); + web3Config.EnableAnalytics); GUILayout.Label( "We will collect data for analytics to help improve your experience with our SDK. This data allows us to optimize performance, introduce new features, and ensure seamless integration. You can opt out at any time, but we encourage keeping analytics enabled for the best results!", wrappedGreyMiniLabel); if (EditorGUI.EndChangeCheck()) { - EditorUtility.SetDirty(projectConfig); + EditorUtility.SetDirty(web3Config); - if (projectConfig.ProjectId != previousProjectId) + if (web3Config.ProjectId != previousProjectId) { - ValidateProjectID(projectConfig.ProjectId); - previousProjectId = projectConfig.ProjectId; + ValidateProjectID(web3Config.ProjectId); + previousProjectId = web3Config.ProjectId; } - if (projectConfig.EnableAnalytics) + if (web3Config.EnableAnalytics) ScriptingDefineSymbols.TryAddDefineSymbol(EnableAnalyticsScriptingDefineSymbol); else ScriptingDefineSymbols.TryRemoveDefineSymbol(EnableAnalyticsScriptingDefineSymbol); @@ -208,9 +208,9 @@ private void DrawChainsTabContent() if (GUILayout.Button("+")) { var newChainConfig = ChainConfigEntry.Empty; - projectConfig.ChainConfigs.Add(newChainConfig); + web3Config.ChainConfigs.Add(newChainConfig); chainSettingPanels.Add(new ChainSettingsPanel(this, newChainConfig)); - EditorUtility.SetDirty(projectConfig); + EditorUtility.SetDirty(web3Config); } GUILayout.EndScrollView(); @@ -223,7 +223,7 @@ private void DrawFooter() private void RemoveChainConfigEntry(string chainId) { - var index = projectConfig.ChainConfigs.FindIndex(entry => entry.ChainId == chainId); + var index = web3Config.ChainConfigs.FindIndex(entry => entry.ChainId == chainId); if (index < 0) { @@ -231,8 +231,8 @@ private void RemoveChainConfigEntry(string chainId) return; } - projectConfig.ChainConfigs.RemoveAt(index); - EditorUtility.SetDirty(projectConfig); + web3Config.ChainConfigs.RemoveAt(index); + EditorUtility.SetDirty(web3Config); chainSettingPanels.RemoveAt(chainSettingPanels.FindIndex(panel => panel.ChainId == chainId)); } @@ -313,17 +313,17 @@ private async void TryFetchSupportedChains() private void OnChainListFetched() { - if (projectConfig.ChainConfigs.Count != 0) + if (web3Config.ChainConfigs.Count != 0) { - chainSettingPanels = projectConfig.ChainConfigs + chainSettingPanels = web3Config.ChainConfigs .Select((chainConfig) => new ChainSettingsPanel(this, chainConfig)) .ToList(); } else { var newChainConfig = ChainConfigEntry.Default; - projectConfig.ChainConfigs.Add(newChainConfig); - EditorUtility.SetDirty(projectConfig); + web3Config.ChainConfigs.Add(newChainConfig); + EditorUtility.SetDirty(web3Config); chainSettingPanels = new List { diff --git a/Packages/io.chainsafe.web3-unity/Editor/WebGLTemplateSync.cs b/Packages/io.chainsafe.web3-unity/Editor/WebGLTemplateSync.cs index 9f7c9329a..48467ad4f 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/WebGLTemplateSync.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/WebGLTemplateSync.cs @@ -50,7 +50,7 @@ private static bool DirectoryInSync(DirectoryInfo reference, DirectoryInfo check return true; } - [MenuItem("ChainSafe SDK/Sync WebGL Templates")] + [MenuItem("ChainSafe SDK/Sync WebGL Templates", priority = 0)] public static void Syncronize() { AssetDatabase.DisallowAutoRefresh(); diff --git a/Packages/io.chainsafe.web3-unity/Editor/WebGLThreadPatcherInstaller.cs b/Packages/io.chainsafe.web3-unity/Editor/WebGLThreadPatcherInstaller.cs index 1d018c8ef..5e3d34b84 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/WebGLThreadPatcherInstaller.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/WebGLThreadPatcherInstaller.cs @@ -33,7 +33,7 @@ static WebGLThreadPatcherInstaller() #endif } - [MenuItem("ChainSafe SDK/Install WebGLThreadingPatcher")] + [MenuItem("ChainSafe SDK/Install WebGLThreadingPatcher", priority = 0)] public static void TryInstallThreadPatcher() { Manifest manifest = JsonConvert.DeserializeObject(File.ReadAllText(ManifestPath)); diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs index eaa3ffec5..6be408ad6 100644 --- a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs @@ -10,16 +10,16 @@ public static class ProjectConfigUtilities { private const string AssetName = "Web3Config"; - public static ProjectConfigAsset Load() + public static Web3ConfigAsset Load() { - var projectConfig = Resources.Load(AssetName); + var projectConfig = Resources.Load(AssetName); return projectConfig ? projectConfig : null; } - public static ProjectConfigAsset Create(string projectId, string chainId, string chain, string network, + public static Web3ConfigAsset Create(string projectId, string chainId, string chain, string network, string symbol, string rpc, string blockExplorerUrl, bool enableAnalytics, string ws = "") { - var projectConfig = ScriptableObject.CreateInstance(); + var projectConfig = ScriptableObject.CreateInstance(); projectConfig.ProjectId = projectId; projectConfig.EnableAnalytics = enableAnalytics; @@ -41,7 +41,7 @@ public static ProjectConfigAsset Create(string projectId, string chainId, string } #if UNITY_EDITOR - public static ProjectConfigAsset CreateOrLoad() + public static Web3ConfigAsset CreateOrLoad() { var projectConfig = Load(); @@ -54,7 +54,7 @@ public static ProjectConfigAsset CreateOrLoad() Directory.CreateDirectory(assetDirectory); } - projectConfig = ScriptableObject.CreateInstance(); + projectConfig = ScriptableObject.CreateInstance(); UnityEditor.AssetDatabase.CreateAsset(projectConfig, Path.Combine("Assets", nameof(Resources), $"{AssetName}.asset")); } @@ -62,9 +62,9 @@ public static ProjectConfigAsset CreateOrLoad() return projectConfig; } - public static void Save(ProjectConfigAsset projectConfig) + public static void Save(Web3ConfigAsset web3Config) { - UnityEditor.EditorUtility.SetDirty(projectConfig); + UnityEditor.EditorUtility.SetDirty(web3Config); UnityEditor.AssetDatabase.SaveAssets(); } #endif diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Web3ConfigAsset.cs similarity index 81% rename from Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs rename to Packages/io.chainsafe.web3-unity/Runtime/Scripts/Web3ConfigAsset.cs index d8a736cec..c6aa06ed6 100644 --- a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Web3ConfigAsset.cs @@ -5,8 +5,8 @@ namespace ChainSafe.Gaming { - [CreateAssetMenu(menuName = "ChainSafe/Project Configuration", fileName = "ProjectConfig", order = 0)] - public class ProjectConfigAsset : ScriptableObject, ICompleteProjectConfig + [CreateAssetMenu(menuName = "ChainSafe/Project Configuration", fileName = "Web3Config", order = -100)] + public class Web3ConfigAsset : ScriptableObject, ICompleteProjectConfig { [field: SerializeField] public string ProjectId { get; set; } [field: SerializeField] public bool EnableAnalytics { get; set; } diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs.meta b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Web3ConfigAsset.cs.meta similarity index 100% rename from Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigAsset.cs.meta rename to Packages/io.chainsafe.web3-unity/Runtime/Scripts/Web3ConfigAsset.cs.meta diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK Lootboxes/2.6.0/Lootboxes Samples/Scripts/Menues.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK Lootboxes/2.6.0/Lootboxes Samples/Scripts/Menues.cs index df875e797..b6fb0b7ad 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK Lootboxes/2.6.0/Lootboxes Samples/Scripts/Menues.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK Lootboxes/2.6.0/Lootboxes Samples/Scripts/Menues.cs @@ -2,6 +2,6 @@ { public static class Menues { - public const string Root = "ChainSafe/Chainlink - LootBoxes/"; + public const string Root = "ChainSafe/Lootboxes/"; } } \ No newline at end of file diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK Lootboxes/2.6.0/Lootboxes Samples/Scripts/Scene/StageItems/LootBoxesFrontEndDataSet.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK Lootboxes/2.6.0/Lootboxes Samples/Scripts/Scene/StageItems/LootBoxesFrontEndDataSet.cs index 91cfc6fac..df0903357 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK Lootboxes/2.6.0/Lootboxes Samples/Scripts/Scene/StageItems/LootBoxesFrontEndDataSet.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK Lootboxes/2.6.0/Lootboxes Samples/Scripts/Scene/StageItems/LootBoxesFrontEndDataSet.cs @@ -5,7 +5,7 @@ namespace LootBoxes.Chainlink.Scene.StageItems { - [CreateAssetMenu(menuName = Menues.Root + "LootBoxPrefabSet", fileName = "LootBoxPrefabSet", order = 0)] + [CreateAssetMenu(menuName = Menues.Root + "Lootbox Prefab Set", fileName = "LootboxPrefabSet", order = 0)] public class LootBoxesFrontEndDataSet : ScriptableObject { [Serializable] From 051c864e4abf13e29b9028f5ef0ee57abf260b45 Mon Sep 17 00:00:00 2001 From: creeppak Date: Sat, 14 Sep 2024 11:20:35 +0100 Subject: [PATCH 22/24] Multicall fix --- .../Scripts/Connection/IConnectionHandler.cs | 2 +- src/ChainSafe.Gaming/MultiCall/MultiCall.cs | 41 +++++++++++++++---- .../MultiCall/MultiCallConfig.cs | 11 +++++ .../MultiCall/MultiCallDefaults.cs | 1 + 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Connection/IConnectionHandler.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Connection/IConnectionHandler.cs index 8b54fe109..c8d009d37 100644 --- a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Connection/IConnectionHandler.cs +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/Connection/IConnectionHandler.cs @@ -62,7 +62,7 @@ private void ConfigureCommonServices(IWeb3ServiceCollection services) services .UseUnityEnvironment() .UseGelato(GelatoApiKey) - // .UseMultiCall() + .UseMultiCall() .UseRpcProvider() .UseMarketplace(); diff --git a/src/ChainSafe.Gaming/MultiCall/MultiCall.cs b/src/ChainSafe.Gaming/MultiCall/MultiCall.cs index 9f9ed36d3..fa1b34862 100644 --- a/src/ChainSafe.Gaming/MultiCall/MultiCall.cs +++ b/src/ChainSafe.Gaming/MultiCall/MultiCall.cs @@ -6,6 +6,7 @@ using ChainSafe.Gaming.Web3; using ChainSafe.Gaming.Web3.Core; using ChainSafe.Gaming.Web3.Core.Chains; +using ChainSafe.Gaming.Web3.Environment; using Nethereum.ABI.FunctionEncoding; using Nethereum.Contracts.QueryHandlers.MultiCall; using Nethereum.Util; @@ -24,6 +25,7 @@ public class MultiCall : IMultiCall, IChainSwitchHandler, ILifecycleParticipant private readonly IContractBuilder builder; private readonly IChainConfig chainConfig; private readonly MultiCallConfig config; + private readonly ILogWriter logWriter; private Contract multiCallContract; @@ -33,8 +35,9 @@ public class MultiCall : IMultiCall, IChainSwitchHandler, ILifecycleParticipant /// An implementation of the contract builder. /// The blockchain configuration for the associated chain. /// The configuration settings for MultiCall. - public MultiCall(IContractBuilder builder, IChainConfig chainConfig, MultiCallConfig config) + public MultiCall(IContractBuilder builder, IChainConfig chainConfig, MultiCallConfig config, ILogWriter logWriter) { + this.logWriter = logWriter; this.config = config; this.chainConfig = chainConfig; this.builder = builder; @@ -42,7 +45,7 @@ public MultiCall(IContractBuilder builder, IChainConfig chainConfig, MultiCallCo public ValueTask WillStartAsync() { - BuildMulticallContract(); + BuildMultiCallContract(); return default; } @@ -51,8 +54,8 @@ public ValueTask WillStartAsync() public Task HandleChainSwitching() { // build a new instance of the contract client - BuildMulticallContract(); - return default; + BuildMultiCallContract(); + return Task.CompletedTask; } /// @@ -64,6 +67,8 @@ public Task HandleChainSwitching() /// A list of results from executing the batched calls. public async Task> MultiCallAsync(Call3Value[] multiCalls, int pageSize = DefaultCallsPerRequest) { + AssertContractAvailable(); + if (multiCalls.Any(x => x.Value > 0)) { var results = new List(); @@ -114,8 +119,10 @@ public async Task> MultiCallAsync(Call3Value[] multiCalls, int page } } - private void BuildMulticallContract() + private void BuildMultiCallContract() { + multiCallContract = null; + try { if (MultiCallDefaults.DeployedNetworks.Contains(chainConfig.ChainId)) @@ -134,9 +141,19 @@ private void BuildMulticallContract() throw new Web3Exception("Error occured while building Multicall contract client.", e); } - throw new Web3Exception( - "Couldn't build Multicall contract. " + - $"No configuration for chain id \"{chainConfig.ChainId}\" were found."); + switch (config.UnavailableBehaviour) + { + case MultiCallConfig.UnavailableBehaviourType.Throw: + throw new Web3Exception( + "Couldn't build Multicall contract. " + + $"No configuration for chain id \"{chainConfig.ChainId}\" were found."); + case MultiCallConfig.UnavailableBehaviourType.DisableAndLog: + logWriter.Log("Couldn't build Multicall contract. " + + $"No configuration for chain id \"{chainConfig.ChainId}\" were found."); + break; + default: + throw new ArgumentOutOfRangeException(); + } } /// @@ -156,5 +173,13 @@ private List ExtractResults(IReadOnlyList results) return parsed; } + + private void AssertContractAvailable() + { + if (multiCallContract is null) + { + throw new Web3Exception("Can't use MultiCall. No contract for the active chain is available."); + } + } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/MultiCall/MultiCallConfig.cs b/src/ChainSafe.Gaming/MultiCall/MultiCallConfig.cs index dc3c76262..b6b51189f 100644 --- a/src/ChainSafe.Gaming/MultiCall/MultiCallConfig.cs +++ b/src/ChainSafe.Gaming/MultiCall/MultiCallConfig.cs @@ -9,6 +9,17 @@ public MultiCallConfig(Dictionary customNetworks) CustomNetworks = customNetworks; } + public enum UnavailableBehaviourType + { + Throw, + DisableAndLog, + } + + /// + /// Contract address of the MultiCall contract by Chain ID. + /// public IReadOnlyDictionary CustomNetworks { get; } + + public UnavailableBehaviourType UnavailableBehaviour { get; set; } = UnavailableBehaviourType.Throw; } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/MultiCall/MultiCallDefaults.cs b/src/ChainSafe.Gaming/MultiCall/MultiCallDefaults.cs index fc5475c7f..5bb762126 100644 --- a/src/ChainSafe.Gaming/MultiCall/MultiCallDefaults.cs +++ b/src/ChainSafe.Gaming/MultiCall/MultiCallDefaults.cs @@ -16,6 +16,7 @@ public static class MultiCallDefaults "5", "3", "11155111", + "11155420", "10", "69", "420", From 47643f2f0469528bc6657acfd65bc642d3390ebf Mon Sep 17 00:00:00 2001 From: creeppak Date: Sat, 14 Sep 2024 11:21:37 +0100 Subject: [PATCH 23/24] Chain Switching: Fixes and polish --- .../Web3/Core/Chains/ChainManager.cs | 6 +++++ .../Core/Nethereum/NethereumAccountAdapter.cs | 2 +- .../Scenes/SampleMain.unity | 2 +- .../SwitchChain/SwitchChainCalls.cs | 22 +++++++++++++------ 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs index 65be7b8bf..134e8d534 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Chains/ChainManager.cs @@ -54,6 +54,12 @@ public async Task SwitchChain(string newChainId) // todo add cancellation token "Can't switch chain. The last chain switching procedure has not yet finished."); } + if (newChainId == Current.ChainId) + { + logWriter.Log("Tried to switch to the chain id that's currently active. Ignoring..."); + return; + } + IsSwitching = true; try diff --git a/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumAccountAdapter.cs b/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumAccountAdapter.cs index 30dabdb31..387c54bd0 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumAccountAdapter.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumAccountAdapter.cs @@ -33,7 +33,7 @@ public NethereumAccountAdapter(IChainConfig chainConfig, ISigner signer, ITransa public Task HandleChainSwitching() { transactionManager.SetChainConfig(chainConfig); - return default; + return Task.CompletedTask; } } } \ No newline at end of file diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity index f3eb6735e..93e622dff 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scenes/SampleMain.unity @@ -16075,7 +16075,7 @@ PrefabInstance: m_Modifications: - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.size - value: 2 + value: 1 objectReference: {fileID: 0} - target: {fileID: 266788301277215411, guid: fed17eec8cc2ebf4abd403772e93693d, type: 3} propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.data[0].m_Mode diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs index 68e167333..873d66ae8 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.6.0/Web3.Unity Samples/Scripts/Scenes/SampleMain/SwitchChain/SwitchChainCalls.cs @@ -14,20 +14,28 @@ public class SwitchChainCalls : MonoBehaviour public async void ToggleChain() { - if (Web3Accessor.Web3.Chains.IsSwitching) - { - Debug.LogError("The last chain switching operation has not yet been completed."); - return; - } - // get next chain id + var previousChainIndex = currentChainIndex; currentChainIndex = (currentChainIndex + 1) % chainSetups.Length; var chainId = chainSetups[currentChainIndex].chainId; Debug.Log("Switching the chain... Make sure you confirm the chain change in your wallet."); // switch chains - await Web3Accessor.Web3.Chains.SwitchChain(chainId); + SampleFeedback.Instance.Activate(); + try + { + await Web3Accessor.Web3.Chains.SwitchChain(chainId); + } + catch + { + currentChainIndex = previousChainIndex; // revert chain index toggling + throw; + } + finally + { + SampleFeedback.Instance.Deactivate(); + } } public async void CallSmartContract() From eaa727cb4647b2bf5df546f900606e3587a171e2 Mon Sep 17 00:00:00 2001 From: creeppak Date: Sat, 14 Sep 2024 11:32:17 +0100 Subject: [PATCH 24/24] Tests fix attempt --- src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientProvider.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientProvider.cs b/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientProvider.cs index 8356c6f6d..2e033e5a7 100644 --- a/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientProvider.cs +++ b/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientProvider.cs @@ -50,9 +50,8 @@ public async Task Perform(string method, params object[] parameters) }); } - return JsonSerializer - .Create() - .Deserialize(new JTokenReader(response.Result))!; + var serializer = JsonSerializer.Create(); + return serializer.Deserialize(new JTokenReader(response.Result))!; } protected override async Task SendAsync(RpcRequestMessage request, string route = null)