Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Thirdweb;

public partial class EcosystemWallet
{
internal class EnclaveUserStatusResponse
public class UserStatusResponse
{
[JsonProperty("linkedAccounts")]
internal List<LinkedAccount> LinkedAccounts { get; set; }
Expand Down
330 changes: 211 additions & 119 deletions Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ internal abstract class ServerBase
internal abstract Task<List<Server.LinkedAccount>> LinkAccountAsync(string currentAccountToken, string authTokenToConnect);
internal abstract Task<List<Server.LinkedAccount>> GetLinkedAccountsAsync(string currentAccountToken);

internal abstract Task<Server.UserWallet> 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<string> FetchAuthShareAsync(string authToken);

Expand Down Expand Up @@ -79,46 +76,6 @@ internal override async Task<List<LinkedAccount>> GetLinkedAccountsAsync(string
return res == null || res.LinkedAccounts == null || res.LinkedAccounts.Count == 0 ? [] : res.LinkedAccounts;
}

// embedded-wallet/embedded-wallet-user-details
internal override async Task<UserWallet> FetchUserDetailsAsync(string emailOrPhone, string authToken)
{
Dictionary<string, string> 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<UserWallet>(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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Server.VerifyResult> VerifyEmailOtpAsync(string emailAddress, string otp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,93 +15,11 @@ internal async void UpdateSessionData(LocalStorage.DataStorage data)
await this._localStorage.SaveDataAsync(data).ConfigureAwait(false);
}

internal async Task<VerifyResult> 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<User> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Server.VerifyResult> VerifyPhoneOtpAsync(string phoneNumber, string otp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
48 changes: 48 additions & 0 deletions Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.Types.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Newtonsoft.Json;

namespace Thirdweb;

/// <summary>
/// Specifies the authentication providers available for the in-app wallet.
/// </summary>
public enum AuthProvider
{
Default,
Google,
Apple,
Facebook,
JWT,
AuthEndpoint,
Discord,
Farcaster,
Telegram,
Siwe,
Line,
Guest,
X,
Coinbase,
Github,
Twitch
}

/// <summary>
/// Represents a linked account.
/// </summary>
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);
}
}
Loading
Loading