diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs index f292cbfa..9ce02980 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -27,7 +27,7 @@ var privateKey = Environment.GetEnvironmentVariable("PRIVATE_KEY"); // Fetch timeout options are optional, default is 120000ms -var client = ThirdwebClient.Create(secretKey: secretKey); +var client = ThirdwebClient.Create(secretKey: "4qXoZMCqQo9SD8YkrdvO5Ci9gYKrgRADHSY84Q0wwKHZS53_R1QNcIs2XbFBWR0xE7HTQPER45T1sN1JvdFKlA"); // Create a private key wallet var privateKeyWallet = await PrivateKeyWallet.Generate(client); @@ -340,21 +340,46 @@ #endregion -#region Engine Wallet +#region Server Wallet -// // EngineWallet is compatible with IThirdwebWallet and can be used with any SDK method/extension -// var engineWallet = await EngineWallet.Create( +// // ServerWallet is compatible with IThirdwebWallet and can be used with any SDK method/extension +// var serverWallet = await ServerWallet.Create( // client: client, -// engineUrl: Environment.GetEnvironmentVariable("ENGINE_URL"), -// authToken: Environment.GetEnvironmentVariable("ENGINE_ACCESS_TOKEN"), -// walletAddress: Environment.GetEnvironmentVariable("ENGINE_BACKEND_WALLET_ADDRESS"), -// timeoutSeconds: null, // no timeout -// additionalHeaders: null // can set things like x-account-address if using basic session keys +// label: "Test", +// // Optional, defaults to Auto - we choose between EIP-7702, EIP-4337 or native zkSync AA execution / EOA is also available +// executionOptions: new AutoExecutionOptions() // ); +// var serverWalletAddress = await serverWallet.GetAddress(); +// Console.WriteLine($"Server Wallet address: {serverWalletAddress}"); + +// var serverWalletPersonalSig = await serverWallet.PersonalSign("Hello, Thirdweb!"); +// Console.WriteLine($"Server Wallet personal sign: {serverWalletPersonalSig}"); + +// var json = +// /*lang=json,strict*/ +// "{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallet\",\"type\":\"address\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person\"},{\"name\":\"contents\",\"type\":\"string\"}]},\"primaryType\":\"Mail\",\"domain\":{\"name\":\"Ether Mail\",\"version\":\"1\",\"chainId\":84532,\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\"},\"message\":{\"from\":{\"name\":\"Cow\",\"wallet\":\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\"},\"to\":{\"name\":\"Bob\",\"wallet\":\"0xbBbBBBBbbBBBbbbBbbBbbBBbBbbBbBbBbBbbBBbB\"},\"contents\":\"Hello, Bob!\"}}"; +// var serverWalletTypedDataSign = await serverWallet.SignTypedDataV4(json); +// Console.WriteLine($"Server Wallet typed data sign: {serverWalletTypedDataSign}"); + // // Simple self transfer -// var receipt = await engineWallet.Transfer(chainId: 11155111, toAddress: await engineWallet.GetAddress(), weiAmount: 0); -// Console.WriteLine($"Receipt: {receipt}"); +// var serverWalletReceipt = await serverWallet.Transfer(chainId: 84532, toAddress: await serverWallet.GetAddress(), weiAmount: 0); +// Console.WriteLine($"Server Wallet Hash: {serverWalletReceipt.TransactionHash}"); + +// // ServerWallet forcing ERC-4337 Execution Mode +// var smartServerWallet = await ServerWallet.Create(client: client, label: "Test", executionOptions: new ERC4337ExecutionOptions(chainId: 84532, signerAddress: serverWalletAddress)); +// var smartServerWalletAddress = await smartServerWallet.GetAddress(); +// Console.WriteLine($"Smart Server Wallet address: {smartServerWalletAddress}"); + +// var smartServerWalletPersonalSig = await smartServerWallet.PersonalSign("Hello, Thirdweb!"); +// Console.WriteLine($"Smart Server Wallet personal sign: {smartServerWalletPersonalSig}"); + +// var smartServerWalletTypedDataSign = await smartServerWallet.SignTypedDataV4(json); +// Console.WriteLine($"Smart Server Wallet typed data sign: {smartServerWalletTypedDataSign}"); + +// // Simple self transfer +// var smartServerWalletReceipt = await smartServerWallet.Transfer(chainId: 84532, toAddress: await smartServerWallet.GetAddress(), weiAmount: 0); +// Console.WriteLine($"Server Wallet Hash: {smartServerWalletReceipt.TransactionHash}"); #endregion diff --git a/Thirdweb/Thirdweb.Utils/Constants.cs b/Thirdweb/Thirdweb.Utils/Constants.cs index 8cdf33e5..d8370b73 100644 --- a/Thirdweb/Thirdweb.Utils/Constants.cs +++ b/Thirdweb/Thirdweb.Utils/Constants.cs @@ -10,6 +10,7 @@ public static class Constants internal const string PIN_URI = "https://storage.thirdweb.com/ipfs/upload"; internal const string FALLBACK_IPFS_GATEWAY = "https://ipfs.io/ipfs/"; internal const string NEBULA_API_URL = "https://nebula-api.thirdweb.com"; + internal const string ENGINE_API_URL = "https://engine.thirdweb.com"; internal const string NEBULA_DEFAULT_MODEL = "t0-003"; internal const int DEFAULT_FETCH_TIMEOUT = 120000; diff --git a/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs b/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs index 876fcd76..1de38e59 100644 --- a/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs @@ -11,6 +11,7 @@ namespace Thirdweb; /// /// Enclave based secure cross ecosystem wallet. /// +[Obsolete("The EngineWallet is deprecated and will be removed in a future version. Please use ServerWallet instead.")] public partial class EngineWallet : IThirdwebWallet { public ThirdwebClient Client { get; } diff --git a/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.Types.cs b/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.Types.cs new file mode 100644 index 00000000..711d6901 --- /dev/null +++ b/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.Types.cs @@ -0,0 +1,145 @@ +using System.Numerics; +using Newtonsoft.Json; + +namespace Thirdweb; + +/// +/// Base class for execution options +/// +[JsonObject] +public class ExecutionOptions +{ + [JsonProperty("chainId")] + public BigInteger? ChainId { get; set; } = null; + + [JsonProperty("idempotencyKey")] + public string IdempotencyKey { get; set; } +} + +/// +/// Auto determine execution options +/// +[JsonObject] +public class AutoExecutionOptions : ExecutionOptions +{ + [JsonProperty("type")] + public string Type { get; set; } = "auto"; + + [JsonProperty("from")] + public string From { get; set; } +} + +/// +/// Externally Owned Account (EOA) execution options +/// +[JsonObject] +public class EIP7702ExecutionOptions : ExecutionOptions +{ + [JsonProperty("type")] + public string Type { get; set; } = "EIP7702"; + + [JsonProperty("from")] + public string From { get; set; } +} + +/// +/// Externally Owned Account (EOA) execution options +/// +[JsonObject] +public class EOAExecutionOptions : ExecutionOptions +{ + [JsonProperty("type")] + public string Type { get; set; } = "EOA"; + + [JsonProperty("from")] + public string From { get; set; } +} + +/// +/// ERC-4337 execution options +/// +[JsonObject] +public class ERC4337ExecutionOptions : ExecutionOptions +{ + [JsonProperty("type")] + public string Type { get; set; } = "ERC4337"; + + [JsonProperty("signerAddress")] + public string SignerAddress { get; set; } + + [JsonProperty("accountSalt")] + public string AccountSalt { get; set; } + + [JsonProperty("smartAccountAddress")] + public string SmartAccountAddress { get; set; } + + [JsonProperty("entrypointAddress")] + public string EntrypointAddress { get; set; } + + [JsonProperty("entrypointVersion")] + public string EntrypointVersion { get; set; } + + [JsonProperty("factoryAddress")] + public string FactoryAddress { get; set; } + + public ERC4337ExecutionOptions(BigInteger chainId, string signerAddress) + { + this.ChainId = chainId; + this.SignerAddress = signerAddress; + } +} + +/// +/// Response wrapper for queued transactions +/// +[JsonObject] +internal class QueuedTransactionResponse +{ + [JsonProperty("result")] + public QueuedTransactionResult Result { get; set; } +} + +/// +/// Result containing the transactions array +/// +[JsonObject] +internal class QueuedTransactionResult +{ + [JsonProperty("transactions")] + public QueuedTransaction[] Transactions { get; set; } +} + +/// +/// Queued transaction response +/// +[JsonObject] +internal class QueuedTransaction +{ + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("batchIndex")] + public long BatchIndex { get; set; } + + [JsonProperty("executionParams")] + public ExecutionOptions ExecutionParams { get; set; } + + [JsonProperty("transactionParams")] + public InnerTransaction[] TransactionParams { get; set; } +} + +/// +/// Inner transaction data +/// +[JsonObject] +internal class InnerTransaction +{ + [JsonProperty("to")] + public string To { get; set; } + + [JsonProperty("data")] + public string Data { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } +} diff --git a/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.cs b/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.cs new file mode 100644 index 00000000..58040591 --- /dev/null +++ b/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.cs @@ -0,0 +1,455 @@ +using System.Numerics; +using System.Text; +using Nethereum.ABI.EIP712; +using Nethereum.Signer; +using Nethereum.Signer.EIP712; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Thirdweb; + +/// +/// Enclave based secure cross ecosystem wallet. +/// +public partial class ServerWallet : IThirdwebWallet +{ + public ThirdwebClient Client { get; } + public ThirdwebAccountType AccountType => ThirdwebAccountType.ExternalAccount; + public string WalletId => "server"; + + private readonly string _walletAddress; + private readonly IThirdwebHttpClient _engineClient; + private readonly ExecutionOptions _executionOptions; + + private readonly JsonSerializerSettings _jsonSerializerSettings = new() { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.Indented }; + + internal ServerWallet(ThirdwebClient client, IThirdwebHttpClient engineClient, string walletAddress, ExecutionOptions executionOptions) + { + this.Client = client; + this._walletAddress = walletAddress; + this._engineClient = engineClient; + this._executionOptions = executionOptions; + } + + #region Creation + + /// + /// Creates an instance of the ServerWallet. + /// + /// The Thirdweb client. + /// The label of your created server wallet. + /// The execution options for the server wallet, defaults to auto if not passed. + /// The vault access token for the server wallet if self-managed. + /// A new instance of the ServerWallet. + /// Thrown when client or label is null or empty. + /// Thrown when no server wallets are found or the specified label does not match any existing server wallet. + public static async Task Create(ThirdwebClient client, string label, ExecutionOptions executionOptions = null, string vaultAccessToken = null) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client), "Client cannot be null."); + } + + if (string.IsNullOrEmpty(label)) + { + throw new ArgumentNullException(nameof(label), "Label cannot be null or empty."); + } + + var engineClient = Utils.ReconstructHttpClient(client.HttpClient, new Dictionary { { "X-Secret-Key", client.SecretKey } }); + if (!string.IsNullOrEmpty(vaultAccessToken)) + { + engineClient.AddHeader("X-Vault-Access-Token", vaultAccessToken); + } + var serverWalletListResponse = await engineClient.GetAsync($"{Constants.ENGINE_API_URL}/v1/accounts").ConfigureAwait(false); + _ = serverWalletListResponse.EnsureSuccessStatusCode(); + var content = await serverWalletListResponse.Content.ReadAsStringAsync().ConfigureAwait(false); + + var responseObj = JObject.Parse(content); + var accounts = responseObj["result"]?["accounts"]?.ToObject(); // TODO: Support pagination + + if (accounts == null || accounts.Count == 0) + { + throw new InvalidOperationException("No server wallets found in the account."); + } + + var matchingAccount = + accounts.FirstOrDefault(account => account["label"]?.ToString() == label) + ?? throw new InvalidOperationException( + $"Server wallet with label '{label}' not found. Available labels: {string.Join(", ", accounts.Select(a => a["label"]?.ToString()).Where(l => !string.IsNullOrEmpty(l)))}" + ); + + var signerWalletAddress = matchingAccount["address"]?.ToString().ToChecksumAddress(); + var smartWalletAddress = executionOptions is ERC4337ExecutionOptions ? matchingAccount["smartAccountAddress"]?.ToString() : null; + if (string.IsNullOrEmpty(signerWalletAddress)) + { + throw new InvalidOperationException($"Server wallet with label '{label}' found but has no address."); + } + + executionOptions ??= new AutoExecutionOptions { IdempotencyKey = Guid.NewGuid().ToString(), From = signerWalletAddress.ToChecksumAddress() }; + if (executionOptions is ERC4337ExecutionOptions erc4337ExecutionOptions) + { + erc4337ExecutionOptions.SmartAccountAddress = smartWalletAddress; + erc4337ExecutionOptions.SignerAddress = signerWalletAddress; + } + else if (executionOptions is EIP7702ExecutionOptions eip7702ExecutionOptions) + { + eip7702ExecutionOptions.From = signerWalletAddress.ToChecksumAddress(); + } + else if (executionOptions is EOAExecutionOptions eoaExecutionOptions) + { + eoaExecutionOptions.From = signerWalletAddress.ToChecksumAddress(); + } + else if (executionOptions is AutoExecutionOptions autoExecutionOptions) + { + autoExecutionOptions.From ??= signerWalletAddress.ToChecksumAddress(); + } + else + { + throw new InvalidOperationException( + $"Unsupported execution options type: {executionOptions.GetType().Name}. Supported types are AutoExecutionOptions, EIP7702ExecutionOptions, EOAExecutionOptions, and ERC4337ExecutionOptions." + ); + } + + var wallet = new ServerWallet(client, engineClient, smartWalletAddress ?? signerWalletAddress, executionOptions); + Utils.TrackConnection(wallet); + return wallet; + } + + #endregion + + #region Wallet Specific + + public async Task WaitForTransactionHash(string txid) + { + var cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(this.Client.FetchTimeoutOptions.GetTimeout(TimeoutType.Other)); + var transactionHash = string.Empty; + while (string.IsNullOrEmpty(transactionHash) && !cancellationToken.IsCancellationRequested) + { + await ThirdwebTask.Delay(100); + + var statusResponse = await this._engineClient.GetAsync($"{Constants.ENGINE_API_URL}/v1/transactions?id={txid}").ConfigureAwait(false); + var content = await statusResponse.Content.ReadAsStringAsync(); + var response = JObject.Parse(content); + var transaction = (response["result"]?["transactions"]?.FirstOrDefault()) ?? throw new Exception($"Failed to fetch transaction status for ID: {txid}"); + var errorMessage = transaction?["errorMessage"]?.ToString(); + if (!string.IsNullOrEmpty(errorMessage)) + { + throw new Exception($"Sending transaction errored: {errorMessage}"); + } + + transactionHash = transaction?["transactionHash"]?.ToString(); + } + return transactionHash; + } + + private object ToEngineTransaction(ThirdwebTransactionInput transaction) + { + if (transaction == null) + { + throw new ArgumentNullException(nameof(transaction)); + } + + this._executionOptions.ChainId = transaction.ChainId; + + return new + { + executionOptions = this._executionOptions, + @params = new[] + { + new + { + to = transaction.To, + data = transaction.Data ?? "0x", + value = transaction.Value?.HexValue ?? "0x00", + authorizationList = transaction.AuthorizationList != null && transaction.AuthorizationList.Count > 0 + ? transaction + .AuthorizationList.Select(authorization => new + { + chainId = authorization.ChainId.HexToNumber(), + address = authorization.Address, + nonce = authorization.Nonce.HexToNumber(), + yParity = authorization.YParity.HexToNumber(), + r = authorization.R, + s = authorization.S, + }) + .ToArray() + : null, + }, + }, + }; + } + + #endregion + + #region IThirdwebWallet + + public Task GetAddress() + { + if (!string.IsNullOrEmpty(this._walletAddress)) + { + return Task.FromResult(this._walletAddress.ToChecksumAddress()); + } + else + { + return Task.FromResult(this._walletAddress); + } + } + + public Task EthSign(byte[] rawMessage) + { + if (rawMessage == null) + { + throw new ArgumentNullException(nameof(rawMessage), "Message to sign cannot be null."); + } + + throw new NotImplementedException(); + } + + public Task EthSign(string message) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message), "Message to sign cannot be null."); + } + + throw new NotImplementedException(); + } + + public async Task PersonalSign(byte[] rawMessage) + { + if (rawMessage == null) + { + throw new ArgumentNullException(nameof(rawMessage), "Message to sign cannot be null."); + } + + var url = $"{Constants.ENGINE_API_URL}/v1/sign/message"; + + var address = await this.GetAddress(); + + var payload = new + { + signingOptions = new + { + type = "auto", + from = address, + chainId = this._executionOptions.ChainId, + }, + @params = new[] { new { message = rawMessage.BytesToHex(), format = "hex" } }, + }; + + var requestContent = new StringContent(JsonConvert.SerializeObject(payload, this._jsonSerializerSettings), Encoding.UTF8, "application/json"); + + var response = await this._engineClient.PostAsync(url, requestContent).ConfigureAwait(false); + _ = response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + return JObject.Parse(content)["result"]?[0]?["result"]?["signature"].Value(); + } + + public async Task PersonalSign(string message) + { + if (string.IsNullOrEmpty(message)) + { + throw new ArgumentNullException(nameof(message), "Message to sign cannot be null."); + } + + var url = $"{Constants.ENGINE_API_URL}/v1/sign/message"; + + var address = await this.GetAddress(); + + var payload = new + { + signingOptions = new + { + type = "auto", + from = address, + chainId = this._executionOptions.ChainId, + }, + @params = new[] { new { message, format = "text" } }, + }; + + var requestContent = new StringContent(JsonConvert.SerializeObject(payload, this._jsonSerializerSettings), Encoding.UTF8, "application/json"); + + var response = await this._engineClient.PostAsync(url, requestContent).ConfigureAwait(false); + _ = response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + return JObject.Parse(content)["result"]?[0]?["result"]?["signature"].Value(); + } + + public async Task SignTypedDataV4(string json) + { + if (string.IsNullOrEmpty(json)) + { + throw new ArgumentNullException(nameof(json), "Json to sign cannot be null."); + } + + var processedJson = Utils.PreprocessTypedDataJson(json); + + var url = $"{Constants.ENGINE_API_URL}/v1/sign/typed-data"; + + var address = await this.GetAddress(); + + var payload = new + { + signingOptions = new + { + type = "auto", + from = address, + chainId = BigInteger.Parse(JObject.Parse(processedJson)["domain"]?["chainId"]?.Value()), + }, + @params = new[] { processedJson }, + }; + var requestContent = new StringContent(JsonConvert.SerializeObject(payload, this._jsonSerializerSettings), Encoding.UTF8, "application/json"); + + var response = await this._engineClient.PostAsync(url, requestContent).ConfigureAwait(false); + _ = response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + return JObject.Parse(content)["result"]?[0]?["result"]?["signature"].Value(); + } + + public async Task SignTypedDataV4(T data, TypedData typedData) + where TDomain : IDomain + { + if (data == null) + { + throw new ArgumentNullException(nameof(data), "Data to sign cannot be null."); + } + + var safeJson = Utils.ToJsonExternalWalletFriendly(typedData, data); + return await this.SignTypedDataV4(safeJson).ConfigureAwait(false); + } + + public Task SignTransaction(ThirdwebTransactionInput transaction) + { + throw new NotImplementedException("SignTransaction is not implemented for ServerWallet. Use SendTransaction instead."); + } + + public Task IsConnected() + { + return Task.FromResult(this._walletAddress != null); + } + + public async Task SendTransaction(ThirdwebTransactionInput transaction) + { + if (transaction == null) + { + throw new ArgumentNullException(nameof(transaction)); + } + + var payload = this.ToEngineTransaction(transaction); + + var url = $"{Constants.ENGINE_API_URL}/v1/write/transaction"; + + var requestContent = new StringContent(JsonConvert.SerializeObject(payload, this._jsonSerializerSettings), Encoding.UTF8, "application/json"); + + var response = await this._engineClient.PostAsync(url, requestContent).ConfigureAwait(false); + _ = response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var queuedTransactionResponse = JsonConvert.DeserializeObject(content); + var txid = queuedTransactionResponse.Result?.Transactions?.FirstOrDefault()?.Id; + if (string.IsNullOrEmpty(txid)) + { + throw new Exception("Failed to queue the transaction. No transaction ID returned."); + } + return await this.WaitForTransactionHash(txid).ConfigureAwait(false); + } + + public async Task ExecuteTransaction(ThirdwebTransactionInput transactionInput) + { + var hash = await this.SendTransaction(transactionInput); + return await ThirdwebTransaction.WaitForTransactionReceipt(this.Client, transactionInput.ChainId.Value, hash).ConfigureAwait(false); + } + + public Task Disconnect() + { + return Task.CompletedTask; + } + + public virtual Task RecoverAddressFromEthSign(string message, string signature) + { + 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); + } + + public Task SignAuthorization(BigInteger chainId, string contractAddress, bool willSelfExecute) + { + throw new NotImplementedException(); + } + + public Task SwitchNetwork(BigInteger chainId) + { + return Task.CompletedTask; + } + + public 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, + string defaultSessionIdOverride = null, + List forceWalletIds = null + ) + { + throw new NotImplementedException(); + } + + public Task> UnlinkAccount(LinkedAccount accountToUnlink) + { + throw new NotImplementedException(); + } + + public Task> GetLinkedAccounts() + { + throw new NotImplementedException(); + } + + #endregion +}