diff --git a/ChainSafe.Gaming.sln.DotSettings b/ChainSafe.Gaming.sln.DotSettings index 035e93aac..f1d07a668 100644 --- a/ChainSafe.Gaming.sln.DotSettings +++ b/ChainSafe.Gaming.sln.DotSettings @@ -1,2 +1,3 @@  - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity.mud/Editor.meta b/Packages/io.chainsafe.web3-unity.mud/Editor.meta new file mode 100644 index 000000000..5f612106e --- /dev/null +++ b/Packages/io.chainsafe.web3-unity.mud/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 11a7864b1ce44989a0c8c4f8e2519489 +timeCreated: 1723644511 \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity.mud/Editor/MudConfigAssetInspector.cs b/Packages/io.chainsafe.web3-unity.mud/Editor/MudConfigAssetInspector.cs new file mode 100644 index 000000000..5e0279d8d --- /dev/null +++ b/Packages/io.chainsafe.web3-unity.mud/Editor/MudConfigAssetInspector.cs @@ -0,0 +1,74 @@ +using System; +using ChainSafe.Gaming.Mud.Unity; +using UnityEditor; +using UnityEngine; + +namespace ChainSafe.Gaming.Mud.UnityEditor +{ + [CustomEditor(typeof(MudConfigAsset))] + public class MudConfigAssetInspector : Editor + { + private MudConfigAsset asset; + private GUIStyle warningStyle; + + public override void OnInspectorGUI() + { + asset = (MudConfigAsset)target; + warningStyle ??= new GUIStyle(EditorStyles.largeLabel) + { + wordWrap = true + }; + + serializedObject.Update(); + var storageProperty = serializedObject.FindProperty(nameof(MudConfigAsset.StorageType)); + + EditorGUILayout.PropertyField(storageProperty, new GUIContent("From Block Number")); + EditorGUILayout.Space(); + GUILayout.Label("Storage Settings", EditorStyles.boldLabel); + + EditorGUI.indentLevel++; + EditorGUILayout.BeginVertical(); + + switch (asset.StorageType) + { + case MudStorageType.LocalStorage: + DrawLocalStorageGui(); + break; + case MudStorageType.OffchainIndexer: + DrawOffchainIndexerStorageGui(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + EditorGUILayout.EndVertical(); + EditorGUI.indentLevel--; + + serializedObject.ApplyModifiedProperties(); + } + + private void DrawLocalStorageGui() + { + EditorGUILayout.PropertyField( + serializedObject.FindProperty(nameof(MudConfigAsset.InMemoryFromBlockNumber)), + new GUIContent("Scan From Block Number")); + } + + private void DrawOffchainIndexerStorageGui() + { + EditorGUILayout.LabelField("Offchain Indexer Storage is not implemented yet. Please use Local Storage for now.", warningStyle); + + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Switch to Local Storage")) + { + asset.StorageType = MudStorageType.LocalStorage; + EditorUtility.SetDirty(asset); + } + + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + } + } +} \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity.mud/Editor/MudConfigAssetInspector.cs.meta b/Packages/io.chainsafe.web3-unity.mud/Editor/MudConfigAssetInspector.cs.meta new file mode 100644 index 000000000..38702f907 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity.mud/Editor/MudConfigAssetInspector.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9ecc20721abf4136bec5ec41196b0b90 +timeCreated: 1723644650 \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity.mud/Editor/io.chainsafe.web3-unity.mud.editor.asmdef b/Packages/io.chainsafe.web3-unity.mud/Editor/io.chainsafe.web3-unity.mud.editor.asmdef new file mode 100644 index 000000000..66675b98d --- /dev/null +++ b/Packages/io.chainsafe.web3-unity.mud/Editor/io.chainsafe.web3-unity.mud.editor.asmdef @@ -0,0 +1,19 @@ +{ + "name": "io.chainsafe.web3-unity.mud.editor", + "rootNamespace": "ChainSafe.Gaming.Mud.UnityEditor", + "references": [ + "GUID:5426c6b788696eb4c88f4198b59839eb", + "GUID:92a3dff85d90408b8d1f8462122eb7d5" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity.mud/Editor/io.chainsafe.web3-unity.mud.editor.asmdef.meta b/Packages/io.chainsafe.web3-unity.mud/Editor/io.chainsafe.web3-unity.mud.editor.asmdef.meta new file mode 100644 index 000000000..c223b8559 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity.mud/Editor/io.chainsafe.web3-unity.mud.editor.asmdef.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bd8b93e754b5421996f07da73283c36e +timeCreated: 1723644553 \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity.mud/Runtime/MudConfigAsset.cs b/Packages/io.chainsafe.web3-unity.mud/Runtime/MudConfigAsset.cs new file mode 100644 index 000000000..471bd210d --- /dev/null +++ b/Packages/io.chainsafe.web3-unity.mud/Runtime/MudConfigAsset.cs @@ -0,0 +1,33 @@ +using System; +using ChainSafe.Gaming.Mud.Storages; +using ChainSafe.Gaming.Mud.Storages.InMemory; +using ChainSafe.Gaming.Web3; +using UnityEngine; + +namespace ChainSafe.Gaming.Mud.Unity +{ + /// + /// Represents a configuration asset for MUD module. + /// + [CreateAssetMenu(menuName = "ChainSafe/Mud Config Asset", fileName = "MudConfigAsset", order = 0)] + public class MudConfigAsset : ScriptableObject, IMudConfig + { + public MudStorageType StorageType; + public ulong InMemoryFromBlockNumber; + + public IMudStorageConfig StorageConfig => BuildStorageConfig(); + + private IMudStorageConfig BuildStorageConfig() + { + switch (StorageType) + { + case MudStorageType.LocalStorage: + return new InMemoryMudStorageConfig { FromBlockNumber = InMemoryFromBlockNumber }; + case MudStorageType.OffchainIndexer: + throw new Web3Exception("Offchain Indexer Storage is not implemented yet."); + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity.mud/Runtime/MudConfigAsset.cs.meta b/Packages/io.chainsafe.web3-unity.mud/Runtime/MudConfigAsset.cs.meta new file mode 100644 index 000000000..e143b43e2 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity.mud/Runtime/MudConfigAsset.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9144050b840b41d29f70ad254a875651 +timeCreated: 1723643882 \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity.mud/Runtime/MudStorageType.cs b/Packages/io.chainsafe.web3-unity.mud/Runtime/MudStorageType.cs new file mode 100644 index 000000000..ac04be949 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity.mud/Runtime/MudStorageType.cs @@ -0,0 +1,14 @@ +namespace ChainSafe.Gaming.Mud.Unity +{ + /// + /// Represents the different types of storage strategies for a MUD World. + /// + public enum MudStorageType + { + /// Use Local Storage Strategy. + LocalStorage, + + ///Use Off-Chain Indexer Storage Strategy. + OffchainIndexer + } +} \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity.mud/Runtime/MudStorageType.cs.meta b/Packages/io.chainsafe.web3-unity.mud/Runtime/MudStorageType.cs.meta new file mode 100644 index 000000000..1df46d3ad --- /dev/null +++ b/Packages/io.chainsafe.web3-unity.mud/Runtime/MudStorageType.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f619dd1d0d184130b68ca1ceb96270b3 +timeCreated: 1723644519 \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity.mud/Runtime/io.chainsafe.web3-unity.mud.runtime.asmdef b/Packages/io.chainsafe.web3-unity.mud/Runtime/io.chainsafe.web3-unity.mud.runtime.asmdef new file mode 100644 index 000000000..b0e2a7a23 --- /dev/null +++ b/Packages/io.chainsafe.web3-unity.mud/Runtime/io.chainsafe.web3-unity.mud.runtime.asmdef @@ -0,0 +1,16 @@ +{ + "name": "io.chainsafe.web3-unity.mud.runtime", + "rootNamespace": "ChainSafe.Gaming.Mud.Unity", + "references": [ + "GUID:5426c6b788696eb4c88f4198b59839eb" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity.mud/Runtime/io.chainsafe.web3-unity.mud.runtime.asmdef.meta b/Packages/io.chainsafe.web3-unity.mud/Runtime/io.chainsafe.web3-unity.mud.runtime.asmdef.meta new file mode 100644 index 000000000..922dc1fdf --- /dev/null +++ b/Packages/io.chainsafe.web3-unity.mud/Runtime/io.chainsafe.web3-unity.mud.runtime.asmdef.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 92a3dff85d90408b8d1f8462122eb7d5 +timeCreated: 1723643922 \ No newline at end of file diff --git a/Packages/io.chainsafe.web3-unity/Editor/ServerSettings.cs b/Packages/io.chainsafe.web3-unity/Editor/ServerSettings.cs index 8dd4a526f..77f1c7956 100644 --- a/Packages/io.chainsafe.web3-unity/Editor/ServerSettings.cs +++ b/Packages/io.chainsafe.web3-unity/Editor/ServerSettings.cs @@ -74,7 +74,7 @@ private enum FetchingStatus private void Awake() { // Get saved settings or revert to default - var projectConfig = ProjectConfigUtilities.Load(); + 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; diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs index 19c875698..504f7ac82 100644 --- a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/ProjectConfigUtilities.cs @@ -8,31 +8,6 @@ namespace ChainSafe.Gaming.UnityPackage { public static class ProjectConfigUtilities { - private class LocalhostChainConfig : IChainConfig - { - public LocalhostChainConfig(string chainId, string symbol, string chain, string network, string port) - { - var localhostEndPoint = $"127.0.0.1:{port}"; - - ChainId = chainId; - Symbol = symbol; - Chain = chain; - Network = network; - Rpc = $"http://{localhostEndPoint}"; - Ws = $"$ws://{localhostEndPoint}"; - BlockExplorerUrl = $"http://{localhostEndPoint}"; - } - - public string ChainId { get; } - public string Symbol { get; } - public string Chain { get; } - public string Network { get; } - public string Rpc { get; } - public string Ipc => null; - public string Ws { get; } - public string BlockExplorerUrl { get; } - } - private const string AssetName = "ProjectConfigData"; public static ProjectConfigScriptableObject Load() @@ -93,5 +68,29 @@ public static void Save(ProjectConfigScriptableObject projectConfig) UnityEditor.AssetDatabase.SaveAssets(); } #endif + private class LocalhostChainConfig : IChainConfig + { + public LocalhostChainConfig(string chainId, string symbol, string chain, string network, string port) + { + var localhostEndPoint = $"127.0.0.1:{port}"; + + ChainId = chainId; + Symbol = symbol; + Chain = chain; + Network = network; + Rpc = $"http://{localhostEndPoint}"; + Ws = $"ws://{localhostEndPoint}"; + BlockExplorerUrl = $"http://{localhostEndPoint}"; + } + + public string ChainId { get; } + public string Symbol { get; } + public string Chain { get; } + public string Network { get; } + public string Rpc { get; } + public string Ipc => null; + public string Ws { get; } + public string BlockExplorerUrl { get; } + } } } \ No newline at end of file diff --git a/scripts/debug-publish-to-unity-package.bat b/scripts/debug-publish-to-unity-package.bat index 07f9a713d..1d40e8c45 100644 --- a/scripts/debug-publish-to-unity-package.bat +++ b/scripts/debug-publish-to-unity-package.bat @@ -10,6 +10,11 @@ pushd "%SCRIPT_DIR%\..\src\ChainSafe.Gaming.Unity" rem Publish the project dotnet publish ChainSafe.Gaming.Unity.csproj -c Debug /property:Unity=true +IF %ERRORLEVEL% NEQ 0 ( + echo Execution failed + exit /b %ERRORLEVEL% +) + set PUBLISH_PATH=bin\Debug\netstandard2.1\publish rem List generated DLLs diff --git a/src/ChainSafe.Gaming.Mud/ChainSafe.Gaming.Mud.csproj b/src/ChainSafe.Gaming.Mud/ChainSafe.Gaming.Mud.csproj index bfe56d6eb..7ff81156e 100644 --- a/src/ChainSafe.Gaming.Mud/ChainSafe.Gaming.Mud.csproj +++ b/src/ChainSafe.Gaming.Mud/ChainSafe.Gaming.Mud.csproj @@ -14,9 +14,6 @@ - - - diff --git a/src/ChainSafe.Gaming.Mud/IMudConfig.cs b/src/ChainSafe.Gaming.Mud/IMudConfig.cs new file mode 100644 index 000000000..b4a6b60de --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/IMudConfig.cs @@ -0,0 +1,9 @@ +using ChainSafe.Gaming.Mud.Storages; + +namespace ChainSafe.Gaming.Mud +{ + public interface IMudConfig + { + IMudStorageConfig StorageConfig { get; } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/MudConfig.cs b/src/ChainSafe.Gaming.Mud/MudConfig.cs new file mode 100644 index 000000000..d25ae558e --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/MudConfig.cs @@ -0,0 +1,9 @@ +using ChainSafe.Gaming.Mud.Storages; + +namespace ChainSafe.Gaming.Mud +{ + public class MudConfig : IMudConfig + { + public IMudStorageConfig StorageConfig { get; set; } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/MudException.cs b/src/ChainSafe.Gaming.Mud/MudException.cs new file mode 100644 index 000000000..6884cd4e6 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/MudException.cs @@ -0,0 +1,22 @@ +using System; +using ChainSafe.Gaming.Web3; + +namespace ChainSafe.Gaming.Mud +{ + /// + /// Represents an exception that is thrown when a MUD-related error occurs. + /// + /// + public class MudException : Web3Exception + { + internal MudException(string message) + : base(message) + { + } + + internal MudException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/MudExtensions.cs b/src/ChainSafe.Gaming.Mud/MudExtensions.cs index e847048b7..50f707421 100644 --- a/src/ChainSafe.Gaming.Mud/MudExtensions.cs +++ b/src/ChainSafe.Gaming.Mud/MudExtensions.cs @@ -1,18 +1,68 @@ -using System.Linq; +using ChainSafe.Gaming.Mud.Storages; +using ChainSafe.Gaming.Mud.Storages.InMemory; +using ChainSafe.Gaming.Mud.Worlds; +using ChainSafe.Gaming.RPC.Events; using ChainSafe.Gaming.Web3.Build; using ChainSafe.Gaming.Web3.Core.Nethereum; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace ChainSafe.Gaming.Mud { public static class MudExtensions { + /// + /// Configures and enables the use of MUD for the Web3 client that is being built. + /// + /// The Web3 service collection to configure. + /// The MUD configuration. + /// The same service collection that was passed in. This enables fluent style. + public static IWeb3ServiceCollection UseMud(this IWeb3ServiceCollection services, IMudConfig mudConfig) + { + services.AssertServiceNotBound(); + services.AssertConfigurationNotBound(); + + services.ConfigureMud(mudConfig); + services.UseMud(); + + return services; + } + + /// + /// Configures the MUD services with the provided MUD configuration. + /// + /// The Web3 service collection to configure. + /// The MUD configuration. + /// The same service collection that was passed in. This enables fluent style. + public static IWeb3ServiceCollection ConfigureMud(this IWeb3ServiceCollection services, IMudConfig mudConfig) + { + services.Replace(ServiceDescriptor.Singleton(mudConfig)); + + return services; + } + + /// + /// Enables the use of MUD for the Web3 client that is being built. + /// + /// The Web3 service collection to configure. + /// The same service collection that was passed in. This enables fluent style. public static IWeb3ServiceCollection UseMud(this IWeb3ServiceCollection services) { - services.AddSingleton(typeof(MudFacade)); + services.AssertServiceNotBound(); + + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + + // Storage strategies + services.AddTransient(); // todo implement OffchainIndexerMudStorage, then register it in the next line + + if (!services.IsBound()) + { + services.UseEvents(); + } - if (!services.IsNethereumAdaptersBound()) + if (!services.IsBound()) { services.UseNethereumAdapters(); } @@ -20,6 +70,11 @@ public static IWeb3ServiceCollection UseMud(this IWeb3ServiceCollection services return services; } + /// + /// Retrieves the MudFacade instance, a facade class for all the MUD-related functionality, from the Web3 service provider. + /// + /// The Web3 client. + /// The MudFacade instance. public static MudFacade Mud(this Web3.Web3 web3) => web3.ServiceProvider.GetRequiredService(); } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/MudFacade.cs b/src/ChainSafe.Gaming.Mud/MudFacade.cs index 85b3d6c90..dc4503e0d 100644 --- a/src/ChainSafe.Gaming.Mud/MudFacade.cs +++ b/src/ChainSafe.Gaming.Mud/MudFacade.cs @@ -1,23 +1,35 @@ +using System.Diagnostics; +using System.Threading.Tasks; +using ChainSafe.Gaming.Mud.Worlds; +using ChainSafe.Gaming.Web3.Environment; + namespace ChainSafe.Gaming.Mud { + /// + /// A facade class for all the MUD-related functionality. + /// public class MudFacade { private readonly MudWorldFactory worldFactory; + private readonly ILogWriter logWriter; - public MudFacade(MudWorldFactory worldFactory) + public MudFacade(MudWorldFactory worldFactory, ILogWriter logWriter) { + this.logWriter = logWriter; this.worldFactory = worldFactory; } /// - /// Builds a MUD World Client to exchange messages with a World Contract. + /// Builds a new MudWorld client based on the provided configuration. /// - /// The address of the World Contract. - /// The ABI of the World Contract. - /// The client for the MUD World Contract. - public MudWorld BuildWorld(string contractAddress, string worldContractAbi) + /// The configuration settings for the world. + /// A Task that represents the asynchronous operation. The Task's result is the created MudWorld. + public Task BuildWorld(IMudWorldConfig worldConfig) { - return worldFactory.Build(contractAddress, worldContractAbi); + var stopwatch = Stopwatch.StartNew(); + var world = worldFactory.Build(worldConfig); + logWriter.Log($"Loaded world {worldConfig.ContractAddress} in {stopwatch.Elapsed}"); + return world; } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/MudUtils.cs b/src/ChainSafe.Gaming.Mud/MudUtils.cs new file mode 100644 index 000000000..b9c6a3920 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/MudUtils.cs @@ -0,0 +1,23 @@ +using Nethereum.Mud.EncodingDecoding; + +namespace ChainSafe.Gaming.Mud +{ + public static class MudUtils + { + public static byte[] TableResourceId(string @namespace, string tableName, bool isOffChain = false) + { + var trimmedName = ResourceEncoder.TrimNameAsValidSize(tableName); + + return isOffChain + ? ResourceEncoder.EncodeOffchainTable(@namespace, trimmedName) + : ResourceEncoder.EncodeTable(@namespace, trimmedName); + } + + public static string NamespaceFunctionName(string @namespace, string function) + { + return !function.StartsWith($"{@namespace}__") + ? $"{@namespace}__{function}" + : function; // already contains namespace + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/MudWorld.cs b/src/ChainSafe.Gaming.Mud/MudWorld.cs deleted file mode 100644 index 2a49846ab..000000000 --- a/src/ChainSafe.Gaming.Mud/MudWorld.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Threading.Tasks; -using ChainSafe.Gaming.Evm.Contracts; -using ChainSafe.Gaming.Evm.Transactions; -using Nethereum.Contracts; -using Nethereum.Hex.HexTypes; -using Nethereum.Mud; -using Nethereum.Mud.Contracts.World; -using Nethereum.Web3; - -namespace ChainSafe.Gaming.Mud -{ - // todo add event subscription - public class MudWorld : IMudWorld, IContract - { - private readonly IContract contract; - - public MudWorld(IWeb3 nethWeb3, IContract contract) - { - this.contract = contract; - WorldService = new WorldService(nethWeb3, contract.Address); - } - - string IContract.Address => contract.Address; - - /// - /// A Nethereum World Service. Use this if you need more control over the World. - /// - public WorldService WorldService { get; } - - public async Task Query() - where TRecord : TableRecordSingleton, new() - where TValue : class, new() - { - return (await WorldService.GetRecordTableQueryAsync()).Values; - } - - public async Task Query(TKey key) - where TRecord : TableRecord, new() - where TKey : class, new() - where TValue : class, new() - { - var record = new TRecord { Keys = key }; - return (await WorldService.GetRecordTableQueryAsync(record)).Values; - } - - IContract IContract.Attach(string address) - { - return contract.Attach(address); - } - - Task IContract.Call(string method, object[] parameters = null, TransactionRequest overwrite = null) - { - return contract.Call(method, parameters, overwrite); - } - - object[] IContract.Decode(string method, string output) - { - return contract.Decode(method, output); - } - - Task IContract.Send(string method, object[] parameters = null, TransactionRequest overwrite = null) - { - return contract.Send(method, parameters, overwrite); - } - - Task<(object[] response, TransactionReceipt receipt)> IContract.SendWithReceipt(string method, object[] parameters = null, TransactionRequest overwrite = null) - { - return contract.SendWithReceipt(method, parameters, overwrite); - } - - Task IContract.EstimateGas(string method, object[] parameters, TransactionRequest overwrite = null) - { - return contract.EstimateGas(method, parameters, overwrite); - } - - string IContract.Calldata(string method, object[] parameters = null) - { - return contract.Calldata(method, parameters); - } - - public Task PrepareTransactionRequest(string method, object[] parameters, bool isReadCall = false, TransactionRequest overwrite = null) - { - return contract.PrepareTransactionRequest(method, parameters, isReadCall, overwrite); - } - } -} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/MudWorldFactory.cs b/src/ChainSafe.Gaming.Mud/MudWorldFactory.cs deleted file mode 100644 index 0fcde3592..000000000 --- a/src/ChainSafe.Gaming.Mud/MudWorldFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -using ChainSafe.Gaming.Evm.Contracts; -using ChainSafe.Gaming.Web3.Core.Nethereum; - -namespace ChainSafe.Gaming.Mud -{ - public class MudWorldFactory - { - private readonly INethereumWeb3Adapter nethWeb3; - private IContractBuilder contractBuilder; - - public MudWorldFactory(INethereumWeb3Adapter nethWeb3, IContractBuilder contractBuilder) - { - this.contractBuilder = contractBuilder; - this.nethWeb3 = nethWeb3; - } - - public MudWorld Build(string worldAddress, string worldContractAbi) - { - var contract = contractBuilder.Build(worldContractAbi, worldAddress); - var mudWorld = new MudWorld(nethWeb3, contract); - return mudWorld; - } - } -} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Storages/IMudStorage.cs b/src/ChainSafe.Gaming.Mud/Storages/IMudStorage.cs new file mode 100644 index 000000000..0718c9bac --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Storages/IMudStorage.cs @@ -0,0 +1,23 @@ +using System.Linq; +using System.Threading.Tasks; +using ChainSafe.Gaming.Mud.Storages.InMemory; +using ChainSafe.Gaming.Mud.Tables; + +namespace ChainSafe.Gaming.Mud.Storages +{ + public interface IMudStorage + { + event RecordSetDelegate RecordSet; + + event RecordDeletedDelegate RecordDeleted; + + Task Initialize(IMudStorageConfig mudStorageConfig, string worldAddress); + + Task Terminate(); + + Task Query(MudTableSchema tableSchema, MudQuery query); + + async Task QuerySingle(MudTableSchema tableSchema, MudQuery query) + => (await Query(tableSchema, query)).Single(); + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Storages/IMudStorageConfig.cs b/src/ChainSafe.Gaming.Mud/Storages/IMudStorageConfig.cs new file mode 100644 index 000000000..e1c59f682 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Storages/IMudStorageConfig.cs @@ -0,0 +1,18 @@ +using System; + +namespace ChainSafe.Gaming.Mud.Storages +{ + /// + /// Interface for configuring the MUD storage. + /// + public interface IMudStorageConfig + { + /// + /// Gets the type of the storage strategy. + /// + /// + /// The type of the storage strategy. + /// + Type StorageStrategyType { get; } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Storages/IMudStorageFactory.cs b/src/ChainSafe.Gaming.Mud/Storages/IMudStorageFactory.cs new file mode 100644 index 000000000..b8e0de95a --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Storages/IMudStorageFactory.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace ChainSafe.Gaming.Mud.Storages +{ + public interface IMudStorageFactory + { + Task Build(IMudStorageConfig mudStorageConfig, string worldAddress); + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Storages/InMemory/IInMemoryMudStorageConfig.cs b/src/ChainSafe.Gaming.Mud/Storages/InMemory/IInMemoryMudStorageConfig.cs new file mode 100644 index 000000000..80036c658 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Storages/InMemory/IInMemoryMudStorageConfig.cs @@ -0,0 +1,12 @@ +using System; +using System.Numerics; + +namespace ChainSafe.Gaming.Mud.Storages.InMemory +{ + public interface IInMemoryMudStorageConfig : IMudStorageConfig + { + Type IMudStorageConfig.StorageStrategyType => typeof(InMemoryMudStorage); + + BigInteger FromBlockNumber { get; } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Storages/InMemory/InMemoryMudStorage.cs b/src/ChainSafe.Gaming.Mud/Storages/InMemory/InMemoryMudStorage.cs new file mode 100644 index 000000000..476de062c --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Storages/InMemory/InMemoryMudStorage.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ChainSafe.Gaming.Mud.Tables; +using ChainSafe.Gaming.RPC.Events; +using ChainSafe.Gaming.Web3.Core.Nethereum; +using Nethereum.Hex.HexConvertors.Extensions; +using Nethereum.Mud; +using Nethereum.Mud.Contracts.Core.StoreEvents; +using Nethereum.Mud.EncodingDecoding; +using Nethereum.Mud.TableRepository; +using Nethereum.Util; + +namespace ChainSafe.Gaming.Mud.Storages.InMemory +{ + public class InMemoryMudStorage : IMudStorage + { + private readonly INethereumWeb3Adapter nWeb3; + private readonly EventManager eventManager; + + private readonly SemaphoreSlim storeUpdateSemaphore = new(1); + + private IInMemoryMudStorageConfig config; + private InMemoryTableRepository inMemoryRepository; + + public InMemoryMudStorage(INethereumWeb3Adapter nWeb3, EventManager eventManager) + { + this.eventManager = eventManager; + this.nWeb3 = nWeb3; + } + + public event RecordSetDelegate RecordSet; + + public event RecordDeletedDelegate RecordDeleted; + + public async Task Initialize(IMudStorageConfig mudStorageConfig, string worldAddress) + { + config = (IInMemoryMudStorageConfig)mudStorageConfig; + inMemoryRepository = new InMemoryTableRepository(); + var storeLogProcessingService = new StoreEventsLogProcessingService(nWeb3, worldAddress); + await storeLogProcessingService.ProcessAllStoreChangesAsync( + inMemoryRepository, + config.FromBlockNumber, + null, + CancellationToken.None); + + await eventManager.Subscribe(OnStoreSetRecord); + await eventManager.Subscribe(OnStoreSpliceStaticData); + await eventManager.Subscribe(OnStoreSpliceDynamicDataEventDTO); + await eventManager.Subscribe(OnStoreDeleteRecord); + } + + public async Task Terminate() + { + await eventManager.Unsubscribe(OnStoreSetRecord); + await eventManager.Unsubscribe(OnStoreSpliceStaticData); + await eventManager.Unsubscribe(OnStoreSpliceDynamicDataEventDTO); + await eventManager.Unsubscribe(OnStoreDeleteRecord); + } + + public async Task Query(MudTableSchema tableSchema, MudQuery query) + { + var columnParameters = tableSchema.ColumnsToValueParametersOutput().ToArray(); + var encodedRecords = await inMemoryRepository.GetRecordsAsync(tableSchema.ResourceId); + + var rawRecords = encodedRecords.Select(ToRawRecord); + var filteredRecords = Filter(tableSchema, rawRecords, query); + + return filteredRecords.ToArray(); + + object[] ToRawRecord(EncodedTableRecord encodedRecord) + { + var encodedValues = encodedRecord.EncodedValues; + var encodedBytes = ByteUtil.Merge(encodedValues.StaticData) + .Concat(encodedValues.EncodedLengths) + .Concat(ByteUtil.Merge(encodedValues.DynamicData)) + .ToArray(); + + var resultParameters = ValueEncoderDecoder.DecodeValues(encodedBytes, columnParameters); + return resultParameters.Select(p => p.Result).ToArray(); + } + } + + private static IEnumerable Filter( + MudTableSchema tableSchema, + IEnumerable rawRecords, + MudQuery query) + { + if (query.FindWithKey) + { + var keyIndices = tableSchema.KeyIndices; + var record = rawRecords.SingleOrDefault(record => KeyEquals(record, keyIndices, query.KeyFilter)); + + if (record != null) + { + return new[] { record }; + } + else + { + return Array.Empty(); + } + + bool KeyEquals(object[] record, int[] keyColumnIndices, object[] keys) + { + if (keyColumnIndices.Length == 0) + { + throw new InvalidOperationException($"{nameof(keyColumnIndices)} is empty"); + } + + for (var keyIndex = 0; keyIndex < keyColumnIndices.Length; keyIndex++) + { + var columnIndex = keyColumnIndices[keyIndex]; + + if (!record[columnIndex].Equals(keys[keyIndex])) + { + return false; + } + } + + return true; + } + } + + // fallback: return original + return rawRecords; + } + + private async Task RecordExists(byte[] tableId, List keyTuple) + { + var existingRecord = await inMemoryRepository.GetRecordAsync( + tableId.ToHex(true), + InMemoryTableRepository.ConvertKeyToCommaSeparatedHex(keyTuple)); + var recordExists = existingRecord != null; + return recordExists; + } + + private async void OnStoreSetRecord(StoreSetRecordEventDTO obj) + { + var recordExists = await RecordExists(obj.TableId, obj.KeyTuple); + + await storeUpdateSemaphore.WaitAsync(); + try + { + await inMemoryRepository.SetRecordAsync(obj.TableId, obj.KeyTuple, obj.StaticData, obj.EncodedLengths, obj.DynamicData); + } + finally + { + storeUpdateSemaphore.Release(); + } + + RecordSet.Invoke(obj.TableId, obj.KeyTuple, !recordExists); + } + + private async void OnStoreSpliceStaticData(StoreSpliceStaticDataEventDTO obj) + { + var recordExists = await RecordExists(obj.TableId, obj.KeyTuple); + + await storeUpdateSemaphore.WaitAsync(); + try + { + await inMemoryRepository.SetSpliceStaticDataAsync(obj.TableId, obj.KeyTuple, obj.Start, obj.Data); + } + finally + { + storeUpdateSemaphore.Release(); + } + + RecordSet.Invoke(obj.TableId, obj.KeyTuple, !recordExists); + } + + private async void OnStoreSpliceDynamicDataEventDTO(StoreSpliceDynamicDataEventDTO obj) + { + var recordExists = await RecordExists(obj.TableId, obj.KeyTuple); + + await storeUpdateSemaphore.WaitAsync(); + try + { + await inMemoryRepository.SetSpliceDynamicDataAsync(obj.TableId, obj.KeyTuple, obj.Start, obj.Data, obj.DeleteCount, obj.EncodedLengths); + } + finally + { + storeUpdateSemaphore.Release(); + } + + RecordSet.Invoke(obj.TableId, obj.KeyTuple, !recordExists); + } + + private async void OnStoreDeleteRecord(StoreDeleteRecordEventDTO obj) + { + await storeUpdateSemaphore.WaitAsync(); + try + { + await inMemoryRepository.DeleteRecordAsync(obj.TableId, obj.KeyTuple); + } + finally + { + storeUpdateSemaphore.Release(); + } + + RecordDeleted.Invoke(obj.TableId, obj.KeyTuple); + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Storages/InMemory/InMemoryMudStorageConfig.cs b/src/ChainSafe.Gaming.Mud/Storages/InMemory/InMemoryMudStorageConfig.cs new file mode 100644 index 000000000..7f6783291 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Storages/InMemory/InMemoryMudStorageConfig.cs @@ -0,0 +1,19 @@ +using System.Numerics; + +namespace ChainSafe.Gaming.Mud.Storages.InMemory +{ + /// + /// Represents the configuration for in-memory MUD storage. + /// + public class InMemoryMudStorageConfig : IInMemoryMudStorageConfig + { + /// + /// Gets or sets the starting block number for retrieving data. + /// + /// + /// This property represents the block number from which the data retrieval should start. + /// The value of this property should be a non-negative integer. + /// + public BigInteger FromBlockNumber { get; set; } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Storages/InMemory/MudTableSchemaExtensions.cs b/src/ChainSafe.Gaming.Mud/Storages/InMemory/MudTableSchemaExtensions.cs new file mode 100644 index 000000000..304ac1284 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Storages/InMemory/MudTableSchemaExtensions.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using ChainSafe.Gaming.Mud.Tables; +using Nethereum.ABI.FunctionEncoding; +using Nethereum.ABI.Model; + +namespace ChainSafe.Gaming.Mud.Storages.InMemory +{ + public static class MudTableSchemaExtensions + { + public static IEnumerable ColumnsToValueParametersOutput(this MudTableSchema tableSchema) + { + return tableSchema.Columns + .Where(pair => tableSchema.KeyColumns.Length == 0 || !tableSchema.KeyColumns.Contains(pair.Key)) // skip keys + .Select((pair, i) => + { + var order = i + 1; + var name = pair.Key; + var type = pair.Value; + var parameter = new Parameter(type, name, order); + + // we can only GetDefaultDecodingType after parameter gets constructed + parameter.DecodedType = parameter.ABIType.GetDefaultDecodingType(); + + return new ParameterOutput { Parameter = parameter, }; + }); + } + + public static IEnumerable KeyToParametersOutput(this MudTableSchema tableSchema) + { + return tableSchema.KeyColumns.Select((columnName, i) => + { + var columnType = tableSchema.GetColumnType(columnName); + var parameter = new Parameter(columnType, columnName, i); + + // we can only GetDefaultDecodingType after parameter gets constructed + parameter.DecodedType = parameter.ABIType.GetDefaultDecodingType(); + + return new ParameterOutput { Parameter = parameter }; + }); + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Storages/MudStorageFactory.cs b/src/ChainSafe.Gaming.Mud/Storages/MudStorageFactory.cs new file mode 100644 index 000000000..1212a2b78 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Storages/MudStorageFactory.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using ChainSafe.Gaming.Web3; +using Microsoft.Extensions.DependencyInjection; + +namespace ChainSafe.Gaming.Mud.Storages +{ + public class MudStorageFactory : IMudStorageFactory + { + private readonly IServiceProvider serviceProvider; + + public MudStorageFactory(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + public async Task Build(IMudStorageConfig mudStorageConfig, string worldAddress) + { + if (!typeof(IMudStorage).IsAssignableFrom(mudStorageConfig.StorageStrategyType)) + { + throw new MudException($"Provided MUD Storage Strategy type doesn't implement {nameof(IMudStorage)}"); + } + + var storageStrategy = (IMudStorage)serviceProvider.GetRequiredService(mudStorageConfig.StorageStrategyType); + await storageStrategy.Initialize(mudStorageConfig, worldAddress); + return storageStrategy; + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Storages/RecordDeletedDelegate.cs b/src/ChainSafe.Gaming.Mud/Storages/RecordDeletedDelegate.cs new file mode 100644 index 000000000..d315964b9 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Storages/RecordDeletedDelegate.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; + +namespace ChainSafe.Gaming.Mud.Storages +{ + public delegate void RecordDeletedDelegate(byte[] tableId, List key); +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Storages/RecordSetDelegate.cs b/src/ChainSafe.Gaming.Mud/Storages/RecordSetDelegate.cs new file mode 100644 index 000000000..3753c32ae --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Storages/RecordSetDelegate.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; + +namespace ChainSafe.Gaming.Mud.Storages +{ + public delegate void RecordSetDelegate(byte[] tableId, List key, bool newRecord); +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Systems/MudSystems.cs b/src/ChainSafe.Gaming.Mud/Systems/MudSystems.cs new file mode 100644 index 000000000..be00f7556 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Systems/MudSystems.cs @@ -0,0 +1,85 @@ +using System.Threading.Tasks; +using ChainSafe.Gaming.Evm.Contracts; +using ChainSafe.Gaming.Evm.Transactions; +using Nethereum.Hex.HexTypes; + +namespace ChainSafe.Gaming.Mud.Systems +{ + /// + /// Represents a MUD systems client, which is a wrapper class that extends the functionality of a Contract and adds namespace support. + /// + /// + /// Use this to call system functions. + /// You don't need to specify namespace when calling a function, just use the function name. + /// + public class MudSystems : IContract + { + private readonly string ns; + + private readonly Contract contract; + + /// + /// Initializes a new instance of the class. + /// + /// The namespace to be used. + /// The contract to be used. + public MudSystems(string @namespace, Contract contract) + { + this.contract = contract; + ns = @namespace; + } + + public string Address => contract.Address; + + public IContract Attach(string address) + { + return contract.Attach(address); + } + + public Task Call(string method, object[] parameters = null, TransactionRequest overwrite = null) + { + return contract.Call(MudUtils.NamespaceFunctionName(ns, method), parameters, overwrite); + } + + public object[] Decode(string method, string output) + { + return contract.Decode(MudUtils.NamespaceFunctionName(ns, method), output); + } + + public Task Send(string method, object[] parameters = null, TransactionRequest overwrite = null) + { + return contract.Send(MudUtils.NamespaceFunctionName(ns, method), parameters, overwrite); + } + + public Task<(object[] response, TransactionReceipt receipt)> SendWithReceipt(string method, object[] parameters = null, TransactionRequest overwrite = null) + { + return contract.SendWithReceipt(MudUtils.NamespaceFunctionName(ns, method), parameters, overwrite); + } + + public Task EstimateGas(string method, object[] parameters, TransactionRequest overwrite = null) + { + return contract.EstimateGas( + MudUtils.NamespaceFunctionName(ns, method), + parameters, + overwrite); + } + + public string Calldata(string method, object[] parameters = null) + { + return contract.Calldata(MudUtils.NamespaceFunctionName(ns, method), parameters); + } + + public Task PrepareTransactionRequest( + string method, + object[] parameters, + bool isReadCall = false, + TransactionRequest overwrite = null) + { + return contract.PrepareTransactionRequest( + MudUtils.NamespaceFunctionName(ns, method), + parameters, + isReadCall, + overwrite); + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Systems/MudWorldSystems.cs b/src/ChainSafe.Gaming.Mud/Systems/MudWorldSystems.cs new file mode 100644 index 000000000..b04adeb93 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Systems/MudWorldSystems.cs @@ -0,0 +1,19 @@ +using ChainSafe.Gaming.Evm.Contracts; + +namespace ChainSafe.Gaming.Mud.Systems +{ + public class MudWorldSystems + { + private readonly Contract contract; + + public MudWorldSystems(Contract contract) + { + this.contract = contract; + } + + public MudSystems GetSystemsForNamespace(string @namespace) + { + return new MudSystems(@namespace, contract); + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Tables/MudQuery.cs b/src/ChainSafe.Gaming.Mud/Tables/MudQuery.cs new file mode 100644 index 000000000..bd3e2f4d3 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Tables/MudQuery.cs @@ -0,0 +1,41 @@ +namespace ChainSafe.Gaming.Mud.Tables +{ + /// + /// This class represents a query for filtering records of a MUD table. + /// + public class MudQuery // todo extend to support complex filters + { + private MudQuery() + { + } + + /// + /// A query that doesn't filter any records. + /// + public static MudQuery All { get; } = new(); + + /// + /// Should this Query simply look for a record using record key. + /// + public bool FindWithKey { get; private set; } + + /// + /// The key to be used when looking for a record by it's key. + /// + public object[] KeyFilter { get; private set; } + + /// + /// Creates a new instance of MudQuery that looks for a specific record by it's key. + /// + /// The key used to find the record. + /// A new instance of MudQuery with FindWithKey set to true and KeyFilter set to the specified key. + public static MudQuery ByKey(object[] key) + { + return new MudQuery + { + FindWithKey = true, + KeyFilter = key, + }; + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Tables/MudTable.cs b/src/ChainSafe.Gaming.Mud/Tables/MudTable.cs new file mode 100644 index 000000000..35c8d5636 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Tables/MudTable.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ChainSafe.Gaming.Mud.Storages; +using ChainSafe.Gaming.Mud.Storages.InMemory; +using ChainSafe.Gaming.Web3.Environment; +using Nethereum.Mud.EncodingDecoding; + +namespace ChainSafe.Gaming.Mud.Tables +{ + /// + /// Represents a table of a MUD world. + /// + public class MudTable // todo support LINQ or IQueryable + { + private readonly IMudStorage storage; + private readonly MudTableSchema tableSchema; + private readonly IMainThreadRunner mainThreadRunner; + + /// + /// Initializes a new instance of the class. + /// + /// The schema of the table. + /// The storage implementation. + /// The main thread runner implementation. + public MudTable(MudTableSchema tableSchema, IMudStorage storage, IMainThreadRunner mainThreadRunner) + { + this.mainThreadRunner = mainThreadRunner; + this.tableSchema = tableSchema; + this.storage = storage; + + storage.RecordSet += OnStorageRecordSet; + storage.RecordDeleted += OnStorageRecordDeleted; + } + + /// + /// Event that is raised when a record is added to a table. + /// + /// + /// This event is triggered when a new record is added to the table. The event handler will be invoked with information about the added record. + /// + public event RecordSetDelegate? RecordAdded; + + /// + /// Represents an event that is raised when a record has been updated. + /// + /// + /// The RecordUpdated event is typically used to notify subscribers that a record has been updated in a MUD table. + /// + public event RecordSetDelegate? RecordUpdated; + + /// + /// Event that is raised when a record is removed. + /// + public event RecordDeletedDelegate? RecordRemoved; + + /// + /// Gets the resource identifier as a byte array. + /// + /// + /// The resource identifier as a byte array. + /// + public byte[] ResourceId => tableSchema.ResourceId; + + /// + /// Executes a query on the storage using the specified MudQuery object. + /// + /// The MudQuery object representing the query to be executed. + /// + /// A Task containing an array of records that represent the results of the query. + /// Each sub-array element represents values for each column of the record. + /// + public Task Query(MudQuery query) + { + return storage.Query(tableSchema, query); + } + + /// + /// Queries the database for a single object based on the given query. + /// Ensures there is only one element that fits the query. + /// + /// The query to be executed. + /// + /// A task that represents the asynchronous operation. The task result contains an array that contains values of each column of the record. + /// + /// The query result contains more or less than 1 element. + public Task QuerySingle(MudQuery query) + { + return storage.QuerySingle(tableSchema, query); + } + + /// + /// Queries the storage for a single object based on the specified key. + /// + /// The key used to query the object. + /// + /// An awaitable task that represents the asynchronous operation. The task result contains an array that represents values of each column of the record. + /// + public Task QueryByKey(object[] key) + { + return storage.QuerySingle(tableSchema, MudQuery.ByKey(key)); + } + + private async void OnStorageRecordSet(byte[] tableId, List key, bool newRecord) + { + if (!tableId.SequenceEqual(tableSchema.ResourceId)) + { + return; + } + + var decodedKey = DecodeKey(key); + var value = decodedKey.Length > 0 + ? await storage.QuerySingle(tableSchema, MudQuery.ByKey(decodedKey)) + : await storage.QuerySingle(tableSchema, MudQuery.All); // singleton + + if (newRecord) + { + mainThreadRunner.Enqueue(() => RecordAdded?.Invoke(decodedKey, value)); + } + else + { + mainThreadRunner.Enqueue(() => RecordUpdated?.Invoke(decodedKey, value)); + } + } + + private void OnStorageRecordDeleted(byte[] tableId, List key) + { + if (!tableId.SequenceEqual(tableSchema.ResourceId)) + { + return; + } + + var decodedKey = DecodeKey(key); + mainThreadRunner.Enqueue(() => RecordRemoved?.Invoke(decodedKey)); + } + + private object[] DecodeKey(List key) + { + if (key.Count == 0) + { + return Array.Empty(); + } + + return KeyEncoderDecoder + .DecodeKey(key, tableSchema.KeyToParametersOutput().ToArray()) + .Select(output => output.Result) + .ToArray(); + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Tables/MudTableSchema.cs b/src/ChainSafe.Gaming.Mud/Tables/MudTableSchema.cs new file mode 100644 index 000000000..fbc9f3050 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Tables/MudTableSchema.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace ChainSafe.Gaming.Mud.Tables +{ + /// + /// Represents a schema for a table in the MUD world. + /// + public class MudTableSchema + { + [MaybeNull] + private byte[] resourceId; + + [MaybeNull] + private int[] keyIndices; + + /// + /// Gets or sets the namespace. + /// + /// + /// The namespace of the table. + /// + public string Namespace { get; set; } + + /// + /// Gets or sets the name of the table. + /// + /// + /// The name of the table. + /// + public string TableName { get; set; } + + /// + /// EVM data type (value) by name (key), for each column in the table. + /// + /// Please note that the order is taken into account and must match the order of the columns in your table. + public List> Columns { get; set; } // this is not Dictionary because we care about the order + + /// + /// Gets or sets the key column names. + /// + /// + /// The key columns names. + /// + public string[] KeyColumns { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether the object is off-chain. + /// + /// + /// true if the object is off-chain; otherwise, false. + /// + public bool IsOffChain { get; set; } + + /// + /// Retrieves the resource ID for a given resource. + /// + /// + /// The resource ID as a byte array. + /// + public byte[] ResourceId => resourceId ??= MudUtils.TableResourceId(Namespace, TableName, IsOffChain); + + /// + /// Gets the indices of the key columns. + /// + /// + /// The indices are calculated lazily upon first access and then cached for subsequent access. + /// + public int[] KeyIndices => keyIndices ??= FindKeyIndices().ToArray(); + + /// + /// Retrieves the column type for the specified column name. + /// + /// The name of the column. + /// The column type. + public string GetColumnType(string columnName) => Columns.Single(pair => pair.Key == columnName).Value; + + private IEnumerable FindKeyIndices() + { + if (KeyColumns.Length == 0) + { + yield break; + } + + for (var i = 0; i < Columns.Count; i++) + { + if (KeyColumns.Contains(Columns[i].Key)) + { + yield return i; + } + } + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Tables/MudWorldTables.cs b/src/ChainSafe.Gaming.Mud/Tables/MudWorldTables.cs new file mode 100644 index 000000000..ec0a36116 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Tables/MudWorldTables.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ChainSafe.Gaming.Mud.Storages; +using ChainSafe.Gaming.Web3.Environment; + +namespace ChainSafe.Gaming.Mud.Tables +{ + public class MudWorldTables + { + private readonly IMudStorage storage; + private readonly List tables; + private readonly IMainThreadRunner mainThreadRunner; + + public MudWorldTables(List tableSchemas, IMudStorage storage, IMainThreadRunner mainThreadRunner) + { + this.mainThreadRunner = mainThreadRunner; + this.storage = storage; + tables = tableSchemas.Select(BuildTable).ToList(); + } + + public MudTable GetTable(byte[] resourceId) + { + return tables.Single(table => table.ResourceId.AsSpan().SequenceEqual(resourceId.AsSpan())); + } + + public MudTable GetTable(string @namespace, string tableName) + { + return GetTable(MudUtils.TableResourceId(@namespace, tableName)); + } + + private MudTable BuildTable(MudTableSchema tableSchema) + { + return new MudTable(tableSchema, storage, mainThreadRunner); + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Tables/RecordDeletedDelegate.cs b/src/ChainSafe.Gaming.Mud/Tables/RecordDeletedDelegate.cs new file mode 100644 index 000000000..d8a3eea61 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Tables/RecordDeletedDelegate.cs @@ -0,0 +1,4 @@ +namespace ChainSafe.Gaming.Mud.Tables +{ + public delegate void RecordDeletedDelegate(object[] key); +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Tables/RecordSetDelegate.cs b/src/ChainSafe.Gaming.Mud/Tables/RecordSetDelegate.cs new file mode 100644 index 000000000..24c2eac38 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Tables/RecordSetDelegate.cs @@ -0,0 +1,4 @@ +namespace ChainSafe.Gaming.Mud.Tables +{ + public delegate void RecordSetDelegate(object[] key, object[] record); +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Worlds/IMudWorldConfig.cs b/src/ChainSafe.Gaming.Mud/Worlds/IMudWorldConfig.cs new file mode 100644 index 000000000..b77cae638 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Worlds/IMudWorldConfig.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using ChainSafe.Gaming.Mud.Storages; +using ChainSafe.Gaming.Mud.Tables; + +namespace ChainSafe.Gaming.Mud.Worlds +{ + /// + /// Represents the configuration for the MUD World. + /// + public interface IMudWorldConfig + { + /// + /// Gets the MUD world contract address. + /// + /// + /// The contract address as a string. + /// + string ContractAddress { get; } + + /// + /// Gets the Contract Application Binary Interface (ABI). + /// + /// + /// The contract ABI, a JSON representation of the smart contract's interface. + /// + string ContractAbi { get; } + + /// + /// Gets the default namespace to be used by the MUD World. + /// + /// + /// The default namespace used by the MUD World or null if not specified. + /// + string? DefaultNamespace { get; } + + /// + /// Gets the storage configuration override for the MUD World. + /// + /// + /// An instance of the IMudStorageConfig interface representing the storage configuration override for the MUD World. Returns null if no override is set. + /// + IMudStorageConfig? StorageConfigOverride { get; } + + /// + /// Gets the list of table schemas. + /// + /// + /// This property returns a List of MudTableSchema objects that represent the schemas of the tables. + /// + /// + /// A List of MudTableSchema objects. + /// + List TableSchemas { get; } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Worlds/MudWorld.cs b/src/ChainSafe.Gaming.Mud/Worlds/MudWorld.cs new file mode 100644 index 000000000..b36195f5d --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Worlds/MudWorld.cs @@ -0,0 +1,136 @@ +using System.Threading.Tasks; +using ChainSafe.Gaming.Evm.Contracts; +using ChainSafe.Gaming.Evm.Transactions; +using ChainSafe.Gaming.Mud.Storages; +using ChainSafe.Gaming.Mud.Systems; +using ChainSafe.Gaming.Mud.Tables; +using ChainSafe.Gaming.Web3.Environment; +using Nethereum.Hex.HexTypes; + +namespace ChainSafe.Gaming.Mud.Worlds +{ + /// + /// Represents a client for a MUD world. + /// + public class MudWorld : IContract + { + private readonly MudWorldSystems systems; + private readonly MudWorldTables tables; + + private readonly Contract contract; + private readonly string? defaultNamespace; + + /// + /// Initializes a new instance of the class. + /// + public MudWorld(IMudWorldConfig config, IMudStorage storage, IContractBuilder contractBuilder, IMainThreadRunner mainThreadRunner) + { + contract = contractBuilder.Build(config.ContractAbi, config.ContractAddress); + defaultNamespace = config.DefaultNamespace; + + tables = new MudWorldTables(config.TableSchemas, storage, mainThreadRunner); + systems = new MudWorldSystems(contract); + } + + /// + /// Retrieve MudTable for the specified table name in the default namespace. + /// + /// The MudTable for the specified table name. + public MudTable GetTable(string tableName) + { + AssertDefaultNamespaceAvailable(); + return tables.GetTable(defaultNamespace!, tableName); + } + + /// + /// Retrieve MudTable for the specified table name and namespace. + /// + /// The MudTable for the specified table name and namespace. + public MudTable GetTable(string tableName, string @namespace) + { + return tables.GetTable(@namespace, tableName); + } + + /// + /// Retrieve client for Mud Systems for the default namespace. + /// + /// Use this to call system functions. + /// The MudSystems for the default namespace. + public MudSystems GetSystems() + { + AssertDefaultNamespaceAvailable(); + return systems.GetSystemsForNamespace(defaultNamespace!); + } + + /// + /// Retrieve client for Mud Systems for the specified namespace. + /// + /// Use this to call system functions. + /// The MudSystems for the specified namespace. + public MudSystems GetSystems(string @namespace) + { + return systems.GetSystemsForNamespace(@namespace); + } + + private void AssertDefaultNamespaceAvailable() + { + if (defaultNamespace is null) + { + throw new MudException("Default Namespace was not provided in World Config."); + } + } + +#pragma warning disable SA1124 + #region IContract delegation +#pragma warning restore SA1124 +#pragma warning disable SA1201 + public string Address => contract.Address; +#pragma warning restore SA1201 + + public IContract Attach(string address) + { + return contract.Attach(address); + } + + public Task Call(string method, object[] parameters = null, TransactionRequest overwrite = null) + { + return contract.Call(method, parameters, overwrite); + } + + public object[] Decode(string method, string output) + { + return contract.Decode(method, output); + } + + public Task Send(string method, object[] parameters = null, TransactionRequest overwrite = null) + { + return contract.Send(method, parameters, overwrite); + } + + public Task<(object[] response, TransactionReceipt receipt)> SendWithReceipt(string method, object[] parameters = null, TransactionRequest overwrite = null) + { + return contract.SendWithReceipt(method, parameters, overwrite); + } + + public Task EstimateGas(string method, object[] parameters, TransactionRequest overwrite = null) + { + return contract.EstimateGas(method, parameters, overwrite); + } + + public string Calldata(string method, object[] parameters = null) + { + return contract.Calldata(method, parameters); + } + + public Task PrepareTransactionRequest( + string method, + object[] parameters, + bool isReadCall = false, + TransactionRequest overwrite = null) + { + return contract.PrepareTransactionRequest(method, parameters, isReadCall, overwrite); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Worlds/MudWorldConfig.cs b/src/ChainSafe.Gaming.Mud/Worlds/MudWorldConfig.cs new file mode 100644 index 000000000..83c5441a0 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Worlds/MudWorldConfig.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using ChainSafe.Gaming.Mud.Storages; +using ChainSafe.Gaming.Mud.Tables; + +namespace ChainSafe.Gaming.Mud.Worlds +{ + [Serializable] + public class MudWorldConfig : IMudWorldConfig + { + public string ContractAddress { get; set; } + + public string ContractAbi { get; set; } + + public string? DefaultNamespace { get; set; } + + public IMudStorageConfig? StorageConfigOverride { get; set; } + + public List TableSchemas { get; set; } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Mud/Worlds/MudWorldFactory.cs b/src/ChainSafe.Gaming.Mud/Worlds/MudWorldFactory.cs new file mode 100644 index 000000000..dd0e4c115 --- /dev/null +++ b/src/ChainSafe.Gaming.Mud/Worlds/MudWorldFactory.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using ChainSafe.Gaming.Evm.Contracts; +using ChainSafe.Gaming.Mud.Storages; +using ChainSafe.Gaming.Web3.Environment; + +namespace ChainSafe.Gaming.Mud.Worlds +{ + public class MudWorldFactory + { + private readonly IMudStorageFactory storageFactory; + private readonly IMudStorageConfig defaultStorageConfig; + private readonly IContractBuilder contractBuilder; + private readonly IMainThreadRunner mainThreadRunner; + + public MudWorldFactory(IMudStorageFactory storageFactory, IMudConfig mudConfig, IContractBuilder contractBuilder, IMainThreadRunner mainThreadRunner) + { + this.mainThreadRunner = mainThreadRunner; + this.contractBuilder = contractBuilder; + defaultStorageConfig = mudConfig.StorageConfig; + this.storageFactory = storageFactory; + } + + public async Task Build(IMudWorldConfig config) + { + var storageConfig = config.StorageConfigOverride ?? defaultStorageConfig; + var storage = await storageFactory.Build(storageConfig, config.ContractAddress); + return new MudWorld(config, storage, contractBuilder, mainThreadRunner); + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Tests/ChainSafe.Gaming.Tests.csproj b/src/ChainSafe.Gaming.Tests/ChainSafe.Gaming.Tests.csproj index c047b4811..c2006ba78 100644 --- a/src/ChainSafe.Gaming.Tests/ChainSafe.Gaming.Tests.csproj +++ b/src/ChainSafe.Gaming.Tests/ChainSafe.Gaming.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/src/ChainSafe.Gaming.Tests/MudTests.cs b/src/ChainSafe.Gaming.Tests/MudTests.cs new file mode 100644 index 000000000..5dc07d399 --- /dev/null +++ b/src/ChainSafe.Gaming.Tests/MudTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using ChainSafe.Gaming.Mud; +using ChainSafe.Gaming.Mud.Storages.InMemory; +using ChainSafe.Gaming.Mud.Tables; +using ChainSafe.Gaming.Mud.Worlds; +using NUnit.Framework; + +namespace ChainSafe.Gaming.Tests +{ + [TestFixture(Ignore = "CI test environment should be configured first to meet the prerequisites for the MUD project execution.")] + public class MudTests + { + private const string ContractAddress = "0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b"; + private const string ContractAbi = "[\n {\n \"type\": \"function\",\n \"name\": \"app__addDebt\",\n \"inputs\": [\n {\n \"name\": \"id\",\n \"type\": \"uint32\",\n \"internalType\": \"uint32\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"\",\n \"type\": \"uint32\",\n \"internalType\": \"uint32\"\n }\n ],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"app__increment\",\n \"inputs\": [],\n \"outputs\": [\n {\n \"name\": \"\",\n \"type\": \"uint32\",\n \"internalType\": \"uint32\"\n }\n ],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"batchCall\",\n \"inputs\": [\n {\n \"name\": \"systemCalls\",\n \"type\": \"tuple[]\",\n \"internalType\": \"struct SystemCallData[]\",\n \"components\": [\n {\n \"name\": \"systemId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"callData\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ]\n }\n ],\n \"outputs\": [\n {\n \"name\": \"returnDatas\",\n \"type\": \"bytes[]\",\n \"internalType\": \"bytes[]\"\n }\n ],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"batchCallFrom\",\n \"inputs\": [\n {\n \"name\": \"systemCalls\",\n \"type\": \"tuple[]\",\n \"internalType\": \"struct SystemCallFromData[]\",\n \"components\": [\n {\n \"name\": \"from\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n },\n {\n \"name\": \"systemId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"callData\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ]\n }\n ],\n \"outputs\": [\n {\n \"name\": \"returnDatas\",\n \"type\": \"bytes[]\",\n \"internalType\": \"bytes[]\"\n }\n ],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"call\",\n \"inputs\": [\n {\n \"name\": \"systemId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"callData\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"stateMutability\": \"payable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"callFrom\",\n \"inputs\": [\n {\n \"name\": \"delegator\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n },\n {\n \"name\": \"systemId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"callData\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"stateMutability\": \"payable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"creator\",\n \"inputs\": [],\n \"outputs\": [\n {\n \"name\": \"\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"deleteRecord\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"getDynamicField\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"dynamicFieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"getDynamicFieldLength\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"dynamicFieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"getDynamicFieldSlice\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"dynamicFieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n },\n {\n \"name\": \"start\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"end\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"data\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"getField\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"fieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n },\n {\n \"name\": \"fieldLayout\",\n \"type\": \"bytes32\",\n \"internalType\": \"FieldLayout\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"data\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"getField\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"fieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"data\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"getFieldLayout\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"fieldLayout\",\n \"type\": \"bytes32\",\n \"internalType\": \"FieldLayout\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"getFieldLength\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"fieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n },\n {\n \"name\": \"fieldLayout\",\n \"type\": \"bytes32\",\n \"internalType\": \"FieldLayout\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"getFieldLength\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"fieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"getKeySchema\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"keySchema\",\n \"type\": \"bytes32\",\n \"internalType\": \"Schema\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"getRecord\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"fieldLayout\",\n \"type\": \"bytes32\",\n \"internalType\": \"FieldLayout\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"staticData\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n },\n {\n \"name\": \"encodedLengths\",\n \"type\": \"bytes32\",\n \"internalType\": \"EncodedLengths\"\n },\n {\n \"name\": \"dynamicData\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"getRecord\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"staticData\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n },\n {\n \"name\": \"encodedLengths\",\n \"type\": \"bytes32\",\n \"internalType\": \"EncodedLengths\"\n },\n {\n \"name\": \"dynamicData\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"getStaticField\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"fieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n },\n {\n \"name\": \"fieldLayout\",\n \"type\": \"bytes32\",\n \"internalType\": \"FieldLayout\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"\",\n \"type\": \"bytes32\",\n \"internalType\": \"bytes32\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"getValueSchema\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"valueSchema\",\n \"type\": \"bytes32\",\n \"internalType\": \"Schema\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"grantAccess\",\n \"inputs\": [\n {\n \"name\": \"resourceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"grantee\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"initialize\",\n \"inputs\": [\n {\n \"name\": \"initModule\",\n \"type\": \"address\",\n \"internalType\": \"contract IModule\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"installModule\",\n \"inputs\": [\n {\n \"name\": \"module\",\n \"type\": \"address\",\n \"internalType\": \"contract IModule\"\n },\n {\n \"name\": \"encodedArgs\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"installRootModule\",\n \"inputs\": [\n {\n \"name\": \"module\",\n \"type\": \"address\",\n \"internalType\": \"contract IModule\"\n },\n {\n \"name\": \"encodedArgs\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"popFromDynamicField\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"dynamicFieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n },\n {\n \"name\": \"byteLengthToPop\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"pushToDynamicField\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"dynamicFieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n },\n {\n \"name\": \"dataToPush\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"registerDelegation\",\n \"inputs\": [\n {\n \"name\": \"delegatee\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n },\n {\n \"name\": \"delegationControlId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"initCallData\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"registerFunctionSelector\",\n \"inputs\": [\n {\n \"name\": \"systemId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"systemFunctionSignature\",\n \"type\": \"string\",\n \"internalType\": \"string\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"worldFunctionSelector\",\n \"type\": \"bytes4\",\n \"internalType\": \"bytes4\"\n }\n ],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"registerNamespace\",\n \"inputs\": [\n {\n \"name\": \"namespaceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"registerNamespaceDelegation\",\n \"inputs\": [\n {\n \"name\": \"namespaceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"delegationControlId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"initCallData\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"registerRootFunctionSelector\",\n \"inputs\": [\n {\n \"name\": \"systemId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"worldFunctionSignature\",\n \"type\": \"string\",\n \"internalType\": \"string\"\n },\n {\n \"name\": \"systemFunctionSignature\",\n \"type\": \"string\",\n \"internalType\": \"string\"\n }\n ],\n \"outputs\": [\n {\n \"name\": \"worldFunctionSelector\",\n \"type\": \"bytes4\",\n \"internalType\": \"bytes4\"\n }\n ],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"registerStoreHook\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"hookAddress\",\n \"type\": \"address\",\n \"internalType\": \"contract IStoreHook\"\n },\n {\n \"name\": \"enabledHooksBitmap\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"registerSystem\",\n \"inputs\": [\n {\n \"name\": \"systemId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"system\",\n \"type\": \"address\",\n \"internalType\": \"contract System\"\n },\n {\n \"name\": \"publicAccess\",\n \"type\": \"bool\",\n \"internalType\": \"bool\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"registerSystemHook\",\n \"inputs\": [\n {\n \"name\": \"systemId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"hookAddress\",\n \"type\": \"address\",\n \"internalType\": \"contract ISystemHook\"\n },\n {\n \"name\": \"enabledHooksBitmap\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"registerTable\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"fieldLayout\",\n \"type\": \"bytes32\",\n \"internalType\": \"FieldLayout\"\n },\n {\n \"name\": \"keySchema\",\n \"type\": \"bytes32\",\n \"internalType\": \"Schema\"\n },\n {\n \"name\": \"valueSchema\",\n \"type\": \"bytes32\",\n \"internalType\": \"Schema\"\n },\n {\n \"name\": \"keyNames\",\n \"type\": \"string[]\",\n \"internalType\": \"string[]\"\n },\n {\n \"name\": \"fieldNames\",\n \"type\": \"string[]\",\n \"internalType\": \"string[]\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"renounceOwnership\",\n \"inputs\": [\n {\n \"name\": \"namespaceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"revokeAccess\",\n \"inputs\": [\n {\n \"name\": \"resourceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"grantee\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"setDynamicField\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"dynamicFieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n },\n {\n \"name\": \"data\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"setField\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"fieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n },\n {\n \"name\": \"data\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"setField\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"fieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n },\n {\n \"name\": \"data\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n },\n {\n \"name\": \"fieldLayout\",\n \"type\": \"bytes32\",\n \"internalType\": \"FieldLayout\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"setRecord\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"staticData\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n },\n {\n \"name\": \"encodedLengths\",\n \"type\": \"bytes32\",\n \"internalType\": \"EncodedLengths\"\n },\n {\n \"name\": \"dynamicData\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"setStaticField\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"fieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n },\n {\n \"name\": \"data\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n },\n {\n \"name\": \"fieldLayout\",\n \"type\": \"bytes32\",\n \"internalType\": \"FieldLayout\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"spliceDynamicData\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"dynamicFieldIndex\",\n \"type\": \"uint8\",\n \"internalType\": \"uint8\"\n },\n {\n \"name\": \"startWithinField\",\n \"type\": \"uint40\",\n \"internalType\": \"uint40\"\n },\n {\n \"name\": \"deleteCount\",\n \"type\": \"uint40\",\n \"internalType\": \"uint40\"\n },\n {\n \"name\": \"data\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"spliceStaticData\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"start\",\n \"type\": \"uint48\",\n \"internalType\": \"uint48\"\n },\n {\n \"name\": \"data\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"storeVersion\",\n \"inputs\": [],\n \"outputs\": [\n {\n \"name\": \"version\",\n \"type\": \"bytes32\",\n \"internalType\": \"bytes32\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"function\",\n \"name\": \"transferBalanceToAddress\",\n \"inputs\": [\n {\n \"name\": \"fromNamespaceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"toAddress\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n },\n {\n \"name\": \"amount\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"transferBalanceToNamespace\",\n \"inputs\": [\n {\n \"name\": \"fromNamespaceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"toNamespaceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"amount\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"transferOwnership\",\n \"inputs\": [\n {\n \"name\": \"namespaceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"newOwner\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"unregisterDelegation\",\n \"inputs\": [\n {\n \"name\": \"delegatee\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"unregisterNamespaceDelegation\",\n \"inputs\": [\n {\n \"name\": \"namespaceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"unregisterStoreHook\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"hookAddress\",\n \"type\": \"address\",\n \"internalType\": \"contract IStoreHook\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"unregisterSystemHook\",\n \"inputs\": [\n {\n \"name\": \"systemId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"hookAddress\",\n \"type\": \"address\",\n \"internalType\": \"contract ISystemHook\"\n }\n ],\n \"outputs\": [],\n \"stateMutability\": \"nonpayable\"\n },\n {\n \"type\": \"function\",\n \"name\": \"worldVersion\",\n \"inputs\": [],\n \"outputs\": [\n {\n \"name\": \"\",\n \"type\": \"bytes32\",\n \"internalType\": \"bytes32\"\n }\n ],\n \"stateMutability\": \"view\"\n },\n {\n \"type\": \"event\",\n \"name\": \"HelloStore\",\n \"inputs\": [\n {\n \"name\": \"storeVersion\",\n \"type\": \"bytes32\",\n \"indexed\": true,\n \"internalType\": \"bytes32\"\n }\n ],\n \"anonymous\": false\n },\n {\n \"type\": \"event\",\n \"name\": \"HelloWorld\",\n \"inputs\": [\n {\n \"name\": \"worldVersion\",\n \"type\": \"bytes32\",\n \"indexed\": true,\n \"internalType\": \"bytes32\"\n }\n ],\n \"anonymous\": false\n },\n {\n \"type\": \"event\",\n \"name\": \"Store_DeleteRecord\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"indexed\": true,\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"indexed\": false,\n \"internalType\": \"bytes32[]\"\n }\n ],\n \"anonymous\": false\n },\n {\n \"type\": \"event\",\n \"name\": \"Store_SetRecord\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"indexed\": true,\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"indexed\": false,\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"staticData\",\n \"type\": \"bytes\",\n \"indexed\": false,\n \"internalType\": \"bytes\"\n },\n {\n \"name\": \"encodedLengths\",\n \"type\": \"bytes32\",\n \"indexed\": false,\n \"internalType\": \"EncodedLengths\"\n },\n {\n \"name\": \"dynamicData\",\n \"type\": \"bytes\",\n \"indexed\": false,\n \"internalType\": \"bytes\"\n }\n ],\n \"anonymous\": false\n },\n {\n \"type\": \"event\",\n \"name\": \"Store_SpliceDynamicData\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"indexed\": true,\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"indexed\": false,\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"dynamicFieldIndex\",\n \"type\": \"uint8\",\n \"indexed\": false,\n \"internalType\": \"uint8\"\n },\n {\n \"name\": \"start\",\n \"type\": \"uint48\",\n \"indexed\": false,\n \"internalType\": \"uint48\"\n },\n {\n \"name\": \"deleteCount\",\n \"type\": \"uint40\",\n \"indexed\": false,\n \"internalType\": \"uint40\"\n },\n {\n \"name\": \"encodedLengths\",\n \"type\": \"bytes32\",\n \"indexed\": false,\n \"internalType\": \"EncodedLengths\"\n },\n {\n \"name\": \"data\",\n \"type\": \"bytes\",\n \"indexed\": false,\n \"internalType\": \"bytes\"\n }\n ],\n \"anonymous\": false\n },\n {\n \"type\": \"event\",\n \"name\": \"Store_SpliceStaticData\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"indexed\": true,\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"keyTuple\",\n \"type\": \"bytes32[]\",\n \"indexed\": false,\n \"internalType\": \"bytes32[]\"\n },\n {\n \"name\": \"start\",\n \"type\": \"uint48\",\n \"indexed\": false,\n \"internalType\": \"uint48\"\n },\n {\n \"name\": \"data\",\n \"type\": \"bytes\",\n \"indexed\": false,\n \"internalType\": \"bytes\"\n }\n ],\n \"anonymous\": false\n },\n {\n \"type\": \"error\",\n \"name\": \"EncodedLengths_InvalidLength\",\n \"inputs\": [\n {\n \"name\": \"length\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"FieldLayout_Empty\",\n \"inputs\": []\n },\n {\n \"type\": \"error\",\n \"name\": \"FieldLayout_InvalidStaticDataLength\",\n \"inputs\": [\n {\n \"name\": \"staticDataLength\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"computedStaticDataLength\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"FieldLayout_StaticLengthDoesNotFitInAWord\",\n \"inputs\": [\n {\n \"name\": \"index\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"FieldLayout_StaticLengthIsNotZero\",\n \"inputs\": [\n {\n \"name\": \"index\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"FieldLayout_StaticLengthIsZero\",\n \"inputs\": [\n {\n \"name\": \"index\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"FieldLayout_TooManyDynamicFields\",\n \"inputs\": [\n {\n \"name\": \"numFields\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"maxFields\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"FieldLayout_TooManyFields\",\n \"inputs\": [\n {\n \"name\": \"numFields\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"maxFields\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Module_AlreadyInstalled\",\n \"inputs\": []\n },\n {\n \"type\": \"error\",\n \"name\": \"Module_MissingDependency\",\n \"inputs\": [\n {\n \"name\": \"dependency\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Module_NonRootInstallNotSupported\",\n \"inputs\": []\n },\n {\n \"type\": \"error\",\n \"name\": \"Module_RootInstallNotSupported\",\n \"inputs\": []\n },\n {\n \"type\": \"error\",\n \"name\": \"Schema_InvalidLength\",\n \"inputs\": [\n {\n \"name\": \"length\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Schema_StaticTypeAfterDynamicType\",\n \"inputs\": []\n },\n {\n \"type\": \"error\",\n \"name\": \"Slice_OutOfBounds\",\n \"inputs\": [\n {\n \"name\": \"data\",\n \"type\": \"bytes\",\n \"internalType\": \"bytes\"\n },\n {\n \"name\": \"start\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"end\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Store_IndexOutOfBounds\",\n \"inputs\": [\n {\n \"name\": \"length\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"accessedIndex\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Store_InvalidBounds\",\n \"inputs\": [\n {\n \"name\": \"start\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"end\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Store_InvalidFieldNamesLength\",\n \"inputs\": [\n {\n \"name\": \"expected\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"received\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Store_InvalidKeyNamesLength\",\n \"inputs\": [\n {\n \"name\": \"expected\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"received\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Store_InvalidResourceType\",\n \"inputs\": [\n {\n \"name\": \"expected\",\n \"type\": \"bytes2\",\n \"internalType\": \"bytes2\"\n },\n {\n \"name\": \"resourceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"resourceIdString\",\n \"type\": \"string\",\n \"internalType\": \"string\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Store_InvalidSplice\",\n \"inputs\": [\n {\n \"name\": \"startWithinField\",\n \"type\": \"uint40\",\n \"internalType\": \"uint40\"\n },\n {\n \"name\": \"deleteCount\",\n \"type\": \"uint40\",\n \"internalType\": \"uint40\"\n },\n {\n \"name\": \"fieldLength\",\n \"type\": \"uint40\",\n \"internalType\": \"uint40\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Store_InvalidStaticDataLength\",\n \"inputs\": [\n {\n \"name\": \"expected\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"received\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Store_InvalidValueSchemaDynamicLength\",\n \"inputs\": [\n {\n \"name\": \"expected\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"received\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Store_InvalidValueSchemaLength\",\n \"inputs\": [\n {\n \"name\": \"expected\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"received\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Store_InvalidValueSchemaStaticLength\",\n \"inputs\": [\n {\n \"name\": \"expected\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"received\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Store_TableAlreadyExists\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"tableIdString\",\n \"type\": \"string\",\n \"internalType\": \"string\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"Store_TableNotFound\",\n \"inputs\": [\n {\n \"name\": \"tableId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"tableIdString\",\n \"type\": \"string\",\n \"internalType\": \"string\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_AccessDenied\",\n \"inputs\": [\n {\n \"name\": \"resource\",\n \"type\": \"string\",\n \"internalType\": \"string\"\n },\n {\n \"name\": \"caller\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_AlreadyInitialized\",\n \"inputs\": []\n },\n {\n \"type\": \"error\",\n \"name\": \"World_CallbackNotAllowed\",\n \"inputs\": [\n {\n \"name\": \"functionSelector\",\n \"type\": \"bytes4\",\n \"internalType\": \"bytes4\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_DelegationNotFound\",\n \"inputs\": [\n {\n \"name\": \"delegator\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n },\n {\n \"name\": \"delegatee\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_FunctionSelectorAlreadyExists\",\n \"inputs\": [\n {\n \"name\": \"functionSelector\",\n \"type\": \"bytes4\",\n \"internalType\": \"bytes4\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_FunctionSelectorNotFound\",\n \"inputs\": [\n {\n \"name\": \"functionSelector\",\n \"type\": \"bytes4\",\n \"internalType\": \"bytes4\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_InsufficientBalance\",\n \"inputs\": [\n {\n \"name\": \"balance\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n },\n {\n \"name\": \"amount\",\n \"type\": \"uint256\",\n \"internalType\": \"uint256\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_InterfaceNotSupported\",\n \"inputs\": [\n {\n \"name\": \"contractAddress\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n },\n {\n \"name\": \"interfaceId\",\n \"type\": \"bytes4\",\n \"internalType\": \"bytes4\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_InvalidNamespace\",\n \"inputs\": [\n {\n \"name\": \"namespace\",\n \"type\": \"bytes14\",\n \"internalType\": \"bytes14\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_InvalidResourceId\",\n \"inputs\": [\n {\n \"name\": \"resourceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"resourceIdString\",\n \"type\": \"string\",\n \"internalType\": \"string\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_InvalidResourceType\",\n \"inputs\": [\n {\n \"name\": \"expected\",\n \"type\": \"bytes2\",\n \"internalType\": \"bytes2\"\n },\n {\n \"name\": \"resourceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"resourceIdString\",\n \"type\": \"string\",\n \"internalType\": \"string\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_ResourceAlreadyExists\",\n \"inputs\": [\n {\n \"name\": \"resourceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"resourceIdString\",\n \"type\": \"string\",\n \"internalType\": \"string\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_ResourceNotFound\",\n \"inputs\": [\n {\n \"name\": \"resourceId\",\n \"type\": \"bytes32\",\n \"internalType\": \"ResourceId\"\n },\n {\n \"name\": \"resourceIdString\",\n \"type\": \"string\",\n \"internalType\": \"string\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_SystemAlreadyExists\",\n \"inputs\": [\n {\n \"name\": \"system\",\n \"type\": \"address\",\n \"internalType\": \"address\"\n }\n ]\n },\n {\n \"type\": \"error\",\n \"name\": \"World_UnlimitedDelegationNotAllowed\",\n \"inputs\": []\n }\n]"; + + [Test] + public void RecordsAccumulateInMemoryAndHaveCorrectValuesTest() + { + // Assemble + var web3 = BuildWeb3(); + + // Act + var world = BuildMudWorld(web3); + + var table = world.GetTable("Counter"); + + var queryTask = table.Query(MudQuery.All); + var records = queryTask.Result; + + // Assert + Assert.AreEqual(1, records.Length); + + var firstColumnValueOfSingleRecord = (BigInteger)records[0][0]; + Assert.Greater(firstColumnValueOfSingleRecord, 0); + + Console.WriteLine($"Counter is {firstColumnValueOfSingleRecord}"); + } + + [Test] + public void SystemCallsTest() // todo run + { + // Assemble + var web3 = BuildWeb3(); + var world = BuildMudWorld(web3); + + // Act + var valueBeforeIncrement = (BigInteger)world.GetTable("Counter").Query(MudQuery.All).Result[0][0]; + world.GetSystems().Send("increment").Wait(); + var valueAfterIncrement = (BigInteger)world.GetTable("Counter").Query(MudQuery.All).Result[0][0]; // todo make tables update on log events + + // Assert + Assert.Greater(valueAfterIncrement, valueBeforeIncrement); + } + + private static Web3.Web3 BuildWeb3() + { + var web3Task = Web3Util.CreateWeb3(services => + { + services.UseMud(new MudConfig + { + StorageConfig = new InMemoryMudStorageConfig + { + FromBlockNumber = 0, + }, + }); + }); + + var web3 = web3Task.Result; + return web3; + } + + private static MudWorld BuildMudWorld(Web3.Web3 web3) + { + var worldTask = web3.Mud().BuildWorld(new MudWorldConfig + { + ContractAddress = ContractAddress, + ContractAbi = ContractAbi, + DefaultNamespace = "app", + TableSchemas = new List + { + new() + { + Namespace = "app", + TableName = "Counter", + Columns = new List> + { + new("value", "uint32"), + }, + }, + }, + }); + + var world = worldTask.Result; + return world; + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.Tests/Web3Util.cs b/src/ChainSafe.Gaming.Tests/Web3Util.cs index f5b4327cf..0c67bee45 100644 --- a/src/ChainSafe.Gaming.Tests/Web3Util.cs +++ b/src/ChainSafe.Gaming.Tests/Web3Util.cs @@ -59,6 +59,7 @@ internal static class Web3Util ChainId = "31337", Network = "GoChain Testnet", Rpc = "http://127.0.0.1:8545", + Ws = "ws://127.0.0.1:8545", }) .Configure(services => { diff --git a/src/ChainSafe.Gaming/RPC/Events/EventExtensions.cs b/src/ChainSafe.Gaming/RPC/Events/EventExtensions.cs new file mode 100644 index 000000000..739b57061 --- /dev/null +++ b/src/ChainSafe.Gaming/RPC/Events/EventExtensions.cs @@ -0,0 +1,15 @@ +using ChainSafe.Gaming.Web3.Build; +using ChainSafe.Gaming.Web3.Core; + +namespace ChainSafe.Gaming.RPC.Events +{ + public static class EventExtensions + { + public static IWeb3ServiceCollection UseEvents(this IWeb3ServiceCollection services) + { + // todo bind EventPoller implementation of IEventManager when running in WebGL build + return services + .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 new file mode 100644 index 000000000..9a6bb7e83 --- /dev/null +++ b/src/ChainSafe.Gaming/RPC/Events/EventManager.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ChainSafe.Gaming.Web3; +using ChainSafe.Gaming.Web3.Core; +using ChainSafe.Gaming.Web3.Environment; +using Nethereum.ABI.FunctionEncoding.Attributes; +using Nethereum.Contracts; +using Nethereum.JsonRpc.WebSocketStreamingClient; +using Nethereum.RPC.Eth.DTOs; +using Nethereum.RPC.Reactive.Eth.Subscriptions; + +namespace ChainSafe.Gaming.RPC.Events +{ + public class EventManager : IEventManager, ILifecycleParticipant + { + private readonly IChainConfig chainConfig; + private readonly Dictionary subscriptions = new(); + private readonly ILogWriter logWriter; + + private StreamingWebSocketClient webSocketClient; + + public EventManager(IChainConfig chainConfig, ILogWriter logWriter) + { + this.logWriter = logWriter; + this.chainConfig = chainConfig; + } + + public async ValueTask WillStartAsync() + { + if (string.IsNullOrWhiteSpace(chainConfig.Ws)) + { + throw new Web3Exception("No WebSocket URL was provided in config."); + } + + webSocketClient = new StreamingWebSocketClient(chainConfig.Ws); + await webSocketClient.StartAsync(); + } + + public async ValueTask WillStopAsync() + { + for (var i = subscriptions.Count - 1; i >= 0; i--) + { + var type = subscriptions.Last().Key; + await TerminateSubscriptionForType(type); + } + + webSocketClient?.Dispose(); + } + + public async Task Subscribe(Action handler) + where TEvent : IEventDTO, new() + { + if (!subscriptions.TryGetValue(typeof(TEvent), out var rawSubscription)) + { + rawSubscription = await InitializeSubscriptionForType(); + } + + var subscription = (Subscription)rawSubscription; + subscription.Handlers.Add(handler); + } + + public async Task Unsubscribe(Action handler) + where TEvent : IEventDTO, new() + { + if (!subscriptions.ContainsKey(typeof(TEvent))) + { + throw new Web3Exception($"Can't unsubscribe. No subscription of type {nameof(TEvent)} was registered."); + } + + var subscription = (Subscription)subscriptions[typeof(TEvent)]; + subscription.Handlers.Remove(handler); + + if (subscription.Handlers.Count == 0) + { + await TerminateSubscriptionForType(); + } + } + + private async Task InitializeSubscriptionForType() + where TEvent : IEventDTO, new() + { + Subscription rawSubscription = new Subscription(webSocketClient); + var eventFilter = Event.GetEventABI().CreateFilterInput(); + rawSubscription + .NethSubscription + .GetSubscriptionDataResponsesAsObservable() + .Subscribe(new FilterLogObserver(HandleLog)); + + await rawSubscription.NethSubscription.SubscribeAsync(eventFilter); + + subscriptions[typeof(TEvent)] = rawSubscription; + return rawSubscription; + + void HandleLog(FilterLog log) + { + EventLog decoded; + try + { + decoded = Event.DecodeEvent(log); + } + catch (Exception ex) + { + throw new Web3Exception($"There was an error processing event log data for event type {nameof(TEvent)}.", ex); + } + + var subscription = (Subscription)rawSubscription; + foreach (var subscriptionHandler in subscription.Handlers) + { + try + { + subscriptionHandler.Invoke(decoded.Event); + } + catch (Exception e) + { + logWriter.LogError($"Error occured in one of the {nameof(TEvent)} handlers.\n{e.Message}\n{e.StackTrace}"); + } + } + } + } + + private Task TerminateSubscriptionForType() + where TEvent : IEventDTO, new() + { + return TerminateSubscriptionForType(typeof(TEvent)); + } + + private Task TerminateSubscriptionForType(Type type) + { + var subscription = subscriptions[type]; + subscriptions.Remove(type); + return subscription.NethSubscription.UnsubscribeAsync(); + } + + private abstract class Subscription + { + public EthLogsObservableSubscription NethSubscription { get; set; } + } + + private class Subscription : Subscription + { + public Subscription(StreamingWebSocketClient webSocketClient) + { + NethSubscription = new EthLogsObservableSubscription(webSocketClient); + Handlers = new List>(); + } + + public List> Handlers { get; set; } + } + + private class FilterLogObserver : IObserver + { + private readonly Action handler; + + public FilterLogObserver(Action handler) + { + this.handler = handler; + } + + public void OnCompleted() + { + // empty + } + + public void OnError(Exception error) + { + // empty + } + + public void OnNext(FilterLog value) + { + handler.Invoke(value); + } + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/RPC/Events/IEventManager.cs b/src/ChainSafe.Gaming/RPC/Events/IEventManager.cs new file mode 100644 index 000000000..00f6bc192 --- /dev/null +++ b/src/ChainSafe.Gaming/RPC/Events/IEventManager.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; +using Nethereum.ABI.FunctionEncoding.Attributes; + +namespace ChainSafe.Gaming.RPC.Events +{ + public interface IEventManager + { + Task Subscribe(Action handler) + where TEvent : IEventDTO, new(); + + Task Unsubscribe(Action handler) + where TEvent : IEventDTO, new(); + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs b/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs index 44f000a81..04b5e3b2b 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs @@ -4,6 +4,7 @@ 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.Evm.EventPoller; using ChainSafe.Gaming.Web3.Core.Logout; @@ -26,7 +27,7 @@ private Web3Builder() // Bind default services serviceCollection - .UseEventPoller() + .UseEventPoller() // todo: remove in favor of EventManager which supports WebSocket connection .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumExtensions.cs b/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumExtensions.cs index 317e064f7..265519f56 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumExtensions.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Nethereum/NethereumExtensions.cs @@ -19,10 +19,5 @@ public static IWeb3ServiceCollection UseNethereumAdapters(this IWeb3ServiceColle return services; } - - public static bool IsNethereumAdaptersBound(this IWeb3ServiceCollection services) - { - return services.IsBound(); - } } } \ No newline at end of file diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/MudSample.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/MudSample.cs index 2fcd9b2a7..cedbb93cb 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/MudSample.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/MudSample.cs @@ -1,45 +1,34 @@ +using System.Collections.Generic; +using System.Linq; using System.Numerics; using ChainSafe.Gaming.Debugging; -using ChainSafe.Gaming.Evm.Contracts; using ChainSafe.Gaming.Evm.JsonRpc; using ChainSafe.Gaming.Mud; +using ChainSafe.Gaming.Mud.Tables; +using ChainSafe.Gaming.Mud.Unity; +using ChainSafe.Gaming.Mud.Worlds; using ChainSafe.Gaming.UnityPackage; using ChainSafe.Gaming.Wallets; using ChainSafe.Gaming.Web3; using ChainSafe.Gaming.Web3.Build; using ChainSafe.Gaming.Web3.Unity; -using Nethereum.ABI.FunctionEncoding.Attributes; -using Nethereum.Mud; using TMPro; -using Nethereum.Contracts; using UnityEngine; public class MudSample : MonoBehaviour { - public string WorldContractAddress; - public TextAsset WorldContractAbi; - public TMP_Text CounterLabel; + public MudConfigAsset mudConfig; + public string worldContractAddress; + public TextAsset worldContractAbi; + public TMP_Text counterLabel; private Web3 web3; private MudWorld world; - public class CounterRecord : TableRecordSingleton // singleton table record - no key required - { - public class CounterValue - { - [Parameter("uint32", "value", 1)] // column name - public BigInteger Counter { get; set; } - } - - public CounterRecord() : base("app", "Counter") // table name - { - } - } - private async void Awake() { Debug.Log("To run this sample successfully you should have the MUD tutorial project running in the background.\n" + - "Follow the link https://mud.dev/quickstart"); + "Follow the link to get started: https://mud.dev/quickstart"); // 1. Initialize Web3 client. web3 = await new Web3Builder(ProjectConfigUtilities.Load(), ProjectConfigUtilities.BuildLocalhostConfig()) @@ -52,18 +41,45 @@ private async void Awake() services.Debug().UseJsonRpcWallet(new JsonRpcWalletConfig { AccountIndex = 0 }); // Enable MUD - services.UseMud(); - }).LaunchAsync(); - Debug.Log("Web3 client ready"); + services.UseMud(mudConfig); + }) + .LaunchAsync(); + Debug.Log($"Web3 client ready. Player address: {web3.Signer.PublicAddress}"); // 2. Create MUD World client. - world = web3.Mud().BuildWorld(WorldContractAddress, WorldContractAbi.text); + world = await web3.Mud().BuildWorld(new MudWorldConfig + { + ContractAddress = worldContractAddress, + ContractAbi = worldContractAbi.text, + DefaultNamespace = "app", + TableSchemas = new List + { + new() + { + Namespace = "app", + TableName = "Counter", + Columns = new List> + { + new("value", "uint32"), + }, + KeyColumns = new string[0], // empty - singleton table + }, + }, + }); Debug.Log("MUD World client ready"); + + // 3. Get Table client. + var table = world.GetTable("Counter"); - // 3. Query current counter value. - var counterValue = (await world.Query()).Counter; + // 4. Query counter value + var allRecords = await table.Query(MudQuery.All); // Query all records of the Counter table + var singleRecord = allRecords.Single(); // Get single record + var counterValue = (BigInteger)singleRecord[0]; // Get value of the first column Debug.Log($"Counter value on load: {counterValue}"); - CounterLabel.text = counterValue.ToString("d"); + UpdateGui(counterValue); + + // 5. Subscribe to table updates. + table.RecordUpdated += OnCounterRecordUpdated; } public async void IncrementCounter() @@ -74,14 +90,21 @@ public async void IncrementCounter() return; } - // 4. Send transaction to execute the Increment function of the World contract. + // 5. Send transaction to execute the Increment function of the World contract. Debug.Log("Sending transaction to execute the Increment function.."); - await world.Send("app__increment"); + await world.GetSystems().Send("increment"); Debug.Log($"Increment successful"); - - // 5. Query new counter value. - var counterValue = (await world.Query()).Counter; - Debug.Log($"Counter value after increment: {counterValue}"); - CounterLabel.text = counterValue.ToString("d"); + } + + private void OnCounterRecordUpdated(object[] key, object[] record) + { + var counterValue = (BigInteger)record[0]; + Debug.Log($"Counter value updated: {counterValue}"); + UpdateGui(counterValue); + } + + private void UpdateGui(BigInteger counterValue) + { + counterLabel.text = counterValue.ToString("d"); } } diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/MudSample.unity b/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/MudSample.unity index dcaf5e85e..3af9f2a27 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/MudSample.unity +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/MudSample.unity @@ -883,9 +883,10 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 0784d678e93c29d44b5621a24b931617, type: 3} m_Name: m_EditorClassIdentifier: - WorldContractAddress: 0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b - WorldContractAbi: {fileID: 4900000, guid: 95864876aa70fc9418d44d22c274ff16, type: 3} - CounterLabel: {fileID: 1231023402} + mudConfig: {fileID: 11400000, guid: 46875b4ce5fed5742b226cb3bac356b7, type: 2} + worldContractAddress: 0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b + worldContractAbi: {fileID: 4900000, guid: 95864876aa70fc9418d44d22c274ff16, type: 3} + counterLabel: {fileID: 1231023402} --- !u!4 &1709610250 Transform: m_ObjectHideFlags: 0 diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/Sample Mud Config.asset b/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/Sample Mud Config.asset new file mode 100644 index 000000000..5527d82fb --- /dev/null +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/Sample Mud Config.asset @@ -0,0 +1,16 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9144050b840b41d29f70ad254a875651, type: 3} + m_Name: Sample Mud Config + m_EditorClassIdentifier: + StorageType: 0 + InMemoryFromBlockNumber: 0 diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/Sample Mud Config.asset.meta b/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/Sample Mud Config.asset.meta new file mode 100644 index 000000000..e280e8a52 --- /dev/null +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK MUD/Sample Mud Config.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 46875b4ce5fed5742b226cb3bac356b7 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/UnitySampleProject/UnitySampleProject.sln.DotSettings b/src/UnitySampleProject/UnitySampleProject.sln.DotSettings new file mode 100644 index 000000000..ef25ec43b --- /dev/null +++ b/src/UnitySampleProject/UnitySampleProject.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file