using UnityEngine; using System.Collections.Generic; using System.Numerics; using System.Threading.Tasks; using System.Linq; using System; using System.IO; using Newtonsoft.Json; namespace Thirdweb.Unity { [Serializable] public enum WalletProvider { PrivateKeyWallet, InAppWallet, WalletConnectWallet, MetaMaskWallet, EcosystemWallet } [Serializable] public class InAppWalletOptions : EcosystemWalletOptions { public InAppWalletOptions( string email = null, string phoneNumber = null, AuthProvider authprovider = AuthProvider.Default, string jwtOrPayload = null, string storageDirectoryPath = null, IThirdwebWallet siweSigner = null, string legacyEncryptionKey = null, string walletSecret = null, List forceSiweExternalWalletIds = null, ExecutionMode executionMode = ExecutionMode.EOA ) : base( email: email, phoneNumber: phoneNumber, authprovider: authprovider, jwtOrPayload: jwtOrPayload, storageDirectoryPath: storageDirectoryPath, siweSigner: siweSigner, legacyEncryptionKey: legacyEncryptionKey, walletSecret: walletSecret, forceSiweExternalWalletIds: forceSiweExternalWalletIds, executionMode: executionMode ) { } } [Serializable] public class EcosystemWalletOptions { [JsonProperty("ecosystemId")] public string EcosystemId; [JsonProperty("ecosystemPartnerId")] public string EcosystemPartnerId; [JsonProperty("email")] public string Email; [JsonProperty("phoneNumber")] public string PhoneNumber; [JsonProperty("authProvider")] public AuthProvider AuthProvider; [JsonProperty("jwtOrPayload")] public string JwtOrPayload; [JsonProperty("storageDirectoryPath")] public string StorageDirectoryPath; [JsonProperty("siweSigner")] public IThirdwebWallet SiweSigner; [JsonProperty("legacyEncryptionKey")] public string LegacyEncryptionKey; [JsonProperty("walletSecret")] public string WalletSecret; [JsonProperty("forceSiweExternalWalletIds")] public List ForceSiweExternalWalletIds; [JsonProperty("executionMode")] public ExecutionMode ExecutionMode = ExecutionMode.EOA; public EcosystemWalletOptions( string ecosystemId = null, string ecosystemPartnerId = null, string email = null, string phoneNumber = null, AuthProvider authprovider = AuthProvider.Default, string jwtOrPayload = null, string storageDirectoryPath = null, IThirdwebWallet siweSigner = null, string legacyEncryptionKey = null, string walletSecret = null, List forceSiweExternalWalletIds = null, ExecutionMode executionMode = ExecutionMode.EOA ) { EcosystemId = ecosystemId; EcosystemPartnerId = ecosystemPartnerId; Email = email; PhoneNumber = phoneNumber; AuthProvider = authprovider; JwtOrPayload = jwtOrPayload; StorageDirectoryPath = storageDirectoryPath ?? Path.Combine(Application.persistentDataPath, "Thirdweb", "EcosystemWallet"); SiweSigner = siweSigner; LegacyEncryptionKey = legacyEncryptionKey; WalletSecret = walletSecret; ForceSiweExternalWalletIds = forceSiweExternalWalletIds; ExecutionMode = executionMode; } } [Serializable] public class SmartWalletOptions { [JsonProperty("sponsorGas")] public bool SponsorGas; [JsonProperty("factoryAddress")] public string FactoryAddress; [JsonProperty("accountAddressOverride")] public string AccountAddressOverride; [JsonProperty("entryPoint")] public string EntryPoint; [JsonProperty("bundlerUrl")] public string BundlerUrl; [JsonProperty("paymasterUrl")] public string PaymasterUrl; [JsonProperty("tokenPaymaster")] public TokenPaymaster TokenPaymaster; public SmartWalletOptions( bool sponsorGas, string factoryAddress = null, string accountAddressOverride = null, string entryPoint = null, string bundlerUrl = null, string paymasterUrl = null, TokenPaymaster tokenPaymaster = TokenPaymaster.NONE ) { SponsorGas = sponsorGas; FactoryAddress = factoryAddress; AccountAddressOverride = accountAddressOverride; EntryPoint = entryPoint; BundlerUrl = bundlerUrl; PaymasterUrl = paymasterUrl; TokenPaymaster = tokenPaymaster; } } [Serializable] public class WalletOptions { [JsonProperty("provider")] public WalletProvider Provider; [JsonProperty("chainId")] public BigInteger ChainId; [JsonProperty("inAppWalletOptions")] public InAppWalletOptions InAppWalletOptions; [JsonProperty("ecosystemWalletOptions", NullValueHandling = NullValueHandling.Ignore)] public EcosystemWalletOptions EcosystemWalletOptions; [JsonProperty("smartWalletOptions", NullValueHandling = NullValueHandling.Ignore)] public SmartWalletOptions SmartWalletOptions; public WalletOptions( WalletProvider provider, BigInteger chainId, InAppWalletOptions inAppWalletOptions = null, EcosystemWalletOptions ecosystemWalletOptions = null, SmartWalletOptions smartWalletOptions = null ) { Provider = provider; ChainId = chainId; InAppWalletOptions = inAppWalletOptions ?? new InAppWalletOptions(); SmartWalletOptions = smartWalletOptions; EcosystemWalletOptions = ecosystemWalletOptions; } } [Serializable] public struct RpcOverride { public ulong ChainId; public string RpcUrl; } [HelpURL("http://portal.thirdweb.com/unity/v5/thirdwebmanager")] public abstract class ThirdwebManagerBase : MonoBehaviour { [field: SerializeField] protected bool InitializeOnAwake { get; set; } = true; [field: SerializeField] protected bool ShowDebugLogs { get; set; } = true; [field: SerializeField] protected bool AutoConnectLastWallet { get; set; } = false; [field: SerializeField] protected ulong[] SupportedChains { get; set; } = new ulong[] { 421614 }; [field: SerializeField] protected string[] IncludedWalletIds { get; set; } = null; [field: SerializeField] protected string RedirectPageHtmlOverride { get; set; } = null; [field: SerializeField] protected List RpcOverrides { get; set; } = null; public ThirdwebClient Client { get; protected set; } public IThirdwebWallet ActiveWallet { get; protected set; } public bool Initialized { get; protected set; } public static ThirdwebManagerBase Instance { get; protected set; } public static readonly string THIRDWEB_UNITY_SDK_VERSION = "5.22.0"; protected const string THIRDWEB_AUTO_CONNECT_OPTIONS_KEY = "ThirdwebAutoConnectOptions"; protected Dictionary _walletMapping; protected abstract ThirdwebClient CreateClient(); protected abstract string MobileRedirectScheme { get; } // ------------------------------------------------------ // Lifecycle Methods // ------------------------------------------------------ protected virtual void Awake() { if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); } else { Destroy(gameObject); return; } ThirdwebDebug.IsEnabled = ShowDebugLogs; if (InitializeOnAwake) { Initialize(); } } public virtual async void Initialize() { Client = CreateClient(); if (Client == null) { ThirdwebDebug.LogError("Failed to initialize ThirdwebManager."); return; } ThirdwebDebug.Log("ThirdwebManager initialized."); _walletMapping = new Dictionary(); if (AutoConnectLastWallet && GetAutoConnectOptions(out var lastWalletOptions)) { ThirdwebDebug.Log("Auto-connecting to last wallet."); try { _ = await ConnectWallet(lastWalletOptions); ThirdwebDebug.Log("Auto-connected to last wallet."); } catch (Exception e) { ThirdwebDebug.LogError("Failed to auto-connect to last wallet: " + e.Message); } } Initialized = true; } // ------------------------------------------------------ // Contract Methods // ------------------------------------------------------ public virtual async Task GetContract(string address, BigInteger chainId, string abi = null) { if (!Initialized) { throw new InvalidOperationException("ThirdwebManager is not initialized."); } return await ThirdwebContract.Create(Client, address, chainId, abi); } // ------------------------------------------------------ // Active Wallet Methods // ------------------------------------------------------ public virtual IThirdwebWallet GetActiveWallet() { return ActiveWallet; } public virtual void SetActiveWallet(IThirdwebWallet wallet) { ActiveWallet = wallet; } public virtual IThirdwebWallet GetWallet(string address) { if (_walletMapping.TryGetValue(address, out var wallet)) { return wallet; } throw new KeyNotFoundException($"Wallet with address {address} not found."); } public virtual async Task AddWallet(IThirdwebWallet wallet) { var address = await wallet.GetAddress(); _walletMapping.TryAdd(address, wallet); return wallet; } public virtual void RemoveWallet(string address) { if (_walletMapping.ContainsKey(address)) { _walletMapping.Remove(address, out var _); } } // ------------------------------------------------------ // Connection Methods // ------------------------------------------------------ public virtual async Task ConnectWallet(WalletOptions walletOptions) { if (walletOptions == null) { throw new ArgumentNullException(nameof(walletOptions)); } if (walletOptions.ChainId <= 0) { throw new ArgumentException("ChainId must be greater than 0."); } IThirdwebWallet wallet = null; switch (walletOptions.Provider) { case WalletProvider.PrivateKeyWallet: wallet = await PrivateKeyWallet.Generate(client: Client); break; case WalletProvider.InAppWallet: wallet = await InAppWallet.Create( client: Client, email: walletOptions.InAppWalletOptions.Email, phoneNumber: walletOptions.InAppWalletOptions.PhoneNumber, authProvider: walletOptions.InAppWalletOptions.AuthProvider, storageDirectoryPath: walletOptions.InAppWalletOptions.StorageDirectoryPath, siweSigner: walletOptions.InAppWalletOptions.SiweSigner, legacyEncryptionKey: walletOptions.InAppWalletOptions.LegacyEncryptionKey, walletSecret: walletOptions.InAppWalletOptions.WalletSecret, executionMode: walletOptions.InAppWalletOptions.ExecutionMode ); break; case WalletProvider.EcosystemWallet: if (walletOptions.EcosystemWalletOptions == null) { throw new ArgumentException("EcosystemWalletOptions must be provided for EcosystemWallet provider."); } if (string.IsNullOrEmpty(walletOptions.EcosystemWalletOptions.EcosystemId)) { throw new ArgumentException("EcosystemId must be provided for EcosystemWallet provider."); } wallet = await EcosystemWallet.Create( client: Client, ecosystemId: walletOptions.EcosystemWalletOptions.EcosystemId, ecosystemPartnerId: walletOptions.EcosystemWalletOptions.EcosystemPartnerId, email: walletOptions.EcosystemWalletOptions.Email, phoneNumber: walletOptions.EcosystemWalletOptions.PhoneNumber, authProvider: walletOptions.EcosystemWalletOptions.AuthProvider, storageDirectoryPath: walletOptions.EcosystemWalletOptions.StorageDirectoryPath, siweSigner: walletOptions.EcosystemWalletOptions.SiweSigner, legacyEncryptionKey: walletOptions.EcosystemWalletOptions.LegacyEncryptionKey, walletSecret: walletOptions.EcosystemWalletOptions.WalletSecret, executionMode: walletOptions.EcosystemWalletOptions.ExecutionMode ); break; case WalletProvider.WalletConnectWallet: var supportedChains = SupportedChains.Select(chain => new BigInteger(chain)).ToArray(); var includedWalletIds = IncludedWalletIds == null || IncludedWalletIds.Length == 0 ? null : IncludedWalletIds; wallet = await WalletConnectWallet.Create(client: Client, initialChainId: walletOptions.ChainId, supportedChains: supportedChains, includedWalletIds: includedWalletIds); break; case WalletProvider.MetaMaskWallet: wallet = await MetaMaskWallet.Create(client: Client, activeChainId: walletOptions.ChainId); break; } // InAppWallet auth flow if (walletOptions.Provider == WalletProvider.InAppWallet && !await wallet.IsConnected()) { ThirdwebDebug.Log("Session does not exist or is expired, proceeding with InAppWallet authentication."); var inAppWallet = wallet as InAppWallet; switch (walletOptions.InAppWalletOptions.AuthProvider) { case AuthProvider.Default: await inAppWallet.SendOTP(); _ = await InAppWalletModal.LoginWithOtp(inAppWallet); break; case AuthProvider.Siwe: _ = await inAppWallet.LoginWithSiwe(walletOptions.ChainId); break; case AuthProvider.JWT: _ = await inAppWallet.LoginWithJWT(walletOptions.InAppWalletOptions.JwtOrPayload); break; case AuthProvider.AuthEndpoint: _ = await inAppWallet.LoginWithAuthEndpoint(walletOptions.InAppWalletOptions.JwtOrPayload); break; case AuthProvider.Guest: _ = await inAppWallet.LoginWithGuest(SystemInfo.deviceUniqueIdentifier); break; case AuthProvider.Backend: _ = await inAppWallet.LoginWithBackend(); break; case AuthProvider.SiweExternal: _ = await inAppWallet.LoginWithSiweExternal( isMobile: Application.isMobilePlatform, browserOpenAction: (url) => Application.OpenURL(url), forceWalletIds: walletOptions.InAppWalletOptions.ForceSiweExternalWalletIds == null || walletOptions.InAppWalletOptions.ForceSiweExternalWalletIds.Count == 0 ? null : walletOptions.InAppWalletOptions.ForceSiweExternalWalletIds, mobileRedirectScheme: MobileRedirectScheme, browser: new CrossPlatformUnityBrowser(RedirectPageHtmlOverride) ); break; default: _ = await inAppWallet.LoginWithOauth( isMobile: Application.isMobilePlatform, browserOpenAction: (url) => Application.OpenURL(url), mobileRedirectScheme: MobileRedirectScheme, browser: new CrossPlatformUnityBrowser(RedirectPageHtmlOverride) ); break; } } // EcosystemWallet auth flow if (walletOptions.Provider == WalletProvider.EcosystemWallet && !await wallet.IsConnected()) { ThirdwebDebug.Log("Session does not exist or is expired, proceeding with EcosystemWallet authentication."); var ecosystemWallet = wallet as EcosystemWallet; switch (walletOptions.EcosystemWalletOptions.AuthProvider) { case AuthProvider.Default: await ecosystemWallet.SendOTP(); _ = await EcosystemWalletModal.LoginWithOtp(ecosystemWallet); break; case AuthProvider.Siwe: _ = await ecosystemWallet.LoginWithSiwe(walletOptions.ChainId); break; case AuthProvider.JWT: _ = await ecosystemWallet.LoginWithJWT(walletOptions.EcosystemWalletOptions.JwtOrPayload); break; case AuthProvider.AuthEndpoint: _ = await ecosystemWallet.LoginWithAuthEndpoint(walletOptions.EcosystemWalletOptions.JwtOrPayload); break; case AuthProvider.Guest: _ = await ecosystemWallet.LoginWithGuest(SystemInfo.deviceUniqueIdentifier); break; case AuthProvider.Backend: _ = await ecosystemWallet.LoginWithBackend(); break; case AuthProvider.SiweExternal: _ = await ecosystemWallet.LoginWithSiweExternal( isMobile: Application.isMobilePlatform, browserOpenAction: (url) => Application.OpenURL(url), forceWalletIds: walletOptions.EcosystemWalletOptions.ForceSiweExternalWalletIds == null || walletOptions.EcosystemWalletOptions.ForceSiweExternalWalletIds.Count == 0 ? null : walletOptions.EcosystemWalletOptions.ForceSiweExternalWalletIds, mobileRedirectScheme: MobileRedirectScheme, browser: new CrossPlatformUnityBrowser(RedirectPageHtmlOverride) ); break; default: _ = await ecosystemWallet.LoginWithOauth( isMobile: Application.isMobilePlatform, browserOpenAction: (url) => Application.OpenURL(url), mobileRedirectScheme: MobileRedirectScheme, browser: new CrossPlatformUnityBrowser(RedirectPageHtmlOverride) ); break; } } var address = await wallet.GetAddress(); var isSmartWallet = walletOptions.SmartWalletOptions != null; SetAutoConnectOptions(walletOptions); // If SmartWallet, do upgrade if (isSmartWallet) { ThirdwebDebug.Log("Upgrading to SmartWallet."); return await UpgradeToSmartWallet(wallet, walletOptions.ChainId, walletOptions.SmartWalletOptions); } else { await AddWallet(wallet); SetActiveWallet(wallet); return wallet; } } public virtual async Task UpgradeToSmartWallet(IThirdwebWallet personalWallet, BigInteger chainId, SmartWalletOptions smartWalletOptions) { if (personalWallet.AccountType == ThirdwebAccountType.SmartAccount) { ThirdwebDebug.LogWarning("Wallet is already a SmartWallet."); return personalWallet as SmartWallet; } if (smartWalletOptions == null) { throw new ArgumentNullException(nameof(smartWalletOptions)); } if (chainId <= 0) { throw new ArgumentException("ChainId must be greater than 0."); } var wallet = await SmartWallet.Create( personalWallet: personalWallet, chainId: chainId, gasless: smartWalletOptions.SponsorGas, factoryAddress: smartWalletOptions.FactoryAddress, accountAddressOverride: smartWalletOptions.AccountAddressOverride, entryPoint: smartWalletOptions.EntryPoint, bundlerUrl: smartWalletOptions.BundlerUrl, paymasterUrl: smartWalletOptions.PaymasterUrl, tokenPaymaster: smartWalletOptions.TokenPaymaster ); await AddWallet(wallet); SetActiveWallet(wallet); // Persist "smartWalletOptions" to auto-connect if (AutoConnectLastWallet && GetAutoConnectOptions(out var lastWalletOptions)) { lastWalletOptions.SmartWalletOptions = smartWalletOptions; SetAutoConnectOptions(lastWalletOptions); } return wallet; } public virtual async Task> LinkAccount(IThirdwebWallet mainWallet, IThirdwebWallet walletToLink, string otp = null, BigInteger? chainId = null, string jwtOrPayload = null) { return await mainWallet.LinkAccount( walletToLink: walletToLink, otp: otp, isMobile: Application.isMobilePlatform, browserOpenAction: (url) => Application.OpenURL(url), mobileRedirectScheme: MobileRedirectScheme, browser: new CrossPlatformUnityBrowser(RedirectPageHtmlOverride), chainId: chainId, jwt: jwtOrPayload, payload: jwtOrPayload ); } protected virtual bool GetAutoConnectOptions(out WalletOptions lastWalletOptions) { var connectOptionsStr = PlayerPrefs.GetString(THIRDWEB_AUTO_CONNECT_OPTIONS_KEY, null); if (!string.IsNullOrEmpty(connectOptionsStr)) { try { lastWalletOptions = JsonConvert.DeserializeObject(connectOptionsStr); return true; } catch { ThirdwebDebug.LogWarning("Failed to load last wallet options."); PlayerPrefs.DeleteKey(THIRDWEB_AUTO_CONNECT_OPTIONS_KEY); lastWalletOptions = null; return false; } } lastWalletOptions = null; return false; } protected virtual void SetAutoConnectOptions(WalletOptions walletOptions) { if (AutoConnectLastWallet && walletOptions.Provider != WalletProvider.WalletConnectWallet) { try { PlayerPrefs.SetString(THIRDWEB_AUTO_CONNECT_OPTIONS_KEY, JsonConvert.SerializeObject(walletOptions)); } catch { ThirdwebDebug.LogWarning("Failed to save last wallet options."); PlayerPrefs.DeleteKey(THIRDWEB_AUTO_CONNECT_OPTIONS_KEY); } } } } }