diff --git a/ChainSafe.Gaming.sln b/ChainSafe.Gaming.sln index dee5b62e9..b61728066 100644 --- a/ChainSafe.Gaming.sln +++ b/ChainSafe.Gaming.sln @@ -35,8 +35,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chainsafe.Gaming.Chainlink" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChainSafe.Gaming.Debugging", "src\ChainSafe.Gaming.Debugging\ChainSafe.Gaming.Debugging.csproj", "{88B2E2F4-96BC-44B1-AE4D-5FBA6B4C399F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChainSafe.Gaming.MetaMask", "src\ChainSafe.Gaming.MetaMask\ChainSafe.Gaming.MetaMask.csproj", "{0631D9F5-2942-4ECE-A0B5-7C3AF54E87AA}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChainSafe.Gaming.MetaMask.Unity", "src\ChainSafe.Gaming.MetaMask.Unity\ChainSafe.Gaming.MetaMask.Unity.csproj", "{AF45F008-6ABC-4F37-82BF-4814B7F15B30}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChainSafe.Gaming.SygmaClient", "src\ChainSafe.Gaming.SygmaClient\ChainSafe.Gaming.SygmaClient.csproj", "{6D329479-C391-4BF9-B6FC-FFA3AD3A12D7}" @@ -123,12 +121,6 @@ Global {88B2E2F4-96BC-44B1-AE4D-5FBA6B4C399F}.Release|Any CPU.Build.0 = Release|Any CPU {88B2E2F4-96BC-44B1-AE4D-5FBA6B4C399F}.Test|Any CPU.ActiveCfg = Debug|Any CPU {88B2E2F4-96BC-44B1-AE4D-5FBA6B4C399F}.Test|Any CPU.Build.0 = Debug|Any CPU - {0631D9F5-2942-4ECE-A0B5-7C3AF54E87AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0631D9F5-2942-4ECE-A0B5-7C3AF54E87AA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0631D9F5-2942-4ECE-A0B5-7C3AF54E87AA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0631D9F5-2942-4ECE-A0B5-7C3AF54E87AA}.Release|Any CPU.Build.0 = Release|Any CPU - {0631D9F5-2942-4ECE-A0B5-7C3AF54E87AA}.Test|Any CPU.ActiveCfg = Debug|Any CPU - {0631D9F5-2942-4ECE-A0B5-7C3AF54E87AA}.Test|Any CPU.Build.0 = Debug|Any CPU {AF45F008-6ABC-4F37-82BF-4814B7F15B30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AF45F008-6ABC-4F37-82BF-4814B7F15B30}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF45F008-6ABC-4F37-82BF-4814B7F15B30}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectConfigSO.cs b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectConfigSO.cs index 870024ee9..25552c641 100644 --- a/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectConfigSO.cs +++ b/Packages/io.chainsafe.web3-unity/Runtime/Scripts/WalletConnect/WalletConnectConfigSO.cs @@ -10,6 +10,9 @@ namespace ChainSafe.Gaming.WalletConnect [CreateAssetMenu(menuName = "ChainSafe/WalletConnect/WalletConnect Config", fileName = "WalletConnectConfig", order = 0)] public class WalletConnectConfigSO : ScriptableObject, IWalletConnectConfig { + public string SignMessageRpcMethodName => "personal_sign"; + public string SignTypedMessageRpcMethodName => "eth_signTypedData"; + [field: SerializeField] public bool AutoRenewSession { get; set; } = true; [field: SerializeField] public string ProjectName { get; set; } [field: SerializeField] public string ProjectId { get; set; } diff --git a/Packages/io.chainsafe.web3-unity/Tests/Runtime/SampleTestsBase.cs b/Packages/io.chainsafe.web3-unity/Tests/Runtime/SampleTestsBase.cs index d23939369..d2a20af6f 100644 --- a/Packages/io.chainsafe.web3-unity/Tests/Runtime/SampleTestsBase.cs +++ b/Packages/io.chainsafe.web3-unity/Tests/Runtime/SampleTestsBase.cs @@ -62,8 +62,8 @@ internal static ValueTask BuildTestWeb3(Web3Builder.ConfigureServicesDeleg services.AddSingleton(new StubWalletConnectProviderConfig()); // can be replaced services.AddSingleton(); - services.UseWalletConnectSigner(); - services.UseWalletConnectTransactionExecutor(); + services.UseWalletSigner(); + services.UseWalletTransactionExecutor(); // Add any contracts we would want to use services.ConfigureRegisteredContracts(contracts => diff --git a/scripts/data/published_dependencies.txt b/scripts/data/published_dependencies.txt index 96a73ea71..284c36f465 100644 --- a/scripts/data/published_dependencies.txt +++ b/scripts/data/published_dependencies.txt @@ -1,3 +1,3 @@ -Packages/io.chainsafe.web3-unity/Runtime/Libraries/:ADRaffy.ENSNormalize;Nethereum.Model;BouncyCastle.Crypto;Nethereum.RLP;ChainSafe.Gaming.Debugging;Nethereum.RPC;ChainSafe.Gaming.Gelato;ChainSafe.Gaming.SygmaClient;Nethereum.Signer.EIP712;ChainSafe.Gaming.InProcessSigner;Nethereum.Signer;ChainSafe.Gaming.InProcessTransactionExecutor;Nethereum.Util;ChainSafe.Gaming.Unity.ThirdParty;Nethereum.Web3;ChainSafe.Gaming.Unity;System.Buffers;ChainSafe.Gaming.WalletConnect;System.Memory;ChainSafe.Gaming;System.Numerics.Vectors;Microsoft.Bcl.AsyncInterfaces;System.Reactive;Microsoft.Extensions.DependencyInjection.Abstractions;System.Runtime.CompilerServices.Unsafe;Microsoft.Extensions.DependencyInjection;System.Runtime.InteropServices.WindowsRuntime;Microsoft.Extensions.Logging.Abstractions;System.Security.Cryptography.Cng;Microsoft.IdentityModel.Abstractions;System.Text.Encodings.Web;Microsoft.IdentityModel.Logging;System.Text.Json;Microsoft.IdentityModel.Tokens;System.Threading.Channels;NBitcoin;System.Threading.Tasks.Extensions;Nethereum.ABI;WalletConnectSharp.Auth;Nethereum.Accounts;WalletConnectSharp.Common;WalletConnectSharp.Events;Nethereum.BlockchainProcessing;WalletConnectSharp.Core;Nethereum.Contracts;WalletConnectSharp.Crypto;Nethereum.Hex;Nethereum.JsonRpc.Client;WalletConnectSharp.Network.Websocket;Nethereum.JsonRpc.IpcClient;WalletConnectSharp.Network;Nethereum.JsonRpc.RpcClient;WalletConnectSharp.Sign;Nethereum.KeyStore;WalletConnectSharp.Storage;Nethereum.Merkle.Patricia;WalletConnectSharp.Web3Wallet;Nethereum.Merkle;Websocket.Client;Nethereum.Metamask;Nethereum.Siwe.Core;Nethereum.Siwe;Nethereum.UI;Nethereum.Unity.Metamask;Nethereum.Unity;ChainSafe.Gaming.MetaMask;ChainSafe.Gaming.MetaMask.Unity;ChainSafe.Gaming.Marketplace +Packages/io.chainsafe.web3-unity/Runtime/Libraries/:ADRaffy.ENSNormalize;Nethereum.Model;BouncyCastle.Crypto;Nethereum.RLP;ChainSafe.Gaming.Debugging;Nethereum.RPC;ChainSafe.Gaming.Gelato;ChainSafe.Gaming.SygmaClient;Nethereum.Signer.EIP712;ChainSafe.Gaming.InProcessSigner;Nethereum.Signer;ChainSafe.Gaming.InProcessTransactionExecutor;Nethereum.Util;ChainSafe.Gaming.Unity.ThirdParty;Nethereum.Web3;ChainSafe.Gaming.Unity;System.Buffers;ChainSafe.Gaming.WalletConnect;System.Memory;ChainSafe.Gaming;System.Numerics.Vectors;Microsoft.Bcl.AsyncInterfaces;System.Reactive;Microsoft.Extensions.DependencyInjection.Abstractions;System.Runtime.CompilerServices.Unsafe;Microsoft.Extensions.DependencyInjection;System.Runtime.InteropServices.WindowsRuntime;Microsoft.Extensions.Logging.Abstractions;System.Security.Cryptography.Cng;Microsoft.IdentityModel.Abstractions;System.Text.Encodings.Web;Microsoft.IdentityModel.Logging;System.Text.Json;Microsoft.IdentityModel.Tokens;System.Threading.Channels;NBitcoin;System.Threading.Tasks.Extensions;Nethereum.ABI;WalletConnectSharp.Auth;Nethereum.Accounts;WalletConnectSharp.Common;WalletConnectSharp.Events;Nethereum.BlockchainProcessing;WalletConnectSharp.Core;Nethereum.Contracts;WalletConnectSharp.Crypto;Nethereum.Hex;Nethereum.JsonRpc.Client;WalletConnectSharp.Network.Websocket;Nethereum.JsonRpc.IpcClient;WalletConnectSharp.Network;Nethereum.JsonRpc.RpcClient;WalletConnectSharp.Sign;Nethereum.KeyStore;WalletConnectSharp.Storage;Nethereum.Merkle.Patricia;WalletConnectSharp.Web3Wallet;Nethereum.Merkle;Websocket.Client;Nethereum.Metamask;Nethereum.Siwe.Core;Nethereum.Siwe;Nethereum.UI;Nethereum.Unity.Metamask;Nethereum.Unity;ChainSafe.Gaming.MetaMask.Unity;ChainSafe.Gaming.Marketplace Packages/io.chainsafe.web3-unity.lootboxes/Chainlink/Runtime/Libraries/:Chainsafe.Gaming.Chainlink;ChainSafe.Gaming.Lootboxes.Chainlink Packages/io.chainsafe.web3-unity.hyperplay/Runtime/Libraries/:ChainSafe.Gaming.HyperPlay \ No newline at end of file diff --git a/src/ChainSafe.Gaming.HyperPlay/HyperPlayConfig.cs b/src/ChainSafe.Gaming.HyperPlay/HyperPlayConfig.cs index eaf0ebd34..c39893da9 100644 --- a/src/ChainSafe.Gaming.HyperPlay/HyperPlayConfig.cs +++ b/src/ChainSafe.Gaming.HyperPlay/HyperPlayConfig.cs @@ -5,6 +5,10 @@ namespace ChainSafe.Gaming.HyperPlay /// public class HyperPlayConfig : IHyperPlayConfig { + public string SignMessageRpcMethodName => "personal_sign"; + + public string SignTypedMessageRpcMethodName => "eth_signTypedData_v3"; + /// /// Remember the HyperPlay session. /// Like remember me for login. diff --git a/src/ChainSafe.Gaming.HyperPlay/HyperPlayExtensions.cs b/src/ChainSafe.Gaming.HyperPlay/HyperPlayExtensions.cs index f418b75a6..f1fde1cb0 100644 --- a/src/ChainSafe.Gaming.HyperPlay/HyperPlayExtensions.cs +++ b/src/ChainSafe.Gaming.HyperPlay/HyperPlayExtensions.cs @@ -20,41 +20,15 @@ public static class HyperPlayExtensions /// The same service collection that was passed in. This enables fluent style. public static IWeb3ServiceCollection UseHyperPlay(this IWeb3ServiceCollection collection, IHyperPlayConfig config) { - collection.AssertServiceNotBound(); - - collection.AddSingleton(); + collection.AssertServiceNotBound(); collection.AddSingleton(); - collection.Replace(ServiceDescriptor.Singleton(typeof(IHyperPlayConfig), config)); - - return collection; - } - - /// - /// Binds implementation of as to Web3 as a service. - /// - /// Service collection to bind implementations to. - /// The same service collection that was passed in. This enables fluent style. - public static IWeb3ServiceCollection UseHyperPlaySigner(this IWeb3ServiceCollection collection) - { - collection.AssertServiceNotBound(); + collection.AssertServiceNotBound(); - collection.AddSingleton(); - - return collection; - } - - /// - /// Binds implementation of as to Web3 as a service. - /// - /// Service collection to bind implementations to. - /// The same service collection that was passed in. This enables fluent style. - public static IWeb3ServiceCollection UseHyperPlayTransactionExecutor(this IWeb3ServiceCollection collection) - { - collection.AssertServiceNotBound(); + collection.Replace(ServiceDescriptor.Singleton(typeof(IHyperPlayConfig), config)); - collection.AddSingleton(); + collection.UseWalletProvider(config); return collection; } diff --git a/src/ChainSafe.Gaming.HyperPlay/HyperPlayProvider.cs b/src/ChainSafe.Gaming.HyperPlay/HyperPlayProvider.cs index 5e7774bdd..b2f34687d 100644 --- a/src/ChainSafe.Gaming.HyperPlay/HyperPlayProvider.cs +++ b/src/ChainSafe.Gaming.HyperPlay/HyperPlayProvider.cs @@ -54,7 +54,7 @@ public override async Task Connect() { string[] accounts = await Perform("eth_accounts"); - string account = accounts[0].AssertIsPublicAddress(nameof(account)); + string account = accounts[0]; // Saved account exists. if (data.RememberSession && data.SavedAccount == account) @@ -66,20 +66,7 @@ public override async Task Connect() string hash = await Perform("personal_sign", message, account); - // Verify signature. - // TODO: Make into a Util Method. - EthECDSASignature signature = MessageSigner.ExtractEcdsaSignature(hash); - - string messageToHash = "\x19" + "Ethereum Signed Message:\n" + message.Length + message; - - byte[] messageHash = new Sha3Keccack().CalculateHash(Encoding.UTF8.GetBytes(messageToHash)); - - var key = EthECKey.RecoverFromSignature(signature, messageHash); - - if (key.GetPublicAddress().ToLower().Trim() != account.ToLower().Trim()) - { - throw new Web3Exception("Fetched address does not match the signing address."); - } + hash.AssertSignatureValid(message, account); if (config.RememberSession) { diff --git a/src/ChainSafe.Gaming.HyperPlay/HyperPlaySigner.cs b/src/ChainSafe.Gaming.HyperPlay/HyperPlaySigner.cs deleted file mode 100644 index c829a54cd..000000000 --- a/src/ChainSafe.Gaming.HyperPlay/HyperPlaySigner.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Threading.Tasks; -using ChainSafe.Gaming.Evm.Signers; -using ChainSafe.Gaming.Web3.Core; -using ChainSafe.Gaming.Web3.Core.Evm; -using ChainSafe.Gaming.Web3.Core.Logout; -using ChainSafe.Gaming.Web3.Evm.Wallet; - -namespace ChainSafe.Gaming.HyperPlay -{ - /// - /// Concrete implementation of via HyperPlay desktop client. - /// - public class HyperPlaySigner : ISigner, ILifecycleParticipant, ILogoutHandler - { - private readonly IWalletProvider walletProvider; - - /// - /// Initializes a new instance of the class. - /// - /// HyperPlay connection provider to connect and make RPC requests. - public HyperPlaySigner(IWalletProvider walletProvider) - { - this.walletProvider = walletProvider; - } - - public string PublicAddress { get; private set; } - - public async ValueTask WillStartAsync() - { - PublicAddress = await walletProvider.Connect(); - } - - /// - /// Sign message via HyperPlay desktop client. - /// - /// Message to sign. - /// Signed message hash. - public Task SignMessage(string message) - { - return walletProvider.Perform("personal_sign", message, PublicAddress); - } - - /// - /// Sign typed data via HyperPlay desktop client. - /// - /// A serializable domain separator. - /// Data to be signed. - /// Data type of data to be signed. - /// Hash response of a successfully signed typed data. - public Task SignTypedData(SerializableDomain domain, TStructType message) - { - SerializableTypedData typedData = new SerializableTypedData(domain, message); - - return walletProvider.Perform("eth_signTypedData_v3", PublicAddress, typedData); - } - - public ValueTask WillStopAsync() - { - return new ValueTask(Task.CompletedTask); - } - - public async Task OnLogout() - { - await walletProvider.Disconnect(); - } - } -} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.HyperPlay/HyperPlayTransactionExecutor.cs b/src/ChainSafe.Gaming.HyperPlay/HyperPlayTransactionExecutor.cs deleted file mode 100644 index 621e3c7a1..000000000 --- a/src/ChainSafe.Gaming.HyperPlay/HyperPlayTransactionExecutor.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using ChainSafe.Gaming.Evm.Providers; -using ChainSafe.Gaming.Evm.Signers; -using ChainSafe.Gaming.Evm.Transactions; -using ChainSafe.Gaming.Web3; -using ChainSafe.Gaming.Web3.Core.Evm; -using ChainSafe.Gaming.Web3.Evm.Wallet; -using Nethereum.RPC.Eth.DTOs; - -namespace ChainSafe.Gaming.HyperPlay -{ - /// - /// Concrete implementation of via HyperPlay desktop client. - /// - public class HyperPlayTransactionExecutor : ITransactionExecutor - { - private readonly IWalletProvider walletProvider; - private readonly ISigner signer; - - /// - /// Initializes a new instance of the class. - /// - /// HyperPlay provider for making RPC requests. - /// Signer for fetching public address. - public HyperPlayTransactionExecutor(IWalletProvider walletProvider, ISigner signer) - { - this.walletProvider = walletProvider; - this.signer = signer; - } - - /// - /// Send a Transaction via HyperPlay desktop Client. - /// This prompts user to approve a transaction on HyperPlay. - /// - /// Transaction to send. - /// Hash response of a successfully executed transaction. - /// Throws Exception if executing transaction fails. - public async Task SendTransaction(TransactionRequest transaction) - { - if (string.IsNullOrEmpty(transaction.From)) - { - transaction.From = signer.PublicAddress; - } - - TransactionInput transactionInput = new TransactionInput - { - From = transaction.From, - To = transaction.To, - Gas = transaction.GasLimit, - GasPrice = transaction.GasPrice, - Value = transaction.Value, - Data = transaction.Data ?? "0x", - Nonce = transaction.Nonce, - AccessList = transaction.AccessList, - }; - - string hash = await walletProvider.Perform("eth_sendTransaction", transactionInput); - - string hashPattern = @"^0x[a-fA-F0-9]{64}$"; - if (!Regex.IsMatch(hash, hashPattern)) - { - throw new Web3Exception($"incorrect txn response format {hash}"); - } - - return await walletProvider.GetTransaction(hash); - } - } -} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.HyperPlay/IHyperPlayConfig.cs b/src/ChainSafe.Gaming.HyperPlay/IHyperPlayConfig.cs index c1019e971..d2ba52c57 100644 --- a/src/ChainSafe.Gaming.HyperPlay/IHyperPlayConfig.cs +++ b/src/ChainSafe.Gaming.HyperPlay/IHyperPlayConfig.cs @@ -1,9 +1,11 @@ +using ChainSafe.Gaming.Web3.Evm.Wallet; + namespace ChainSafe.Gaming.HyperPlay { /// /// Config for a HyperPlay connection. /// - public interface IHyperPlayConfig + public interface IHyperPlayConfig : IWalletProviderConfig { /// /// Url for connecting to HyperPlay desktop client. diff --git a/src/ChainSafe.Gaming.MetaMask.Unity/ChainSafe.Gaming.MetaMask.Unity.csproj b/src/ChainSafe.Gaming.MetaMask.Unity/ChainSafe.Gaming.MetaMask.Unity.csproj index 5b6542ee5..6e6a40fc0 100644 --- a/src/ChainSafe.Gaming.MetaMask.Unity/ChainSafe.Gaming.MetaMask.Unity.csproj +++ b/src/ChainSafe.Gaming.MetaMask.Unity/ChainSafe.Gaming.MetaMask.Unity.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/ChainSafe.Gaming.MetaMask.Unity/MetaMaskConfig.cs b/src/ChainSafe.Gaming.MetaMask.Unity/MetaMaskConfig.cs new file mode 100644 index 000000000..d7f921016 --- /dev/null +++ b/src/ChainSafe.Gaming.MetaMask.Unity/MetaMaskConfig.cs @@ -0,0 +1,14 @@ +using ChainSafe.Gaming.Web3.Evm.Wallet; + +namespace ChainSafe.Gaming.MetaMask.Unity +{ + /// + /// Concrete implementation of for connecting to MetaMask wallet. + /// + public class MetaMaskConfig : IWalletProviderConfig + { + public string SignMessageRpcMethodName => "personal_sign"; + + public string SignTypedMessageRpcMethodName => "eth_signTypedData_v4"; + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.MetaMask.Unity/MetaMaskProvider.cs b/src/ChainSafe.Gaming.MetaMask.Unity/MetaMaskProvider.cs index fbfe64000..f3de37d0e 100644 --- a/src/ChainSafe.Gaming.MetaMask.Unity/MetaMaskProvider.cs +++ b/src/ChainSafe.Gaming.MetaMask.Unity/MetaMaskProvider.cs @@ -11,9 +11,9 @@ namespace ChainSafe.Gaming.MetaMask.Unity { /// - /// Concrete implementation of . + /// Concrete implementation of . /// - public class MetaMaskProvider : WalletProvider, ILogoutHandler + public class MetaMaskProvider : WalletProvider { private readonly ILogWriter logWriter; @@ -63,7 +63,8 @@ public MetaMaskProvider(ILogWriter logWriter, IAnalyticsClient analyticsClient, public override Task Disconnect() { - // Currently no disconnect logic for MetaMask lib on NEthereum. + Object.Destroy(metaMaskController.gameObject); + return Task.CompletedTask; } @@ -89,12 +90,5 @@ public override async Task Connect() return await metaMaskController.Connect(); } - - public Task OnLogout() - { - Object.Destroy(metaMaskController.gameObject); - - return Task.CompletedTask; - } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming.MetaMask.Unity/MetaMaskProviderExtensions.cs b/src/ChainSafe.Gaming.MetaMask.Unity/MetaMaskProviderExtensions.cs index 1f67696b9..0435a101d 100644 --- a/src/ChainSafe.Gaming.MetaMask.Unity/MetaMaskProviderExtensions.cs +++ b/src/ChainSafe.Gaming.MetaMask.Unity/MetaMaskProviderExtensions.cs @@ -17,10 +17,8 @@ public static class MetaMaskProviderExtensions /// The same service collection that was passed in. This enables fluent style. public static IWeb3ServiceCollection UseMetaMask(this IWeb3ServiceCollection collection) { - collection.AssertServiceNotBound(); - // wallet - collection.AddSingleton(); + collection.UseWalletProvider(new MetaMaskConfig()); return collection; } diff --git a/src/ChainSafe.Gaming.MetaMask/ChainSafe.Gaming.MetaMask.csproj b/src/ChainSafe.Gaming.MetaMask/ChainSafe.Gaming.MetaMask.csproj deleted file mode 100644 index d695df281..000000000 --- a/src/ChainSafe.Gaming.MetaMask/ChainSafe.Gaming.MetaMask.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - netstandard2.1 - 9.0 - ChainSafe.Gaming.MetaMask - True - ../../global.ruleset - Debug;Release;Test - AnyCPU - - - - - - - - - - - - - - diff --git a/src/ChainSafe.Gaming.MetaMask/MetaMaskExtensions.cs b/src/ChainSafe.Gaming.MetaMask/MetaMaskExtensions.cs deleted file mode 100644 index 5d4fbb25a..000000000 --- a/src/ChainSafe.Gaming.MetaMask/MetaMaskExtensions.cs +++ /dev/null @@ -1,44 +0,0 @@ -using ChainSafe.Gaming.Evm.Signers; -using ChainSafe.Gaming.Web3.Build; -using ChainSafe.Gaming.Web3.Core; -using ChainSafe.Gaming.Web3.Core.Evm; -using Microsoft.Extensions.DependencyInjection; - -namespace ChainSafe.Gaming.MetaMask -{ - /// - /// and extension methods. - /// - public static class MetaMaskExtensions - { - /// - /// Binds implementation of as to Web3 as a service. - /// - /// Service collection to bind implementations to. - /// The same service collection that was passed in. This enables fluent style. - public static IWeb3ServiceCollection UseMetaMaskSigner(this IWeb3ServiceCollection collection) - { - collection.AssertServiceNotBound(); - - // wallet - collection.AddSingleton(); - - return collection; - } - - /// - /// Binds implementation of as to Web3 as a service. - /// - /// Service collection to bind implementations to. - /// The same service collection that was passed in. This enables fluent style. - public static IWeb3ServiceCollection UseMetaMaskTransactionExecutor(this IWeb3ServiceCollection collection) - { - collection.AssertServiceNotBound(); - - // wallet - collection.AddSingleton(); - - return collection; - } - } -} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.MetaMask/MetaMaskSigner.cs b/src/ChainSafe.Gaming.MetaMask/MetaMaskSigner.cs deleted file mode 100644 index 62565eadb..000000000 --- a/src/ChainSafe.Gaming.MetaMask/MetaMaskSigner.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Threading.Tasks; -using ChainSafe.Gaming.Evm.Signers; -using ChainSafe.Gaming.Web3.Core; -using ChainSafe.Gaming.Web3.Core.Evm; -using ChainSafe.Gaming.Web3.Environment; -using ChainSafe.Gaming.Web3.Evm.Wallet; - -namespace ChainSafe.Gaming.MetaMask -{ - /// - /// Implementation of for Metamask. - /// - public class MetaMaskSigner : ISigner, ILifecycleParticipant - { - private readonly IWalletProvider walletProvider; - - /// - /// Initializes a new instance of the class. - /// - /// Metamask provider that connects to Metamask and makes JsonRPC requests. - public MetaMaskSigner(IWalletProvider walletProvider) - { - this.walletProvider = walletProvider; - } - - public string PublicAddress { get; private set; } - - /// - /// Implementation of . - /// Lifetime event method, called during initialization. - /// - /// async awaitable task. - public async ValueTask WillStartAsync() - { - PublicAddress = await walletProvider.Connect(); - } - - /// - /// Implementation of . - /// Sign message using MetaMask. - /// This prompts user to sign a message on MetaMask. - /// - /// Message to sign. - /// Hash response of a successfully signed message. - public Task SignMessage(string message) - { - return walletProvider.Perform("personal_sign", message, PublicAddress); - } - - /// - /// Implementation of . - /// Sign Typed Data using MetaMask. - /// - /// A serializable domain separator. - /// Data to be signed. - /// Data type of data to be signed. - /// Hash response of a successfully signed typed data. - public Task SignTypedData(SerializableDomain domain, TStructType message) - { - SerializableTypedData typedData = new SerializableTypedData(domain, message); - - // MetaMask doesn't work with regular eth_signTypedData method, has to be eth_signTypedData_v4. - return walletProvider.Perform("eth_signTypedData_v4", typedData, PublicAddress); - } - - /// - /// Implementation of . - /// Lifetime event method, called during "Web3.TerminateAsync". - /// - /// async awaitable task. - public ValueTask WillStopAsync() => new(Task.CompletedTask); - } -} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.MetaMask/MetaMaskTransactionExecutor.cs b/src/ChainSafe.Gaming.MetaMask/MetaMaskTransactionExecutor.cs deleted file mode 100644 index ec3ed050d..000000000 --- a/src/ChainSafe.Gaming.MetaMask/MetaMaskTransactionExecutor.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using ChainSafe.Gaming.Evm.Providers; -using ChainSafe.Gaming.Evm.Signers; -using ChainSafe.Gaming.Evm.Transactions; -using ChainSafe.Gaming.Web3; -using ChainSafe.Gaming.Web3.Core.Evm; -using ChainSafe.Gaming.Web3.Environment; -using ChainSafe.Gaming.Web3.Evm.Wallet; -using Nethereum.RPC.Eth.DTOs; - -namespace ChainSafe.Gaming.MetaMask -{ - /// - /// Implementation of for Metamask. - /// - public class MetaMaskTransactionExecutor : ITransactionExecutor - { - private readonly ILogWriter logWriter; - - private readonly IWalletProvider walletProvider; - - private readonly ISigner signer; - - /// - /// Initializes a new instance of the class. - /// - /// Log Writer used for logging messages and errors. - /// Metamask provider that connects to Metamask and makes JsonRPC requests. - /// Signer for fetching address. - public MetaMaskTransactionExecutor(ILogWriter logWriter, IWalletProvider walletProvider, ISigner signer) - { - this.logWriter = logWriter; - - this.walletProvider = walletProvider; - - this.signer = signer; - } - - /// - /// Implementation of . - /// Send a transaction using Metamask. - /// This prompts user to approve a transaction on Metamask. - /// - /// Transaction to send. - /// Hash response of a successfully executed transaction. - /// Throws Exception if executing transaction fails. - public async Task SendTransaction(TransactionRequest transaction) - { - if (string.IsNullOrEmpty(transaction.From)) - { - transaction.From = signer.PublicAddress; - } - - TransactionInput transactionInput = new TransactionInput - { - From = transaction.From, - To = transaction.To, - Gas = transaction.GasLimit, - GasPrice = transaction.GasPrice, - Value = transaction.Value, - Data = transaction.Data ?? "0x", - Nonce = transaction.Nonce, - AccessList = transaction.AccessList, - }; - - string hash = await walletProvider.Perform("eth_sendTransaction", transactionInput); - - string hashPattern = @"^0x[a-fA-F0-9]{64}$"; - if (!Regex.IsMatch(hash, hashPattern)) - { - throw new Web3Exception($"incorrect txn response format {hash}"); - } - - logWriter.Log($"Transaction executed with hash {hash}"); - - return await walletProvider.GetTransaction(hash); - } - } -} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.WalletConnect/IWalletConnectConfig.cs b/src/ChainSafe.Gaming.WalletConnect/IWalletConnectConfig.cs index 34a16a543..c57d92dd5 100644 --- a/src/ChainSafe.Gaming.WalletConnect/IWalletConnectConfig.cs +++ b/src/ChainSafe.Gaming.WalletConnect/IWalletConnectConfig.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; using ChainSafe.Gaming.WalletConnect.Connection; +using ChainSafe.Gaming.Web3.Evm.Wallet; using WalletConnectSharp.Core; using WalletConnectSharp.Network.Interfaces; @@ -9,7 +10,7 @@ namespace ChainSafe.Gaming.WalletConnect /// /// Interface for WalletConnect configuration. /// - public interface IWalletConnectConfig + public interface IWalletConnectConfig : IWalletProviderConfig { /// /// Set to true if you want to store this session on a disk for the next time. diff --git a/src/ChainSafe.Gaming.WalletConnect/Methods/EthSendTransaction.cs b/src/ChainSafe.Gaming.WalletConnect/Methods/EthSendTransaction.cs index 3e2ee0f05..3b87f2235 100644 --- a/src/ChainSafe.Gaming.WalletConnect/Methods/EthSendTransaction.cs +++ b/src/ChainSafe.Gaming.WalletConnect/Methods/EthSendTransaction.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using ChainSafe.Gaming.WalletConnect.Models; +using Nethereum.RPC.Eth.DTOs; using WalletConnectSharp.Common.Utils; using WalletConnectSharp.Network.Models; @@ -16,8 +18,17 @@ public class EthSendTransaction : List /// Initializes a new instance of the class. /// /// Transaction to be sent. - public EthSendTransaction(params TransactionModel[] transactions) - : base(transactions) + public EthSendTransaction(params TransactionInput[] transactions) + : base(Array.ConvertAll(transactions, input => new TransactionModel + { + From = input.From, + To = input.To, + Gas = input.Gas?.HexValue, + GasPrice = input.GasPrice?.HexValue, + Value = input.Value?.HexValue, + Data = input.Data, + Nonce = input.Nonce?.HexValue, + })) { } diff --git a/src/ChainSafe.Gaming.WalletConnect/WalletConnectConfig.cs b/src/ChainSafe.Gaming.WalletConnect/WalletConnectConfig.cs deleted file mode 100644 index 2cd951a17..000000000 --- a/src/ChainSafe.Gaming.WalletConnect/WalletConnectConfig.cs +++ /dev/null @@ -1,39 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using ChainSafe.Gaming.WalletConnect.Connection; -using WalletConnectSharp.Core; -using WalletConnectSharp.Network.Interfaces; - -namespace ChainSafe.Gaming.WalletConnect -{ - public class WalletConnectConfig : IWalletConnectConfig - { - public bool RememberSession { get; set; } - - public bool ForceNewSession { get; set; } - - public bool AutoRenewSession { get; set; } = true; - - public string ProjectName { get; set; } - - public string ProjectId { get; set; } - - public string BaseContext { get; set; } - - public IConnectionBuilder ConnectionBuilder { get; set; } - - public Metadata Metadata { get; set; } - - public string StoragePath { get; set; } - - public IList? EnabledWallets { get; set; } // todo note in comments that some supported wallets are wierd and it's better to decide on which wallets you want to use or don't want to use - - public IList? DisabledWallets { get; set; } - - public WalletLocationOption WalletLocationOption { get; set; } - - public IConnectionHandlerProvider ConnectionHandlerProvider { get; set; } - - public string? OverrideRegistryUri { get; set; } - } -} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.WalletConnect/WalletConnectExtensions.cs b/src/ChainSafe.Gaming.WalletConnect/WalletConnectExtensions.cs index ea5b9c93f..fa1a83d36 100644 --- a/src/ChainSafe.Gaming.WalletConnect/WalletConnectExtensions.cs +++ b/src/ChainSafe.Gaming.WalletConnect/WalletConnectExtensions.cs @@ -15,28 +15,16 @@ namespace ChainSafe.Gaming.WalletConnect { public static class WalletConnectExtensions { - /// - /// Use this to configure WalletConnect functionality for this instance of . - /// - /// The same service collection that was passed in. This enables fluent style. - public static IWeb3ServiceCollection ConfigureWalletConnect(this IWeb3ServiceCollection services, IWalletConnectConfig config) - { - services.Replace(ServiceDescriptor.Singleton(typeof(IWalletConnectConfig), config)); - return services; - } - /// /// Use this to enable WalletConnect functionality for this instance of Web3. /// /// The same service collection that was passed in. This enables fluent style. - public static IWeb3ServiceCollection UseWalletConnect(this IWeb3ServiceCollection services) + private static IWeb3ServiceCollection UseWalletConnect(this IWeb3ServiceCollection services) { - services.AssertServiceNotBound(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); return services; } @@ -47,41 +35,11 @@ public static IWeb3ServiceCollection UseWalletConnect(this IWeb3ServiceCollectio /// The same service collection that was passed in. This enables fluent style. public static IWeb3ServiceCollection UseWalletConnect(this IWeb3ServiceCollection services, IWalletConnectConfig config) { - services.AssertServiceNotBound(); - - services.ConfigureWalletConnect(config); - services.UseWalletConnect(); - - return services; - } - - /// - /// Use this to set as the for this instance of Web3. - /// - /// The same service collection that was passed in. This enables fluent style. - public static IWeb3ServiceCollection UseWalletConnectSigner(this IWeb3ServiceCollection services) - { - EnsureProviderBoundFirst(services); - services.AssertServiceNotBound(); + services = UseWalletConnect(services); - services.AddSingleton(); - - return services; - } - - /// - /// Use this to set as the - /// for this instance of Web3. - /// - /// The same service collection that was passed in. This enables fluent style. - public static IWeb3ServiceCollection UseWalletConnectTransactionExecutor(this IWeb3ServiceCollection services) - { - EnsureProviderBoundFirst(services); - services.AssertServiceNotBound(); - - services.AddSingleton(); + services.Replace(ServiceDescriptor.Singleton(typeof(IWalletConnectConfig), config)); - return services; + return services.UseWalletProvider(config); } /// @@ -119,19 +77,5 @@ public static IConnectionHelper ConnectionHelper(this WalletConnectSubCategory w { return ((IWeb3SubCategory)walletConnect).Web3.ServiceProvider.GetRequiredService(); } - - /// - /// We need this to be sure - /// initializes first as a . - /// - private static void EnsureProviderBoundFirst(IWeb3ServiceCollection services) - { - if (services.Any(descriptor => descriptor.ServiceType == typeof(IWalletProvider))) - { - return; - } - - throw new Web3BuildException($"You should bind {nameof(IWalletProvider)} first."); - } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming.WalletConnect/WalletConnectProvider.cs b/src/ChainSafe.Gaming.WalletConnect/WalletConnectProvider.cs index 45eeda7e8..2e1934a91 100644 --- a/src/ChainSafe.Gaming.WalletConnect/WalletConnectProvider.cs +++ b/src/ChainSafe.Gaming.WalletConnect/WalletConnectProvider.cs @@ -29,7 +29,7 @@ namespace ChainSafe.Gaming.WalletConnect { /// - /// Default implementation of . + /// Default implementation of . /// public class WalletConnectProvider : WalletProvider, ILifecycleParticipant, IConnectionHelper { @@ -48,6 +48,7 @@ public class WalletConnectProvider : WalletProvider, ILifecycleParticipant, ICon private LocalData localData; private SessionStruct session; private bool connected; + private bool initialized; private IAnalyticsClient analyticsClient; private ConnectionHandlerConfig connectionHandlerConfig; @@ -81,8 +82,18 @@ public WalletConnectProvider( private static bool SessionExpired(SessionStruct s) => s.Expiry != null && Clock.IsExpired((long)s.Expiry); - async ValueTask ILifecycleParticipant.WillStartAsync() + public async ValueTask WillStartAsync() { + await Initialize(); + } + + private async Task Initialize() + { + if (initialized) + { + return; + } + analyticsClient.CaptureEvent(new AnalyticsEvent() { EventName = "Wallet Connect Initialized", @@ -138,6 +149,8 @@ async ValueTask ILifecycleParticipant.WillStartAsync() } }, }; + + initialized = true; } private void ValidateConfig() @@ -153,10 +166,12 @@ private void ValidateConfig() } } - async ValueTask ILifecycleParticipant.WillStopAsync() + public ValueTask WillStopAsync() { signClient?.Dispose(); core?.Dispose(); + + return new ValueTask(Task.CompletedTask); } public override async Task Connect() @@ -167,6 +182,11 @@ public override async Task Connect() $"Tried connecting {nameof(WalletConnectProvider)}, but it's already been connected."); } + if (!initialized) + { + await Initialize(); + } + try { var sessionStored = !string.IsNullOrEmpty(localData.SessionTopic); diff --git a/src/ChainSafe.Gaming.WalletConnect/WalletConnectSigner.cs b/src/ChainSafe.Gaming.WalletConnect/WalletConnectSigner.cs deleted file mode 100644 index 06c8d79c0..000000000 --- a/src/ChainSafe.Gaming.WalletConnect/WalletConnectSigner.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Threading.Tasks; -using ChainSafe.Gaming.Evm.Signers; -using ChainSafe.Gaming.Web3; -using ChainSafe.Gaming.Web3.Core; -using ChainSafe.Gaming.Web3.Core.Evm; -using ChainSafe.Gaming.Web3.Core.Logout; -using ChainSafe.Gaming.Web3.Evm.Wallet; -using WalletConnectSharp.Common.Logging; - -namespace ChainSafe.Gaming.WalletConnect -{ - /// - /// WalletConnect implementation of . - /// - public class WalletConnectSigner : ISigner, ILifecycleParticipant, ILogoutHandler - { - private readonly IWalletProvider provider; - - public WalletConnectSigner(IWalletProvider provider) - { - this.provider = provider; - } - - public string PublicAddress { get; private set; } - - public async ValueTask WillStartAsync() - { - PublicAddress = await provider.Connect(); - } - - ValueTask ILifecycleParticipant.WillStopAsync() => new(Task.CompletedTask); - - public Task OnLogout() => provider.Disconnect(); - - public async Task SignMessage(string message) - { - var hash = await provider.Perform("personal_sign", message, PublicAddress); - - if (!ValidateSignResponse(hash)) - { - throw new Web3Exception("Incorrect response format from signing."); - } - - WCLogger.Log("Successfully signed message."); - return hash; - } - - public async Task SignTypedData(SerializableDomain domain, TStructType message) - { - SerializableTypedData typedData = new SerializableTypedData(domain, message); - - var hash = await provider.Perform("eth_signTypedData", PublicAddress, typedData); - - if (!ValidateSignResponse(hash)) - { - throw new Web3Exception("Incorrect response format from signing."); - } - - WCLogger.Log("Successfully signed typed data."); - return hash; - } - - private static bool ValidateSignResponse(string response) - { - // TODO: validate with regex - return response.StartsWith("0x") && response.Length == 132; - } - } -} \ No newline at end of file diff --git a/src/ChainSafe.Gaming.WalletConnect/WalletConnectTransactionExecutor.cs b/src/ChainSafe.Gaming.WalletConnect/WalletConnectTransactionExecutor.cs deleted file mode 100644 index 6ccb4d53f..000000000 --- a/src/ChainSafe.Gaming.WalletConnect/WalletConnectTransactionExecutor.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using ChainSafe.Gaming.Evm.Providers; -using ChainSafe.Gaming.Evm.Signers; -using ChainSafe.Gaming.Evm.Transactions; -using ChainSafe.Gaming.WalletConnect.Models; -using ChainSafe.Gaming.Web3; -using ChainSafe.Gaming.Web3.Core.Evm; -using ChainSafe.Gaming.Web3.Evm.Wallet; -using WalletConnectSharp.Common.Logging; - -namespace ChainSafe.Gaming.WalletConnect -{ - /// - /// WalletConnect implementation of . - /// - public class WalletConnectTransactionExecutor : ITransactionExecutor - { - private readonly IWalletProvider provider; - private readonly ISigner signer; - - public WalletConnectTransactionExecutor(IWalletProvider provider, ISigner signer) - { - this.provider = provider; - this.signer = signer; - } - - public async Task SendTransaction(TransactionRequest transaction) - { - if (string.IsNullOrEmpty(transaction.From)) - { - transaction.From = signer.PublicAddress; - } - - var requestData = new TransactionModel - { - From = transaction.From, - To = transaction.To, - Gas = transaction.GasLimit?.HexValue, - GasPrice = transaction.GasPrice?.HexValue, - Value = transaction.Value?.HexValue, - Data = transaction.Data ?? "0x", - Nonce = transaction.Nonce?.HexValue, - }; - - var hash = await provider.Perform("eth_sendTransaction", requestData); - if (!ValidateResponseHash(hash)) - { - throw new Web3Exception($"Incorrect transaction response format: \"{hash}\"."); - } - - WCLogger.Log($"Transaction executed successfully. Hash: {hash}."); - - return await provider.GetTransaction(hash); - - bool ValidateResponseHash(string hash) - { - string hashPattern = @"^0x[a-fA-F0-9]{64}$"; - return Regex.IsMatch(hash, hashPattern); - } - } - } -} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Debug/AddressExtensions.cs b/src/ChainSafe.Gaming/Web3/Core/Debug/AddressExtensions.cs index e914a3c04..74d2aaae3 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Debug/AddressExtensions.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Debug/AddressExtensions.cs @@ -1,3 +1,5 @@ +using System.Text.RegularExpressions; + namespace ChainSafe.Gaming.Web3.Core.Debug { public static class AddressExtensions @@ -9,22 +11,22 @@ public static class AddressExtensions /// True if the string provided is a public address. public static bool IsPublicAddress(string value) { - // TODO: more accurate test/Regex - return !string.IsNullOrEmpty(value) && value.Length == 42; + string regexPattern = @"^0x[a-fA-F0-9]{40}$"; + + return !string.IsNullOrEmpty(value) && Regex.IsMatch(value, regexPattern); } /// /// Assert that the string provided is a public address. /// /// String to check. - /// Name of the string variable (nameof). /// String that was checked. /// The string provided is not public address. - public static string AssertIsPublicAddress(this string value, string variableName) + public static string AssertIsPublicAddress(this string value) { if (!IsPublicAddress(value)) { - throw new Web3AssertionException($"\"{variableName}\" is not a public address."); + throw new Web3AssertionException($"\"{value}\" is not a public address."); } return value; diff --git a/src/ChainSafe.Gaming/Web3/Core/Debug/HashExtensions.cs b/src/ChainSafe.Gaming/Web3/Core/Debug/HashExtensions.cs new file mode 100644 index 000000000..82f5a71d8 --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Core/Debug/HashExtensions.cs @@ -0,0 +1,162 @@ +using System.Text; +using System.Text.RegularExpressions; +using ChainSafe.Gaming.Web3.Core.Evm; +using Nethereum.ABI.EIP712; +using Nethereum.Signer; +using Nethereum.Util; + +namespace ChainSafe.Gaming.Web3.Core.Debug +{ + public static class HashExtensions + { + /// + /// Make sure transaction hash is valid. + /// + /// Hash of transaction to be validated. + /// Is Hash Valid. + public static bool IsTransactionHashValid(this string hash) + { + string regexPattern = @"^0x[a-fA-F0-9]{64}$"; + + return !string.IsNullOrEmpty(hash) && Regex.IsMatch(hash, regexPattern); + } + + /// + /// Is Signature Hash Valid. + /// + /// Hash of signature to be validated. + /// Is Hash Valid. + public static bool IsSignatureHashValid(this string hash) + { + string regexPattern = @"^0x[a-fA-F0-9]{130}$"; + + return !string.IsNullOrEmpty(hash) && Regex.IsMatch(hash, regexPattern); + } + + /// + /// Make sure signature hash is valid and also authentic (signer is account). + /// + /// Signature Hash to be validated. + /// Signed Message. + /// Signer account. + /// Is signature valid. + public static bool IsSignatureValid(this string hash, string message, string account) + { + return IsSignatureHashValid(hash) && IsSignatureAuthentic(hash, message, account); + } + + /// + /// Make sure typed data signature hash is valid and also authentic (signer is account). + /// + /// Signature has to be validated. + /// Signed typed data. + /// Signer account. + /// Message type to be signed. + /// Is signature valid. + public static bool IsTypedDataSignatureValid(this string hash, SerializableTypedData typedData, string account) + { + return IsSignatureHashValid(hash) && IsTypedDataSignatureAuthentic(hash, typedData, account); + } + + /// + /// Assert if transaction hash is valid. + /// + /// Transaction hash to be validated. + /// Validated transaction hash. + /// Throws if transaction hash is not valid. + public static string AssertTransactionValid(this string hash) + { + if (!IsTransactionHashValid(hash)) + { + throw new Web3AssertionException("Transaction hash is not valid."); + } + + return hash; + } + + /// + /// Assert if signature is valid and authentic (signer is account). + /// + /// Signature hash. + /// Signed Message. + /// Signer account. + /// Validated signature hash. + /// Throws if signature isn't valid or authentic. + public static string AssertSignatureValid(this string hash, string message, string account) + { + if (!IsSignatureValid(hash, message, account)) + { + throw new Web3AssertionException("Signature is not valid."); + } + + return hash; + } + + /// + /// Assert if signature is valid and authentic (signer is account). + /// + /// Signature hash. + /// Signed Typed data. + /// Signer account. + /// Type of message. + /// Validated signature hash. + /// Throws if signature isn't valid or authentic. + public static string AssertTypedDataSignatureValid(this string hash, SerializableTypedData typedData, string account) + { + if (!IsTypedDataSignatureValid(hash, typedData, account)) + { + throw new Web3AssertionException("Typed Data Signature is not valid."); + } + + return hash; + } + + /// + /// Check if signer account signed the message. + /// + /// Hash of signed message. + /// Signed message. + /// Signer public account. + /// Did signer sign message. + private static bool IsSignatureAuthentic(string hash, string message, string account) + { + if (!AddressExtensions.IsPublicAddress(account)) + { + return false; + } + + string messageToHash = "\x19" + "Ethereum Signed Message:\n" + message.Length + message; + + return IsSignatureBytesAuthentic(Encoding.UTF8.GetBytes(messageToHash), hash, account); + } + + private static bool IsTypedDataSignatureAuthentic(string hash, SerializableTypedData typedData, string account) + { + if (!AddressExtensions.IsPublicAddress(account)) + { + return false; + } + + var rawTypedData = new TypedDataRaw + { + Types = typedData.Types, + PrimaryType = typedData.PrimaryType, + Message = MemberValueFactory.CreateFromMessage(typedData.Message), + DomainRawValues = MemberValueFactory.CreateFromMessage(typedData.Domain), + }; + + return IsSignatureBytesAuthentic(Eip712TypedDataEncoder.Current.EncodeTypedDataRaw(rawTypedData), hash, account); + } + + private static bool IsSignatureBytesAuthentic(byte[] bytes, string hash, string account) + { + EthECDSASignature signature = MessageSigner.ExtractEcdsaSignature(hash); + + byte[] messageHash = new Sha3Keccack().CalculateHash(bytes); + + var key = EthECKey.RecoverFromSignature(signature, messageHash); + + return key.GetPublicAddress().ToLower().Trim() == account.ToLower().Trim(); + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Evm/Dto/TransactionRequest.cs b/src/ChainSafe.Gaming/Web3/Core/Evm/Dto/TransactionRequest.cs index 996ea4143..82d3f8189 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Evm/Dto/TransactionRequest.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Evm/Dto/TransactionRequest.cs @@ -85,6 +85,21 @@ public object Clone() return this.MemberwiseClone(); } + public TransactionInput ToTransactionInput() + { + return new TransactionInput + { + From = From, + To = To, + Gas = GasLimit, + GasPrice = GasPrice, + Value = Value, + Data = Data, + Nonce = Nonce, + AccessList = AccessList, + }; + } + public Dictionary ToRPCParam() { var param = new Dictionary(); diff --git a/src/ChainSafe.Gaming/Web3/Evm/Wallet/IWalletProviderConfig.cs b/src/ChainSafe.Gaming/Web3/Evm/Wallet/IWalletProviderConfig.cs new file mode 100644 index 000000000..e8d1c5867 --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Evm/Wallet/IWalletProviderConfig.cs @@ -0,0 +1,18 @@ +namespace ChainSafe.Gaming.Web3.Evm.Wallet +{ + /// + /// Wallet provider config for connecting to a wallet. + /// + public interface IWalletProviderConfig + { + /// + /// Sign message RPC method name, usually "personal_sign". + /// + public string SignMessageRpcMethodName { get; } + + /// + /// Sign Typed message RPC method name, usually "eth_signTypedData". + /// + public string SignTypedMessageRpcMethodName { get; } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProviderExtensions.cs b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProviderExtensions.cs new file mode 100644 index 000000000..4f5656a97 --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletProviderExtensions.cs @@ -0,0 +1,60 @@ +using ChainSafe.Gaming.Evm.Signers; +using ChainSafe.Gaming.Web3.Build; +using ChainSafe.Gaming.Web3.Core; +using ChainSafe.Gaming.Web3.Core.Evm; +using ChainSafe.Gaming.Web3.Core.Logout; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace ChainSafe.Gaming.Web3.Evm.Wallet +{ + public static class WalletProviderExtensions + { + /// + /// Inject a Wallet Provider for . + /// + /// Collection of services. + /// Config for wallet connection, injected . + /// Concrete Type of provider to be injected. + /// Collection of services with services injected. + public static IWeb3ServiceCollection UseWalletProvider(this IWeb3ServiceCollection collection, IWalletProviderConfig config) + where TProvider : WalletProvider + { + collection.AssertServiceNotBound(); + + collection.AddSingleton(); + + collection.Replace(ServiceDescriptor.Singleton(typeof(IWalletProviderConfig), config)); + + return collection; + } + + /// + /// Inject for . + /// + /// Collection of registered services. + /// Collection of registered services with a registered . + public static IWeb3ServiceCollection UseWalletSigner(this IWeb3ServiceCollection collection) + { + collection.AssertServiceNotBound(); + + collection.AddSingleton(); + + return collection; + } + + /// + /// Inject for . + /// + /// Collection of registered services. + /// Collection of registered services with a registered . + public static IWeb3ServiceCollection UseWalletTransactionExecutor(this IWeb3ServiceCollection collection) + { + collection.AssertServiceNotBound(); + + collection.AddSingleton(); + + return collection; + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletSigner.cs b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletSigner.cs new file mode 100644 index 000000000..f2b6931e9 --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletSigner.cs @@ -0,0 +1,60 @@ +using System.Threading.Tasks; +using ChainSafe.Gaming.Evm.Signers; +using ChainSafe.Gaming.Web3.Core; +using ChainSafe.Gaming.Web3.Core.Debug; +using ChainSafe.Gaming.Web3.Core.Evm; +using ChainSafe.Gaming.Web3.Core.Logout; + +namespace ChainSafe.Gaming.Web3.Evm.Wallet +{ + /// + /// Concrete implementation of for signing messages using a wallet. + /// This can be used with any wallet provider that implements . + /// + public class WalletSigner : ISigner, ILifecycleParticipant, ILogoutHandler + { + private readonly IWalletProvider walletProvider; + private readonly IWalletProviderConfig walletConfig; + + public WalletSigner(IWalletProvider walletProvider, IWalletProviderConfig walletConfig) + { + this.walletProvider = walletProvider; + this.walletConfig = walletConfig; + } + + public string PublicAddress { get; private set; } + + public virtual async ValueTask WillStartAsync() + { + string address = await walletProvider.Connect(); + + PublicAddress = address.AssertIsPublicAddress(); + } + + public virtual async Task SignMessage(string message) + { + string hash = await walletProvider.Perform(walletConfig.SignMessageRpcMethodName, message, PublicAddress); + + return hash.AssertSignatureValid(message, PublicAddress); + } + + public virtual async Task SignTypedData(SerializableDomain domain, TStructType message) + { + SerializableTypedData typedData = new SerializableTypedData(domain, message); + + string hash = await walletProvider.Perform(walletConfig.SignTypedMessageRpcMethodName, PublicAddress, typedData); + + return hash.AssertTypedDataSignatureValid(typedData, PublicAddress); + } + + public virtual ValueTask WillStopAsync() + { + return new ValueTask(Task.CompletedTask); + } + + public async Task OnLogout() + { + await walletProvider.Disconnect(); + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletTransactionExecutor.cs b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletTransactionExecutor.cs new file mode 100644 index 000000000..5974a645c --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Evm/Wallet/WalletTransactionExecutor.cs @@ -0,0 +1,42 @@ +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using ChainSafe.Gaming.Evm.Providers; +using ChainSafe.Gaming.Evm.Signers; +using ChainSafe.Gaming.Evm.Transactions; +using ChainSafe.Gaming.Web3.Core.Debug; +using ChainSafe.Gaming.Web3.Core.Evm; +using ChainSafe.Gaming.Web3.Environment; +using Nethereum.RPC.Eth.DTOs; + +namespace ChainSafe.Gaming.Web3.Evm.Wallet +{ + /// + /// Concrete implementation of for sending transactions using a wallet. + /// This can be used with any wallet provider that implements . + /// + public class WalletTransactionExecutor : ITransactionExecutor + { + private readonly IWalletProvider walletProvider; + + private readonly ISigner signer; + + public WalletTransactionExecutor(IWalletProvider walletProvider, ISigner signer) + { + this.walletProvider = walletProvider; + + this.signer = signer; + } + + public async Task SendTransaction(TransactionRequest transaction) + { + transaction.From ??= signer.PublicAddress; + transaction.Data ??= "0x"; + + string hash = await walletProvider.Perform("eth_sendTransaction", transaction.ToTransactionInput()); + + hash = hash.AssertTransactionValid(); + + return await walletProvider.GetTransaction(hash); + } + } +} \ No newline at end of file diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK HyperPlay/1.0.0/Web3.Unity HyperPlay Samples/Scripts/HyperPlayLoginProvider.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK HyperPlay/1.0.0/Web3.Unity HyperPlay Samples/Scripts/HyperPlayLoginProvider.cs index 7ccf6694d..f71ba2bad 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK HyperPlay/1.0.0/Web3.Unity HyperPlay Samples/Scripts/HyperPlayLoginProvider.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK HyperPlay/1.0.0/Web3.Unity HyperPlay Samples/Scripts/HyperPlayLoginProvider.cs @@ -1,6 +1,7 @@ using ChainSafe.Gaming.HyperPlay; using ChainSafe.Gaming.UnityPackage.Common; using ChainSafe.Gaming.Web3.Build; +using ChainSafe.Gaming.Web3.Evm.Wallet; using Microsoft.Extensions.DependencyInjection; using Scenes; using UnityEngine; @@ -46,7 +47,7 @@ public Web3Builder ConfigureServices(Web3Builder web3Builder) services.UseHyperPlay(new HyperPlayConfig { RememberSession = rememberMeToggle.isOn || _storedSessionAvailable, - }).UseHyperPlaySigner().UseHyperPlayTransactionExecutor(); + }).UseWalletSigner().UseWalletTransactionExecutor(); }); } diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.5.0/Web3.Unity Samples/Scripts/Scenes/MetaMaskLoginProvider.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.5.0/Web3.Unity Samples/Scripts/Scenes/MetaMaskLoginProvider.cs index 11fb3938f..21c2dc125 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.5.0/Web3.Unity Samples/Scripts/Scenes/MetaMaskLoginProvider.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.5.0/Web3.Unity Samples/Scripts/Scenes/MetaMaskLoginProvider.cs @@ -3,6 +3,7 @@ #if UNITY_WEBGL && !UNITY_EDITOR using ChainSafe.Gaming.MetaMask; using ChainSafe.Gaming.MetaMask.Unity; +using ChainSafe.Gaming.Web3.Evm.Wallet; #endif using ChainSafe.Gaming.Web3.Build; using Scenes; @@ -36,7 +37,7 @@ public Web3Builder ConfigureServices(Web3Builder web3Builder) // Currently Metamask browser connections can only run in WebGL builds. // See point 5 in https://github.com/Nethereum/Unity3dSampleTemplate?tab=readme-ov-file#important-notes. #if UNITY_WEBGL && !UNITY_EDITOR - services.UseMetaMask().UseMetaMaskSigner().UseMetaMaskTransactionExecutor(); + services.UseMetaMask().UseWalletSigner().UseWalletTransactionExecutor(); #else Debug.LogError("Metamask browser connection, currently, only works on WebGL Builds (not in editor)."); #endif diff --git a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.5.0/Web3.Unity Samples/Scripts/Scenes/WalletConnectLoginProvider.cs b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.5.0/Web3.Unity Samples/Scripts/Scenes/WalletConnectLoginProvider.cs index 21d7dd2ec..8c7673cd3 100644 --- a/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.5.0/Web3.Unity Samples/Scripts/Scenes/WalletConnectLoginProvider.cs +++ b/src/UnitySampleProject/Assets/Samples/web3.unity SDK/2.5.0/Web3.Unity Samples/Scripts/Scenes/WalletConnectLoginProvider.cs @@ -1,6 +1,7 @@ using ChainSafe.Gaming.UnityPackage.Common; using ChainSafe.Gaming.WalletConnect; using ChainSafe.Gaming.Web3.Build; +using ChainSafe.Gaming.Web3.Evm.Wallet; using Scenes; using UnityEngine; using UnityEngine.UI; @@ -43,8 +44,7 @@ public Web3Builder ConfigureServices(Web3Builder web3Builder) var rememberSession = rememberSessionToggle.isOn || storedSessionAvailable; services.UseWalletConnect(walletConnectConfig.WithRememberSession(rememberSession)) - .UseWalletConnectSigner() - .UseWalletConnectTransactionExecutor(); + .UseWalletSigner().UseWalletTransactionExecutor(); }); }