From 42cd677d537a995b0f9030eb12638adf5d416f65 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Wed, 29 Jan 2025 04:44:36 +0700 Subject: [PATCH 1/4] Engine Wallet & 7702 Session Keys --- Thirdweb.Console/Program.cs | 192 ++++---- .../Thirdweb.Contracts/ThirdwebContract.cs | 8 +- Thirdweb/Thirdweb.Utils/Utils.cs | 2 + Thirdweb/Thirdweb.Wallets/EIP712.cs | 47 ++ .../EngineWallet/EngineWallet.cs | 414 ++++++++++++++++++ .../Thirdweb.AccountAbstraction/AATypes.cs | 28 ++ 6 files changed, 606 insertions(+), 85 deletions(-) create mode 100644 Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs index 3e6c1333..6a2514a9 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -3,11 +3,15 @@ using System.Diagnostics; using System.Numerics; +using System.Text; using dotenv.net; +using Nethereum.ABI; +using Nethereum.Hex.HexConvertors.Extensions; using Nethereum.Hex.HexTypes; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Thirdweb; +using Thirdweb.AccountAbstraction; using Thirdweb.AI; using Thirdweb.Pay; @@ -145,87 +149,113 @@ #region EIP-7702 -// // Chain and contract addresses -// var chainWith7702 = 911867; -// var erc20ContractAddress = "0xAA462a5BE0fc5214507FDB4fB2474a7d5c69065b"; // Fake ERC20 -// var delegationContractAddress = "0x654F42b74885EE6803F403f077bc0409f1066c58"; // BatchCallDelegation - -// // Initialize contracts normally -// var erc20Contract = await ThirdwebContract.Create(client: client, address: erc20ContractAddress, chain: chainWith7702); -// var delegationContract = await ThirdwebContract.Create(client: client, address: delegationContractAddress, chain: chainWith7702); - -// // Initialize a (to-be) 7702 EOA -// var eoaWallet = await PrivateKeyWallet.Generate(client); -// var eoaWalletAddress = await eoaWallet.GetAddress(); -// Console.WriteLine($"EOA address: {eoaWalletAddress}"); - -// // Initialize another wallet, the "executor" that will hit the eoa's (to-be) execute function -// var executorWallet = await PrivateKeyWallet.Generate(client); -// var executorWalletAddress = await executorWallet.GetAddress(); -// Console.WriteLine($"Executor address: {executorWalletAddress}"); - -// // Fund the executor wallet -// var fundingWallet = await PrivateKeyWallet.Create(client, privateKey); -// var fundingHash = (await fundingWallet.Transfer(chainWith7702, executorWalletAddress, BigInteger.Parse("0.001".ToWei()))).TransactionHash; -// Console.WriteLine($"Funded Executor Wallet: {fundingHash}"); - -// // Sign the authorization to make it point to the delegation contract -// var authorization = await eoaWallet.SignAuthorization(chainId: chainWith7702, contractAddress: delegationContractAddress, willSelfExecute: false); -// Console.WriteLine($"Authorization: {JsonConvert.SerializeObject(authorization, Formatting.Indented)}"); - -// // Execute the delegation -// var tx = await ThirdwebTransaction.Create(executorWallet, new ThirdwebTransactionInput(chainId: chainWith7702, to: executorWalletAddress, authorization: authorization)); -// var hash = (await ThirdwebTransaction.SendAndWaitForTransactionReceipt(tx)).TransactionHash; -// Console.WriteLine($"Authorization execution transaction hash: {hash}"); - -// // Prove that code has been deployed to the eoa -// var rpc = ThirdwebRPC.GetRpcInstance(client, chainWith7702); -// var code = await rpc.SendRequestAsync("eth_getCode", eoaWalletAddress, "latest"); -// Console.WriteLine($"EOA code: {code}"); - -// // Log erc20 balance of executor before the claim -// var executorBalanceBefore = await erc20Contract.ERC20_BalanceOf(executorWalletAddress); -// Console.WriteLine($"Executor balance before: {executorBalanceBefore}"); - -// // Prepare the claim call -// var claimCallData = erc20Contract.CreateCallData( -// "claim", -// new object[] -// { -// executorWalletAddress, // receiver -// 100, // quantity -// Constants.NATIVE_TOKEN_ADDRESS, // currency -// 0, // pricePerToken -// new object[] { Array.Empty(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO }, // allowlistProof -// Array.Empty() // data -// } -// ); - -// // Embed the claim call in the execute call -// var executeCallData = delegationContract.CreateCallData( -// method: "execute", -// parameters: new object[] -// { -// new List -// { -// new() -// { -// Data = claimCallData.HexToBytes(), -// To = erc20ContractAddress, -// Value = BigInteger.Zero -// } -// } -// } -// ); - -// // Execute from the executor wallet targeting the eoa which is pointing to the delegation contract -// var tx2 = await ThirdwebTransaction.Create(executorWallet, new ThirdwebTransactionInput(chainId: chainWith7702, to: eoaWalletAddress, data: executeCallData)); -// var hash2 = (await ThirdwebTransaction.SendAndWaitForTransactionReceipt(tx2)).TransactionHash; -// Console.WriteLine($"Token claim transaction hash: {hash2}"); - -// // Log erc20 balance of executor after the claim -// var executorBalanceAfter = await erc20Contract.ERC20_BalanceOf(executorWalletAddress); -// Console.WriteLine($"Executor balance after: {executorBalanceAfter}"); +// -------------------------------------------------------------------------- +// Configuration +// -------------------------------------------------------------------------- + +var chainWith7702 = 911867; +var delegationContractAddress = "0x08e47c0d38feb3d849abc01e2b7fb5d3d0d626e9"; // MinimalAccount + +// Required environment variables +var executorWalletAddress = Environment.GetEnvironmentVariable("ENGINE_EXECUTOR_WALLET_ADDRESS") ?? throw new Exception("ENGINE_EXECUTOR_WALLET_ADDRESS is required"); +var engineUrl = Environment.GetEnvironmentVariable("ENGINE_URL") ?? throw new Exception("ENGINE_URL is required"); +var engineAccessToken = Environment.GetEnvironmentVariable("ENGINE_ACCESS_TOKEN") ?? throw new Exception("ENGINE_ACCESS_TOKEN is required"); + +// -------------------------------------------------------------------------- +// Initialize Engine Wallet +// -------------------------------------------------------------------------- + +var engineWallet = await EngineWallet.Create(client, engineUrl, engineAccessToken, executorWalletAddress, 15); + +// -------------------------------------------------------------------------- +// Delegation Contract Implementation +// -------------------------------------------------------------------------- + +var delegationContract = await ThirdwebContract.Create(client, delegationContractAddress, chainWith7702); + +// Initialize a (to-be) 7702 EOA +var eoaWallet = await PrivateKeyWallet.Generate(client); +var eoaWalletAddress = await eoaWallet.GetAddress(); +Console.WriteLine($"EOA address: {eoaWalletAddress}"); + +// Sign the authorization to point to the delegation contract +var authorization = await eoaWallet.SignAuthorization(chainWith7702, delegationContractAddress, willSelfExecute: false); +Console.WriteLine($"Authorization: {JsonConvert.SerializeObject(authorization, Formatting.Indented)}"); + +// Sign message for session key +var sessionKeyParams = new SessionKeyParams_7702() +{ + Signer = executorWalletAddress, + NativeTokenLimitPerTransaction = 0, + StartTimestamp = 0, + EndTimestamp = Utils.GetUnixTimeStampNow() + (3600 * 24), + ApprovedTargets = new List { Constants.ADDRESS_ZERO }, + Uid = Guid.NewGuid().ToByteArray() +}; +var sessionKeySig = await EIP712.GenerateSignature_SmartAccount_7702("MinimalAccount", "1", chainWith7702, eoaWalletAddress, sessionKeyParams, eoaWallet); + +// Create call data for the session key +var sessionKeyCallData = delegationContract.CreateCallData("createSessionKeyWithSig", sessionKeyParams, sessionKeySig.HexToBytes()); + +// Execute the delegation & session key creation!!!!!!!!!! +var delegationReceipt = await engineWallet.ExecuteTransaction(new ThirdwebTransactionInput(chainId: chainWith7702, to: eoaWalletAddress, data: sessionKeyCallData, authorization: authorization)); +Console.WriteLine($"Delegation Execution Receipt: {JsonConvert.SerializeObject(delegationReceipt, Formatting.Indented)}"); + +// Verify contract code deployed to the EOA +var rpc = ThirdwebRPC.GetRpcInstance(client, chainWith7702); +var code = await rpc.SendRequestAsync("eth_getCode", eoaWalletAddress, "latest"); +Console.WriteLine($"EOA code: {code}"); + +// The EOA is now a contract +var eoaContract = await ThirdwebContract.Create(client, eoaWalletAddress, chainWith7702, delegationContract.Abi); + +// -------------------------------------------------------------------------- +// Mint Tokens (DropERC20) to the EOA Using the Executor +// -------------------------------------------------------------------------- + +var erc20ContractAddress = "0xAA462a5BE0fc5214507FDB4fB2474a7d5c69065b"; // DropERC20 +var erc20Contract = await ThirdwebContract.Create(client, erc20ContractAddress, chainWith7702); + +// Log ERC20 balance before mint +var eoaBalanceBefore = await erc20Contract.ERC20_BalanceOf(eoaWalletAddress); +Console.WriteLine($"EOA balance before: {eoaBalanceBefore}"); + +// Create execution call data (calling 'claim' on the DropERC20) +var executeCallData = eoaContract.CreateCallData( + "execute", + new object[] + { + new List + { + new() + { + Data = erc20Contract + .CreateCallData( + "claim", + new object[] + { + eoaWalletAddress, // receiver + 100, // quantity + Constants.NATIVE_TOKEN_ADDRESS, // currency + 0, // pricePerToken + new object[] { Array.Empty(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO }, // allowlistProof + Array.Empty() // data + } + ) + .HexToBytes(), + To = erc20ContractAddress, + Value = BigInteger.Zero + } + } + } +); + +var executeReceipt = await engineWallet.ExecuteTransaction(new ThirdwebTransactionInput(chainId: chainWith7702, to: eoaWalletAddress, data: executeCallData)); +Console.WriteLine($"Execute receipt: {JsonConvert.SerializeObject(executeReceipt, Formatting.Indented)}"); + +// Log ERC20 balance after mint +var eoaBalanceAfter = await erc20Contract.ERC20_BalanceOf(eoaWalletAddress); +Console.WriteLine($"EOA balance after: {eoaBalanceAfter}"); #endregion diff --git a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs index 97b011a5..90bc8623 100644 --- a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs +++ b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs @@ -12,10 +12,10 @@ namespace Thirdweb; /// public class ThirdwebContract { - internal ThirdwebClient Client { get; private set; } - internal string Address { get; private set; } - internal BigInteger Chain { get; private set; } - internal string Abi { get; private set; } + public ThirdwebClient Client { get; private set; } + public string Address { get; private set; } + public BigInteger Chain { get; private set; } + public string Abi { get; private set; } private static readonly Dictionary _contractAbiCache = new(); private static readonly object _cacheLock = new(); diff --git a/Thirdweb/Thirdweb.Utils/Utils.cs b/Thirdweb/Thirdweb.Utils/Utils.cs index 3fe8e917..7f65fc2f 100644 --- a/Thirdweb/Thirdweb.Utils/Utils.cs +++ b/Thirdweb/Thirdweb.Utils/Utils.cs @@ -755,6 +755,8 @@ public static bool IsEip1559Supported(string chainId) case "841": // Taraxa Testnet case "842": + // Odyssey Testnet + case "911867": return false; default: return true; diff --git a/Thirdweb/Thirdweb.Wallets/EIP712.cs b/Thirdweb/Thirdweb.Wallets/EIP712.cs index 30fab61b..bb8838cc 100644 --- a/Thirdweb/Thirdweb.Wallets/EIP712.cs +++ b/Thirdweb/Thirdweb.Wallets/EIP712.cs @@ -14,6 +14,29 @@ public static class EIP712 { #region Generation + /// + /// Generates a signature for a 7702 smart account session key. + /// + /// The domain name. + /// The version. + /// The chain ID. + /// The verifying contract. + /// The session key request. + /// The wallet signer. + /// The generated signature. + public static async Task GenerateSignature_SmartAccount_7702( + string domainName, + string version, + BigInteger chainId, + string verifyingContract, + AccountAbstraction.SessionKeyParams_7702 sessionKeyParams, + IThirdwebWallet signer + ) + { + var typedData = GetTypedDefinition_SmartAccount_7702(domainName, version, chainId, verifyingContract); + return await signer.SignTypedDataV4(sessionKeyParams, typedData); + } + /// /// Generates a signature for a smart account permission request. /// @@ -180,6 +203,30 @@ IThirdwebWallet signer #region Typed Definitions + /// + /// Gets the typed data definition for a 7702 smart account session key. + /// + /// The domain name. + /// The version. + /// The chain ID. + /// The verifying contract. + /// The typed data definition. + public static TypedData GetTypedDefinition_SmartAccount_7702(string domainName, string version, BigInteger chainId, string verifyingContract) + { + return new TypedData + { + Domain = new Domain + { + Name = domainName, + Version = version, + ChainId = chainId, + VerifyingContract = verifyingContract, + }, + Types = MemberDescriptionFactory.GetTypesMemberDescription(typeof(Domain), typeof(AccountAbstraction.SessionKeyParams_7702)), + PrimaryType = "SessionKeyParams", + }; + } + /// /// Gets the typed data definition for a smart account permission request. /// diff --git a/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs b/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs new file mode 100644 index 00000000..4daeb654 --- /dev/null +++ b/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs @@ -0,0 +1,414 @@ +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 EngineWallet : IThirdwebWallet +{ + public ThirdwebClient Client { get; } + public ThirdwebAccountType AccountType => ThirdwebAccountType.ExternalAccount; + public string WalletId => "engine"; + + private readonly string _engineUrl; + private readonly string _walletAddress; + private readonly IThirdwebHttpClient _engineClient; + private readonly int? _timeoutSeconds; + + internal EngineWallet(ThirdwebClient client, IThirdwebHttpClient engineClient, string engineUrl, string walletAddress, int? timeoutSeconds) + { + this.Client = client; + this._engineUrl = engineUrl; + this._walletAddress = walletAddress; + this._engineClient = engineClient; + this._timeoutSeconds = timeoutSeconds; + } + + #region Creation + + /// + /// Creates an instance of the EngineWallet. + /// + /// The Thirdweb client. + /// The URL of the engine. + /// The access token to use for the engine. + /// The backend wallet address to use. + /// The timeout in seconds for the transaction. Defaults to no timeout. + /// Additional headers to include in requests. Authorization and X-Backend-Wallet-Address automatically included. + public static async Task Create( + ThirdwebClient client, + string engineUrl, + string authToken, + string walletAddress, + int? timeoutSeconds = null, + Dictionary additionalHeaders = null + ) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client), "Client cannot be null."); + } + + if (string.IsNullOrWhiteSpace(engineUrl)) + { + throw new ArgumentNullException(nameof(engineUrl), "Engine URL cannot be null or empty."); + } + + if (string.IsNullOrWhiteSpace(authToken)) + { + throw new ArgumentNullException(nameof(authToken), "Auth token cannot be null or empty."); + } + + if (string.IsNullOrWhiteSpace(walletAddress)) + { + throw new ArgumentNullException(nameof(walletAddress), "Wallet address cannot be null or empty."); + } + + if (engineUrl.EndsWith('/')) + { + engineUrl = engineUrl[..^1]; + } + + var engineClient = Utils.ReconstructHttpClient(client.HttpClient, new Dictionary { { "Authorization", $"Bearer {authToken}" }, }); + var allWalletsResponse = await engineClient.GetAsync($"{engineUrl}/backend-wallet/get-all").ConfigureAwait(false); + _ = allWalletsResponse.EnsureSuccessStatusCode(); + var allWallets = JObject.Parse(await allWalletsResponse.Content.ReadAsStringAsync().ConfigureAwait(false)); + var walletExists = allWallets["result"].Any(w => string.Equals(w["address"].Value(), walletAddress, StringComparison.OrdinalIgnoreCase)); + if (!walletExists) + { + throw new Exception("Wallet does not exist in the engine."); + } + engineClient.AddHeader("X-Backend-Wallet-Address", walletAddress); + if (additionalHeaders != null) + { + foreach (var header in additionalHeaders) + { + engineClient.AddHeader(header.Key, header.Value); + } + } + return new EngineWallet(client, engineClient, engineUrl, walletAddress, timeoutSeconds); + } + + #endregion + + #region Wallet Specific + + private async Task WaitForQueueId(string queueId) + { + var transactionHash = string.Empty; + while (string.IsNullOrEmpty(transactionHash)) + { + await ThirdwebTask.Delay(100); + + var statusResponse = await this._engineClient.GetAsync($"{this._engineUrl}/transaction/status/{queueId}"); + _ = statusResponse.EnsureSuccessStatusCode(); + var content = await statusResponse.Content.ReadAsStringAsync(); + var status = JObject.Parse(content); + + var isErrored = status["result"]?["status"]?.ToString() is "errored" or "cancelled"; + if (isErrored) + { + throw new Exception("Transaction errored or cancelled"); + } + + transactionHash = status["result"]?["transactionHash"]?.ToString(); + } + return transactionHash; + } + + private object ToEngineTransaction(ThirdwebTransactionInput transaction) + { + if (transaction == null) + { + throw new ArgumentNullException(nameof(transaction)); + } + + return new + { + toAddress = transaction.To, + data = transaction.Data, + 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, + txOverrides = this._timeoutSeconds != null || transaction.Gas != null || transaction.GasPrice != null || transaction.MaxFeePerGas != null || transaction.MaxPriorityFeePerGas != null + ? new + { + gas = transaction.Gas?.Value.ToString(), + gasPrice = transaction.GasPrice?.Value.ToString(), + maxFeePerGas = transaction.MaxFeePerGas?.Value.ToString(), + maxPriorityFeePerGas = transaction.MaxPriorityFeePerGas?.Value.ToString(), + timeoutSeconds = this._timeoutSeconds, + } + : 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 = $"{this._engineUrl}/backend-wallet/sign-message"; + var payload = new { messagePayload = new { message = rawMessage.BytesToHex(), isBytes = true } }; + + var requestContent = new StringContent(JsonConvert.SerializeObject(payload), 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"].Value(); + } + + public async Task PersonalSign(string message) + { + if (string.IsNullOrEmpty(message)) + { + throw new ArgumentNullException(nameof(message), "Message to sign cannot be null."); + } + + var url = $"{this._engineUrl}/backend-wallet/sign-message"; + var payload = new { messagePayload = new { message, isBytes = false } }; + + var requestContent = new StringContent(JsonConvert.SerializeObject(payload), 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"].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 = $"{this._engineUrl}/backend-wallet/sign-typed-data"; + + var requestContent = new StringContent(processedJson, 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"].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 async Task SignTransaction(ThirdwebTransactionInput transaction) + { + if (transaction == null) + { + throw new ArgumentNullException(nameof(transaction)); + } + + object payload = new { transaction = this.ToEngineTransaction(transaction), }; + + var url = $"{this._engineUrl}/backend-wallet/sign-transaction"; + + var requestContent = new StringContent(JsonConvert.SerializeObject(payload, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }), 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"].Value(); + } + + 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 = $"{this._engineUrl}/backend-wallet/{transaction.ChainId.Value}/send-transaction"; + + var requestContent = new StringContent(JsonConvert.SerializeObject(payload, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }), Encoding.UTF8, "application/json"); + Console.WriteLine(JsonConvert.SerializeObject(payload, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); + + var response = await this._engineClient.PostAsync(url, requestContent).ConfigureAwait(false); + _ = response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var queueId = JObject.Parse(content)["result"]?["queueId"]?.ToString() ?? throw new Exception("Failed to queue the transaction"); + return await this.WaitForQueueId(queueId).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 +} diff --git a/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/AATypes.cs b/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/AATypes.cs index 5505461b..a2614cbe 100644 --- a/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/AATypes.cs +++ b/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/AATypes.cs @@ -485,3 +485,31 @@ public class Erc6492Signature [Parameter("bytes", "callData", 3)] public byte[] SigToValidate { get; set; } } + +[Struct("SessionKeyParams")] +public class SessionKeyParams_7702 +{ + [Parameter("address", "signer", 1)] + [JsonProperty("signer")] + public string Signer { get; set; } + + [Parameter("uint256", "nativeTokenLimitPerTransaction", 2)] + [JsonProperty("nativeTokenLimitPerTransaction")] + public BigInteger NativeTokenLimitPerTransaction { get; set; } + + [Parameter("uint256", "startTimestamp", 3)] + [JsonProperty("startTimestamp")] + public BigInteger StartTimestamp { get; set; } + + [Parameter("uint256", "endTimestamp", 4)] + [JsonProperty("endTimestamp")] + public BigInteger EndTimestamp { get; set; } + + [Parameter("address[]", "approvedTargets", 5)] + [JsonProperty("approvedTargets")] + public List ApprovedTargets { get; set; } + + [Parameter("bytes32", "uid", 6)] + [JsonProperty("uid")] + public byte[] Uid { get; set; } +} From 9f266593d32734ad82cf789d97294432fc1f98f0 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Wed, 29 Jan 2025 23:05:41 +0700 Subject: [PATCH 2/4] Support 3rd party executors, fix sign typed data --- Thirdweb.Console/Program.Types.cs | 16 ----- Thirdweb.Console/Program.cs | 63 +++++++++++++++++-- Thirdweb/Thirdweb.Wallets/EIP712.cs | 46 ++++++++++++++ .../EngineWallet/EngineWallet.cs | 6 +- .../Thirdweb.AccountAbstraction/AATypes.cs | 28 +++++++++ 5 files changed, 136 insertions(+), 23 deletions(-) delete mode 100644 Thirdweb.Console/Program.Types.cs diff --git a/Thirdweb.Console/Program.Types.cs b/Thirdweb.Console/Program.Types.cs deleted file mode 100644 index 701c22ca..00000000 --- a/Thirdweb.Console/Program.Types.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Numerics; -using Nethereum.ABI.FunctionEncoding.Attributes; - -namespace Thirdweb.Console; - -public class Call -{ - [Parameter("bytes", "data", 1)] - public required byte[] Data { get; set; } - - [Parameter("address", "to", 2)] - public required string To { get; set; } - - [Parameter("uint256", "value", 3)] - public required BigInteger Value { get; set; } -} diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs index 6a2514a9..bf83b9ab 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -154,10 +154,10 @@ // -------------------------------------------------------------------------- var chainWith7702 = 911867; -var delegationContractAddress = "0x08e47c0d38feb3d849abc01e2b7fb5d3d0d626e9"; // MinimalAccount +var delegationContractAddress = "0xb012446cba783d0f7723daf96cf4c49005022307"; // MinimalAccount // Required environment variables -var executorWalletAddress = Environment.GetEnvironmentVariable("ENGINE_EXECUTOR_WALLET_ADDRESS") ?? throw new Exception("ENGINE_EXECUTOR_WALLET_ADDRESS is required"); +var backendWalletAddress = Environment.GetEnvironmentVariable("ENGINE_BACKEND_WALLET_ADDRESS") ?? throw new Exception("ENGINE_BACKEND_WALLET_ADDRESS is required"); var engineUrl = Environment.GetEnvironmentVariable("ENGINE_URL") ?? throw new Exception("ENGINE_URL is required"); var engineAccessToken = Environment.GetEnvironmentVariable("ENGINE_ACCESS_TOKEN") ?? throw new Exception("ENGINE_ACCESS_TOKEN is required"); @@ -165,7 +165,7 @@ // Initialize Engine Wallet // -------------------------------------------------------------------------- -var engineWallet = await EngineWallet.Create(client, engineUrl, engineAccessToken, executorWalletAddress, 15); +var engineWallet = await EngineWallet.Create(client, engineUrl, engineAccessToken, backendWalletAddress, 15); // -------------------------------------------------------------------------- // Delegation Contract Implementation @@ -185,7 +185,7 @@ // Sign message for session key var sessionKeyParams = new SessionKeyParams_7702() { - Signer = executorWalletAddress, + Signer = backendWalletAddress, NativeTokenLimitPerTransaction = 0, StartTimestamp = 0, EndTimestamp = Utils.GetUnixTimeStampNow() + (3600 * 24), @@ -210,7 +210,7 @@ var eoaContract = await ThirdwebContract.Create(client, eoaWalletAddress, chainWith7702, delegationContract.Abi); // -------------------------------------------------------------------------- -// Mint Tokens (DropERC20) to the EOA Using the Executor +// Mint Tokens (DropERC20) to the EOA Using the backend session key // -------------------------------------------------------------------------- var erc20ContractAddress = "0xAA462a5BE0fc5214507FDB4fB2474a7d5c69065b"; // DropERC20 @@ -225,7 +225,7 @@ "execute", new object[] { - new List + new List { new() { @@ -257,6 +257,57 @@ var eoaBalanceAfter = await erc20Contract.ERC20_BalanceOf(eoaWalletAddress); Console.WriteLine($"EOA balance after: {eoaBalanceAfter}"); +// -------------------------------------------------------------------------- +// Mint Tokens (DropERC20) to the EOA Using an alternative executor +// -------------------------------------------------------------------------- + +// Executor wallet (managed) +var executorWallet = await PrivateKeyWallet.Create(client, privateKey); + +// Log ERC20 balance before mint +eoaBalanceBefore = await erc20Contract.ERC20_BalanceOf(eoaWalletAddress); +Console.WriteLine($"EOA balance before: {eoaBalanceBefore}"); + +// Sign wrapped calls 712 using an authorized session key (backend wallet in this case) +var wrappedCalls = new WrappedCalls() +{ + Calls = new List + { + new() + { + Data = erc20Contract + .CreateCallData( + "claim", + new object[] + { + eoaWalletAddress, // receiver + 100, // quantity + Constants.NATIVE_TOKEN_ADDRESS, // currency + 0, // pricePerToken + new object[] { Array.Empty(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO }, // allowlistProof + Array.Empty() // data + } + ) + .HexToBytes(), + To = erc20ContractAddress, + Value = BigInteger.Zero + } + }, + Uid = Guid.NewGuid().ToByteArray().BytesToHex().HexToBytes32() +}; +var wrappedCallsSig = await EIP712.GenerateSignature_SmartAccount_7702_WrappedCalls("MinimalAccount", "1", chainWith7702, eoaWalletAddress, wrappedCalls, engineWallet); + +// Create execution call data, this time in a way that can be broadcast by anyone +executeCallData = eoaContract.CreateCallData("executeWithSig", wrappedCalls, wrappedCallsSig.HexToBytes()); + +var executeTx = await ThirdwebTransaction.Create(wallet: executorWallet, txInput: new ThirdwebTransactionInput(chainId: chainWith7702, to: eoaWalletAddress, data: executeCallData)); +executeReceipt = await ThirdwebTransaction.SendAndWaitForTransactionReceipt(executeTx); +Console.WriteLine($"Execute receipt: {JsonConvert.SerializeObject(executeReceipt, Formatting.Indented)}"); + +// Log ERC20 balance after mint +eoaBalanceAfter = await erc20Contract.ERC20_BalanceOf(eoaWalletAddress); +Console.WriteLine($"EOA balance after: {eoaBalanceAfter}"); + #endregion #region Smart Ecosystem Wallet diff --git a/Thirdweb/Thirdweb.Wallets/EIP712.cs b/Thirdweb/Thirdweb.Wallets/EIP712.cs index bb8838cc..da749f15 100644 --- a/Thirdweb/Thirdweb.Wallets/EIP712.cs +++ b/Thirdweb/Thirdweb.Wallets/EIP712.cs @@ -14,6 +14,28 @@ public static class EIP712 { #region Generation + /// + /// Generates a signature for a 7702 smart account wrapped calls request. + /// + /// The domain name. + /// The version. + /// The chain ID. + /// The verifying contract. + /// The wrapped calls request. + /// The wallet signer. + public static async Task GenerateSignature_SmartAccount_7702_WrappedCalls( + string domainName, + string version, + BigInteger chainId, + string verifyingContract, + AccountAbstraction.WrappedCalls wrappedCalls, + IThirdwebWallet signer + ) + { + var typedData = GetTypedDefinition_SmartAccount_7702_WrappedCalls(domainName, version, chainId, verifyingContract); + return await signer.SignTypedDataV4(wrappedCalls, typedData); + } + /// /// Generates a signature for a 7702 smart account session key. /// @@ -203,6 +225,30 @@ IThirdwebWallet signer #region Typed Definitions + /// + /// Gets the typed data definition for a 7702 smart account wrapped calls request. + /// + /// The domain name. + /// The version. + /// The chain ID. + /// The verifying contract. + /// The typed data definition. + public static TypedData GetTypedDefinition_SmartAccount_7702_WrappedCalls(string domainName, string version, BigInteger chainId, string verifyingContract) + { + return new TypedData + { + Domain = new Domain + { + Name = domainName, + Version = version, + ChainId = chainId, + VerifyingContract = verifyingContract, + }, + Types = MemberDescriptionFactory.GetTypesMemberDescription(typeof(Domain), typeof(AccountAbstraction.WrappedCalls), typeof(AccountAbstraction.Call)), + PrimaryType = "WrappedCalls", + }; + } + /// /// Gets the typed data definition for a 7702 smart account session key. /// diff --git a/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs b/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs index 4daeb654..db259f84 100644 --- a/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs @@ -246,6 +246,11 @@ public async Task SignTypedDataV4(string json) } var processedJson = Utils.PreprocessTypedDataJson(json); + // TODO: remove this sanitization when engine is upgraded to match spec + processedJson = processedJson.Replace("message", "value"); + var tempObj = JObject.Parse(processedJson); + _ = tempObj["types"].Value().Remove("EIP712Domain"); + processedJson = tempObj.ToString(); var url = $"{this._engineUrl}/backend-wallet/sign-typed-data"; @@ -307,7 +312,6 @@ public async Task SendTransaction(ThirdwebTransactionInput transaction) var url = $"{this._engineUrl}/backend-wallet/{transaction.ChainId.Value}/send-transaction"; var requestContent = new StringContent(JsonConvert.SerializeObject(payload, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }), Encoding.UTF8, "application/json"); - Console.WriteLine(JsonConvert.SerializeObject(payload, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); var response = await this._engineClient.PostAsync(url, requestContent).ConfigureAwait(false); _ = response.EnsureSuccessStatusCode(); diff --git a/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/AATypes.cs b/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/AATypes.cs index a2614cbe..4da64e62 100644 --- a/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/AATypes.cs +++ b/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/AATypes.cs @@ -513,3 +513,31 @@ public class SessionKeyParams_7702 [JsonProperty("uid")] public byte[] Uid { get; set; } } + +[Struct("Call")] +public class Call +{ + [Parameter("bytes", "data", 1)] + [JsonProperty("data")] + public byte[] Data { get; set; } + + [Parameter("address", "to", 2)] + [JsonProperty("to")] + public string To { get; set; } + + [Parameter("uint256", "value", 3)] + [JsonProperty("value")] + public BigInteger Value { get; set; } +} + +[Struct("WrappedCalls")] +public class WrappedCalls +{ + [Parameter("tuple[]", "calls", 1, "Call[]")] + [JsonProperty("calls")] + public List Calls { get; set; } + + [Parameter("bytes32", "uid", 2)] + [JsonProperty("uid")] + public byte[] Uid { get; set; } +} From f9eca6655a2154bad52ec19dd20026aa85a49ac2 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Thu, 30 Jan 2025 03:43:04 +0700 Subject: [PATCH 3/4] Update EngineWallet.cs --- .../Thirdweb.Wallets/EngineWallet/EngineWallet.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs b/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs index db259f84..513abae1 100644 --- a/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs @@ -100,25 +100,24 @@ public static async Task Create( #region Wallet Specific - private async Task WaitForQueueId(string queueId) + public static async Task WaitForQueueId(IThirdwebHttpClient httpClient, string engineUrl, string queueId) { var transactionHash = string.Empty; while (string.IsNullOrEmpty(transactionHash)) { await ThirdwebTask.Delay(100); - var statusResponse = await this._engineClient.GetAsync($"{this._engineUrl}/transaction/status/{queueId}"); - _ = statusResponse.EnsureSuccessStatusCode(); + var statusResponse = await httpClient.GetAsync($"{engineUrl}/transaction/status/{queueId}"); var content = await statusResponse.Content.ReadAsStringAsync(); - var status = JObject.Parse(content); + var response = JObject.Parse(content); - var isErrored = status["result"]?["status"]?.ToString() is "errored" or "cancelled"; + var isErrored = response["result"]?["status"]?.ToString() is "errored" or "cancelled"; if (isErrored) { - throw new Exception("Transaction errored or cancelled"); + throw new Exception($"Failed to send transaction: {response["result"]?["errorMessage"]?.ToString()}"); } - transactionHash = status["result"]?["transactionHash"]?.ToString(); + transactionHash = response["result"]?["transactionHash"]?.ToString(); } return transactionHash; } @@ -318,7 +317,7 @@ public async Task SendTransaction(ThirdwebTransactionInput transaction) var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var queueId = JObject.Parse(content)["result"]?["queueId"]?.ToString() ?? throw new Exception("Failed to queue the transaction"); - return await this.WaitForQueueId(queueId).ConfigureAwait(false); + return await WaitForQueueId(this._engineClient, this._engineUrl, queueId).ConfigureAwait(false); } public async Task ExecuteTransaction(ThirdwebTransactionInput transactionInput) From 54b1739de083930a2cc01911e49a6d744c6abfa1 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Fri, 21 Feb 2025 20:51:35 +0700 Subject: [PATCH 4/4] Cleanup --- Thirdweb.Console/Program.cs | 283 ++++++++---------- .../Thirdweb.Extensions/ThirdwebExtensions.cs | 4 + Thirdweb/Thirdweb.Utils/Utils.cs | 18 +- 3 files changed, 141 insertions(+), 164 deletions(-) diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs index bf83b9ab..de0dec3a 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -147,166 +147,133 @@ #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}"); + +#endregion + #region EIP-7702 -// -------------------------------------------------------------------------- -// Configuration -// -------------------------------------------------------------------------- - -var chainWith7702 = 911867; -var delegationContractAddress = "0xb012446cba783d0f7723daf96cf4c49005022307"; // MinimalAccount - -// Required environment variables -var backendWalletAddress = Environment.GetEnvironmentVariable("ENGINE_BACKEND_WALLET_ADDRESS") ?? throw new Exception("ENGINE_BACKEND_WALLET_ADDRESS is required"); -var engineUrl = Environment.GetEnvironmentVariable("ENGINE_URL") ?? throw new Exception("ENGINE_URL is required"); -var engineAccessToken = Environment.GetEnvironmentVariable("ENGINE_ACCESS_TOKEN") ?? throw new Exception("ENGINE_ACCESS_TOKEN is required"); - -// -------------------------------------------------------------------------- -// Initialize Engine Wallet -// -------------------------------------------------------------------------- - -var engineWallet = await EngineWallet.Create(client, engineUrl, engineAccessToken, backendWalletAddress, 15); - -// -------------------------------------------------------------------------- -// Delegation Contract Implementation -// -------------------------------------------------------------------------- - -var delegationContract = await ThirdwebContract.Create(client, delegationContractAddress, chainWith7702); - -// Initialize a (to-be) 7702 EOA -var eoaWallet = await PrivateKeyWallet.Generate(client); -var eoaWalletAddress = await eoaWallet.GetAddress(); -Console.WriteLine($"EOA address: {eoaWalletAddress}"); - -// Sign the authorization to point to the delegation contract -var authorization = await eoaWallet.SignAuthorization(chainWith7702, delegationContractAddress, willSelfExecute: false); -Console.WriteLine($"Authorization: {JsonConvert.SerializeObject(authorization, Formatting.Indented)}"); - -// Sign message for session key -var sessionKeyParams = new SessionKeyParams_7702() -{ - Signer = backendWalletAddress, - NativeTokenLimitPerTransaction = 0, - StartTimestamp = 0, - EndTimestamp = Utils.GetUnixTimeStampNow() + (3600 * 24), - ApprovedTargets = new List { Constants.ADDRESS_ZERO }, - Uid = Guid.NewGuid().ToByteArray() -}; -var sessionKeySig = await EIP712.GenerateSignature_SmartAccount_7702("MinimalAccount", "1", chainWith7702, eoaWalletAddress, sessionKeyParams, eoaWallet); - -// Create call data for the session key -var sessionKeyCallData = delegationContract.CreateCallData("createSessionKeyWithSig", sessionKeyParams, sessionKeySig.HexToBytes()); - -// Execute the delegation & session key creation!!!!!!!!!! -var delegationReceipt = await engineWallet.ExecuteTransaction(new ThirdwebTransactionInput(chainId: chainWith7702, to: eoaWalletAddress, data: sessionKeyCallData, authorization: authorization)); -Console.WriteLine($"Delegation Execution Receipt: {JsonConvert.SerializeObject(delegationReceipt, Formatting.Indented)}"); - -// Verify contract code deployed to the EOA -var rpc = ThirdwebRPC.GetRpcInstance(client, chainWith7702); -var code = await rpc.SendRequestAsync("eth_getCode", eoaWalletAddress, "latest"); -Console.WriteLine($"EOA code: {code}"); - -// The EOA is now a contract -var eoaContract = await ThirdwebContract.Create(client, eoaWalletAddress, chainWith7702, delegationContract.Abi); - -// -------------------------------------------------------------------------- -// Mint Tokens (DropERC20) to the EOA Using the backend session key -// -------------------------------------------------------------------------- - -var erc20ContractAddress = "0xAA462a5BE0fc5214507FDB4fB2474a7d5c69065b"; // DropERC20 -var erc20Contract = await ThirdwebContract.Create(client, erc20ContractAddress, chainWith7702); - -// Log ERC20 balance before mint -var eoaBalanceBefore = await erc20Contract.ERC20_BalanceOf(eoaWalletAddress); -Console.WriteLine($"EOA balance before: {eoaBalanceBefore}"); - -// Create execution call data (calling 'claim' on the DropERC20) -var executeCallData = eoaContract.CreateCallData( - "execute", - new object[] - { - new List - { - new() - { - Data = erc20Contract - .CreateCallData( - "claim", - new object[] - { - eoaWalletAddress, // receiver - 100, // quantity - Constants.NATIVE_TOKEN_ADDRESS, // currency - 0, // pricePerToken - new object[] { Array.Empty(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO }, // allowlistProof - Array.Empty() // data - } - ) - .HexToBytes(), - To = erc20ContractAddress, - Value = BigInteger.Zero - } - } - } -); - -var executeReceipt = await engineWallet.ExecuteTransaction(new ThirdwebTransactionInput(chainId: chainWith7702, to: eoaWalletAddress, data: executeCallData)); -Console.WriteLine($"Execute receipt: {JsonConvert.SerializeObject(executeReceipt, Formatting.Indented)}"); - -// Log ERC20 balance after mint -var eoaBalanceAfter = await erc20Contract.ERC20_BalanceOf(eoaWalletAddress); -Console.WriteLine($"EOA balance after: {eoaBalanceAfter}"); - -// -------------------------------------------------------------------------- -// Mint Tokens (DropERC20) to the EOA Using an alternative executor -// -------------------------------------------------------------------------- - -// Executor wallet (managed) -var executorWallet = await PrivateKeyWallet.Create(client, privateKey); - -// Log ERC20 balance before mint -eoaBalanceBefore = await erc20Contract.ERC20_BalanceOf(eoaWalletAddress); -Console.WriteLine($"EOA balance before: {eoaBalanceBefore}"); - -// Sign wrapped calls 712 using an authorized session key (backend wallet in this case) -var wrappedCalls = new WrappedCalls() -{ - Calls = new List - { - new() - { - Data = erc20Contract - .CreateCallData( - "claim", - new object[] - { - eoaWalletAddress, // receiver - 100, // quantity - Constants.NATIVE_TOKEN_ADDRESS, // currency - 0, // pricePerToken - new object[] { Array.Empty(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO }, // allowlistProof - Array.Empty() // data - } - ) - .HexToBytes(), - To = erc20ContractAddress, - Value = BigInteger.Zero - } - }, - Uid = Guid.NewGuid().ToByteArray().BytesToHex().HexToBytes32() -}; -var wrappedCallsSig = await EIP712.GenerateSignature_SmartAccount_7702_WrappedCalls("MinimalAccount", "1", chainWith7702, eoaWalletAddress, wrappedCalls, engineWallet); - -// Create execution call data, this time in a way that can be broadcast by anyone -executeCallData = eoaContract.CreateCallData("executeWithSig", wrappedCalls, wrappedCallsSig.HexToBytes()); - -var executeTx = await ThirdwebTransaction.Create(wallet: executorWallet, txInput: new ThirdwebTransactionInput(chainId: chainWith7702, to: eoaWalletAddress, data: executeCallData)); -executeReceipt = await ThirdwebTransaction.SendAndWaitForTransactionReceipt(executeTx); -Console.WriteLine($"Execute receipt: {JsonConvert.SerializeObject(executeReceipt, Formatting.Indented)}"); - -// Log ERC20 balance after mint -eoaBalanceAfter = await erc20Contract.ERC20_BalanceOf(eoaWalletAddress); -Console.WriteLine($"EOA balance after: {eoaBalanceAfter}"); +// // -------------------------------------------------------------------------- +// // Configuration +// // -------------------------------------------------------------------------- + +// var chainWith7702 = 911867; +// var delegationContractAddress = "0xb012446cba783d0f7723daf96cf4c49005022307"; // MinimalAccount + +// // Required environment variables +// var backendWalletAddress = Environment.GetEnvironmentVariable("ENGINE_BACKEND_WALLET_ADDRESS") ?? throw new Exception("ENGINE_BACKEND_WALLET_ADDRESS is required"); +// var engineUrl = Environment.GetEnvironmentVariable("ENGINE_URL") ?? throw new Exception("ENGINE_URL is required"); +// var engineAccessToken = Environment.GetEnvironmentVariable("ENGINE_ACCESS_TOKEN") ?? throw new Exception("ENGINE_ACCESS_TOKEN is required"); + +// // -------------------------------------------------------------------------- +// // Initialize Engine Wallet +// // -------------------------------------------------------------------------- + +// var engineWallet = await EngineWallet.Create(client, engineUrl, engineAccessToken, backendWalletAddress, 15); + +// // -------------------------------------------------------------------------- +// // Delegation Contract Implementation +// // -------------------------------------------------------------------------- + +// var delegationContract = await ThirdwebContract.Create(client, delegationContractAddress, chainWith7702); + +// // Initialize a (to-be) 7702 EOA +// var eoaWallet = await PrivateKeyWallet.Generate(client); +// var eoaWalletAddress = await eoaWallet.GetAddress(); +// Console.WriteLine($"EOA address: {eoaWalletAddress}"); + +// // Sign the authorization to point to the delegation contract +// var authorization = await eoaWallet.SignAuthorization(chainWith7702, delegationContractAddress, willSelfExecute: false); +// Console.WriteLine($"Authorization: {JsonConvert.SerializeObject(authorization, Formatting.Indented)}"); + +// // Sign message for session key +// var sessionKeyParams = new SessionKeyParams_7702() +// { +// Signer = backendWalletAddress, +// NativeTokenLimitPerTransaction = 0, +// StartTimestamp = 0, +// EndTimestamp = Utils.GetUnixTimeStampNow() + (3600 * 24), +// ApprovedTargets = new List { Constants.ADDRESS_ZERO }, +// Uid = Guid.NewGuid().ToByteArray() +// }; +// var sessionKeySig = await EIP712.GenerateSignature_SmartAccount_7702("MinimalAccount", "1", chainWith7702, eoaWalletAddress, sessionKeyParams, eoaWallet); + +// // Create call data for the session key +// var sessionKeyCallData = delegationContract.CreateCallData("createSessionKeyWithSig", sessionKeyParams, sessionKeySig.HexToBytes()); + +// // Execute the delegation & session key creation in one go, from the backend! +// var delegationReceipt = await engineWallet.ExecuteTransaction(new ThirdwebTransactionInput(chainId: chainWith7702, to: eoaWalletAddress, data: sessionKeyCallData, authorization: authorization)); +// Console.WriteLine($"Delegation Execution Receipt: {JsonConvert.SerializeObject(delegationReceipt, Formatting.Indented)}"); + +// // Verify contract code deployed to the EOA +// var rpc = ThirdwebRPC.GetRpcInstance(client, chainWith7702); +// var code = await rpc.SendRequestAsync("eth_getCode", eoaWalletAddress, "latest"); +// Console.WriteLine($"EOA code: {code}"); + +// // The EOA is now a contract +// var eoaContract = await ThirdwebContract.Create(client, eoaWalletAddress, chainWith7702, delegationContract.Abi); + +// // -------------------------------------------------------------------------- +// // Mint Tokens (DropERC20) to the EOA Using the backend session key +// // -------------------------------------------------------------------------- + +// var erc20ContractAddress = "0xAA462a5BE0fc5214507FDB4fB2474a7d5c69065b"; // DropERC20 +// var erc20Contract = await ThirdwebContract.Create(client, erc20ContractAddress, chainWith7702); + +// // Log ERC20 balance before mint +// var eoaBalanceBefore = await erc20Contract.ERC20_BalanceOf(eoaWalletAddress); +// Console.WriteLine($"EOA balance before: {eoaBalanceBefore}"); + +// // Create execution call data (calling 'claim' on the DropERC20) +// var executeCallData = eoaContract.CreateCallData( +// "execute", +// new object[] +// { +// new List +// { +// new() +// { +// Data = erc20Contract +// .CreateCallData( +// "claim", +// new object[] +// { +// eoaWalletAddress, // receiver +// 100, // quantity +// Constants.NATIVE_TOKEN_ADDRESS, // currency +// 0, // pricePerToken +// new object[] { Array.Empty(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO }, // allowlistProof +// Array.Empty() // data +// } +// ) +// .HexToBytes(), +// To = erc20ContractAddress, +// Value = BigInteger.Zero +// } +// } +// } +// ); + +// var executeReceipt = await engineWallet.ExecuteTransaction(new ThirdwebTransactionInput(chainId: chainWith7702, to: eoaWalletAddress, data: executeCallData)); +// Console.WriteLine($"Execute receipt: {JsonConvert.SerializeObject(executeReceipt, Formatting.Indented)}"); + +// // Log ERC20 balance after mint +// var eoaBalanceAfter = await erc20Contract.ERC20_BalanceOf(eoaWalletAddress); +// Console.WriteLine($"EOA balance after: {eoaBalanceAfter}"); #endregion diff --git a/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs b/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs index acf04801..543e518f 100644 --- a/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs +++ b/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs @@ -1235,7 +1235,9 @@ public static async Task ERC721_GetNFT(this ThirdwebContract contract, BigI } catch (Exception e) { +#pragma warning disable IDE0059 // Unnecessary assignment of a value metadata = new NFTMetadata { Description = e.Message }; +#pragma warning restore IDE0059 // Unnecessary assignment of a value } metadata.Id = tokenId.ToString(); @@ -1386,7 +1388,9 @@ public static async Task ERC1155_GetNFT(this ThirdwebContract contract, Big } catch (Exception e) { +#pragma warning disable IDE0059 // Unnecessary assignment of a value metadata = new NFTMetadata { Description = e.Message }; +#pragma warning restore IDE0059 // Unnecessary assignment of a value } metadata.Id = tokenId.ToString(); diff --git a/Thirdweb/Thirdweb.Utils/Utils.cs b/Thirdweb/Thirdweb.Utils/Utils.cs index d2175c15..de8fc98e 100644 --- a/Thirdweb/Thirdweb.Utils/Utils.cs +++ b/Thirdweb/Thirdweb.Utils/Utils.cs @@ -944,6 +944,13 @@ public static async Task FetchGasPrice(ThirdwebClient client, BigInt return (gasPrice, gasPrice); } + // Arbitrum, Arbitrum Nova & Arbitrum Sepolia + if (chainId == (BigInteger)42161 || chainId == (BigInteger)42170 || chainId == (BigInteger)421614) + { + var gasPrice = await FetchGasPrice(client, chainId, withBump).ConfigureAwait(false); + return (gasPrice, gasPrice); + } + try { var block = await rpc.SendRequestAsync("eth_getBlockByNumber", "latest", true).ConfigureAwait(false); @@ -1167,17 +1174,16 @@ public static List DecodeAutorizationList(byte[] authoriza foreach (var rlpElement in decodedList) { var decodedItem = (RLPCollection)rlpElement; + var signature = RLPSignedDataDecoder.DecodeSignature(decodedItem, 3); var authorizationListItem = new EIP7702Authorization { ChainId = new HexBigInteger(decodedItem[0].RLPData.ToBigIntegerFromRLPDecoded()).HexValue, Address = decodedItem[1].RLPData.BytesToHex().ToChecksumAddress(), - Nonce = new HexBigInteger(decodedItem[2].RLPData.ToBigIntegerFromRLPDecoded()).HexValue + Nonce = new HexBigInteger(decodedItem[2].RLPData.ToBigIntegerFromRLPDecoded()).HexValue, + YParity = signature.V.BytesToHex(), + R = signature.R.BytesToHex(), + S = signature.S.BytesToHex() }; - var signature = RLPSignedDataDecoder.DecodeSignature(decodedItem, 3); - authorizationListItem.YParity = signature.V.BytesToHex(); - authorizationListItem.R = signature.R.BytesToHex(); - authorizationListItem.S = signature.S.BytesToHex(); - authorizationLists.Add(authorizationListItem); }