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 3e6c1333..de0dec3a 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; @@ -143,74 +147,120 @@ #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 -// // Chain and contract addresses +// // -------------------------------------------------------------------------- +// // Configuration +// // -------------------------------------------------------------------------- + // var chainWith7702 = 911867; -// var erc20ContractAddress = "0xAA462a5BE0fc5214507FDB4fB2474a7d5c69065b"; // Fake ERC20 -// var delegationContractAddress = "0x654F42b74885EE6803F403f077bc0409f1066c58"; // BatchCallDelegation +// 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 +// // -------------------------------------------------------------------------- -// // 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); +// 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}"); -// // 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}"); +// // 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)}"); -// // 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 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); -// // 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)}"); +// // Create call data for the session key +// var sessionKeyCallData = delegationContract.CreateCallData("createSessionKeyWithSig", sessionKeyParams, sessionKeySig.HexToBytes()); -// // 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}"); +// // 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)}"); -// // Prove that code has been deployed to the eoa +// // 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}"); -// // Log erc20 balance of executor before the claim -// var executorBalanceBefore = await erc20Contract.ERC20_BalanceOf(executorWalletAddress); -// Console.WriteLine($"Executor balance before: {executorBalanceBefore}"); +// // The EOA is now a contract +// var eoaContract = await ThirdwebContract.Create(client, eoaWalletAddress, chainWith7702, delegationContract.Abi); -// // 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 -// } -// ); +// // -------------------------------------------------------------------------- +// // Mint Tokens (DropERC20) to the EOA Using the backend session key +// // -------------------------------------------------------------------------- + +// var erc20ContractAddress = "0xAA462a5BE0fc5214507FDB4fB2474a7d5c69065b"; // DropERC20 +// var erc20Contract = await ThirdwebContract.Create(client, erc20ContractAddress, chainWith7702); -// // Embed the claim call in the execute call -// var executeCallData = delegationContract.CreateCallData( -// method: "execute", -// parameters: new object[] +// // 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 List // { // new() // { -// Data = claimCallData.HexToBytes(), +// 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 // } @@ -218,14 +268,12 @@ // } // ); -// // 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}"); +// 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 of executor after the claim -// var executorBalanceAfter = await erc20Contract.ERC20_BalanceOf(executorWalletAddress); -// Console.WriteLine($"Executor balance after: {executorBalanceAfter}"); +// // 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.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 935fa6d4..de8fc98e 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; @@ -942,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); @@ -1165,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); } diff --git a/Thirdweb/Thirdweb.Wallets/EIP712.cs b/Thirdweb/Thirdweb.Wallets/EIP712.cs index 30fab61b..da749f15 100644 --- a/Thirdweb/Thirdweb.Wallets/EIP712.cs +++ b/Thirdweb/Thirdweb.Wallets/EIP712.cs @@ -14,6 +14,51 @@ 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. + /// + /// 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 +225,54 @@ 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. + /// + /// 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..513abae1 --- /dev/null +++ b/Thirdweb/Thirdweb.Wallets/EngineWallet/EngineWallet.cs @@ -0,0 +1,417 @@ +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 + + 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 httpClient.GetAsync($"{engineUrl}/transaction/status/{queueId}"); + var content = await statusResponse.Content.ReadAsStringAsync(); + var response = JObject.Parse(content); + + var isErrored = response["result"]?["status"]?.ToString() is "errored" or "cancelled"; + if (isErrored) + { + throw new Exception($"Failed to send transaction: {response["result"]?["errorMessage"]?.ToString()}"); + } + + transactionHash = response["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); + // 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"; + + 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"); + + 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 WaitForQueueId(this._engineClient, this._engineUrl, 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..4da64e62 100644 --- a/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/AATypes.cs +++ b/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/AATypes.cs @@ -485,3 +485,59 @@ 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; } +} + +[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; } +}