diff --git a/Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/BundlerClient.cs b/Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/BundlerClient.cs index 0b74a86b..1de85153 100644 --- a/Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/BundlerClient.cs +++ b/Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/BundlerClient.cs @@ -2,6 +2,7 @@ using System.Net.Http; using System.Threading.Tasks; using Nethereum.JsonRpc.Client.RpcMessages; +using Nethereum.RPC.Eth.DTOs; using Newtonsoft.Json; namespace Thirdweb.AccountAbstraction @@ -69,6 +70,25 @@ string entryPoint } } + public static async Task ZkPaymasterData(string paymasterUrl, string apiKey, string bundleId, object requestId, TransactionInput txInput) + { + var response = await BundlerRequest(paymasterUrl, apiKey, bundleId, requestId, "zk_paymasterData", txInput); + try + { + return JsonConvert.DeserializeObject(response.Result.ToString()); + } + catch + { + return new ZkPaymasterDataResponse() { paymaster = null, paymasterInput = null }; + } + } + + public static async Task ZkBroadcastTransaction(string paymasterUrl, string apiKey, string bundleId, object requestId, object txInput) + { + var response = await BundlerRequest(paymasterUrl, apiKey, bundleId, requestId, "zk_broadcastTransaction", txInput); + return JsonConvert.DeserializeObject(response.Result.ToString()); + } + // Request private static async Task BundlerRequest(string url, string apiKey, string bundleId, object requestId, string method, params object[] args) diff --git a/Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/SmartWallet.cs b/Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/SmartWallet.cs index b9c969a2..1b41f3f6 100644 --- a/Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/SmartWallet.cs +++ b/Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/SmartWallet.cs @@ -18,6 +18,7 @@ using Thirdweb.Wallets; using Nethereum.Signer; using System.Security.Cryptography; +using Newtonsoft.Json.Linq; namespace Thirdweb.AccountAbstraction { @@ -50,6 +51,7 @@ public class SmartWallet public bool IsDeploying => _deploying; private readonly ThirdwebSDK _sdk; + private bool IsZkSync => _sdk.Session.ChainId == 300 || _sdk.Session.ChainId == 324; public SmartWallet(IThirdwebWallet personalWallet, ThirdwebSDK sdk) { @@ -72,6 +74,13 @@ internal async Task Initialize(string smartWalletOverride = null) if (_initialized) return; + if (IsZkSync) + { + Accounts = new List() { await GetPersonalAddress() }; + _initialized = true; + return; + } + var predictedAccount = smartWalletOverride ?? ( @@ -95,6 +104,12 @@ internal async Task Initialize(string smartWalletOverride = null) internal async Task UpdateDeploymentStatus() { + if (IsZkSync) + { + _deployed = true; + return; + } + var web3 = Utils.GetWeb3(_sdk.Session.ChainId, _sdk.Session.Options.clientId, _sdk.Session.Options.bundleId); var bytecode = await web3.Eth.GetCode.SendRequestAsync(Accounts[0]); _deployed = bytecode != "0x"; @@ -102,11 +117,20 @@ internal async Task UpdateDeploymentStatus() internal async Task SetPermissionsForSigner(SignerPermissionRequest signerPermissionRequest, byte[] signature) { + if (IsZkSync) + { + throw new NotImplementedException("SetPermissionsForSigner is not supported on zkSync"); + } return await TransactionManager.ThirdwebWrite(_sdk, Accounts[0], new SetPermissionsForSignerFunction() { Req = signerPermissionRequest, Signature = signature }); } internal async Task ForceDeploy() { + if (IsZkSync) + { + return; + } + if (_deployed) return; @@ -118,6 +142,11 @@ internal async Task ForceDeploy() internal async Task VerifySignature(byte[] hash, byte[] signature) { + if (IsZkSync) + { + throw new NotImplementedException("VerifySignature is not supported on zkSync"); + } + try { var verifyRes = await TransactionManager.ThirdwebRead( @@ -136,6 +165,11 @@ internal async Task VerifySignature(byte[] hash, byte[] signature) internal async Task<(byte[] initCode, BigInteger gas)> GetInitCode() { + if (IsZkSync) + { + throw new NotImplementedException("GetInitCode is not supported on zkSync"); + } + if (_deployed) return (new byte[] { }, 0); @@ -153,6 +187,11 @@ internal async Task Request(RpcRequestMessage requestMessage if (requestMessage.Method == "eth_signTransaction") { + if (IsZkSync) + { + throw new NotImplementedException("eth_signTransaction is not supported on zkSync"); + } + var parameters = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(requestMessage.RawParameters)); var txInput = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(parameters[0])); var partialUserOp = await SignTransactionAsUserOp(txInput, requestMessage.Id); @@ -160,6 +199,13 @@ internal async Task Request(RpcRequestMessage requestMessage } else if (requestMessage.Method == "eth_sendTransaction") { + if (IsZkSync) + { + var paramList = JsonConvert.DeserializeObject>(JsonConvert.SerializeObject(requestMessage.RawParameters)); + var transactionInput = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(paramList[0])); + var hash = await SendZkSyncAATransaction(transactionInput); + return new RpcResponseMessage(requestMessage.Id, hash); + } return await CreateUserOpAndSend(requestMessage); } else if (requestMessage.Method == "eth_chainId") @@ -188,6 +234,82 @@ internal async Task Request(RpcRequestMessage requestMessage } } + private async Task SendZkSyncAATransaction(TransactionInput transactionInput) + { + var transaction = new Transaction(_sdk, transactionInput); + var web3 = Utils.GetWeb3(_sdk.Session.ChainId, _sdk.Session.Options.clientId, _sdk.Session.Options.bundleId); + + if (transactionInput.Nonce == null) + { + var nonce = await web3.Client.SendRequestAsync(method: "eth_getTransactionCount", route: null, paramList: new object[] { Accounts[0], "latest" }); + _ = transaction.SetNonce(nonce.Value.ToString()); + } + + var feeData = await web3.Client.SendRequestAsync(method: "zks_estimateFee", route: null, paramList: new object[] { transactionInput, "latest" }); + var maxFee = feeData["max_fee_per_gas"].ToObject().Value * 10 / 5; + var maxPriorityFee = feeData["max_priority_fee_per_gas"].ToObject().Value * 10 / 5; + var gasPerPubData = feeData["gas_per_pubdata_limit"].ToObject().Value; + var gasLimit = feeData["gas_limit"].ToObject().Value * 10 / 5; + + if (_sdk.Session.Options.smartWalletConfig?.gasless == true) + { + var pmDataResult = await BundlerClient.ZkPaymasterData( + _sdk.Session.Options.smartWalletConfig?.paymasterUrl, + _sdk.Session.Options.clientId, + _sdk.Session.Options.bundleId, + 1, + transactionInput + ); + + var zkTx = new AccountAbstraction.ZkSyncAATransaction + { + TxType = 0x71, + From = new HexBigInteger(transaction.Input.From).Value, + To = new HexBigInteger(transaction.Input.To).Value, + GasLimit = gasLimit, + GasPerPubdataByteLimit = gasPerPubData, + MaxFeePerGas = maxFee, + MaxPriorityFeePerGas = maxPriorityFee, + Paymaster = new HexBigInteger(pmDataResult.paymaster).Value, + Nonce = transaction.Input.Nonce.Value, + Value = transaction.Input.Value?.Value ?? 0, + Data = transaction.Input.Data?.HexToByteArray() ?? new byte[0], + FactoryDeps = new List(), + PaymasterInput = pmDataResult.paymasterInput?.HexToByteArray() ?? new byte[0] + }; + + var zkTxSigned = await EIP712.GenerateSignature_ZkSyncTransaction(_sdk, "zkSync", "2", _sdk.Session.ChainId, zkTx); + + // Match bundler ZkTransactionInput type without recreating + var zkBroadcastResult = await BundlerClient.ZkBroadcastTransaction( + _sdk.Session.Options.smartWalletConfig?.paymasterUrl, + _sdk.Session.Options.clientId, + _sdk.Session.Options.bundleId, + 1, + new + { + nonce = zkTx.Nonce.ToString(), + from = zkTx.From, + to = zkTx.To, + gas = zkTx.GasLimit.ToString(), + gasPrice = string.Empty, + value = zkTx.Value.ToString(), + data = Utils.ByteArrayToHexString(zkTx.Data), + maxFeePerGas = zkTx.MaxFeePerGas.ToString(), + maxPriorityFeePerGas = zkTx.MaxPriorityFeePerGas.ToString(), + chainId = _sdk.Session.ChainId.ToString(), + signedTransaction = zkTxSigned, + paymaster = pmDataResult.paymaster, + } + ); + return zkBroadcastResult.transactionHash; + } + else + { + throw new NotImplementedException("ZkSync Smart Wallet transactions are not supported without gasless mode"); + } + } + private async Task SignTransactionAsUserOp(TransactionInput transactionInput, object requestId = null) { requestId ??= SmartWalletClient.GenerateRpcId(); diff --git a/Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/Types.cs b/Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/Types.cs index 33de8238..6be1adde 100644 --- a/Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/Types.cs +++ b/Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/Types.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using System.Numerics; +using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.RPC.Eth.DTOs; namespace Thirdweb.AccountAbstraction @@ -37,4 +40,58 @@ public class ThirdwebGetUserOperationGasPriceResponse public string maxFeePerGas { get; set; } public string maxPriorityFeePerGas { get; set; } } + + public class ZkPaymasterDataResponse + { + public string paymaster { get; set; } + public string paymasterInput { get; set; } + } + + public class ZkBroadcastTransactionResponse + { + public string transactionHash { get; set; } + } + + [Struct("Transaction")] + public class ZkSyncAATransaction + { + [Parameter("uint256", "txType", 1)] + public virtual BigInteger TxType { get; set; } + + [Parameter("uint256", "from", 2)] + public virtual BigInteger From { get; set; } + + [Parameter("uint256", "to", 3)] + public virtual BigInteger To { get; set; } + + [Parameter("uint256", "gasLimit", 4)] + public virtual BigInteger GasLimit { get; set; } + + [Parameter("uint256", "gasPerPubdataByteLimit", 5)] + public virtual BigInteger GasPerPubdataByteLimit { get; set; } + + [Parameter("uint256", "maxFeePerGas", 6)] + public virtual BigInteger MaxFeePerGas { get; set; } + + [Parameter("uint256", "maxPriorityFeePerGas", 7)] + public virtual BigInteger MaxPriorityFeePerGas { get; set; } + + [Parameter("uint256", "paymaster", 8)] + public virtual BigInteger Paymaster { get; set; } + + [Parameter("uint256", "nonce", 9)] + public virtual BigInteger Nonce { get; set; } + + [Parameter("uint256", "value", 10)] + public virtual BigInteger Value { get; set; } + + [Parameter("bytes", "data", 11)] + public virtual byte[] Data { get; set; } + + [Parameter("bytes32[]", "factoryDeps", 12)] + public virtual List FactoryDeps { get; set; } + + [Parameter("bytes", "paymasterInput", 13)] + public virtual byte[] PaymasterInput { get; set; } + } } diff --git a/Assets/Thirdweb/Core/Scripts/EIP712.cs b/Assets/Thirdweb/Core/Scripts/EIP712.cs index 73ca99c0..7cd96569 100644 --- a/Assets/Thirdweb/Core/Scripts/EIP712.cs +++ b/Assets/Thirdweb/Core/Scripts/EIP712.cs @@ -8,12 +8,18 @@ using TokenERC1155Contract = Thirdweb.Contracts.TokenERC1155.ContractDefinition; using MinimalForwarder = Thirdweb.Contracts.Forwarder.ContractDefinition; using AccountContract = Thirdweb.Contracts.Account.ContractDefinition; +using System; +using System.Collections.Generic; +using Nethereum.Model; +using Nethereum.Hex.HexConvertors.Extensions; +using Nethereum.RLP; +using System.Linq; namespace Thirdweb { public static class EIP712 { - /// SIGNATURE GENERATION /// + #region Signature Generation public async static Task GenerateSignature_MinimalForwarder( ThirdwebSDK sdk, @@ -147,6 +153,16 @@ public async static Task GenerateSignature_SmartAccount_AccountMessage(T return await sdk.Wallet.SignTypedDataV4(accountMessage, typedData); } + public static async Task GenerateSignature_ZkSyncTransaction(ThirdwebSDK sdk, string domainName, string version, BigInteger chainId, AccountAbstraction.ZkSyncAATransaction transaction) + { + var typedData = GetTypedDefinition_ZkSyncTransaction(domainName, version, chainId); + var signatureHex = await sdk.Wallet.SignTypedDataV4(transaction, typedData); + var signatureRaw = EthECDSASignatureFactory.ExtractECDSASignature(signatureHex); + return SerializeEip712(transaction, signatureRaw, chainId); + } + + #endregion + #region Typed Data Definitions public static TypedData GetTypedDefinition_TokenERC20(string domainName, string version, BigInteger chainId, string verifyingContract) @@ -244,10 +260,63 @@ public static TypedData GetTypedDefinition_SmartAccount_AccountMessage(s PrimaryType = nameof(AccountMessage), }; } - } + + public static TypedData GetTypedDefinition_ZkSyncTransaction(string domainName, string version, BigInteger chainId) + { + return new TypedData + { + Domain = new DomainWithNameVersionAndChainId + { + Name = domainName, + Version = version, + ChainId = chainId, + }, + Types = MemberDescriptionFactory.GetTypesMemberDescription(typeof(DomainWithNameVersionAndChainId), typeof(AccountAbstraction.ZkSyncAATransaction)), + PrimaryType = "Transaction", + }; + } #endregion + #region Helpers + + private static string SerializeEip712(AccountAbstraction.ZkSyncAATransaction transaction, EthECDSASignature signature, BigInteger chainId) + { + if (chainId == 0) + { + throw new ArgumentException("Chain ID must be provided for EIP712 transactions!"); + } + + var fields = new List + { + transaction.Nonce == 0 ? new byte[0] : transaction.Nonce.ToByteArray(isUnsigned: true, isBigEndian: true), + transaction.MaxPriorityFeePerGas == 0 ? new byte[0] : transaction.MaxPriorityFeePerGas.ToByteArray(isUnsigned: true, isBigEndian: true), + transaction.MaxFeePerGas.ToByteArray(isUnsigned: true, isBigEndian: true), + transaction.GasLimit.ToByteArray(isUnsigned: true, isBigEndian: true), + transaction.To.ToByteArray(isUnsigned: true, isBigEndian: true), + transaction.Value == 0 ? new byte[0] : transaction.Value.ToByteArray(isUnsigned: true, isBigEndian: true), + transaction.Data == null ? new byte[0] : transaction.Data, + }; + + fields.Add(signature.IsVSignedForYParity() ? new byte[] { 0x1b } : new byte[] { 0x1c }); + fields.Add(signature.R); + fields.Add(signature.S); + + fields.Add(chainId.ToByteArray(isUnsigned: true, isBigEndian: true)); + fields.Add(transaction.From.ToByteArray(isUnsigned: true, isBigEndian: true)); + + // Add meta + fields.Add(transaction.GasPerPubdataByteLimit.ToByteArray(isUnsigned: true, isBigEndian: true)); + fields.Add(new byte[] { }); // TODO: FactoryDeps + fields.Add(signature.CreateStringSignature().HexToByteArray()); + // add array of rlp encoded paymaster/paymasterinput + fields.Add(RLP.EncodeElement(transaction.Paymaster.ToByteArray(isUnsigned: true, isBigEndian: true)).Concat(RLP.EncodeElement(transaction.PaymasterInput)).ToArray()); + + return "0x71" + RLP.EncodeDataItemsAsElementOrListAndCombineAsList(fields.ToArray(), new int[] { 13, 15 }).ToHex(); + } + + #endregion + } public partial class AccountMessage : AccountMessageBase { } diff --git a/Assets/Thirdweb/Core/Scripts/Wallet.cs b/Assets/Thirdweb/Core/Scripts/Wallet.cs index 0e6cbe3d..7892df57 100644 --- a/Assets/Thirdweb/Core/Scripts/Wallet.cs +++ b/Assets/Thirdweb/Core/Scripts/Wallet.cs @@ -371,7 +371,7 @@ public async Task Sign(string message) } else { - if (_sdk.Session.ActiveWallet.GetProvider() == WalletProvider.SmartWallet) + if (_sdk.Session.ActiveWallet.GetProvider() == WalletProvider.SmartWallet && _sdk.Session.ChainId != 300 && _sdk.Session.ChainId != 324) { var sw = _sdk.Session.ActiveWallet as Wallets.ThirdwebSmartWallet; if (!sw.SmartWallet.IsDeployed && !sw.SmartWallet.IsDeploying) @@ -492,14 +492,43 @@ public async Task SignTypedDataV4(T data, TypedData if (_sdk.Session.ActiveWallet.GetProvider() == WalletProvider.SmartWallet) { - // Smart accounts - var hashToken = jsonObject.SelectToken("$.message.message"); - if (hashToken != null) + // Zk AA + if (_sdk.Session.ChainId == 300 || _sdk.Session.ChainId == 324) { - var hashBase64 = hashToken.Value(); - var hashBytes = Convert.FromBase64String(hashBase64); - var hashHex = hashBytes.ByteArrayToHexString(); - hashToken.Replace(hashHex); + var hashToken = jsonObject.SelectToken("$.message.data"); + if (hashToken != null) + { + var hashBase64 = hashToken.Value(); + var hashBytes = Convert.FromBase64String(hashBase64); + var hashHex = hashBytes.ByteArrayToHexString(); + hashToken.Replace(hashHex); + } + // set factory deps to 0x + var factoryDepsToken = jsonObject.SelectToken("$.message.factoryDeps"); + if (factoryDepsToken != null) + { + factoryDepsToken.Replace(new JArray()); + } + var paymasterInputToken = jsonObject.SelectToken("$.message.paymasterInput"); + if (paymasterInputToken != null) + { + var paymasterInputBase64 = paymasterInputToken.Value(); + var paymasterInputBytes = Convert.FromBase64String(paymasterInputBase64); + var paymasterInputHex = paymasterInputBytes.ByteArrayToHexString(); + paymasterInputToken.Replace(paymasterInputHex); + } + } + // Normal AA + else + { + var hashToken = jsonObject.SelectToken("$.message.message"); + if (hashToken != null) + { + var hashBase64 = hashToken.Value(); + var hashBytes = Convert.FromBase64String(hashBase64); + var hashHex = hashBytes.ByteArrayToHexString(); + hashToken.Replace(hashHex); + } } } @@ -537,7 +566,7 @@ public async Task RecoverAddress(string message, string signature) else { var signer = new EthereumMessageSigner(); - if (_sdk.Session.ActiveWallet.GetProvider() == WalletProvider.SmartWallet) + if (_sdk.Session.ActiveWallet.GetProvider() == WalletProvider.SmartWallet && _sdk.Session.ChainId != 300 && _sdk.Session.ChainId != 324) { var sw = _sdk.Session.ActiveWallet as Wallets.ThirdwebSmartWallet; bool isSigValid = await sw.SmartWallet.VerifySignature(message.HashPrefixedMessage().HexStringToByteArray(), signature.HexStringToByteArray());