diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs index 5d1bc3812a..e5bb481a0a 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs @@ -321,6 +321,8 @@ private async UniTask FetchProfileAsync(CancellationToken ct) // When the profile was already in cache, for example your previous account after logout, we need to ensure that all systems related to the profile will update profile.IsDirty = true; + // Catalysts don't manipulate this field, so at this point we assume that the user is connected to web3 + profile.HasConnectedWeb3 = true; profileNameLabel!.Value = profile.Name; characterPreviewController?.Initialize(profile.Avatar); } diff --git a/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrl.cs b/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrl.cs index 288d71d624..f1243bbd57 100644 --- a/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrl.cs +++ b/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrl.cs @@ -24,7 +24,8 @@ public enum DecentralandUrl ApiEvents, ApiAuth, - AuthSignature, + AuthSignatureWebApp, + ApiRpc, GateKeeperSceneAdapter, LocalGateKeeperSceneAdapter, diff --git a/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrlsSource.cs b/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrlsSource.cs index 26a6e19628..2f1089bcd5 100644 --- a/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrlsSource.cs +++ b/Explorer/Assets/DCL/Browser/DecentralandUrls/DecentralandUrlsSource.cs @@ -17,22 +17,23 @@ public class DecentralandUrlsSource : IDecentralandUrlsSource private readonly Dictionary cache = new (); - private readonly string environmentDomainLowerCase; private readonly ILaunchMode launchMode; - public string DecentralandDomain => environmentDomainLowerCase; + public string DecentralandDomain { get; } + public DecentralandEnvironment Environment { get; } public DecentralandUrlsSource(DecentralandEnvironment environment, ILaunchMode launchMode) { - environmentDomainLowerCase = environment.ToString()!.ToLower(); + Environment = environment; + DecentralandDomain = environment.ToString()!.ToLower(); this.launchMode = launchMode; switch (environment) { case DecentralandEnvironment.Org: case DecentralandEnvironment.Zone: - ASSET_BUNDLE_URL = string.Format(ASSET_BUNDLE_URL_TEMPLATE, environmentDomainLowerCase); - GENESIS_URL = string.Format(GENESIS_URL_TEMPLATE, environmentDomainLowerCase); + ASSET_BUNDLE_URL = string.Format(ASSET_BUNDLE_URL_TEMPLATE, DecentralandDomain); + GENESIS_URL = string.Format(GENESIS_URL_TEMPLATE, DecentralandDomain); break; case DecentralandEnvironment.Today: @@ -40,7 +41,7 @@ public DecentralandUrlsSource(DecentralandEnvironment environment, ILaunchMode l //We want to fetch pointers from org, but asset bundles from today //Thats because how peer-testing.decentraland.org works. //Its a catalyst that replicates the org environment and eth network, but doesnt propagate back to the production catalysts - environmentDomainLowerCase = DecentralandEnvironment.Org.ToString()!.ToLower(); + DecentralandDomain = DecentralandEnvironment.Org.ToString()!.ToLower(); ASSET_BUNDLE_URL = "https://ab-cdn.decentraland.today"; //On staging, we hardcode the catalyst because its the only valid one with a valid comms configuration @@ -53,7 +54,7 @@ public string Url(DecentralandUrl decentralandUrl) { if (cache.TryGetValue(decentralandUrl, out string? url) == false) { - url = RawUrl(decentralandUrl).Replace(ENV, environmentDomainLowerCase); + url = RawUrl(decentralandUrl).Replace(ENV, DecentralandDomain); cache[decentralandUrl] = url; } @@ -76,7 +77,8 @@ private static string RawUrl(DecentralandUrl decentralandUrl) => DecentralandUrl.TermsOfUse => $"https://decentraland.{ENV}/terms", DecentralandUrl.ApiPlaces => $"https://places.decentraland.{ENV}/api/places", DecentralandUrl.ApiAuth => $"https://auth-api.decentraland.{ENV}", - DecentralandUrl.AuthSignature => $"https://decentraland.{ENV}/auth/requests", + DecentralandUrl.ApiRpc => $"wss://rpc.decentraland.{ENV}", + DecentralandUrl.AuthSignatureWebApp => $"https://decentraland.{ENV}/auth/requests", DecentralandUrl.BuilderApiDtos => $"https://builder-api.decentraland.{ENV}/v1/collections/[COL-ID]/items", DecentralandUrl.BuilderApiContent => $"https://builder-api.decentraland.{ENV}/v1/storage/contents/", DecentralandUrl.POI => $"https://dcl-lists.decentraland.{ENV}/pois", diff --git a/Explorer/Assets/DCL/Browser/DecentralandUrls/IDecentralandUrlsSource.cs b/Explorer/Assets/DCL/Browser/DecentralandUrls/IDecentralandUrlsSource.cs index 1e12200c7f..194a00dddb 100644 --- a/Explorer/Assets/DCL/Browser/DecentralandUrls/IDecentralandUrlsSource.cs +++ b/Explorer/Assets/DCL/Browser/DecentralandUrls/IDecentralandUrlsSource.cs @@ -7,6 +7,7 @@ public interface IDecentralandUrlsSource const string LAUNCHER_DOWNLOAD_URL = "https://github.com/decentraland/launcher/releases/download"; string DecentralandDomain { get; } + DecentralandEnvironment Environment { get; } string Url(DecentralandUrl decentralandUrl); string GetHostnameForFeatureFlag(); diff --git a/Explorer/Assets/DCL/Profiles/Components/Profile.cs b/Explorer/Assets/DCL/Profiles/Components/Profile.cs index 5b4c4ac9c7..920e4d6117 100644 --- a/Explorer/Assets/DCL/Profiles/Components/Profile.cs +++ b/Explorer/Assets/DCL/Profiles/Components/Profile.cs @@ -62,7 +62,7 @@ internal set } } - public bool HasConnectedWeb3 { get; internal set; } + public bool HasConnectedWeb3 { get; set; } public string? Description { get; set; } public int TutorialStep { get; set; } public string? Email { get; internal set; } diff --git a/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.DappSignatureResponse.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.DappSignatureResponse.cs.meta deleted file mode 100644 index c73a61c21d..0000000000 --- a/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.DappSignatureResponse.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 4300333a810f480a99d6beba832a8a80 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.Default.cs b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.Default.cs new file mode 100644 index 0000000000..488d8b10ca --- /dev/null +++ b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.Default.cs @@ -0,0 +1,86 @@ +using CommunicationData.URLHelpers; +using Cysharp.Threading.Tasks; +using DCL.Browser; +using DCL.Multiplayer.Connections.DecentralandUrls; +using DCL.Web3.Abstract; +using DCL.Web3.Identities; +using System.Collections.Generic; +using System.Threading; + +namespace DCL.Web3.Authenticators +{ + public partial class DappWeb3Authenticator + { + public class Default : IWeb3VerifiedAuthenticator, IVerifiedEthereumApi + { + private readonly IWeb3VerifiedAuthenticator originAuth; + private readonly IVerifiedEthereumApi originApi; + + public Default(IWeb3IdentityCache identityCache, IDecentralandUrlsSource decentralandUrlsSource, IWeb3AccountFactory web3AccountFactory) + { + URLAddress authApiUrl = URLAddress.FromString(decentralandUrlsSource.Url(DecentralandUrl.ApiAuth)); + URLAddress signatureUrl = URLAddress.FromString(decentralandUrlsSource.Url(DecentralandUrl.AuthSignatureWebApp)); + URLDomain rpcServerUrl = URLDomain.FromString(decentralandUrlsSource.Url(DecentralandUrl.ApiRpc)); + + var origin = new DappWeb3Authenticator( + new UnityAppWebBrowser(decentralandUrlsSource), + authApiUrl, + signatureUrl, + rpcServerUrl, + identityCache, + web3AccountFactory, + new HashSet( + new[] + { + "eth_getBalance", + "eth_call", + "eth_blockNumber", + "eth_signTypedData_v4", + } + ), + new HashSet + { + "eth_getTransactionReceipt", + "eth_estimateGas", + "eth_call", + "eth_getBalance", + "eth_getStorageAt", + "eth_blockNumber", + "eth_gasPrice", + "eth_protocolVersion", + "net_version", + "web3_sha3", + "web3_clientVersion", + "eth_getTransactionCount", + "eth_getBlockByNumber", + "eth_getCode", + }, + decentralandUrlsSource.Environment + ); + + originApi = origin; + originAuth = origin; + } + + public void Dispose() + { + originAuth.Dispose(); // Disposes both + } + + public UniTask SendAsync(EthApiRequest request, CancellationToken ct) => + originApi.SendAsync(request, ct); + + public void AddVerificationListener(IVerifiedEthereumApi.VerificationDelegate callback) => + originApi.AddVerificationListener(callback); + + public UniTask LoginAsync(CancellationToken ct) => + originAuth.LoginAsync(ct); + + public UniTask LogoutAsync(CancellationToken cancellationToken) => + originAuth.LogoutAsync(cancellationToken); + + public void SetVerificationListener(IWeb3VerifiedAuthenticator.VerificationDelegate? callback) => + originAuth.SetVerificationListener(callback); + } + } +} diff --git a/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.Default.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.Default.cs.meta new file mode 100644 index 0000000000..beb3b6a916 --- /dev/null +++ b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.Default.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1a04442f11934161bb177ec194107c71 +timeCreated: 1740495000 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.LoginAuthApiRequest.cs b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.LoginAuthApiRequest.cs new file mode 100644 index 0000000000..2dfde7a57f --- /dev/null +++ b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.LoginAuthApiRequest.cs @@ -0,0 +1,14 @@ +using System; + +namespace DCL.Web3.Authenticators +{ + public partial class DappWeb3Authenticator + { + [Serializable] + public struct LoginAuthApiRequest + { + public string method; + public object[] @params; + } + } +} diff --git a/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.LoginAuthApiRequest.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.LoginAuthApiRequest.cs.meta new file mode 100644 index 0000000000..01b70de0e7 --- /dev/null +++ b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.LoginAuthApiRequest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8967138547114248a983f7108af2138d +timeCreated: 1740505587 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.DappSignatureResponse.cs b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.LoginAuthApiResponse.cs similarity index 53% rename from Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.DappSignatureResponse.cs rename to Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.LoginAuthApiResponse.cs index dfe6a23e87..dfbc361d06 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.DappSignatureResponse.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.LoginAuthApiResponse.cs @@ -5,19 +5,11 @@ namespace DCL.Web3.Authenticators public partial class DappWeb3Authenticator { [Serializable] - private struct LoginResponse + private struct LoginAuthApiResponse { public string requestId; public string result; public string sender; } - - [Serializable] - private struct MethodResponse - { - public string requestId; - public T result; - public string sender; - } } } diff --git a/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.LoginAuthApiResponse.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.LoginAuthApiResponse.cs.meta new file mode 100644 index 0000000000..a213ecad68 --- /dev/null +++ b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.LoginAuthApiResponse.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 15fd70ff6f084529985b35e7604ce451 +timeCreated: 1740505579 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.MethodResponse.cs b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.MethodResponse.cs new file mode 100644 index 0000000000..446681d494 --- /dev/null +++ b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.MethodResponse.cs @@ -0,0 +1,15 @@ +using System; + +namespace DCL.Web3.Authenticators +{ + public partial class DappWeb3Authenticator + { + [Serializable] + private struct MethodResponse + { + public string requestId; + public object result; + public string sender; + } + } +} diff --git a/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.MethodResponse.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.MethodResponse.cs.meta new file mode 100644 index 0000000000..f77ae11571 --- /dev/null +++ b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.MethodResponse.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 14054ebbde2f4fa98f664f0e25bcfed6 +timeCreated: 1740505594 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.cs index 497eaa9fc9..667b86c0ce 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/DappWeb3Authenticator.cs @@ -1,9 +1,8 @@ +using CommunicationData.URLHelpers; using Cysharp.Threading.Tasks; using DCL.Browser; using DCL.Multiplayer.Connections.DecentralandUrls; using DCL.Web3.Abstract; -using DCL.Web3.Accounts; -using DCL.Web3.Accounts.Factory; using DCL.Web3.Chains; using DCL.Web3.Identities; using Newtonsoft.Json; @@ -14,6 +13,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Net.WebSockets; using System.Threading; namespace DCL.Web3.Authenticators @@ -21,76 +21,112 @@ namespace DCL.Web3.Authenticators public partial class DappWeb3Authenticator : IWeb3VerifiedAuthenticator, IVerifiedEthereumApi { private const int TIMEOUT_SECONDS = 30; + private const int RPC_BUFFER_SIZE = 50000; + private const string NETWORK_MAINNET = "mainnet"; + private const string NETWORK_SEPOLIA = "sepolia"; + private const string MAINNET_CHAIN_ID = "0x1"; + private const string SEPOLIA_CHAIN_ID = "0xaa36a7"; + private const string MAINNET_NET_VERSION = "1"; + private const string SEPOLIA_NET_VERSION = "11155111"; private readonly IWebBrowser webBrowser; - private readonly string serverUrl; - private readonly string signatureUrl; + private readonly URLAddress authApiUrl; + private readonly URLAddress signatureWebAppUrl; + private readonly URLDomain rpcServerUrl; private readonly IWeb3IdentityCache identityCache; private readonly IWeb3AccountFactory web3AccountFactory; private readonly HashSet whitelistMethods; - - private SocketIO? webSocket; + private readonly HashSet readOnlyMethods; + private readonly DecentralandEnvironment environment; + // Allow only one web3 operation at a time + private readonly SemaphoreSlim mutex = new (1, 1); + private readonly byte[] rpcByteBuffer = new byte[RPC_BUFFER_SIZE]; + private readonly URLBuilder urlBuilder = new (); + + private int authApiPendingOperations; + private int rpcPendingOperations; + private SocketIO? authApiWebSocket; + private ClientWebSocket? rpcWebSocket; private UniTaskCompletionSource? signatureOutcomeTask; private IWeb3VerifiedAuthenticator.VerificationDelegate? loginVerificationCallback; private IVerifiedEthereumApi.VerificationDelegate? signatureVerificationCallback; public DappWeb3Authenticator(IWebBrowser webBrowser, - string serverUrl, - string signatureUrl, + URLAddress authApiUrl, + URLAddress signatureWebAppUrl, + URLDomain rpcServerUrl, IWeb3IdentityCache identityCache, IWeb3AccountFactory web3AccountFactory, - HashSet whitelistMethods - ) + HashSet whitelistMethods, + HashSet readOnlyMethods, + DecentralandEnvironment environment) { this.webBrowser = webBrowser; - this.serverUrl = serverUrl; - this.signatureUrl = signatureUrl; + this.authApiUrl = authApiUrl; + this.signatureWebAppUrl = signatureWebAppUrl; + this.rpcServerUrl = rpcServerUrl; this.identityCache = identityCache; this.web3AccountFactory = web3AccountFactory; this.whitelistMethods = whitelistMethods; + this.readOnlyMethods = readOnlyMethods; + this.environment = environment; } public void Dispose() { - try { webSocket?.Dispose(); } + try { authApiWebSocket?.Dispose(); } catch (ObjectDisposedException) { } } - public async UniTask SendAsync(EthApiRequest request, CancellationToken ct) + public async UniTask SendAsync(EthApiRequest request, CancellationToken ct) { if (!whitelistMethods.Contains(request.method)) throw new Web3Exception($"The method is not allowed: {request.method}"); - try + if (string.Equals(request.method, "eth_accounts") + || string.Equals(request.method, "eth_requestAccounts")) { - await ConnectToServerAsync(); + string[] accounts = Array.Empty(); + + if (identityCache.Identity != null) + accounts = new string[] { identityCache.EnsuredIdentity().Address }; - SignatureIdResponse authenticationResponse = await RequestEthMethodAsync(new AuthorizedEthApiRequest + return new EthApiResponse { - method = request.method, - @params = request.@params, - authChain = identityCache.Identity!.AuthChain.ToArray(), - }, ct); + id = request.id, + jsonrpc = "2.0", + result = accounts, + }; + } - DateTime signatureExpiration = DateTime.UtcNow.AddMinutes(5); + if (string.Equals(request.method, "eth_chainId")) + { + string chainId = GetChainId(); - if (!string.IsNullOrEmpty(authenticationResponse.expiration)) - signatureExpiration = DateTime.Parse(authenticationResponse.expiration, null, DateTimeStyles.RoundtripKind); + return new EthApiResponse + { + id = request.id, + jsonrpc = "2.0", + result = chainId, + }; + } - await UniTask.SwitchToMainThread(ct); + if (string.Equals(request.method, "net_version")) + { + string netVersion = GetNetVersion(); - signatureVerificationCallback?.Invoke(authenticationResponse.code, signatureExpiration); + return new EthApiResponse + { + id = request.id, + jsonrpc = "2.0", + result = netVersion, + }; + } - MethodResponse response = await RequestWalletConfirmationAsync>(authenticationResponse.requestId, signatureExpiration, ct); + if (IsReadOnly(request)) + return await SendWithoutConfirmationAsync(request, ct); - // Strip out the requestId & sender fields. We assume that will not be needed by the client - return response.result; - } - finally - { - await DisconnectFromServerAsync(); - await UniTask.SwitchToMainThread(ct); - } + return await SendWithConfirmationAsync(request, ct); } /// @@ -103,9 +139,15 @@ public async UniTask SendAsync(EthApiRequest request, CancellationToken ct /// public async UniTask LoginAsync(CancellationToken ct) { + await mutex.WaitAsync(ct); + + SynchronizationContext originalSyncContext = SynchronizationContext.Current; + try { - await ConnectToServerAsync(); + await UniTask.SwitchToMainThread(ct); + + await ConnectToAuthApiAsync(); var ephemeralAccount = web3AccountFactory.CreateRandomAccount(); @@ -113,7 +155,7 @@ public async UniTask LoginAsync(CancellationToken ct) DateTime sessionExpiration = DateTime.UtcNow.AddDays(7); string ephemeralMessage = CreateEphemeralMessage(ephemeralAccount, sessionExpiration); - SignatureIdResponse authenticationResponse = await RequestEthMethodAsync(new EthApiRequest + SignatureIdResponse authenticationResponse = await RequestEthMethodWithSignatureAsync(new LoginAuthApiRequest { method = "dcl_personal_sign", @params = new object[] { ephemeralMessage }, @@ -128,9 +170,11 @@ public async UniTask LoginAsync(CancellationToken ct) loginVerificationCallback?.Invoke(authenticationResponse.code, signatureExpiration, authenticationResponse.requestId); - LoginResponse response = await RequestWalletConfirmationAsync(authenticationResponse.requestId, + LoginAuthApiResponse response = await RequestWalletConfirmationAsync(authenticationResponse.requestId, signatureExpiration, ct); + await DisconnectFromAuthApiAsync(); + if (string.IsNullOrEmpty(response.sender)) throw new Web3Exception($"Cannot solve the signer's address from the signature. Request id: {authenticationResponse.requestId}"); @@ -143,17 +187,24 @@ public async UniTask LoginAsync(CancellationToken ct) return new DecentralandIdentity(new Web3Address(response.sender), ephemeralAccount, sessionExpiration, authChain); } + catch (Exception) + { + await DisconnectFromAuthApiAsync(); + throw; + } finally { - await DisconnectFromServerAsync(); - await UniTask.SwitchToMainThread(ct); + if (originalSyncContext != null) + await UniTask.SwitchToSynchronizationContext(originalSyncContext, ct); + else + await UniTask.SwitchToMainThread(ct); + + mutex.Release(); } } - public async UniTask LogoutAsync(CancellationToken cancellationToken) - { - await DisconnectFromServerAsync(); - } + public async UniTask LogoutAsync(CancellationToken cancellationToken) => + await DisconnectFromAuthApiAsync(); public void SetVerificationListener(IWeb3VerifiedAuthenticator.VerificationDelegate? callback) => loginVerificationCallback = callback; @@ -161,31 +212,162 @@ public void SetVerificationListener(IWeb3VerifiedAuthenticator.VerificationDeleg public void AddVerificationListener(IVerifiedEthereumApi.VerificationDelegate callback) => signatureVerificationCallback = callback; - private SocketIO InitializeWebSocket() + private async UniTask DisconnectFromAuthApiAsync() { - if (webSocket != null) return webSocket; + if (authApiWebSocket is { Connected: true }) + await authApiWebSocket.DisconnectAsync(); + } - var uri = new Uri(serverUrl); + private async UniTask SendWithoutConfirmationAsync(EthApiRequest request, CancellationToken ct) + { + SynchronizationContext originalSyncContext = SynchronizationContext.Current; - webSocket = new SocketIO(uri, new SocketIOOptions + try { - Transport = TransportProtocol.WebSocket, - }); + rpcPendingOperations++; + + await mutex.WaitAsync(ct); + + await UniTask.SwitchToMainThread(ct); + + await ConnectToRpcAsync(GetNetworkId(), ct); + + var response = await RequestEthMethodWithoutSignatureAsync(request, ct) + .Timeout(TimeSpan.FromSeconds(TIMEOUT_SECONDS)); + + if (rpcPendingOperations <= 1) + await DisconnectFromRpcAsync(ct); + + return response; + } + catch (Exception) + { + await DisconnectFromRpcAsync(ct); + throw; + } + finally + { + if (originalSyncContext != null) + await UniTask.SwitchToSynchronizationContext(originalSyncContext, ct); + else + await UniTask.SwitchToMainThread(ct); + + mutex.Release(); + rpcPendingOperations--; + } + } + + private async UniTask DisconnectFromRpcAsync(CancellationToken ct) + { + if (rpcWebSocket == null) return; + + await rpcWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", ct); + rpcWebSocket.Abort(); + rpcWebSocket.Dispose(); + rpcWebSocket = null; + } + + private async UniTask ConnectToRpcAsync(string network, CancellationToken ct) + { + if (rpcWebSocket?.State == WebSocketState.Open) return; + + urlBuilder.Clear(); + urlBuilder.AppendDomain(rpcServerUrl); + urlBuilder.AppendPath(new URLPath(network)); + + rpcWebSocket = new ClientWebSocket(); + await rpcWebSocket.ConnectAsync(new Uri(urlBuilder.Build()), ct); + } - webSocket.JsonSerializer = new NewtonsoftJsonSerializer(new JsonSerializerSettings()); + private async UniTask RequestEthMethodWithoutSignatureAsync(EthApiRequest request, CancellationToken ct) + { + string reqJson = JsonConvert.SerializeObject(request); + byte[] bytes = System.Text.Encoding.UTF8.GetBytes(reqJson); + await rpcWebSocket!.SendAsync(bytes, WebSocketMessageType.Text, true, ct); - webSocket.On("outcome", ProcessSignatureOutcomeMessage); + while (!ct.IsCancellationRequested && rpcWebSocket?.State == WebSocketState.Open) + { + WebSocketReceiveResult result = await rpcWebSocket.ReceiveAsync(rpcByteBuffer, ct); + + if (result.MessageType is WebSocketMessageType.Text or WebSocketMessageType.Binary) + { + string resJson = System.Text.Encoding.UTF8.GetString(rpcByteBuffer, 0, result.Count); + EthApiResponse response = JsonConvert.DeserializeObject(resJson); - return webSocket; + if (response.id == request.id) + return response; + } + else if (result.MessageType == WebSocketMessageType.Close) + { + await DisconnectFromRpcAsync(ct); + break; + } + } + + throw new Web3Exception("Unexpected data received from rpc"); } - private async UniTask DisconnectFromServerAsync() + private async UniTask SendWithConfirmationAsync(EthApiRequest request, CancellationToken ct) { - if (webSocket is { Connected: true }) - await webSocket.DisconnectAsync(); + SynchronizationContext originalSyncContext = SynchronizationContext.Current; + + try + { + authApiPendingOperations++; + + await mutex.WaitAsync(ct); + + await UniTask.SwitchToMainThread(ct); + + await ConnectToAuthApiAsync(); + + SignatureIdResponse authenticationResponse = await RequestEthMethodWithSignatureAsync(new AuthorizedEthApiRequest + { + method = request.method, + @params = request.@params, + authChain = identityCache.Identity!.AuthChain.ToArray(), + }, ct); + + DateTime signatureExpiration = DateTime.UtcNow.AddMinutes(5); + + if (!string.IsNullOrEmpty(authenticationResponse.expiration)) + signatureExpiration = DateTime.Parse(authenticationResponse.expiration, null, DateTimeStyles.RoundtripKind); + + await UniTask.SwitchToMainThread(ct); + + signatureVerificationCallback?.Invoke(authenticationResponse.code, signatureExpiration); + + MethodResponse response = await RequestWalletConfirmationAsync(authenticationResponse.requestId, signatureExpiration, ct); + + if (authApiPendingOperations <= 1) + await DisconnectFromAuthApiAsync(); + + // Strip out the requestId & sender fields. We assume that will not be needed by the client + return new EthApiResponse + { + id = request.id, + result = response.result, + jsonrpc = "2.0", + }; + } + catch (Exception) + { + await DisconnectFromAuthApiAsync(); + throw; + } + finally + { + if (originalSyncContext != null) + await UniTask.SwitchToSynchronizationContext(originalSyncContext, ct); + else + await UniTask.SwitchToMainThread(ct); + + mutex.Release(); + authApiPendingOperations--; + } } - private AuthChain CreateAuthChain(LoginResponse response, string ephemeralMessage) + private AuthChain CreateAuthChain(LoginAuthApiResponse response, string ephemeralMessage) { var authChain = AuthChain.Create(); @@ -211,24 +393,33 @@ private AuthChain CreateAuthChain(LoginResponse response, string ephemeralMessag private string CreateEphemeralMessage(IWeb3Account ephemeralAccount, DateTime expiration) => $"Decentraland Login\nEphemeral address: {ephemeralAccount.Address.OriginalFormat}\nExpiration: {expiration:yyyy-MM-ddTHH:mm:ss.fffZ}"; - private async UniTask ConnectToServerAsync() - { - SocketIO webSocket = InitializeWebSocket(); - await webSocket.ConnectAsync().AsUniTask().Timeout(TimeSpan.FromSeconds(TIMEOUT_SECONDS)); - } + private void ProcessSignatureOutcomeMessage(SocketIOResponse response) => + signatureOutcomeTask?.TrySetResult(response); - private void ProcessSignatureOutcomeMessage(SocketIOResponse response) + private async UniTask RequestWalletConfirmationAsync(string requestId, DateTime expiration, CancellationToken ct) { - signatureOutcomeTask?.TrySetResult(response); + webBrowser.OpenUrl($"{signatureWebAppUrl}/{requestId}"); + + signatureOutcomeTask?.TrySetCanceled(ct); + signatureOutcomeTask = new UniTaskCompletionSource(); + + TimeSpan duration = expiration - DateTime.UtcNow; + + try + { + SocketIOResponse response = await signatureOutcomeTask.Task.Timeout(duration).AttachExternalCancellation(ct); + return response.GetValue(); + } + catch (TimeoutException) { throw new SignatureExpiredException(expiration); } } - private async UniTask RequestEthMethodAsync( + private async UniTask RequestEthMethodWithSignatureAsync( object request, CancellationToken ct) { UniTaskCompletionSource task = new (); - await webSocket!.EmitAsync("request", ct, + await authApiWebSocket!.EmitAsync("request", ct, r => { SignatureIdResponse signatureIdResponse = r.GetValue(); @@ -245,73 +436,49 @@ private async UniTask RequestEthMethodAsync( .AttachExternalCancellation(ct); } - private async UniTask RequestWalletConfirmationAsync(string requestId, DateTime expiration, CancellationToken ct) + private async UniTask ConnectToAuthApiAsync() { - webBrowser.OpenUrl($"{signatureUrl}/{requestId}"); + if (authApiWebSocket == null) + { + var uri = new Uri(authApiUrl); - signatureOutcomeTask?.TrySetCanceled(ct); - signatureOutcomeTask = new UniTaskCompletionSource(); + authApiWebSocket = new SocketIO(uri, new SocketIOOptions + { + Transport = TransportProtocol.WebSocket, + }); - TimeSpan duration = expiration - DateTime.UtcNow; + authApiWebSocket.JsonSerializer = new NewtonsoftJsonSerializer(new JsonSerializerSettings()); - try - { - SocketIOResponse response = await signatureOutcomeTask.Task.Timeout(duration).AttachExternalCancellation(ct); - return response.GetValue(); + authApiWebSocket.On("outcome", ProcessSignatureOutcomeMessage); } - catch (TimeoutException) { throw new SignatureExpiredException(expiration); } - } - - public class Default : IWeb3VerifiedAuthenticator, IVerifiedEthereumApi - { - private readonly IWeb3VerifiedAuthenticator originAuth; - private readonly IVerifiedEthereumApi originApi; - public Default(IWeb3IdentityCache identityCache, IDecentralandUrlsSource decentralandUrlsSource, IWeb3AccountFactory web3AccountFactory) - { - string serverUrl = decentralandUrlsSource.Url(DecentralandUrl.ApiAuth); - string signatureUrl = decentralandUrlsSource.Url(DecentralandUrl.AuthSignature); - - var origin = new DappWeb3Authenticator( - new UnityAppWebBrowser(decentralandUrlsSource), - serverUrl, - signatureUrl, - identityCache, - web3AccountFactory, - new HashSet( - new[] - { - "eth_getBalance", - "eth_call", - "eth_blockNumber", - "eth_signTypedData_v4", - } - ) - ); - - originApi = origin; - originAuth = origin; - } + if (authApiWebSocket.Connected) return; - public void Dispose() - { - originAuth.Dispose(); // Disposes both - } + await authApiWebSocket + .ConnectAsync() + .AsUniTask() + .Timeout(TimeSpan.FromSeconds(TIMEOUT_SECONDS)); + } - public UniTask SendAsync(EthApiRequest request, CancellationToken ct) => - originApi.SendAsync(request, ct); + private bool IsReadOnly(EthApiRequest request) + { + foreach (string method in readOnlyMethods) + if (string.Equals(method, request.method, StringComparison.OrdinalIgnoreCase)) + return true; - public void AddVerificationListener(IVerifiedEthereumApi.VerificationDelegate callback) => - originApi.AddVerificationListener(callback); + return false; + } - public UniTask LoginAsync(CancellationToken ct) => - originAuth.LoginAsync(ct); + private string GetNetVersion() => + // TODO: this is a temporary thing until we solve the network in a better way + environment == DecentralandEnvironment.Org ? MAINNET_NET_VERSION : SEPOLIA_NET_VERSION; - public UniTask LogoutAsync(CancellationToken cancellationToken) => - originAuth.LogoutAsync(cancellationToken); + private string GetChainId() => + // TODO: this is a temporary thing until we solve the network in a better way + environment == DecentralandEnvironment.Org ? MAINNET_CHAIN_ID : SEPOLIA_CHAIN_ID; - public void SetVerificationListener(IWeb3VerifiedAuthenticator.VerificationDelegate? callback) => - originAuth.SetVerificationListener(callback); - } + private string GetNetworkId() => + // TODO: this is a temporary thing until we solve the network in a better way (probably it should be parametrized) + environment == DecentralandEnvironment.Org ? NETWORK_MAINNET : NETWORK_SEPOLIA; } } diff --git a/Explorer/Assets/DCL/Web3/EthApiRequest.cs b/Explorer/Assets/DCL/Web3/EthApiRequest.cs index bdf0c3c90e..84a6d1dec5 100644 --- a/Explorer/Assets/DCL/Web3/EthApiRequest.cs +++ b/Explorer/Assets/DCL/Web3/EthApiRequest.cs @@ -5,6 +5,7 @@ namespace DCL.Web3 [Serializable] public struct EthApiRequest { + public long id; public string method; public object[] @params; } diff --git a/Explorer/Assets/DCL/Web3/EthApiResponse.cs b/Explorer/Assets/DCL/Web3/EthApiResponse.cs new file mode 100644 index 0000000000..9987377a8a --- /dev/null +++ b/Explorer/Assets/DCL/Web3/EthApiResponse.cs @@ -0,0 +1,9 @@ +namespace DCL.Web3 +{ + public struct EthApiResponse + { + public long id; + public string jsonrpc; + public object result; + } +} diff --git a/Explorer/Assets/DCL/Web3/EthApiResponse.cs.meta b/Explorer/Assets/DCL/Web3/EthApiResponse.cs.meta new file mode 100644 index 0000000000..ee4adab796 --- /dev/null +++ b/Explorer/Assets/DCL/Web3/EthApiResponse.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f4638c8b7a064622a9be25837283eb60 +timeCreated: 1740497937 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/IEthereumApi.cs b/Explorer/Assets/DCL/Web3/IEthereumApi.cs index 838285989a..a8784f060a 100644 --- a/Explorer/Assets/DCL/Web3/IEthereumApi.cs +++ b/Explorer/Assets/DCL/Web3/IEthereumApi.cs @@ -6,6 +6,6 @@ namespace DCL.Web3 { public interface IEthereumApi : IDisposable { - UniTask SendAsync(EthApiRequest request, CancellationToken ct); + UniTask SendAsync(EthApiRequest request, CancellationToken ct); } } diff --git a/Explorer/Assets/DCL/Web3/Web3.asmdef b/Explorer/Assets/DCL/Web3/Web3.asmdef index 9124f01d06..fff3027649 100644 --- a/Explorer/Assets/DCL/Web3/Web3.asmdef +++ b/Explorer/Assets/DCL/Web3/Web3.asmdef @@ -13,7 +13,8 @@ "GUID:9428ba407ade34e4ebcf01cca4669d4b", "GUID:41931db0c299475fa4e0dcb60b48d656", "GUID:75edf6fa50ff464395ca31529ea14d25", - "GUID:3205adaaf7e904f1a9425faef1a6b323" + "GUID:3205adaaf7e904f1a9425faef1a6b323", + "GUID:8322ea9340a544c59ddc56d4793eac74" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Explorer/Assets/Scripts/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/Scripts/Global/Dynamic/BootstrapContainer.cs index 772dfe32d3..9d9e6c5e45 100644 --- a/Explorer/Assets/Scripts/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/Scripts/Global/Dynamic/BootstrapContainer.cs @@ -1,4 +1,5 @@ using Arch.Core; +using CommunicationData.URLHelpers; using Cysharp.Threading.Tasks; using DCL.AssetsProvision; using DCL.Browser; @@ -208,26 +209,25 @@ private static IAnalyticsService CreateSegmentAnalyticsOrFallbackToDebug(Analyti return new DebugAnalyticsService(); } - private static ( - IVerifiedEthereumApi web3VerifiedAuthenticator, - IWeb3VerifiedAuthenticator web3Authenticator - ) + private static (IVerifiedEthereumApi web3VerifiedAuthenticator, IWeb3VerifiedAuthenticator web3Authenticator) CreateWeb3Dependencies( DynamicSceneLoaderSettings sceneLoaderSettings, IWeb3AccountFactory web3AccountFactory, IWeb3IdentityCache identityCache, IWebBrowser webBrowser, BootstrapContainer container, - IDecentralandUrlsSource decentralandUrlsSource - ) + IDecentralandUrlsSource decentralandUrlsSource) { var dappWeb3Authenticator = new DappWeb3Authenticator( webBrowser, - decentralandUrlsSource.Url(DecentralandUrl.ApiAuth), - decentralandUrlsSource.Url(DecentralandUrl.AuthSignature), + URLAddress.FromString(decentralandUrlsSource.Url(DecentralandUrl.ApiAuth)), + URLAddress.FromString(decentralandUrlsSource.Url(DecentralandUrl.AuthSignatureWebApp)), + URLDomain.FromString(decentralandUrlsSource.Url(DecentralandUrl.ApiRpc)), identityCache, web3AccountFactory, - new HashSet(sceneLoaderSettings.Web3WhitelistMethods) + new HashSet(sceneLoaderSettings.Web3WhitelistMethods), + new HashSet(sceneLoaderSettings.Web3ReadOnlyMethods), + decentralandUrlsSource.Environment ); IWeb3VerifiedAuthenticator coreWeb3Authenticator = new ProxyVerifiedWeb3Authenticator(dappWeb3Authenticator, identityCache); diff --git a/Explorer/Assets/Scripts/Global/Dynamic/DynamicSceneLoaderSettings.asset b/Explorer/Assets/Scripts/Global/Dynamic/DynamicSceneLoaderSettings.asset index 485ead5911..50176d0745 100644 --- a/Explorer/Assets/Scripts/Global/Dynamic/DynamicSceneLoaderSettings.asset +++ b/Explorer/Assets/Scripts/Global/Dynamic/DynamicSceneLoaderSettings.asset @@ -12,7 +12,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 51b5f8f4ed664a47a7204b6a65d2d742, type: 3} m_Name: DynamicSceneLoaderSettings m_EditorClassIdentifier: - k__BackingField: 0 k__BackingField: - https://sdk-team-cdn.decentraland.org/ipfs/goerli-plaza-main - https://sdk-team-cdn.decentraland.org/ipfs/goerli-plaza-main-latest @@ -43,3 +42,17 @@ MonoBehaviour: - eth_signTypedData_v4 - eth_getCode - personal_sign + k__BackingField: + - eth_getTransactionReceipt + - eth_estimateGas + - eth_call + - eth_getStorageAt + - eth_blockNumber + - eth_gasPrice + - eth_protocolVersion + - net_version + - web3_sha3 + - web3_clientVersion + - eth_getTransactionCount + - eth_getBlockByNumber + - eth_getCode diff --git a/Explorer/Assets/Scripts/Global/Dynamic/DynamicSceneLoaderSettings.cs b/Explorer/Assets/Scripts/Global/Dynamic/DynamicSceneLoaderSettings.cs index e5dddb71a5..b08a1f375b 100644 --- a/Explorer/Assets/Scripts/Global/Dynamic/DynamicSceneLoaderSettings.cs +++ b/Explorer/Assets/Scripts/Global/Dynamic/DynamicSceneLoaderSettings.cs @@ -11,6 +11,6 @@ public class DynamicSceneLoaderSettings : ScriptableObject { [field: SerializeField] public List Realms { get; private set; } [field: SerializeField] public List Web3WhitelistMethods { get; private set; } - + [field: SerializeField] public List Web3ReadOnlyMethods { get; private set; } } } diff --git a/Explorer/Assets/Scripts/SceneRuntime/Apis/Modules/Ethereums/EthereumApiWrapper.cs b/Explorer/Assets/Scripts/SceneRuntime/Apis/Modules/Ethereums/EthereumApiWrapper.cs index 7a97bed956..744b5c305d 100644 --- a/Explorer/Assets/Scripts/SceneRuntime/Apis/Modules/Ethereums/EthereumApiWrapper.cs +++ b/Explorer/Assets/Scripts/SceneRuntime/Apis/Modules/Ethereums/EthereumApiWrapper.cs @@ -17,8 +17,7 @@ public class EthereumApiWrapper : IJsApiWrapper private readonly IEthereumApi ethereumApi; private readonly ISceneExceptionsHandler sceneExceptionsHandler; private readonly IWeb3IdentityCache web3IdentityCache; - - private CancellationTokenSource sendCancellationToken; + private readonly CancellationTokenSource sendCancellationToken; private CancellationTokenSource signMessageCancellationToken; public EthereumApiWrapper(IEthereumApi ethereumApi, ISceneExceptionsHandler sceneExceptionsHandler, IWeb3IdentityCache web3IdentityCache) : this( @@ -55,6 +54,11 @@ public object TryPay(decimal amount, string currency, string toAddress) [PublicAPI("Used by StreamingAssets/Js/Modules/EthereumController.js")] public object SignMessage(string message) { + signMessageCancellationToken = signMessageCancellationToken.SafeRestart(); + + return RequestPersonalSignatureAsync(signMessageCancellationToken.Token) + .ToDisconnectedPromise(); + async UniTask RequestPersonalSignatureAsync(CancellationToken ct) { await UniTask.SwitchToMainThread(); @@ -63,8 +67,9 @@ async UniTask RequestPersonalSignatureAsync(CancellationTok try { - string signature = await ethereumApi.SendAsync(new EthApiRequest + var response = await ethereumApi.SendAsync(new EthApiRequest { + id = Guid.NewGuid().GetHashCode(), method = "personal_sign", @params = new object[] { @@ -73,7 +78,7 @@ async UniTask RequestPersonalSignatureAsync(CancellationTok }, }, ct); - return new SignMessageResponse(hex, message, signature); + return new SignMessageResponse(hex, message, (string)response.result); } catch (Exception e) { @@ -83,34 +88,28 @@ async UniTask RequestPersonalSignatureAsync(CancellationTok return new SignMessageResponse(hex, message, string.Empty); } } - - signMessageCancellationToken = signMessageCancellationToken.SafeRestart(); - - return RequestPersonalSignatureAsync(signMessageCancellationToken.Token) - .ToDisconnectedPromise(); } [PublicAPI("Used by StreamingAssets/Js/Modules/EthereumController.js")] public object SendAsync(double id, string method, string jsonParams) { + return SendAndFormatAsync(id, method, JsonConvert.DeserializeObject(jsonParams) ?? Array.Empty(), sendCancellationToken.Token) + .ToDisconnectedPromise(); + async UniTask SendAndFormatAsync(double id, string method, object[] @params, CancellationToken ct) { try { - object result = await ethereumApi.SendAsync(new EthApiRequest + var result = await ethereumApi.SendAsync(new EthApiRequest { + id = (long)id, method = method, @params = @params, }, ct); return new SendEthereumMessageResponse { - jsonAnyResponse = JsonConvert.SerializeObject(new SendEthereumMessageResponse.Payload - { - id = (long)id, - jsonrpc = "2.0", - result = result, - }), + jsonAnyResponse = JsonConvert.SerializeObject(result), }; } catch (Exception e) @@ -119,7 +118,7 @@ async UniTask SendAndFormatAsync(double id, string return new SendEthereumMessageResponse { - jsonAnyResponse = JsonConvert.SerializeObject(new SendEthereumMessageResponse.Payload + jsonAnyResponse = JsonConvert.SerializeObject(new EthApiResponse { id = (long)id, jsonrpc = "2.0", @@ -128,12 +127,6 @@ async UniTask SendAndFormatAsync(double id, string }; } } - - // TODO: support cancellations by id (?) - sendCancellationToken = sendCancellationToken.SafeRestart(); - - return SendAndFormatAsync(id, method, JsonConvert.DeserializeObject(jsonParams), sendCancellationToken.Token) - .ToDisconnectedPromise(); } } } diff --git a/Explorer/Assets/Scripts/SceneRuntime/Apis/Modules/Ethereums/SendEthereumMessageResponse.cs b/Explorer/Assets/Scripts/SceneRuntime/Apis/Modules/Ethereums/SendEthereumMessageResponse.cs index e3ec1c36fd..70008b9b1b 100644 --- a/Explorer/Assets/Scripts/SceneRuntime/Apis/Modules/Ethereums/SendEthereumMessageResponse.cs +++ b/Explorer/Assets/Scripts/SceneRuntime/Apis/Modules/Ethereums/SendEthereumMessageResponse.cs @@ -6,13 +6,5 @@ namespace SceneRuntime.Apis.Modules.Ethereums public struct SendEthereumMessageResponse { public string jsonAnyResponse; - - [Serializable] - public struct Payload - { - public long id; - public string jsonrpc; - public object result; - } } } diff --git a/Explorer/Assets/StreamingAssets/Js/Modules/EthereumController.js b/Explorer/Assets/StreamingAssets/Js/Modules/EthereumController.js index e6511e86e7..ccd39021bc 100644 --- a/Explorer/Assets/StreamingAssets/Js/Modules/EthereumController.js +++ b/Explorer/Assets/StreamingAssets/Js/Modules/EthereumController.js @@ -13,8 +13,7 @@ module.exports.signMessage = async function (message) { } module.exports.sendAsync = async function (message) { - const result = await UnityEthereumApi.SendAsync(message.id, message.method, message.jsonParams) - return result; + return await UnityEthereumApi.SendAsync(message.id, message.method, message.jsonParams); } module.exports.getUserAccount = async function (message) {