From ed105d438e6890f6f535dc1ea547dc43076a045b Mon Sep 17 00:00:00 2001
From: 0xFirekeeper <0xFirekeeper@gmail.com>
Date: Wed, 16 Jul 2025 03:56:29 +0700
Subject: [PATCH 1/2] Add ServerWallet implementation and deprecate
EngineWallet
Introduces the ServerWallet class as a replacement for EngineWallet, including execution options and transaction handling. Marks EngineWallet as obsolete. Updates the console example to use ServerWallet and adds ENGINE_API_URL to constants.
Closes TOOL-5016
--Will be merged once tested w/ auto = EIP7702 in future Engine Cloud release--
---
Thirdweb.Console/Program.cs | 63 ++-
Thirdweb/Thirdweb.Utils/Constants.cs | 1 +
.../EngineWallet/EngineWallet.cs | 1 +
.../ServerWallet/ServerWallet.Types.cs | 119 +++++
.../ServerWallet/ServerWallet.cs | 444 ++++++++++++++++++
5 files changed, 613 insertions(+), 15 deletions(-)
create mode 100644 Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.Types.cs
create mode 100644 Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.cs
diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs
index f292cbfa..6fc43e21 100644
--- a/Thirdweb.Console/Program.cs
+++ b/Thirdweb.Console/Program.cs
@@ -340,21 +340,54 @@
#endregion
-#region Engine Wallet
-
-// // EngineWallet is compatible with IThirdwebWallet and can be used with any SDK method/extension
-// var engineWallet = await EngineWallet.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
-// );
-
-// // Simple self transfer
-// var receipt = await engineWallet.Transfer(chainId: 11155111, toAddress: await engineWallet.GetAddress(), weiAmount: 0);
-// Console.WriteLine($"Receipt: {receipt}");
+#region Server Wallet
+
+// You need only pass this if you are using a self-managed vault (check your dashboard Transactions tab)
+var myAccessToken = Environment.GetEnvironmentVariable("VAULT_ACCESS_TOKEN");
+
+// ServerWallet is compatible with IThirdwebWallet and can be used with any SDK method/extension
+var serverWallet = await ServerWallet.Create(
+ client: client,
+ label: "Test",
+ // Optional, defaults to Auto - we choose between EIP-7702, EIP-4337 or native zkSync AA execution
+ executionOptions: new AutoExecutionOptions(),
+ vaultAccessToken: myAccessToken
+);
+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}");
+
+// ServerWallet forcing ERC-4337 Execution Mode
+var smartServerWallet = await ServerWallet.Create(
+ client: client,
+ label: "Test",
+ executionOptions: new ERC4337ExecutionOptions(chainId: 84532, signerAddress: serverWalletAddress),
+ vaultAccessToken: myAccessToken
+);
+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 serverWalletReceipt = await serverWallet.Transfer(chainId: 421614, toAddress: await serverWallet.GetAddress(), weiAmount: 0);
+Console.WriteLine($"Server Wallet Hash: {serverWalletReceipt.TransactionHash}");
+
+// Simple self transfer
+var smartServerWalletReceipt = await smartServerWallet.Transfer(chainId: 421614, 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..4d9e765a
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.Types.cs
@@ -0,0 +1,119 @@
+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; }
+}
+
+///
+/// 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..bd78b1db
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.cs
@@ -0,0 +1,444 @@
+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"]?.ToObject();
+
+ 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 AutoExecutionOptions autoExecutionOptions)
+ {
+ autoExecutionOptions.From ??= signerWalletAddress.ToChecksumAddress();
+ }
+ else if (executionOptions is ERC4337ExecutionOptions erc4337ExecutionOptions)
+ {
+ erc4337ExecutionOptions.SmartAccountAddress = smartWalletAddress;
+ erc4337ExecutionOptions.SignerAddress = signerWalletAddress;
+ }
+
+ 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
+}
From e18354398f2c6bfbc50eeacfda784f52fec5ef3b Mon Sep 17 00:00:00 2001
From: 0xFirekeeper <0xFirekeeper@gmail.com>
Date: Fri, 18 Jul 2025 07:21:57 +0700
Subject: [PATCH 2/2] Add EIP-7702 and EOA execution options to ServerWallet
Introduces EIP7702ExecutionOptions and EOAExecutionOptions classes for ServerWallet execution modes. Updates ServerWallet logic to support these new execution options and improves type handling. Also comments out ServerWallet usage in the console example and fixes account parsing in ServerWallet creation.
---
Thirdweb.Console/Program.cs | 86 +++++++++----------
.../ServerWallet/ServerWallet.Types.cs | 26 ++++++
.../ServerWallet/ServerWallet.cs | 61 +++++++------
3 files changed, 101 insertions(+), 72 deletions(-)
diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs
index 6fc43e21..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);
@@ -342,52 +342,44 @@
#region Server Wallet
-// You need only pass this if you are using a self-managed vault (check your dashboard Transactions tab)
-var myAccessToken = Environment.GetEnvironmentVariable("VAULT_ACCESS_TOKEN");
-
-// ServerWallet is compatible with IThirdwebWallet and can be used with any SDK method/extension
-var serverWallet = await ServerWallet.Create(
- client: client,
- label: "Test",
- // Optional, defaults to Auto - we choose between EIP-7702, EIP-4337 or native zkSync AA execution
- executionOptions: new AutoExecutionOptions(),
- vaultAccessToken: myAccessToken
-);
-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}");
-
-// ServerWallet forcing ERC-4337 Execution Mode
-var smartServerWallet = await ServerWallet.Create(
- client: client,
- label: "Test",
- executionOptions: new ERC4337ExecutionOptions(chainId: 84532, signerAddress: serverWalletAddress),
- vaultAccessToken: myAccessToken
-);
-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 serverWalletReceipt = await serverWallet.Transfer(chainId: 421614, toAddress: await serverWallet.GetAddress(), weiAmount: 0);
-Console.WriteLine($"Server Wallet Hash: {serverWalletReceipt.TransactionHash}");
-
-// Simple self transfer
-var smartServerWalletReceipt = await smartServerWallet.Transfer(chainId: 421614, toAddress: await smartServerWallet.GetAddress(), weiAmount: 0);
-Console.WriteLine($"Server Wallet Hash: {smartServerWalletReceipt.TransactionHash}");
+// // ServerWallet is compatible with IThirdwebWallet and can be used with any SDK method/extension
+// var serverWallet = await ServerWallet.Create(
+// client: client,
+// 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 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.Wallets/ServerWallet/ServerWallet.Types.cs b/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.Types.cs
index 4d9e765a..711d6901 100644
--- a/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.Types.cs
+++ b/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.Types.cs
@@ -29,6 +29,32 @@ public class AutoExecutionOptions : ExecutionOptions
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
///
diff --git a/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.cs b/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.cs
index bd78b1db..58040591 100644
--- a/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.cs
+++ b/Thirdweb/Thirdweb.Wallets/ServerWallet/ServerWallet.cs
@@ -21,7 +21,7 @@ public partial class ServerWallet : IThirdwebWallet
private readonly IThirdwebHttpClient _engineClient;
private readonly ExecutionOptions _executionOptions;
- private readonly JsonSerializerSettings _jsonSerializerSettings = new() { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.Indented, };
+ private readonly JsonSerializerSettings _jsonSerializerSettings = new() { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.Indented };
internal ServerWallet(ThirdwebClient client, IThirdwebHttpClient engineClient, string walletAddress, ExecutionOptions executionOptions)
{
@@ -65,7 +65,7 @@ public static async Task Create(ThirdwebClient client, string labe
var content = await serverWalletListResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
var responseObj = JObject.Parse(content);
- var accounts = responseObj["result"]?.ToObject();
+ var accounts = responseObj["result"]?["accounts"]?.ToObject(); // TODO: Support pagination
if (accounts == null || accounts.Count == 0)
{
@@ -86,14 +86,28 @@ public static async Task Create(ThirdwebClient client, string labe
}
executionOptions ??= new AutoExecutionOptions { IdempotencyKey = Guid.NewGuid().ToString(), From = signerWalletAddress.ToChecksumAddress() };
- if (executionOptions is AutoExecutionOptions autoExecutionOptions)
+ 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 if (executionOptions is ERC4337ExecutionOptions erc4337ExecutionOptions)
+ else
{
- erc4337ExecutionOptions.SmartAccountAddress = smartWalletAddress;
- erc4337ExecutionOptions.SignerAddress = signerWalletAddress;
+ 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);
@@ -149,22 +163,19 @@ private object ToEngineTransaction(ThirdwebTransactionInput transaction)
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
- }
- )
+ ? 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,
- }
+ },
},
};
}
@@ -222,9 +233,9 @@ public async Task PersonalSign(byte[] rawMessage)
{
type = "auto",
from = address,
- chainId = this._executionOptions.ChainId
+ chainId = this._executionOptions.ChainId,
},
- @params = new[] { new { message = rawMessage.BytesToHex(), format = "hex" } }
+ @params = new[] { new { message = rawMessage.BytesToHex(), format = "hex" } },
};
var requestContent = new StringContent(JsonConvert.SerializeObject(payload, this._jsonSerializerSettings), Encoding.UTF8, "application/json");
@@ -253,9 +264,9 @@ public async Task PersonalSign(string message)
{
type = "auto",
from = address,
- chainId = this._executionOptions.ChainId
+ chainId = this._executionOptions.ChainId,
},
- @params = new[] { new { message, format = "text" } }
+ @params = new[] { new { message, format = "text" } },
};
var requestContent = new StringContent(JsonConvert.SerializeObject(payload, this._jsonSerializerSettings), Encoding.UTF8, "application/json");
@@ -288,7 +299,7 @@ public async Task SignTypedDataV4(string json)
from = address,
chainId = BigInteger.Parse(JObject.Parse(processedJson)["domain"]?["chainId"]?.Value()),
},
- @params = new[] { processedJson }
+ @params = new[] { processedJson },
};
var requestContent = new StringContent(JsonConvert.SerializeObject(payload, this._jsonSerializerSettings), Encoding.UTF8, "application/json");