diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.Types.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.Types.cs index 240a39a1..e1ccfd4b 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.Types.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.Types.cs @@ -5,7 +5,7 @@ namespace Thirdweb; public partial class EcosystemWallet { - internal class EnclaveUserStatusResponse + public class UserStatusResponse { [JsonProperty("linkedAccounts")] internal List LinkedAccounts { get; set; } diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs index c4e935b0..94708c45 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs @@ -2,6 +2,8 @@ using System.Text; using System.Web; using Nethereum.ABI.EIP712; +using Nethereum.Signer; +using Nethereum.Signer.EIP712; using Newtonsoft.Json; using Thirdweb.EWS; @@ -10,25 +12,30 @@ namespace Thirdweb; /// /// Enclave based secure cross ecosystem wallet. /// -public partial class EcosystemWallet : PrivateKeyWallet +public partial class EcosystemWallet : IThirdwebWallet { - private readonly EmbeddedWallet _embeddedWallet; - private readonly IThirdwebHttpClient _httpClient; - private readonly IThirdwebWallet _siweSigner; - private readonly string _email; - private readonly string _phoneNumber; - private readonly string _authProvider; + public ThirdwebClient Client { get; } + public ThirdwebAccountType AccountType => ThirdwebAccountType.PrivateKeyAccount; + + internal readonly EmbeddedWallet EmbeddedWallet; + internal readonly IThirdwebHttpClient HttpClient; + internal readonly IThirdwebWallet SiweSigner; + internal readonly string Email; + internal readonly string PhoneNumber; + internal readonly string AuthProvider; + internal readonly string LegacyEncryptionKey; + + internal string Address; + private readonly string _ecosystemId; private readonly string _ecosystemPartnerId; - private string _address; - private const string EMBEDDED_WALLET_BASE_PATH = "https://embedded-wallet.thirdweb.com/api"; private const string EMBEDDED_WALLET_PATH_2024 = $"{EMBEDDED_WALLET_BASE_PATH}/2024-05-05"; private const string EMBEDDED_WALLET_PATH_V1 = $"{EMBEDDED_WALLET_BASE_PATH}/v1"; private const string ENCLAVE_PATH = $"{EMBEDDED_WALLET_PATH_V1}/enclave-wallet"; - private EcosystemWallet( + internal EcosystemWallet( string ecosystemId, string ecosystemPartnerId, ThirdwebClient client, @@ -37,31 +44,48 @@ private EcosystemWallet( string email, string phoneNumber, string authProvider, - IThirdwebWallet siweSigner + IThirdwebWallet siweSigner, + string legacyEncryptionKey ) - : base(client, null) { + this.Client = client; this._ecosystemId = ecosystemId; this._ecosystemPartnerId = ecosystemPartnerId; - this._embeddedWallet = embeddedWallet; - this._httpClient = httpClient; - this._email = email; - this._phoneNumber = phoneNumber; - this._authProvider = authProvider; - this._siweSigner = siweSigner; + this.LegacyEncryptionKey = legacyEncryptionKey; + this.EmbeddedWallet = embeddedWallet; + this.HttpClient = httpClient; + this.Email = email; + this.PhoneNumber = phoneNumber; + this.AuthProvider = authProvider; + this.SiweSigner = siweSigner; } #region Creation + /// + /// Creates a new instance of the class. + /// + /// Your ecosystem ID (see thirdweb dashboard e.g. ecosystem.the-bonfire). + /// Your ecosystem partner ID (required if you are integrating someone else's ecosystem). + /// The Thirdweb client instance. + /// The email address for Email OTP authentication. + /// The phone number for Phone OTP authentication. + /// The authentication provider to use. + /// The path to the storage directory. + /// The SIWE signer wallet for SIWE authentication. + /// The encryption key that is no longer required but was used in the past. Only pass this if you had used custom auth before this was deprecated. + /// A task that represents the asynchronous operation. The task result contains the created in-app wallet. + /// Thrown when required parameters are not provided. public static async Task Create( ThirdwebClient client, string ecosystemId, string ecosystemPartnerId = null, string email = null, string phoneNumber = null, - AuthProvider authProvider = AuthProvider.Default, + AuthProvider authProvider = Thirdweb.AuthProvider.Default, string storageDirectoryPath = null, - IThirdwebWallet siweSigner = null + IThirdwebWallet siweSigner = null, + string legacyEncryptionKey = null ) { if (client == null) @@ -69,34 +93,29 @@ public static async Task Create( throw new ArgumentNullException(nameof(client), "Client cannot be null."); } - if (string.IsNullOrEmpty(ecosystemId)) - { - throw new ArgumentNullException(nameof(ecosystemId), "Ecosystem ID cannot be null or empty."); - } - - if (string.IsNullOrEmpty(email) && string.IsNullOrEmpty(phoneNumber) && authProvider == AuthProvider.Default) + if (string.IsNullOrEmpty(email) && string.IsNullOrEmpty(phoneNumber) && authProvider == Thirdweb.AuthProvider.Default) { throw new ArgumentException("Email, Phone Number, or OAuth Provider must be provided to login."); } var authproviderStr = authProvider switch { - AuthProvider.Google => "Google", - AuthProvider.Apple => "Apple", - AuthProvider.Facebook => "Facebook", - AuthProvider.JWT => "JWT", - AuthProvider.AuthEndpoint => "AuthEndpoint", - AuthProvider.Discord => "Discord", - AuthProvider.Farcaster => "Farcaster", - AuthProvider.Telegram => "Telegram", - AuthProvider.Siwe => "Siwe", - AuthProvider.Line => "Line", - AuthProvider.Guest => "Guest", - AuthProvider.X => "X", - AuthProvider.Coinbase => "Coinbase", - AuthProvider.Github => "Github", - AuthProvider.Twitch => "Twitch", - AuthProvider.Default => string.IsNullOrEmpty(email) ? "Phone" : "Email", + Thirdweb.AuthProvider.Google => "Google", + Thirdweb.AuthProvider.Apple => "Apple", + Thirdweb.AuthProvider.Facebook => "Facebook", + Thirdweb.AuthProvider.JWT => "JWT", + Thirdweb.AuthProvider.AuthEndpoint => "AuthEndpoint", + Thirdweb.AuthProvider.Discord => "Discord", + Thirdweb.AuthProvider.Farcaster => "Farcaster", + Thirdweb.AuthProvider.Telegram => "Telegram", + Thirdweb.AuthProvider.Siwe => "Siwe", + Thirdweb.AuthProvider.Line => "Line", + Thirdweb.AuthProvider.Guest => "Guest", + Thirdweb.AuthProvider.X => "X", + Thirdweb.AuthProvider.Coinbase => "Coinbase", + Thirdweb.AuthProvider.Github => "Github", + Thirdweb.AuthProvider.Twitch => "Twitch", + Thirdweb.AuthProvider.Default => string.IsNullOrEmpty(email) ? "Phone" : "Email", _ => throw new ArgumentException("Invalid AuthProvider"), }; @@ -129,12 +148,18 @@ public static async Task Create( try { var userAddress = await ResumeEnclaveSession(enclaveHttpClient, embeddedWallet, email, phoneNumber, authproviderStr).ConfigureAwait(false); - return new EcosystemWallet(ecosystemId, ecosystemPartnerId, client, embeddedWallet, enclaveHttpClient, email, phoneNumber, authproviderStr, siweSigner) { _address = userAddress }; + return new EcosystemWallet(ecosystemId, ecosystemPartnerId, client, embeddedWallet, enclaveHttpClient, email, phoneNumber, authproviderStr, siweSigner, legacyEncryptionKey) + { + Address = userAddress + }; } catch { enclaveHttpClient.RemoveHeader("Authorization"); - return new EcosystemWallet(ecosystemId, ecosystemPartnerId, client, embeddedWallet, enclaveHttpClient, email, phoneNumber, authproviderStr, siweSigner) { _address = null }; + return new EcosystemWallet(ecosystemId, ecosystemPartnerId, client, embeddedWallet, enclaveHttpClient, email, phoneNumber, authproviderStr, siweSigner, legacyEncryptionKey) + { + Address = null + }; } } @@ -174,13 +199,13 @@ private static void CreateEnclaveSession(EmbeddedWallet embeddedWallet, string a embeddedWallet.UpdateSessionData(data); } - private static async Task GetUserStatus(IThirdwebHttpClient httpClient) + private static async Task GetUserStatus(IThirdwebHttpClient httpClient) { var url = $"{EMBEDDED_WALLET_PATH_2024}/accounts"; var response = await httpClient.GetAsync(url).ConfigureAwait(false); _ = response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var userStatus = JsonConvert.DeserializeObject(content); + var userStatus = JsonConvert.DeserializeObject(content); return userStatus; } @@ -197,16 +222,16 @@ private static async Task GenerateWallet(IThirdwebHttpClient httpClient) private async Task PostAuth(Server.VerifyResult result) { - this._httpClient.AddHeader("Authorization", $"Bearer embedded-wallet-token:{result.AuthToken}"); + this.HttpClient.AddHeader("Authorization", $"Bearer embedded-wallet-token:{result.AuthToken}"); string address; if (result.IsNewUser) { - address = await GenerateWallet(this._httpClient).ConfigureAwait(false); + address = await GenerateWallet(this.HttpClient).ConfigureAwait(false); } else { - var userStatus = await GetUserStatus(this._httpClient).ConfigureAwait(false); + var userStatus = await GetUserStatus(this.HttpClient).ConfigureAwait(false); if (userStatus.Wallets[0].Type == "enclave") { address = userStatus.Wallets[0].Address; @@ -223,16 +248,18 @@ private async Task PostAuth(Server.VerifyResult result) } else { - CreateEnclaveSession(this._embeddedWallet, result.AuthToken, this._email, this._phoneNumber, this._authProvider, result.AuthIdentifier); - this._address = address.ToChecksumAddress(); - return this._address; + CreateEnclaveSession(this.EmbeddedWallet, result.AuthToken, this.Email, this.PhoneNumber, this.AuthProvider, result.AuthIdentifier); + this.Address = address.ToChecksumAddress(); + return this.Address; } } private async Task MigrateShardToEnclave(Server.VerifyResult authResult) { // TODO: For recovery code, allow old encryption keys as overrides to migrate sharded custom auth? - var (address, encryptedPrivateKeyB64, ivB64, kmsCiphertextB64) = await this._embeddedWallet.GenerateEncryptionDataAsync(authResult.AuthToken, authResult.RecoveryCode).ConfigureAwait(false); + var (address, encryptedPrivateKeyB64, ivB64, kmsCiphertextB64) = await this.EmbeddedWallet + .GenerateEncryptionDataAsync(authResult.AuthToken, this.LegacyEncryptionKey ?? authResult.RecoveryCode) + .ConfigureAwait(false); var url = $"{ENCLAVE_PATH}/migrate"; var payload = new @@ -244,10 +271,10 @@ private async Task MigrateShardToEnclave(Server.VerifyResult authResult) }; var requestContent = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json"); - var response = await this._httpClient.PostAsync(url, requestContent).ConfigureAwait(false); + var response = await this.HttpClient.PostAsync(url, requestContent).ConfigureAwait(false); _ = response.EnsureSuccessStatusCode(); - var userStatus = await GetUserStatus(this._httpClient).ConfigureAwait(false); + var userStatus = await GetUserStatus(this.HttpClient).ConfigureAwait(false); return userStatus.Wallets[0].Address; } @@ -255,20 +282,31 @@ private async Task MigrateShardToEnclave(Server.VerifyResult authResult) #region Wallet Specific + /// + /// Gets the user details from the enclave wallet. + /// + /// A task that represents the asynchronous operation. The task result contains the user details. + public async Task GetUserDetails() + { + return await GetUserStatus(this.HttpClient).ConfigureAwait(false); + } + + [Obsolete("Use GetUserDetails instead.")] public string GetEmail() { - return this._email; + return this.Email; } + [Obsolete("Use GetUserDetails instead.")] public string GetPhoneNumber() { - return this._phoneNumber; + return this.PhoneNumber; } public async Task GetEcosystemDetails() { var url = $"{EMBEDDED_WALLET_PATH_2024}/ecosystem-wallet"; - var response = await this._httpClient.GetAsync(url).ConfigureAwait(false); + var response = await this.HttpClient.GetAsync(url).ConfigureAwait(false); _ = response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); return JsonConvert.DeserializeObject(content); @@ -278,7 +316,7 @@ public async Task GetEcosystemDetails() #region Account Linking - public override async Task> LinkAccount( + public async Task> LinkAccount( IThirdwebWallet walletToLink, string otp = null, bool? isMobile = null, @@ -311,28 +349,28 @@ public override async Task> LinkAccount( } Server.VerifyResult serverRes = null; - switch (ecosystemWallet._authProvider) + switch (ecosystemWallet.AuthProvider) { case "Email": - if (string.IsNullOrEmpty(ecosystemWallet._email)) + if (string.IsNullOrEmpty(ecosystemWallet.Email)) { throw new ArgumentException("Cannot link account with an email wallet that does not have an email address."); } serverRes = await ecosystemWallet.PreAuth_Otp(otp).ConfigureAwait(false); break; case "Phone": - if (string.IsNullOrEmpty(ecosystemWallet._phoneNumber)) + if (string.IsNullOrEmpty(ecosystemWallet.PhoneNumber)) { throw new ArgumentException("Cannot link account with a phone wallet that does not have a phone number."); } serverRes = await ecosystemWallet.PreAuth_Otp(otp).ConfigureAwait(false); break; case "Siwe": - if (ecosystemWallet._siweSigner == null || chainId == null) + if (ecosystemWallet.SiweSigner == null || chainId == null) { throw new ArgumentException("Cannot link account with a Siwe wallet without a signer and chain ID."); } - serverRes = await ecosystemWallet.PreAuth_Siwe(ecosystemWallet._siweSigner, chainId.Value).ConfigureAwait(false); + serverRes = await ecosystemWallet.PreAuth_Siwe(ecosystemWallet.SiweSigner, chainId.Value).ConfigureAwait(false); break; case "JWT": if (string.IsNullOrEmpty(jwt)) @@ -365,13 +403,13 @@ public override async Task> LinkAccount( serverRes = await ecosystemWallet.PreAuth_OAuth(isMobile ?? false, browserOpenAction, mobileRedirectScheme, browser).ConfigureAwait(false); break; default: - throw new ArgumentException($"Cannot link account with an unsupported authentication provider:", ecosystemWallet._authProvider); + throw new ArgumentException($"Cannot link account with an unsupported authentication provider:", ecosystemWallet.AuthProvider); } - var currentAccountToken = this._embeddedWallet.GetSessionData()?.AuthToken; + var currentAccountToken = this.EmbeddedWallet.GetSessionData()?.AuthToken; var authTokenToConnect = serverRes.AuthToken; - var serverLinkedAccounts = await this._embeddedWallet.LinkAccountAsync(currentAccountToken, authTokenToConnect).ConfigureAwait(false); + var serverLinkedAccounts = await this.EmbeddedWallet.LinkAccountAsync(currentAccountToken, authTokenToConnect).ConfigureAwait(false); var linkedAccounts = new List(); foreach (var linkedAccount in serverLinkedAccounts) { @@ -392,10 +430,10 @@ public override async Task> LinkAccount( return linkedAccounts; } - public override async Task> GetLinkedAccounts() + public async Task> GetLinkedAccounts() { - var currentAccountToken = this._embeddedWallet.GetSessionData()?.AuthToken; - var serverLinkedAccounts = await this._embeddedWallet.GetLinkedAccountsAsync(currentAccountToken).ConfigureAwait(false); + var currentAccountToken = this.EmbeddedWallet.GetSessionData()?.AuthToken; + var serverLinkedAccounts = await this.EmbeddedWallet.GetLinkedAccountsAsync(currentAccountToken).ConfigureAwait(false); var linkedAccounts = new List(); foreach (var linkedAccount in serverLinkedAccounts) { @@ -420,18 +458,23 @@ public override async Task> GetLinkedAccounts() #region OTP Auth - public async Task<(bool isNewUser, bool isNewDevice)> SendOTP() + public async Task SendOTP() { - if (string.IsNullOrEmpty(this._email) && string.IsNullOrEmpty(this._phoneNumber)) + if (string.IsNullOrEmpty(this.Email) && string.IsNullOrEmpty(this.PhoneNumber)) { throw new Exception("Email or Phone Number is required for OTP login"); } try { - return this._email == null - ? await this._embeddedWallet.SendPhoneOtpAsync(this._phoneNumber).ConfigureAwait(false) - : await this._embeddedWallet.SendEmailOtpAsync(this._email).ConfigureAwait(false); + if (this.Email == null) + { + await this.EmbeddedWallet.SendPhoneOtpAsync(this.PhoneNumber).ConfigureAwait(false); + } + else + { + await this.EmbeddedWallet.SendEmailOtpAsync(this.Email).ConfigureAwait(false); + } } catch (Exception e) { @@ -447,11 +490,11 @@ public override async Task> GetLinkedAccounts() } var serverRes = - string.IsNullOrEmpty(this._email) && string.IsNullOrEmpty(this._phoneNumber) + string.IsNullOrEmpty(this.Email) && string.IsNullOrEmpty(this.PhoneNumber) ? throw new Exception("Email or Phone Number is required for OTP login") - : this._email == null - ? await this._embeddedWallet.VerifyPhoneOtpAsync(this._phoneNumber, otp).ConfigureAwait(false) - : await this._embeddedWallet.VerifyEmailOtpAsync(this._email, otp).ConfigureAwait(false); + : this.Email == null + ? await this.EmbeddedWallet.VerifyPhoneOtpAsync(this.PhoneNumber, otp).ConfigureAwait(false) + : await this.EmbeddedWallet.VerifyEmailOtpAsync(this.Email, otp).ConfigureAwait(false); return serverRes; } @@ -479,14 +522,17 @@ public async Task LoginWithOtp(string otp) throw new ArgumentNullException(nameof(mobileRedirectScheme), "Mobile redirect scheme cannot be null or empty on this platform."); } - var platform = this._httpClient?.Headers?["x-sdk-name"] == "UnitySDK_WebGL" ? "web" : "dotnet"; + var platform = this.HttpClient?.Headers?["x-sdk-name"] == "UnitySDK_WebGL" ? "web" : "dotnet"; var redirectUrl = isMobile ? mobileRedirectScheme : "http://localhost:8789/"; - var loginUrl = await this._embeddedWallet.FetchHeadlessOauthLoginLinkAsync(this._authProvider, platform).ConfigureAwait(false); - loginUrl = platform == "web" ? loginUrl : $"{loginUrl}&redirectUrl={redirectUrl}&developerClientId={this.Client.ClientId}&authOption={this._authProvider}"; - loginUrl = $"{loginUrl}&ecosystemId={this._ecosystemId}"; - if (!string.IsNullOrEmpty(this._ecosystemPartnerId)) + var loginUrl = await this.EmbeddedWallet.FetchHeadlessOauthLoginLinkAsync(this.AuthProvider, platform).ConfigureAwait(false); + loginUrl = platform == "web" ? loginUrl : $"{loginUrl}&redirectUrl={redirectUrl}&developerClientId={this.Client.ClientId}&authOption={this.AuthProvider}"; + if (!string.IsNullOrEmpty(this._ecosystemId)) { - loginUrl = $"{loginUrl}&ecosystemPartnerId={this._ecosystemPartnerId}"; + loginUrl = $"{loginUrl}&ecosystemId={this._ecosystemId}"; + if (!string.IsNullOrEmpty(this._ecosystemPartnerId)) + { + loginUrl = $"{loginUrl}&ecosystemPartnerId={this._ecosystemPartnerId}"; + } } browser ??= new InAppWalletBrowser(); @@ -501,11 +547,11 @@ public async Task LoginWithOtp(string otp) throw new TimeoutException(browserResult.Error ?? "LoginWithOauth timed out."); case BrowserStatus.UnknownError: default: - throw new Exception($"Failed to login with {this._authProvider}: {browserResult.Status} | {browserResult.Error}"); + throw new Exception($"Failed to login with {this.AuthProvider}: {browserResult.Status} | {browserResult.Error}"); } var callbackUrl = browserResult.Status != BrowserStatus.Success - ? throw new Exception($"Failed to login with {this._authProvider}: {browserResult.Status} | {browserResult.Error}") + ? throw new Exception($"Failed to login with {this.AuthProvider}: {browserResult.Status} | {browserResult.Error}") : browserResult.CallbackUrl; while (string.IsNullOrEmpty(callbackUrl)) @@ -527,7 +573,7 @@ public async Task LoginWithOtp(string otp) authResultJson = queryDict["authResult"]; } - var serverRes = await this._embeddedWallet.SignInWithOauthAsync(authResultJson).ConfigureAwait(false); + var serverRes = await this.EmbeddedWallet.SignInWithOauthAsync(authResultJson).ConfigureAwait(false); return serverRes; } @@ -549,25 +595,25 @@ public async Task LoginWithOauth( private async Task PreAuth_Siwe(IThirdwebWallet siweSigner, BigInteger chainId) { - if (this._siweSigner == null) + if (this.SiweSigner == null) { throw new ArgumentNullException(nameof(siweSigner), "SIWE Signer wallet cannot be null."); } - if (!await this._siweSigner.IsConnected().ConfigureAwait(false)) + if (!await this.SiweSigner.IsConnected().ConfigureAwait(false)) { throw new InvalidOperationException("SIWE Signer wallet must be connected as this operation requires it to sign a message."); } var serverRes = - chainId <= 0 ? throw new ArgumentException("Chain ID must be greater than 0.", nameof(chainId)) : await this._embeddedWallet.SignInWithSiweAsync(siweSigner, chainId).ConfigureAwait(false); + chainId <= 0 ? throw new ArgumentException("Chain ID must be greater than 0.", nameof(chainId)) : await this.EmbeddedWallet.SignInWithSiweAsync(siweSigner, chainId).ConfigureAwait(false); return serverRes; } public async Task LoginWithSiwe(BigInteger chainId) { - var serverRes = await this.PreAuth_Siwe(this._siweSigner, chainId).ConfigureAwait(false); + var serverRes = await this.PreAuth_Siwe(this.SiweSigner, chainId).ConfigureAwait(false); return await this.PostAuth(serverRes).ConfigureAwait(false); } @@ -577,7 +623,7 @@ public async Task LoginWithSiwe(BigInteger chainId) private async Task PreAuth_Guest() { - var sessionData = this._embeddedWallet.GetSessionData(); + var sessionData = this.EmbeddedWallet.GetSessionData(); string sessionId; if (sessionData != null && sessionData.AuthProvider == "Guest" && !string.IsNullOrEmpty(sessionData.AuthIdentifier)) { @@ -587,7 +633,7 @@ public async Task LoginWithSiwe(BigInteger chainId) { sessionId = Guid.NewGuid().ToString(); } - var serverRes = await this._embeddedWallet.SignInWithGuestAsync(sessionId).ConfigureAwait(false); + var serverRes = await this.EmbeddedWallet.SignInWithGuestAsync(sessionId).ConfigureAwait(false); return serverRes; } @@ -603,12 +649,12 @@ public async Task LoginWithGuest() private async Task PreAuth_JWT(string jwt) { - return string.IsNullOrEmpty(jwt) ? throw new ArgumentException(nameof(jwt), "JWT cannot be null or empty.") : await this._embeddedWallet.SignInWithJwtAsync(jwt).ConfigureAwait(false); + return string.IsNullOrEmpty(jwt) ? throw new ArgumentException(nameof(jwt), "JWT cannot be null or empty.") : await this.EmbeddedWallet.SignInWithJwtAsync(jwt).ConfigureAwait(false); } public async Task LoginWithJWT(string jwt) { - var serverRes = string.IsNullOrEmpty(jwt) ? throw new ArgumentException("JWT cannot be null or empty.", nameof(jwt)) : await this._embeddedWallet.SignInWithJwtAsync(jwt).ConfigureAwait(false); + var serverRes = string.IsNullOrEmpty(jwt) ? throw new ArgumentException("JWT cannot be null or empty.", nameof(jwt)) : await this.EmbeddedWallet.SignInWithJwtAsync(jwt).ConfigureAwait(false); return await this.PostAuth(serverRes).ConfigureAwait(false); } @@ -621,7 +667,7 @@ public async Task LoginWithJWT(string jwt) { var serverRes = string.IsNullOrEmpty(payload) ? throw new ArgumentNullException(nameof(payload), "Payload cannot be null or empty.") - : await this._embeddedWallet.SignInWithAuthEndpointAsync(payload).ConfigureAwait(false); + : await this.EmbeddedWallet.SignInWithAuthEndpointAsync(payload).ConfigureAwait(false); return serverRes; } @@ -636,19 +682,19 @@ public async Task LoginWithAuthEndpoint(string payload) #region IThirdwebWallet - public override Task GetAddress() + public Task GetAddress() { - if (!string.IsNullOrEmpty(this._address)) + if (!string.IsNullOrEmpty(this.Address)) { - return Task.FromResult(this._address.ToChecksumAddress()); + return Task.FromResult(this.Address.ToChecksumAddress()); } else { - return Task.FromResult(this._address); + return Task.FromResult(this.Address); } } - public override Task EthSign(byte[] rawMessage) + public Task EthSign(byte[] rawMessage) { if (rawMessage == null) { @@ -658,7 +704,7 @@ public override Task EthSign(byte[] rawMessage) throw new NotImplementedException(); } - public override Task EthSign(string message) + public Task EthSign(string message) { if (message == null) { @@ -668,7 +714,7 @@ public override Task EthSign(string message) throw new NotImplementedException(); } - public override async Task PersonalSign(byte[] rawMessage) + public async Task PersonalSign(byte[] rawMessage) { if (rawMessage == null) { @@ -680,7 +726,7 @@ public override async Task PersonalSign(byte[] rawMessage) var requestContent = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json"); - var response = await this._httpClient.PostAsync(url, requestContent).ConfigureAwait(false); + var response = await this.HttpClient.PostAsync(url, requestContent).ConfigureAwait(false); _ = response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); @@ -688,7 +734,7 @@ public override async Task PersonalSign(byte[] rawMessage) return res.Signature; } - public override async Task PersonalSign(string message) + public async Task PersonalSign(string message) { if (string.IsNullOrEmpty(message)) { @@ -700,7 +746,7 @@ public override async Task PersonalSign(string message) var requestContent = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json"); - var response = await this._httpClient.PostAsync(url, requestContent).ConfigureAwait(false); + var response = await this.HttpClient.PostAsync(url, requestContent).ConfigureAwait(false); _ = response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); @@ -708,7 +754,7 @@ public override async Task PersonalSign(string message) return res.Signature; } - public override async Task SignTypedDataV4(string json) + public async Task SignTypedDataV4(string json) { if (string.IsNullOrEmpty(json)) { @@ -719,7 +765,7 @@ public override async Task SignTypedDataV4(string json) var requestContent = new StringContent(json, Encoding.UTF8, "application/json"); - var response = await this._httpClient.PostAsync(url, requestContent).ConfigureAwait(false); + var response = await this.HttpClient.PostAsync(url, requestContent).ConfigureAwait(false); _ = response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); @@ -727,7 +773,8 @@ public override async Task SignTypedDataV4(string json) return res.Signature; } - public override async Task SignTypedDataV4(T data, TypedData typedData) + public async Task SignTypedDataV4(T data, TypedData typedData) + where TDomain : IDomain { if (data == null) { @@ -738,7 +785,7 @@ public override async Task SignTypedDataV4(T data, TypedData return await this.SignTypedDataV4(safeJson).ConfigureAwait(false); } - public override async Task SignTransaction(ThirdwebTransactionInput transaction) + public async Task SignTransaction(ThirdwebTransactionInput transaction) { if (transaction == null) { @@ -761,7 +808,7 @@ public override async Task SignTransaction(ThirdwebTransactionInput tran var requestContent = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json"); - var response = await this._httpClient.PostAsync(url, requestContent).ConfigureAwait(false); + var response = await this.HttpClient.PostAsync(url, requestContent).ConfigureAwait(false); _ = response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); @@ -769,25 +816,70 @@ public override async Task SignTransaction(ThirdwebTransactionInput tran return res.Signature; } - public override Task IsConnected() + public Task IsConnected() { - return Task.FromResult(this._address != null); + return Task.FromResult(this.Address != null); } - public override Task SendTransaction(ThirdwebTransactionInput transaction) + public Task SendTransaction(ThirdwebTransactionInput transaction) { throw new InvalidOperationException("SendTransaction is not supported for Ecosystem Wallets, please use the unified Contract or ThirdwebTransaction APIs."); } - public override Task ExecuteTransaction(ThirdwebTransactionInput transactionInput) + public Task ExecuteTransaction(ThirdwebTransactionInput transactionInput) { throw new InvalidOperationException("ExecuteTransaction is not supported for Ecosystem Wallets, please use the unified Contract or ThirdwebTransaction APIs."); } - public override async Task Disconnect() + public async Task Disconnect() + { + this.Address = null; + await this.EmbeddedWallet.SignOutAsync().ConfigureAwait(false); + } + + public virtual Task RecoverAddressFromEthSign(string message, string signature) { - this._address = null; - await this._embeddedWallet.SignOutAsync().ConfigureAwait(false); + throw new InvalidOperationException(); + } + + public virtual Task RecoverAddressFromPersonalSign(string message, string signature) + { + if (string.IsNullOrEmpty(message)) + { + throw new ArgumentNullException(nameof(message), "Message to sign cannot be null."); + } + + if (string.IsNullOrEmpty(signature)) + { + throw new ArgumentNullException(nameof(signature), "Signature cannot be null."); + } + + var signer = new EthereumMessageSigner(); + var address = signer.EncodeUTF8AndEcRecover(message, signature); + return Task.FromResult(address); + } + + public virtual Task RecoverAddressFromTypedDataV4(T data, TypedData typedData, string signature) + where TDomain : IDomain + { + if (data == null) + { + throw new ArgumentNullException(nameof(data), "Data to sign cannot be null."); + } + + if (typedData == null) + { + throw new ArgumentNullException(nameof(typedData), "Typed data cannot be null."); + } + + if (signature == null) + { + throw new ArgumentNullException(nameof(signature), "Signature cannot be null."); + } + + var signer = new Eip712TypedDataSigner(); + var address = signer.RecoverFromSignatureV4(data, typedData, signature); + return Task.FromResult(address); } #endregion diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Authentication/Server.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Authentication/Server.cs index 1277945b..680c9141 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Authentication/Server.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Authentication/Server.cs @@ -9,9 +9,6 @@ internal abstract class ServerBase internal abstract Task> LinkAccountAsync(string currentAccountToken, string authTokenToConnect); internal abstract Task> GetLinkedAccountsAsync(string currentAccountToken); - internal abstract Task FetchUserDetailsAsync(string emailAddress, string authToken); - internal abstract Task StoreAddressAndSharesAsync(string walletAddress, string authShare, string encryptedRecoveryShare, string authToken); - internal abstract Task<(string authShare, string recoveryShare)> FetchAuthAndRecoverySharesAsync(string authToken); internal abstract Task FetchAuthShareAsync(string authToken); @@ -79,46 +76,6 @@ internal override async Task> GetLinkedAccountsAsync(string return res == null || res.LinkedAccounts == null || res.LinkedAccounts.Count == 0 ? [] : res.LinkedAccounts; } - // embedded-wallet/embedded-wallet-user-details - internal override async Task FetchUserDetailsAsync(string emailOrPhone, string authToken) - { - Dictionary queryParams = new(); - if (emailOrPhone == null && authToken == null) - { - throw new InvalidOperationException("Must provide either email address or auth token"); - } - - queryParams.Add("email", emailOrPhone ?? "uninitialized"); - queryParams.Add("clientId", this._clientId); - - var uri = MakeUri2023("/embedded-wallet/embedded-wallet-user-details", queryParams); - var response = await this.SendHttpWithAuthAsync(uri, authToken ?? "").ConfigureAwait(false); - await CheckStatusCodeAsync(response).ConfigureAwait(false); - var rv = await DeserializeAsync(response).ConfigureAwait(false); - return rv; - } - - // embedded-wallet/embedded-wallet-shares POST - internal override async Task StoreAddressAndSharesAsync(string walletAddress, string authShare, string encryptedRecoveryShare, string authToken) - { - var encryptedRecoveryShares = new[] { new { share = encryptedRecoveryShare, isClientEncrypted = "true" } }; - - HttpRequestMessage httpRequestMessage = - new(HttpMethod.Post, MakeUri2023("/embedded-wallet/embedded-wallet-shares")) - { - Content = MakeHttpContent( - new - { - authShare, - maybeEncryptedRecoveryShares = encryptedRecoveryShares, - walletAddress, - } - ), - }; - var response = await this.SendHttpWithAuthAsync(httpRequestMessage, authToken).ConfigureAwait(false); - await CheckStatusCodeAsync(response).ConfigureAwait(false); - } - // embedded-wallet/embedded-wallet-shares GET internal override async Task<(string authShare, string recoveryShare)> FetchAuthAndRecoverySharesAsync(string authToken) { diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.EmailOTP.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.EmailOTP.cs index c03a6f01..c6d5729f 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.EmailOTP.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.EmailOTP.cs @@ -2,13 +2,10 @@ internal partial class EmbeddedWallet { - public async Task<(bool isNewUser, bool isNewDevice)> SendEmailOtpAsync(string emailAddress) + public async Task SendEmailOtpAsync(string emailAddress) { emailAddress = emailAddress.ToLower(); - var userWallet = await this._server.FetchUserDetailsAsync(emailAddress, null).ConfigureAwait(false); _ = await this._server.SendEmailOtpAsync(emailAddress).ConfigureAwait(false); - var isNewDevice = userWallet.IsNewUser || this._localStorage.Data?.WalletUserId != userWallet.WalletUserId; - return (userWallet.IsNewUser, isNewDevice); } public async Task VerifyEmailOtpAsync(string emailAddress, string otp) diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.Misc.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.Misc.cs index aa4c452b..ac230732 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.Misc.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.Misc.cs @@ -15,93 +15,11 @@ internal async void UpdateSessionData(LocalStorage.DataStorage data) await this._localStorage.SaveDataAsync(data).ConfigureAwait(false); } - internal async Task PostAuthSetup(Server.VerifyResult result, string twManagedRecoveryCodeOverride, string authProvider) - { - var mainRecoveryCode = (twManagedRecoveryCodeOverride ?? result.RecoveryCode) ?? throw new InvalidOperationException("Server failed to return recovery code."); - - (var account, var deviceShare) = result.IsNewUser - ? await this.CreateAccountAsync(result.AuthToken, mainRecoveryCode).ConfigureAwait(false) - : await this.RecoverAccountAsync(result.AuthToken, mainRecoveryCode).ConfigureAwait(false); - var user = this.MakeUserAsync(result.Email, result.PhoneNumber, account, result.AuthToken, result.WalletUserId, deviceShare, authProvider, result.AuthIdentifier); - return new VerifyResult(user, mainRecoveryCode); - } - public async Task SignOutAsync() { - this._user = null; await this._localStorage.SaveDataAsync(new LocalStorage.DataStorage(null, null, null, null, null, null, null)).ConfigureAwait(false); } - public async Task GetUserAsync(string email, string phone, string authProvider) - { - email = email?.ToLower(); - - if (this._user != null) - { - return this._user; - } - else if (this._localStorage.Data?.AuthToken == null) - { - throw new InvalidOperationException("User is not signed in"); - } - - var userWallet = await this._server.FetchUserDetailsAsync(null, this._localStorage.Data.AuthToken).ConfigureAwait(false); - switch (userWallet.Status) - { - case "Logged Out": - throw new InvalidOperationException("User is logged out"); - case "Logged In, Wallet Uninitialized": - throw new InvalidOperationException("User is logged in but wallet is uninitialized"); - case "Logged In, Wallet Initialized": - if (string.IsNullOrEmpty(this._localStorage.Data?.DeviceShare)) - { - throw new InvalidOperationException("User is logged in but wallet is uninitialized"); - } - - var authShare = await this._server.FetchAuthShareAsync(this._localStorage.Data.AuthToken).ConfigureAwait(false); - var emailAddress = userWallet.StoredToken?.AuthDetails.Email; - var phoneNumber = userWallet.StoredToken?.AuthDetails.PhoneNumber; - - if ((email != null && email != emailAddress) || (phone != null && phone != phoneNumber)) - { - throw new InvalidOperationException("User email or phone number do not match"); - } - else if (email == null && this._localStorage.Data.AuthProvider != authProvider) - { - throw new InvalidOperationException($"User auth provider does not match. Expected {this._localStorage.Data.AuthProvider}, got {authProvider}"); - } - else if (authShare == null) - { - throw new InvalidOperationException("Server failed to return auth share"); - } - - this._user = new User(MakeAccountFromShares(new[] { authShare, this._localStorage.Data.DeviceShare }), emailAddress, phoneNumber); - return this._user; - default: - break; - } - throw new InvalidOperationException($"Unexpected user status '{userWallet.Status}'"); - } - - private User MakeUserAsync(string emailAddress, string phoneNumber, Account account, string authToken, string walletUserId, string deviceShare, string authProvider, string authIdentifier) - { - var data = new LocalStorage.DataStorage(authToken, deviceShare, emailAddress, phoneNumber, walletUserId, authProvider, authIdentifier); - this.UpdateSessionData(data); - this._user = new User(account, emailAddress, phoneNumber); - return this._user; - } - - private async Task<(Account account, string deviceShare)> CreateAccountAsync(string authToken, string recoveryCode) - { - var secret = Secrets.Random(KEY_SIZE); - - (var deviceShare, var recoveryShare, var authShare) = CreateShares(secret); - var encryptedRecoveryShare = await this.EncryptShareAsync(recoveryShare, recoveryCode).ConfigureAwait(false); - Account account = new(secret); - await this._server.StoreAddressAndSharesAsync(account.Address, authShare, encryptedRecoveryShare, authToken).ConfigureAwait(false); - return (account, deviceShare); - } - internal async Task<(Account account, string deviceShare)> RecoverAccountAsync(string authToken, string recoveryCode) { (var authShare, var encryptedRecoveryShare) = await this._server.FetchAuthAndRecoverySharesAsync(authToken).ConfigureAwait(false); diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.PhoneOTP.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.PhoneOTP.cs index 4711ad5e..a841bcfe 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.PhoneOTP.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.PhoneOTP.cs @@ -2,12 +2,9 @@ internal partial class EmbeddedWallet { - public async Task<(bool isNewUser, bool isNewDevice)> SendPhoneOtpAsync(string phoneNumber) + public async Task SendPhoneOtpAsync(string phoneNumber) { - var userWallet = await this._server.FetchUserDetailsAsync(phoneNumber, null).ConfigureAwait(false); _ = await this._server.SendPhoneOtpAsync(phoneNumber).ConfigureAwait(false); - var isNewDevice = userWallet.IsNewUser || this._localStorage.Data?.WalletUserId != userWallet.WalletUserId; - return (userWallet.IsNewUser, isNewDevice); } public async Task VerifyPhoneOtpAsync(string phoneNumber, string otp) diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.cs index 5e7ae073..8f470942 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.cs @@ -5,7 +5,6 @@ internal partial class EmbeddedWallet private readonly LocalStorage _localStorage; private readonly Server _server; private readonly IvGenerator _ivGenerator; - private User _user; private const int DEVICE_SHARE_ID = 1; private const int KEY_SIZE = 256 / 8; diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.Types.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.Types.cs new file mode 100644 index 00000000..0abd7d00 --- /dev/null +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.Types.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; + +namespace Thirdweb; + +/// +/// Specifies the authentication providers available for the in-app wallet. +/// +public enum AuthProvider +{ + Default, + Google, + Apple, + Facebook, + JWT, + AuthEndpoint, + Discord, + Farcaster, + Telegram, + Siwe, + Line, + Guest, + X, + Coinbase, + Github, + Twitch +} + +/// +/// Represents a linked account. +/// +public struct LinkedAccount +{ + public string Type { get; set; } + public LinkedAccountDetails Details { get; set; } + + public struct LinkedAccountDetails + { + public string Email { get; set; } + public string Address { get; set; } + public string Phone { get; set; } + public string Id { get; set; } + } + + public override readonly string ToString() + { + return JsonConvert.SerializeObject(this); + } +} diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.cs index 1576d434..8a7f8b80 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.cs @@ -1,72 +1,26 @@ -using System.Numerics; -using System.Web; -using Nethereum.Signer; -using Newtonsoft.Json; using Thirdweb.EWS; namespace Thirdweb; /// -/// Specifies the authentication providers available for the in-app wallet. +/// Represents an in-app wallet that supports email, phone, social, SIWE and custom authentication. /// -public enum AuthProvider +public class InAppWallet : EcosystemWallet { - Default, - Google, - Apple, - Facebook, - JWT, - AuthEndpoint, - Discord, - Farcaster, - Telegram, - Siwe, - Line, - Guest, - X, - Coinbase, - Github, - Twitch -} - -public struct LinkedAccount -{ - public string Type { get; set; } - public LinkedAccountDetails Details { get; set; } - - public struct LinkedAccountDetails - { - public string Email { get; set; } - public string Address { get; set; } - public string Phone { get; set; } - public string Id { get; set; } - } - - public override readonly string ToString() - { - return JsonConvert.SerializeObject(this); - } -} - -/// -/// Represents an in-app wallet that extends the functionality of a private key wallet. -/// -public class InAppWallet : PrivateKeyWallet -{ - internal EmbeddedWallet EmbeddedWallet; - internal string Email; - internal string PhoneNumber; - internal string AuthProvider; - internal IThirdwebWallet SiweSigner; - - internal InAppWallet(ThirdwebClient client, string email, string phoneNumber, string authProvider, EmbeddedWallet embeddedWallet, EthECKey ecKey, IThirdwebWallet siweSigner) - : base(client, ecKey) + internal InAppWallet( + ThirdwebClient client, + EmbeddedWallet embeddedWallet, + IThirdwebHttpClient httpClient, + string email, + string phoneNumber, + string authProvider, + IThirdwebWallet siweSigner, + string address, + string legacyEncryptionKey + ) + : base(null, null, client, embeddedWallet, httpClient, email, phoneNumber, authProvider, siweSigner, legacyEncryptionKey) { - this.Email = email?.ToLower(); - this.PhoneNumber = phoneNumber; - this.EmbeddedWallet = embeddedWallet; - this.AuthProvider = authProvider; - this.SiweSigner = siweSigner; + this.Address = address; } /// @@ -78,6 +32,7 @@ internal InAppWallet(ThirdwebClient client, string email, string phoneNumber, st /// The authentication provider to use. /// The path to the storage directory. /// The SIWE signer wallet for SIWE authentication. + /// The encryption key that is no longer required but was used in the past. Only pass this if you had used custom auth before this was deprecated. /// A task that represents the asynchronous operation. The task result contains the created in-app wallet. /// Thrown when required parameters are not provided. public static async Task Create( @@ -86,499 +41,22 @@ public static async Task Create( string phoneNumber = null, AuthProvider authProvider = Thirdweb.AuthProvider.Default, string storageDirectoryPath = null, - IThirdwebWallet siweSigner = null + IThirdwebWallet siweSigner = null, + string legacyEncryptionKey = null ) { - if (string.IsNullOrEmpty(email) && string.IsNullOrEmpty(phoneNumber) && authProvider == Thirdweb.AuthProvider.Default) - { - throw new ArgumentException("Email, Phone Number, or OAuth Provider must be provided to login."); - } - - var authproviderStr = authProvider switch - { - Thirdweb.AuthProvider.Google => "Google", - Thirdweb.AuthProvider.Apple => "Apple", - Thirdweb.AuthProvider.Facebook => "Facebook", - Thirdweb.AuthProvider.JWT => "JWT", - Thirdweb.AuthProvider.AuthEndpoint => "AuthEndpoint", - Thirdweb.AuthProvider.Discord => "Discord", - Thirdweb.AuthProvider.Farcaster => "Farcaster", - Thirdweb.AuthProvider.Telegram => "Telegram", - Thirdweb.AuthProvider.Siwe => "Siwe", - Thirdweb.AuthProvider.Line => "Line", - Thirdweb.AuthProvider.Guest => "Guest", - Thirdweb.AuthProvider.X => "X", - Thirdweb.AuthProvider.Coinbase => "Coinbase", - Thirdweb.AuthProvider.Github => "Github", - Thirdweb.AuthProvider.Twitch => "Twitch", - Thirdweb.AuthProvider.Default => string.IsNullOrEmpty(email) ? "Phone" : "Email", - _ => throw new ArgumentException("Invalid AuthProvider"), - }; - storageDirectoryPath ??= Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Thirdweb", "InAppWallet"); - var embeddedWallet = new EmbeddedWallet(client, storageDirectoryPath); - EthECKey ecKey; - try - { - var user = await embeddedWallet.GetUserAsync(email, phoneNumber, authproviderStr).ConfigureAwait(false); - ecKey = new EthECKey(user.Account.PrivateKey); - } - catch - { - ecKey = null; - } - return new InAppWallet(client, email, phoneNumber, authproviderStr, embeddedWallet, ecKey, siweSigner); - } - - /// - /// Disconnects the wallet. - /// - /// A task representing the asynchronous operation. - public override async Task Disconnect() - { - await base.Disconnect().ConfigureAwait(false); - await this.EmbeddedWallet.SignOutAsync().ConfigureAwait(false); - } - - /// - /// Gets the email associated with the in-app wallet. - /// - /// A task representing the asynchronous operation. The task result contains the email address. - public Task GetEmail() - { - return Task.FromResult(this.Email); - } - - /// - /// Gets the phone number associated with the in-app wallet. - /// - /// A task representing the asynchronous operation. The task result contains the phone number. - public Task GetPhoneNumber() - { - return Task.FromResult(this.PhoneNumber); - } - - #region Account Linking - - public override async Task> LinkAccount( - IThirdwebWallet walletToLink, - string otp = null, - bool? isMobile = null, - Action browserOpenAction = null, - string mobileRedirectScheme = "thirdweb://", - IThirdwebBrowser browser = null, - BigInteger? chainId = null, - string jwt = null, - string payload = null - ) - { - if (!await this.IsConnected().ConfigureAwait(false)) - { - throw new InvalidOperationException("Cannot link account with a wallet that is not connected. Please login to the wallet before linking other wallets."); - } - - if (walletToLink == null) - { - throw new ArgumentNullException(nameof(walletToLink), "Wallet to link cannot be null."); - } - - if (walletToLink is not InAppWallet inAppWallet) - { - throw new ArgumentException("Cannot link account with a wallet that is not an InAppWallet."); - } - - if (await inAppWallet.IsConnected().ConfigureAwait(false)) - { - throw new ArgumentException("Cannot link account with a wallet that is already created and connected."); - } - - Server.VerifyResult serverRes = null; - switch (inAppWallet.AuthProvider) - { - case "Email": - if (string.IsNullOrEmpty(inAppWallet.Email)) - { - throw new ArgumentException("Cannot link account with an email wallet that does not have an email address."); - } - serverRes = await inAppWallet.PreAuth_Otp(otp).ConfigureAwait(false); - break; - case "Phone": - if (string.IsNullOrEmpty(inAppWallet.PhoneNumber)) - { - throw new ArgumentException("Cannot link account with a phone wallet that does not have a phone number."); - } - serverRes = await inAppWallet.PreAuth_Otp(otp).ConfigureAwait(false); - break; - case "Siwe": - if (inAppWallet.SiweSigner == null || chainId == null) - { - throw new ArgumentException("Cannot link account with a Siwe wallet without a signer and chain ID."); - } - serverRes = await inAppWallet.PreAuth_Siwe(inAppWallet.SiweSigner, chainId.Value).ConfigureAwait(false); - break; - case "JWT": - if (string.IsNullOrEmpty(jwt)) - { - throw new ArgumentException("Cannot link account with a JWT wallet without a JWT."); - } - serverRes = await inAppWallet.PreAuth_JWT(jwt).ConfigureAwait(false); - break; - case "AuthEndpoint": - if (string.IsNullOrEmpty(payload)) - { - throw new ArgumentException("Cannot link account with an AuthEndpoint wallet without a payload."); - } - serverRes = await inAppWallet.PreAuth_AuthEndpoint(payload).ConfigureAwait(false); - break; - case "Guest": - serverRes = await inAppWallet.PreAuth_Guest().ConfigureAwait(false); - break; - case "Google": - case "Apple": - case "Facebook": - case "Discord": - case "Farcaster": - case "Telegram": - case "Line": - case "X": - case "Coinbase": - case "Github": - case "Twitch": - serverRes = await inAppWallet.PreAuth_OAuth(isMobile ?? false, browserOpenAction, mobileRedirectScheme, browser).ConfigureAwait(false); - break; - default: - throw new ArgumentException($"Cannot link account with an unsupported authentication provider:", inAppWallet.AuthProvider); - } - - var currentAccountToken = this.EmbeddedWallet.GetSessionData()?.AuthToken; - var authTokenToConnect = serverRes.AuthToken; - - var serverLinkedAccounts = await this.EmbeddedWallet.LinkAccountAsync(currentAccountToken, authTokenToConnect).ConfigureAwait(false); - var linkedAccounts = new List(); - foreach (var linkedAccount in serverLinkedAccounts) - { - linkedAccounts.Add( - new LinkedAccount - { - Type = linkedAccount.Type, - Details = new LinkedAccount.LinkedAccountDetails - { - Email = linkedAccount.Details?.Email, - Address = linkedAccount.Details?.Address, - Phone = linkedAccount.Details?.Phone, - Id = linkedAccount.Details?.Id - } - } - ); - } - return linkedAccounts; - } - - public override async Task> GetLinkedAccounts() - { - var currentAccountToken = this.EmbeddedWallet.GetSessionData()?.AuthToken; - var serverLinkedAccounts = await this.EmbeddedWallet.GetLinkedAccountsAsync(currentAccountToken).ConfigureAwait(false); - var linkedAccounts = new List(); - foreach (var linkedAccount in serverLinkedAccounts) - { - linkedAccounts.Add( - new LinkedAccount - { - Type = linkedAccount.Type, - Details = new LinkedAccount.LinkedAccountDetails - { - Email = linkedAccount.Details?.Email, - Address = linkedAccount.Details?.Address, - Phone = linkedAccount.Details?.Phone, - Id = linkedAccount.Details?.Id - } - } - ); - } - return linkedAccounts; - } - - #endregion - - #region OAuth2 Flow - - /// - /// Logs in with OAuth2. - /// - /// Indicates if the login is from a mobile device. - /// The action to open the browser. - /// The mobile redirect scheme. - /// The browser instance. - /// The cancellation token. - /// A task representing the asynchronous operation. The task result contains the login result. - /// Thrown when required parameters are not provided. - /// Thrown when the operation is canceled. - /// Thrown when the operation times out. - public virtual async Task LoginWithOauth( - bool isMobile, - Action browserOpenAction, - string mobileRedirectScheme = "thirdweb://", - IThirdwebBrowser browser = null, - CancellationToken cancellationToken = default - ) - { - var serverRes = await this.PreAuth_OAuth(isMobile, browserOpenAction, mobileRedirectScheme, browser, cancellationToken).ConfigureAwait(false); - return await this.PostAuth(serverRes, null, this.AuthProvider).ConfigureAwait(false); - } - - private async Task PreAuth_OAuth( - bool isMobile, - Action browserOpenAction, - string mobileRedirectScheme = "thirdweb://", - IThirdwebBrowser browser = null, - CancellationToken cancellationToken = default - ) - { - if (isMobile && string.IsNullOrEmpty(mobileRedirectScheme)) - { - throw new ArgumentNullException(nameof(mobileRedirectScheme), "Mobile redirect scheme cannot be null or empty on this platform."); - } - - var platform = this.Client.HttpClient?.Headers?["x-sdk-name"] == "UnitySDK_WebGL" ? "web" : "dotnet"; - var redirectUrl = isMobile ? mobileRedirectScheme : "http://localhost:8789/"; - var loginUrl = await this.EmbeddedWallet.FetchHeadlessOauthLoginLinkAsync(this.AuthProvider, platform); - loginUrl = platform == "web" ? loginUrl : $"{loginUrl}&redirectUrl={redirectUrl}&developerClientId={this.Client.ClientId}&authOption={this.AuthProvider}"; - - browser ??= new InAppWalletBrowser(); - var browserResult = await browser.Login(this.Client, loginUrl, redirectUrl, browserOpenAction, cancellationToken); - switch (browserResult.Status) - { - case BrowserStatus.Success: - break; - case BrowserStatus.UserCanceled: - throw new TaskCanceledException(browserResult.Error ?? "LoginWithOauth was cancelled."); - case BrowserStatus.Timeout: - throw new TimeoutException(browserResult.Error ?? "LoginWithOauth timed out."); - case BrowserStatus.UnknownError: - default: - throw new Exception($"Failed to login with {this.AuthProvider}: {browserResult.Status} | {browserResult.Error}"); - } - var callbackUrl = - browserResult.Status != BrowserStatus.Success - ? throw new Exception($"Failed to login with {this.AuthProvider}: {browserResult.Status} | {browserResult.Error}") - : browserResult.CallbackUrl; - - while (string.IsNullOrEmpty(callbackUrl)) - { - if (cancellationToken.IsCancellationRequested) - { - throw new TaskCanceledException("LoginWithOauth was cancelled."); - } - await ThirdwebTask.Delay(100, cancellationToken).ConfigureAwait(false); - } - - var authResultJson = callbackUrl; - if (!authResultJson.StartsWith('{')) - { - var decodedUrl = HttpUtility.UrlDecode(callbackUrl); - Uri uri = new(decodedUrl); - var queryString = uri.Query; - var queryDict = HttpUtility.ParseQueryString(queryString); - authResultJson = queryDict["authResult"]; - } - - return await this.EmbeddedWallet.SignInWithOauthAsync(authResultJson); - } - - #endregion - - #region OTP Flow - - /// - /// Sends an OTP to the user's email or phone number. - /// - /// A task representing the asynchronous operation. The task result contains a boolean indicating if the user is new and a boolean indicating if the device is new. - /// Thrown when email or phone number is not provided. - public async Task<(bool isNewUser, bool isNewDevice)> SendOTP() - { - if (string.IsNullOrEmpty(this.Email) && string.IsNullOrEmpty(this.PhoneNumber)) - { - throw new Exception("Email or Phone Number is required for OTP login"); - } - - try - { - return this.Email == null - ? await this.EmbeddedWallet.SendPhoneOtpAsync(this.PhoneNumber).ConfigureAwait(false) - : await this.EmbeddedWallet.SendEmailOtpAsync(this.Email).ConfigureAwait(false); - } - catch (Exception e) - { - throw new Exception("Failed to send OTP", e); - } - } - - /// - /// Submits the OTP for verification. - /// - /// The OTP to submit. - /// A task representing the asynchronous operation. The task result contains the address and a boolean indicating if retry is possible. - /// Thrown when OTP is not provided. - /// Thrown when email or phone number is not provided. - public async Task<(string address, bool canRetry)> LoginWithOtp(string otp) - { - if (string.IsNullOrEmpty(otp)) - { - throw new ArgumentNullException(nameof(otp), "OTP cannot be null or empty."); - } - - var serverRes = await this.PreAuth_Otp(otp).ConfigureAwait(false); - try - { - return (await this.PostAuth(serverRes, null, this.Email == null ? "Email" : "Phone").ConfigureAwait(false), false); - } - catch (VerificationException e) - { - return (null, e.CanRetry); - } - } - - private async Task PreAuth_Otp(string otp) - { - if (string.IsNullOrEmpty(otp)) - { - throw new ArgumentNullException(nameof(otp), "OTP cannot be null or empty."); - } - - return string.IsNullOrEmpty(this.Email) && string.IsNullOrEmpty(this.PhoneNumber) - ? throw new Exception("Email or Phone Number is required for OTP login") - : this.Email == null - ? await this.EmbeddedWallet.VerifyPhoneOtpAsync(this.PhoneNumber, otp).ConfigureAwait(false) - : await this.EmbeddedWallet.VerifyEmailOtpAsync(this.Email, otp).ConfigureAwait(false); - } - - #endregion - - #region SIWE Flow - - /// - /// Logs in with SIWE (Sign-In with Ethereum). - /// - /// The chain ID to use for signing the SIWE payload - /// A task representing the asynchronous operation. The task result contains the address. - /// Thrown when external wallet is not provided. - /// Thrown when the external wallet is not connected. - /// Thrown when chain ID is invalid. - public async Task LoginWithSiwe(BigInteger chainId) - { - var serverRes = await this.PreAuth_Siwe(this.SiweSigner, chainId).ConfigureAwait(false); - return await this.PostAuth(serverRes, null, "Siwe").ConfigureAwait(false); - } - - private async Task PreAuth_Siwe(IThirdwebWallet signer, BigInteger chainId) - { - if (signer == null) - { - throw new ArgumentNullException(nameof(signer), "SIWE Signer wallet cannot be null."); - } - - if (!await signer.IsConnected().ConfigureAwait(false)) - { - throw new InvalidOperationException("SIWE Signer wallet must be connected as this operation requires it to sign a message."); - } - - return chainId <= 0 ? throw new ArgumentException(nameof(chainId), "Chain ID must be greater than 0.") : await this.EmbeddedWallet.SignInWithSiweAsync(signer, chainId).ConfigureAwait(false); - } - - #endregion - - #region Guest - - public async Task LoginWithGuest() - { - var serverRes = await this.PreAuth_Guest().ConfigureAwait(false); - return await this.PostAuth(serverRes, null, "Guest").ConfigureAwait(false); - } - - private async Task PreAuth_Guest() - { - var sessionData = this.EmbeddedWallet.GetSessionData(); - string sessionId; - if (sessionData != null && sessionData.AuthProvider == "Guest" && !string.IsNullOrEmpty(sessionData.AuthIdentifier)) - { - sessionId = sessionData.AuthIdentifier; - } - else - { - sessionId = Guid.NewGuid().ToString(); - } - var serverRes = await this.EmbeddedWallet.SignInWithGuestAsync(sessionId).ConfigureAwait(false); - return serverRes; - } - - #endregion - - #region JWT Flow - - /// - /// Logs in with a JWT. - /// - /// The JWT to use for authentication. - /// The encryption key to use. - /// A task representing the asynchronous operation. The task result contains the address. - /// Thrown when JWT or encryption key is not provided. - /// Thrown when the login fails. - public async Task LoginWithJWT(string jwt, string encryptionKey) - { - if (string.IsNullOrEmpty(encryptionKey)) - { - throw new ArgumentException(nameof(encryptionKey), "Encryption key cannot be null or empty."); - } - - var serverRes = await this.PreAuth_JWT(jwt).ConfigureAwait(false); - return await this.PostAuth(serverRes, encryptionKey, "JWT").ConfigureAwait(false); - } - - private async Task PreAuth_JWT(string jwt) - { - return string.IsNullOrEmpty(jwt) ? throw new ArgumentException(nameof(jwt), "JWT cannot be null or empty.") : await this.EmbeddedWallet.SignInWithJwtAsync(jwt).ConfigureAwait(false); - } - - #endregion - - #region Auth Endpoint Flow - - /// - /// Logs in with an authentication endpoint. - /// - /// The payload to use for authentication. - /// The encryption key to use. - /// A task representing the asynchronous operation. The task result contains the login result. - /// Thrown when payload or encryption key is not provided. - /// Thrown when the login fails. - public async Task LoginWithAuthEndpoint(string payload, string encryptionKey) - { - if (string.IsNullOrEmpty(encryptionKey)) - { - throw new ArgumentException(nameof(encryptionKey), "Encryption key cannot be null or empty."); - } - - var serverRes = await this.PreAuth_AuthEndpoint(payload).ConfigureAwait(false); - return await this.PostAuth(serverRes, encryptionKey, "AuthEndpoint").ConfigureAwait(false); - } - - private async Task PreAuth_AuthEndpoint(string payload) - { - return string.IsNullOrEmpty(payload) - ? throw new ArgumentException(nameof(payload), "Payload cannot be null or empty.") - : await this.EmbeddedWallet.SignInWithAuthEndpointAsync(payload).ConfigureAwait(false); - } - - #endregion - - private async Task PostAuth(Server.VerifyResult serverRes, string encryptionKey, string authProvider) - { - var res = await this.EmbeddedWallet.PostAuthSetup(serverRes, encryptionKey, authProvider).ConfigureAwait(false); - if (res.User == null) - { - throw new Exception($"Failed to login with {authProvider}"); - } - this.EcKey = new EthECKey(res.User.Account.PrivateKey); - return await this.GetAddress().ConfigureAwait(false); + var ecoWallet = await Create(client, null, null, email, phoneNumber, authProvider, storageDirectoryPath, siweSigner, legacyEncryptionKey); + return new InAppWallet( + ecoWallet.Client, + ecoWallet.EmbeddedWallet, + ecoWallet.HttpClient, + ecoWallet.Email, + ecoWallet.PhoneNumber, + ecoWallet.AuthProvider, + ecoWallet.SiweSigner, + ecoWallet.Address, + ecoWallet.LegacyEncryptionKey + ); } }