diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7c18596 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +.PHONY: run + +run: + dotnet run --project Thirdweb.Console diff --git a/Thirdweb.Console/Program.Types.cs b/Thirdweb.Console/Program.Types.cs new file mode 100644 index 0000000..701c22c --- /dev/null +++ b/Thirdweb.Console/Program.Types.cs @@ -0,0 +1,16 @@ +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 e8d52ab..ac5b856 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -91,6 +91,92 @@ #endregion +#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}"); + +#endregion + #region Smart Ecosystem Wallet // var eco = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.the-bonfire", authProvider: AuthProvider.Twitch); diff --git a/Thirdweb.Tests/Thirdweb.Contracts/Thirdweb.Contracts.Tests.cs b/Thirdweb.Tests/Thirdweb.Contracts/Thirdweb.Contracts.Tests.cs index b2c9293..e8a8314 100644 --- a/Thirdweb.Tests/Thirdweb.Contracts/Thirdweb.Contracts.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Contracts/Thirdweb.Contracts.Tests.cs @@ -78,6 +78,14 @@ public async Task ReadTest_Tuple() Assert.Equal(0, result.ReturnValue2); } + [Fact(Timeout = 120000)] + public async Task ReadTest_4Bytes() + { + var contract = await this.GetContract(); + var result = await ThirdwebContract.Read(contract, "0x06fdde03"); + Assert.Equal("Kitty DropERC20", result); + } + [Fact(Timeout = 120000)] public async Task ReadTest_FullSig() { diff --git a/Thirdweb.Tests/Thirdweb.Utils/Thirdweb.Utils.Tests.cs b/Thirdweb.Tests/Thirdweb.Utils/Thirdweb.Utils.Tests.cs index 35afbb3..f1802b9 100644 --- a/Thirdweb.Tests/Thirdweb.Utils/Thirdweb.Utils.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Utils/Thirdweb.Utils.Tests.cs @@ -866,4 +866,32 @@ public void PreprocessTypedDataJson_NestedLargeNumbers() Assert.Equal(expectedJObject, processedJObject); } + + [Fact] + public void DecodeTransaction_1559WithAuthList() + { + var signedTxStr = + "0x04f8ca830de9fb8082011882031083025bee94ff5d95e5aa1b5af3f106079518228a92818737728080c0f85ef85c830de9fb94654f42b74885ee6803f403f077bc0409f1066c588080a0a5caed9b0c46657a452250a3279f45937940c87c45854aead6a902d99bc638f39faa58026c6b018d36b8935a42f2bcf68097c712c9f09ca014c70887678e08a980a027ecc69e66eb9e28cbe6edab10fc827fcb6d2a34cdcb89d8b6aabc6e35608692a0750d306b04a50a35de57bd6aca11f207a8dd404f9d92502ce6e3817e52f79a1c"; + (var txInput, var signature) = Utils.DecodeTransaction(signedTxStr); + Assert.Equal("0xfF5D95e5aA1B5Af3F106079518228A9281873772", txInput.To); + Assert.Equal("0x", txInput.Data); + Assert.Equal(0, txInput.Value.Value); + Assert.NotNull(txInput.AuthorizationList); + _ = Assert.Single(txInput.AuthorizationList); + Assert.Equal("0x654F42b74885EE6803F403f077bc0409f1066c58", txInput.AuthorizationList[0].Address); + Assert.Equal("0xde9fb", txInput.AuthorizationList[0].ChainId); + Assert.Equal("0x0", txInput.AuthorizationList[0].Nonce); + + (txInput, var signature2) = Utils.DecodeTransaction(signedTxStr.HexToBytes()); + Assert.Equal("0xfF5D95e5aA1B5Af3F106079518228A9281873772", txInput.To); + Assert.Equal("0x", txInput.Data); + Assert.Equal(0, txInput.Value.Value); + Assert.NotNull(txInput.AuthorizationList); + _ = Assert.Single(txInput.AuthorizationList); + Assert.Equal("0x654F42b74885EE6803F403f077bc0409f1066c58", txInput.AuthorizationList[0].Address); + Assert.Equal("0xde9fb", txInput.AuthorizationList[0].ChainId); + Assert.Equal("0x0", txInput.AuthorizationList[0].Nonce); + + Assert.Equal(signature, signature2); + } } diff --git a/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.PrivateKeyWallet.Tests.cs b/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.PrivateKeyWallet.Tests.cs index 3183341..145c6c7 100644 --- a/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.PrivateKeyWallet.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.PrivateKeyWallet.Tests.cs @@ -209,16 +209,36 @@ public async Task SignTypedDataV4_Typed_NullData() public async Task SignTransaction_Success() { var account = await this.GetAccount(); - var transaction = new ThirdwebTransactionInput(421614) - { - From = await account.GetAddress(), - To = Constants.ADDRESS_ZERO, - // Value = new HexBigInteger(0), - Gas = new HexBigInteger(21000), - // Data = "0x", - Nonce = new HexBigInteger(99999999999), - GasPrice = new HexBigInteger(10000000000), - }; + var transaction = new ThirdwebTransactionInput( + chainId: 421614, + from: await account.GetAddress(), + to: Constants.ADDRESS_ZERO, + value: 0, + gas: 21000, + data: "0x", + nonce: 99999999999, + gasPrice: 10000000000 + ); + var signature = await account.SignTransaction(transaction); + Assert.NotNull(signature); + } + + [Fact(Timeout = 120000)] + public async Task SignTransaction_WithAuthorizationList_Success() + { + var account = await this.GetAccount(); + var authorization = await account.SignAuthorization(421614, Constants.ADDRESS_ZERO, false); + var transaction = new ThirdwebTransactionInput( + chainId: 421614, + from: await account.GetAddress(), + to: Constants.ADDRESS_ZERO, + value: 0, + gas: 21000, + data: "0x", + nonce: 99999999999, + gasPrice: 10000000000, + authorization: authorization + ); var signature = await account.SignTransaction(transaction); Assert.NotNull(signature); } @@ -227,15 +247,7 @@ public async Task SignTransaction_Success() public async Task SignTransaction_NoFrom_Success() { var account = await this.GetAccount(); - var transaction = new ThirdwebTransactionInput(421614) - { - To = Constants.ADDRESS_ZERO, - // Value = new HexBigInteger(0), - Gas = new HexBigInteger(21000), - Data = "0x", - Nonce = new HexBigInteger(99999999999), - GasPrice = new HexBigInteger(10000000000), - }; + var transaction = new ThirdwebTransactionInput(chainId: 421614, to: Constants.ADDRESS_ZERO, value: 0, gas: 21000, data: "0x", nonce: 99999999999, gasPrice: 10000000000); var signature = await account.SignTransaction(transaction); Assert.NotNull(signature); } @@ -469,4 +481,26 @@ public async Task Export_ReturnsPrivateKey() Assert.NotNull(privateKey); Assert.Equal(privateKey, await wallet.Export()); } + + [Fact(Timeout = 120000)] + public async Task SignAuthorization_SelfExecution() + { + var wallet = await PrivateKeyWallet.Generate(this.Client); + var chainId = 911867; + var targetAddress = "0x654F42b74885EE6803F403f077bc0409f1066c58"; + + var currentNonce = await wallet.GetTransactionCount(chainId); + + var authorization = await wallet.SignAuthorization(chainId: chainId, contractAddress: targetAddress, willSelfExecute: false); + + Assert.Equal(chainId.NumberToHex(), authorization.ChainId); + Assert.Equal(targetAddress, authorization.Address); + Assert.True(authorization.Nonce.HexToNumber() == currentNonce); + + authorization = await wallet.SignAuthorization(chainId: chainId, contractAddress: targetAddress, willSelfExecute: true); + + Assert.Equal(chainId.NumberToHex(), authorization.ChainId); + Assert.Equal(targetAddress, authorization.Address); + Assert.True(authorization.Nonce.HexToNumber() == currentNonce + 1); + } } diff --git a/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.SmartWallet.Tests.cs b/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.SmartWallet.Tests.cs index c5ace0e..23c3f3a 100644 --- a/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.SmartWallet.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.SmartWallet.Tests.cs @@ -364,6 +364,18 @@ public async Task SwitchNetwork_NonZkToZk_Success() Assert.NotEqual(addy1, addy2); } + [Fact(Timeout = 120000)] + public async Task SignAuthorization_WithPrivateKeyWallet_Success() + { + var smartWallet = await SmartWallet.Create(personalWallet: await PrivateKeyWallet.Generate(this.Client), chainId: 421614); + var smartWalletSigner = await smartWallet.GetPersonalWallet(); + var signature1 = await smartWallet.SignAuthorization(chainId: 421614, contractAddress: Constants.ADDRESS_ZERO, willSelfExecute: true); + var signature2 = await smartWalletSigner.SignAuthorization(chainId: 421614, contractAddress: Constants.ADDRESS_ZERO, willSelfExecute: true); + Assert.Equal(signature1.ChainId, signature2.ChainId); + Assert.Equal(signature1.Address, signature2.Address); + Assert.Equal(signature1.Nonce, signature2.Nonce); + } + // [Fact(Timeout = 120000)] // public async Task MultiChainTransaction_Success() // { diff --git a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs index 54ff023..97b011a 100644 --- a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs +++ b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs @@ -182,19 +182,6 @@ internal static (string callData, Function function) EncodeFunctionCall(Thirdweb { var contractRaw = new Contract(null, contract.Abi, contract.Address); var function = GetFunctionMatchSignature(contractRaw, method, parameters); - if (function == null) - { - if (method.Contains('(')) - { - var canonicalSignature = ExtractCanonicalSignature(method); - var selector = Nethereum.Util.Sha3Keccack.Current.CalculateHash(canonicalSignature)[..8]; - function = contractRaw.GetFunctionBySignature(selector); - } - else - { - throw new ArgumentException("Method signature not found in contract ABI."); - } - } return (function.GetData(parameters), function); } @@ -207,6 +194,11 @@ internal static (string callData, Function function) EncodeFunctionCall(Thirdweb /// The matching function, or null if no match is found. private static Function GetFunctionMatchSignature(Contract contract, string functionName, params object[] args) { + if (functionName.StartsWith("0x")) + { + return contract.GetFunctionBySignature(functionName); + } + var abi = contract.ContractBuilder.ContractABI; var functions = abi.Functions; var paramsCount = args?.Length ?? 0; @@ -218,7 +210,17 @@ private static Function GetFunctionMatchSignature(Contract contract, string func return contract.GetFunctionBySignature(sha); } } - return null; + + if (functionName.Contains('(')) + { + var canonicalSignature = ExtractCanonicalSignature(functionName); + var selector = Utils.HashMessage(canonicalSignature)[..8]; + return contract.GetFunctionBySignature(selector); + } + else + { + throw new ArgumentException("Method signature not found in contract ABI."); + } } /// diff --git a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs index cd3abb9..7a08e46 100644 --- a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs +++ b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs @@ -261,17 +261,24 @@ public static async Task Simulate(ThirdwebTransaction transaction) public static async Task EstimateGasLimit(ThirdwebTransaction transaction) { var rpc = ThirdwebRPC.GetRpcInstance(transaction._wallet.Client, transaction.Input.ChainId.Value); - - if (await Utils.IsZkSync(transaction._wallet.Client, transaction.Input.ChainId.Value).ConfigureAwait(false)) + var isZkSync = await Utils.IsZkSync(transaction._wallet.Client, transaction.Input.ChainId.Value).ConfigureAwait(false); + BigInteger divider = isZkSync + ? 7 + : transaction.Input.AuthorizationList == null + ? 5 + : 3; + BigInteger baseGas; + if (isZkSync) { var hex = (await rpc.SendRequestAsync("zks_estimateFee", transaction.Input).ConfigureAwait(false))["gas_limit"].ToString(); - return new HexBigInteger(hex).Value * 10 / 5; + baseGas = hex.HexToNumber(); } else { var hex = await rpc.SendRequestAsync("eth_estimateGas", transaction.Input).ConfigureAwait(false); - return new HexBigInteger(hex).Value * 10 / 7; + baseGas = hex.HexToNumber(); } + return baseGas * 10 / divider; } /// @@ -357,6 +364,7 @@ public static async Task Send(ThirdwebTransaction transaction) var rpc = ThirdwebRPC.GetRpcInstance(transaction._wallet.Client, transaction.Input.ChainId.Value); string hash; + if (await Utils.IsZkSync(transaction._wallet.Client, transaction.Input.ChainId.Value).ConfigureAwait(false) && transaction.Input.ZkSync.HasValue) { var zkTx = await ConvertToZkSyncTransaction(transaction).ConfigureAwait(false); diff --git a/Thirdweb/Thirdweb.Transactions/ThirdwebTransactionInput.cs b/Thirdweb/Thirdweb.Transactions/ThirdwebTransactionInput.cs index d6ef3c9..f3ed9cd 100644 --- a/Thirdweb/Thirdweb.Transactions/ThirdwebTransactionInput.cs +++ b/Thirdweb/Thirdweb.Transactions/ThirdwebTransactionInput.cs @@ -26,7 +26,8 @@ public ThirdwebTransactionInput( string data = null, BigInteger? maxFeePerGas = null, BigInteger? maxPriorityFeePerGas = null, - ZkSyncOptions? zkSync = null + ZkSyncOptions? zkSync = null, + EIP7702Authorization? authorization = null ) { this.ChainId = chainId > 0 ? new HexBigInteger(chainId) : throw new ArgumentException("Invalid Chain ID"); @@ -40,6 +41,7 @@ public ThirdwebTransactionInput( this.MaxFeePerGas = maxFeePerGas == null ? null : new HexBigInteger(maxFeePerGas.Value); this.MaxPriorityFeePerGas = maxPriorityFeePerGas == null ? null : new HexBigInteger(maxPriorityFeePerGas.Value); this.ZkSync = zkSync; + this.AuthorizationList = authorization == null ? null : new List { authorization.Value }; } /// @@ -123,6 +125,11 @@ public string Data /// [JsonProperty(PropertyName = "zkSyncOptions", NullValueHandling = NullValueHandling.Ignore)] public ZkSyncOptions? ZkSync { get; set; } + +#nullable enable + [JsonProperty(PropertyName = "authorizationList", NullValueHandling = NullValueHandling.Ignore)] + public List? AuthorizationList { get; set; } +#nullable disable } /// @@ -179,3 +186,34 @@ public ZkSyncOptions(string paymaster = null, string paymasterInput = null, BigI } } } + +public struct EIP7702Authorization +{ + [JsonProperty(PropertyName = "chainId")] + public string ChainId { get; set; } + + [JsonProperty(PropertyName = "address")] + public string Address { get; set; } + + [JsonProperty(PropertyName = "nonce")] + public string Nonce { get; set; } + + [JsonProperty(PropertyName = "yParity")] + public string YParity { get; set; } + + [JsonProperty(PropertyName = "r")] + public string R { get; set; } + + [JsonProperty(PropertyName = "s")] + public string S { get; set; } + + public EIP7702Authorization(BigInteger chainId, string address, BigInteger nonce, byte[] yParity, byte[] r, byte[] s) + { + this.ChainId = new HexBigInteger(chainId).HexValue; + this.Address = address; + this.Nonce = new HexBigInteger(nonce).HexValue; + this.YParity = yParity.BytesToHex(); + this.R = r.BytesToHex(); + this.S = s.BytesToHex(); + } +} diff --git a/Thirdweb/Thirdweb.Utils/Utils.cs b/Thirdweb/Thirdweb.Utils/Utils.cs index 9ecb99a..5a09f07 100644 --- a/Thirdweb/Thirdweb.Utils/Utils.cs +++ b/Thirdweb/Thirdweb.Utils/Utils.cs @@ -12,6 +12,8 @@ using Nethereum.Contracts; using Nethereum.Hex.HexConvertors.Extensions; using Nethereum.Hex.HexTypes; +using Nethereum.Model; +using Nethereum.RLP; using Nethereum.Signer; using Nethereum.Util; using Newtonsoft.Json; @@ -136,11 +138,46 @@ public static byte[] HexToBytes(this string hex) /// /// The hex string to convert. /// The big integer. + [Obsolete("Use HexToNumber instead.")] public static BigInteger HexToBigInt(this string hex) { return new HexBigInteger(hex).Value; } + /// + /// Converts the given hex string to a big integer. + /// + /// The hex string to convert. + /// The big integer. + public static BigInteger HexToNumber(this string hex) + { + return new HexBigInteger(hex).Value; + } + + /// + /// Converts the given big integer to a hex string. + /// + public static string NumberToHex(this BigInteger number) + { + return new HexBigInteger(number).HexValue; + } + + /// + /// Converts the given integer to a hex string. + /// + public static string NumberToHex(this int number) + { + return NumberToHex(new BigInteger(number)); + } + + /// + /// Converts the given long to a hex string. + /// + public static string NumberToHex(this long number) + { + return NumberToHex(new BigInteger(number)); + } + /// /// Converts the given string to a hex string. /// @@ -883,7 +920,7 @@ public static async Task FetchGasPrice(ThirdwebClient client, BigInt { var rpc = ThirdwebRPC.GetRpcInstance(client, chainId); var hex = await rpc.SendRequestAsync("eth_gasPrice").ConfigureAwait(false); - var gasPrice = hex.HexToBigInt(); + var gasPrice = hex.HexToNumber(); return withBump ? gasPrice * 10 / 9 : gasPrice; } @@ -1034,4 +1071,124 @@ public static string SerializeErc6492Signature(string address, byte[] data, byte var encodedParams = encoder.GetABIEncoded(new ABIValue("address", address), new ABIValue("bytes", data), new ABIValue("bytes", signature)); return HexConcat(encodedParams.BytesToHex(), Constants.ERC_6492_MAGIC_VALUE); } + + /// + /// Removes leading zeroes from the given byte array. + /// + public static byte[] TrimZeroes(this byte[] bytes) + { + var trimmed = new List(); + var previousByteWasZero = true; + + for (var i = 0; i < bytes.Length; i++) + { + if (previousByteWasZero && bytes[i] == 0) + { + continue; + } + + previousByteWasZero = false; + trimmed.Add(bytes[i]); + } + + return trimmed.ToArray(); + } + + /// + /// Decodes the given RLP-encoded transaction data. + /// + /// The RLP-encoded signed transaction data. + /// The decoded transaction input and signature. + public static (ThirdwebTransactionInput transactionInput, string signature) DecodeTransaction(string signedRlpData) + { + return DecodeTransaction(signedRlpData.HexToBytes()); + } + + /// + /// Decodes the given RLP-encoded transaction data. + /// + /// The RLP-encoded signed transaction data. + /// The decoded transaction input and signature. + public static (ThirdwebTransactionInput transactionInput, string signature) DecodeTransaction(byte[] signedRlpData) + { + var txType = signedRlpData[0]; + if (txType is 0x04 or 0x02) + { + signedRlpData = signedRlpData.Skip(1).ToArray(); + } + + var decodedList = RLP.Decode(signedRlpData); + var decodedElements = (RLPCollection)decodedList; + var chainId = decodedElements[0].RLPData.ToBigIntegerFromRLPDecoded(); + var nonce = decodedElements[1].RLPData.ToBigIntegerFromRLPDecoded(); + var maxPriorityFeePerGas = decodedElements[2].RLPData.ToBigIntegerFromRLPDecoded(); + var maxFeePerGas = decodedElements[3].RLPData.ToBigIntegerFromRLPDecoded(); + var gasLimit = decodedElements[4].RLPData.ToBigIntegerFromRLPDecoded(); + var receiverAddress = decodedElements[5].RLPData?.BytesToHex(); + var amount = decodedElements[6].RLPData.ToBigIntegerFromRLPDecoded(); + var data = decodedElements[7].RLPData?.BytesToHex(); + // 8th decoded element is access list + var authorizations = txType == 0x04 ? DecodeAutorizationList(decodedElements[9]?.RLPData) : null; + + var signature = RLPSignedDataDecoder.DecodeSignature(decodedElements, txType == 0x04 ? 10 : 9); + return ( + new ThirdwebTransactionInput( + chainId: chainId, + to: receiverAddress.ToChecksumAddress(), + nonce: nonce, + gas: gasLimit, + value: amount, + data: data, + maxFeePerGas: maxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas + ) + { + AuthorizationList = authorizations + }, + signature.CreateStringSignature() + ); + } + + /// + /// Decodes the given RLP-encoded authorization list. + /// + public static List DecodeAutorizationList(byte[] authorizationListEncoded) + { + if (authorizationListEncoded == null || authorizationListEncoded.Length == 0 || authorizationListEncoded[0] == RLP.OFFSET_SHORT_LIST) + { + return null; + } + + var decodedList = (RLPCollection)RLP.Decode(authorizationListEncoded); + + var authorizationLists = new List(); + foreach (var rlpElement in decodedList) + { + var decodedItem = (RLPCollection)rlpElement; + 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 + }; + var signature = RLPSignedDataDecoder.DecodeSignature(decodedItem, 3); + authorizationListItem.YParity = signature.V.BytesToHex(); + authorizationListItem.R = signature.R.BytesToHex(); + authorizationListItem.S = signature.S.BytesToHex(); + + authorizationLists.Add(authorizationListItem); + } + + return authorizationLists; + } + + internal static byte[] ToByteArrayForRLPEncoding(this BigInteger value) + { + if (value == 0) + { + return Array.Empty(); + } + + return value.ToBytesForRLPEncoding(); + } } diff --git a/Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs b/Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs index 164d4b1..54bd155 100644 --- a/Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs @@ -1,4 +1,5 @@ -using Nethereum.ABI.EIP712; +using System.Numerics; +using Nethereum.ABI.EIP712; using Newtonsoft.Json; namespace Thirdweb; @@ -150,7 +151,7 @@ Task> LinkAccount( Action browserOpenAction = null, string mobileRedirectScheme = "thirdweb://", IThirdwebBrowser browser = null, - System.Numerics.BigInteger? chainId = null, + BigInteger? chainId = null, string jwt = null, string payload = null ); @@ -166,6 +167,15 @@ Task> LinkAccount( /// /// A list of objects. Task> GetLinkedAccounts(); + + /// + /// Signs an EIP-7702 authorization to invoke contract functions to an externally owned account. + /// + /// The chain ID of the contract. + /// The address of the contract. + /// Set to true if the wallet will also be the executor of the transaction, otherwise false. + /// The signed authorization as an that can be used with . + Task SignAuthorization(BigInteger chainId, string contractAddress, bool willSelfExecute); } /// diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs index 8a2231e..376cf37 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs @@ -931,5 +931,10 @@ public virtual Task RecoverAddressFromTypedDataV4(T data, Ty return Task.FromResult(address); } + public Task SignAuthorization(BigInteger chainId, string contractAddress, bool willSelfExecute) + { + throw new NotImplementedException(); + } + #endregion } diff --git a/Thirdweb/Thirdweb.Wallets/PrivateKeyWallet/PrivateKeyWallet.cs b/Thirdweb/Thirdweb.Wallets/PrivateKeyWallet/PrivateKeyWallet.cs index 26d90a4..481b92b 100644 --- a/Thirdweb/Thirdweb.Wallets/PrivateKeyWallet/PrivateKeyWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/PrivateKeyWallet/PrivateKeyWallet.cs @@ -2,8 +2,7 @@ using System.Text; using Nethereum.ABI.EIP712; using Nethereum.Hex.HexConvertors.Extensions; -using Nethereum.Hex.HexTypes; -using Nethereum.Model; +using Nethereum.RLP; using Nethereum.Signer; using Nethereum.Signer.EIP712; @@ -299,25 +298,24 @@ public virtual Task SignTransaction(ThirdwebTransactionInput transaction throw new ArgumentNullException(nameof(transaction)); } - var nonce = transaction.Nonce ?? throw new ArgumentNullException(nameof(transaction), "Transaction nonce has not been set"); - - var gasLimit = transaction.Gas; - var value = transaction.Value ?? new HexBigInteger(0); + if (transaction.Nonce == null) + { + throw new ArgumentNullException(nameof(transaction), "Transaction nonce has not been set"); + } string signedTransaction; if (transaction.GasPrice != null) { - var gasPrice = transaction.GasPrice; var legacySigner = new LegacyTransactionSigner(); signedTransaction = legacySigner.SignTransaction( this.EcKey.GetPrivateKey(), transaction.ChainId.Value, transaction.To, - value.Value, - nonce, - gasPrice.Value, - gasLimit.Value, + transaction.Value.Value, + transaction.Nonce.Value, + transaction.GasPrice.Value, + transaction.Gas.Value, transaction.Data ); } @@ -327,13 +325,77 @@ public virtual Task SignTransaction(ThirdwebTransactionInput transaction { throw new InvalidOperationException("Transaction MaxPriorityFeePerGas and MaxFeePerGas must be set for EIP-1559 transactions"); } - var maxPriorityFeePerGas = transaction.MaxPriorityFeePerGas.Value; - var maxFeePerGas = transaction.MaxFeePerGas.Value; - var transaction1559 = new Transaction1559(transaction.ChainId.Value, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, transaction.To, value, transaction.Data, null); - var signer = new Transaction1559Signer(); - _ = signer.SignTransaction(this.EcKey, transaction1559); - signedTransaction = transaction1559.GetRLPEncoded().ToHex(); + var encodedData = new List + { + RLP.EncodeElement(transaction.ChainId.Value.ToByteArrayForRLPEncoding()), + RLP.EncodeElement(transaction.Nonce.Value.ToByteArrayForRLPEncoding()), + RLP.EncodeElement(transaction.MaxPriorityFeePerGas.Value.ToByteArrayForRLPEncoding()), + RLP.EncodeElement(transaction.MaxFeePerGas.Value.ToByteArrayForRLPEncoding()), + RLP.EncodeElement(transaction.Gas.Value.ToByteArrayForRLPEncoding()), + RLP.EncodeElement(transaction.To.HexToBytes()), + RLP.EncodeElement(transaction.Value.Value.ToByteArrayForRLPEncoding()), + RLP.EncodeElement(transaction.Data == null ? Array.Empty() : transaction.Data.HexToBytes()), + new byte[] { 0xc0 }, // AccessList, empty so short list bytes + }; + + if (transaction.AuthorizationList != null) + { + var encodedAuthorizationList = new List(); + foreach (var authorizationList in transaction.AuthorizationList) + { + var encodedItem = new List() + { + RLP.EncodeElement(authorizationList.ChainId.HexToNumber().ToByteArrayForRLPEncoding()), + RLP.EncodeElement(authorizationList.Address.HexToBytes()), + RLP.EncodeElement(authorizationList.Nonce.HexToNumber().ToByteArrayForRLPEncoding()), + RLP.EncodeElement(authorizationList.YParity == "0x00" ? Array.Empty() : authorizationList.YParity.HexToBytes()), + RLP.EncodeElement(authorizationList.R.HexToBytes().TrimZeroes()), + RLP.EncodeElement(authorizationList.S.HexToBytes().TrimZeroes()) + }; + encodedAuthorizationList.Add(RLP.EncodeList(encodedItem.ToArray())); + } + encodedData.Add(RLP.EncodeList(encodedAuthorizationList.ToArray())); + } + + var encodedBytes = RLP.EncodeList(encodedData.ToArray()); + var returnBytes = new byte[encodedBytes.Length + 1]; + Array.Copy(encodedBytes, 0, returnBytes, 1, encodedBytes.Length); + returnBytes[0] = transaction.AuthorizationList != null ? (byte)0x04 : (byte)0x02; + + var rawHash = Utils.HashMessage(returnBytes); + var rawSignature = this.EcKey.SignAndCalculateYParityV(rawHash); + + byte[] v; + byte[] r; + byte[] s; + if (rawSignature.V.Length == 0 || rawSignature.V[0] == 0) + { + v = Array.Empty(); + } + else + { + v = rawSignature.V; + } + v = RLP.EncodeElement(v); + r = RLP.EncodeElement(rawSignature.R.TrimZeroes()); + s = RLP.EncodeElement(rawSignature.S.TrimZeroes()); + + encodedData.Add(v); + encodedData.Add(r); + encodedData.Add(s); + + encodedBytes = RLP.EncodeList(encodedData.ToArray()); + returnBytes = new byte[encodedBytes.Length + 1]; + Array.Copy(encodedBytes, 0, returnBytes, 1, encodedBytes.Length); + returnBytes[0] = transaction.AuthorizationList != null ? (byte)0x04 : (byte)0x02; + + // (var tx, var sig) = Utils.DecodeTransaction(returnBytes); + + signedTransaction = returnBytes.ToHex(); + Console.WriteLine(signedTransaction); + + // (var tx, var sig) = Utils.DecodeTransaction("0x" + signedTransaction); } return Task.FromResult("0x" + signedTransaction); @@ -385,5 +447,27 @@ public Task> UnlinkAccount(LinkedAccount accountToUnlink) throw new InvalidOperationException("UnlinkAccount is not supported for private key wallets."); } + public async Task SignAuthorization(BigInteger chainId, string contractAddress, bool willSelfExecute) + { + var nonce = await this.GetTransactionCount(chainId); + if (willSelfExecute) + { + nonce++; + } + var encodedData = new List + { + RLP.EncodeElement(chainId.ToByteArrayForRLPEncoding()), + RLP.EncodeElement(contractAddress.HexToBytes()), + RLP.EncodeElement(nonce.ToByteArrayForRLPEncoding()) + }; + var encodedBytes = RLP.EncodeList(encodedData.ToArray()); + var returnElements = new byte[encodedBytes.Length + 1]; + Array.Copy(encodedBytes.ToArray(), 0, returnElements, 1, encodedBytes.Length); + returnElements[0] = 0x05; + var authorizationHash = Utils.HashMessage(returnElements); + var authorizationSignature = this.EcKey.SignAndCalculateYParityV(authorizationHash); + return new EIP7702Authorization(chainId, contractAddress, nonce, authorizationSignature.V, authorizationSignature.R, authorizationSignature.S); + } + #endregion } diff --git a/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs b/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs index 68548c8..37215a4 100644 --- a/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs @@ -1225,5 +1225,10 @@ public async Task> GetLinkedAccounts() } } + public Task SignAuthorization(BigInteger chainId, string contractAddress, bool willSelfExecute) + { + return this._personalAccount.SignAuthorization(chainId, contractAddress, willSelfExecute); + } + #endregion }