Skip to content

Commit 596bc41

Browse files
authored
[Native] ZkSync Account Abstraction Support (#194)
1 parent 963f06e commit 596bc41

File tree

5 files changed

+308
-11
lines changed

5 files changed

+308
-11
lines changed

Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/BundlerClient.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Net.Http;
33
using System.Threading.Tasks;
44
using Nethereum.JsonRpc.Client.RpcMessages;
5+
using Nethereum.RPC.Eth.DTOs;
56
using Newtonsoft.Json;
67

78
namespace Thirdweb.AccountAbstraction
@@ -69,6 +70,25 @@ string entryPoint
6970
}
7071
}
7172

73+
public static async Task<ZkPaymasterDataResponse> ZkPaymasterData(string paymasterUrl, string apiKey, string bundleId, object requestId, TransactionInput txInput)
74+
{
75+
var response = await BundlerRequest(paymasterUrl, apiKey, bundleId, requestId, "zk_paymasterData", txInput);
76+
try
77+
{
78+
return JsonConvert.DeserializeObject<ZkPaymasterDataResponse>(response.Result.ToString());
79+
}
80+
catch
81+
{
82+
return new ZkPaymasterDataResponse() { paymaster = null, paymasterInput = null };
83+
}
84+
}
85+
86+
public static async Task<ZkBroadcastTransactionResponse> ZkBroadcastTransaction(string paymasterUrl, string apiKey, string bundleId, object requestId, object txInput)
87+
{
88+
var response = await BundlerRequest(paymasterUrl, apiKey, bundleId, requestId, "zk_broadcastTransaction", txInput);
89+
return JsonConvert.DeserializeObject<ZkBroadcastTransactionResponse>(response.Result.ToString());
90+
}
91+
7292
// Request
7393

7494
private static async Task<RpcResponseMessage> BundlerRequest(string url, string apiKey, string bundleId, object requestId, string method, params object[] args)

Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/SmartWallet.cs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using Thirdweb.Wallets;
1919
using Nethereum.Signer;
2020
using System.Security.Cryptography;
21+
using Newtonsoft.Json.Linq;
2122

2223
namespace Thirdweb.AccountAbstraction
2324
{
@@ -50,6 +51,7 @@ public class SmartWallet
5051
public bool IsDeploying => _deploying;
5152

5253
private readonly ThirdwebSDK _sdk;
54+
private bool IsZkSync => _sdk.Session.ChainId == 300 || _sdk.Session.ChainId == 324;
5355

5456
public SmartWallet(IThirdwebWallet personalWallet, ThirdwebSDK sdk)
5557
{
@@ -72,6 +74,13 @@ internal async Task Initialize(string smartWalletOverride = null)
7274
if (_initialized)
7375
return;
7476

77+
if (IsZkSync)
78+
{
79+
Accounts = new List<string>() { await GetPersonalAddress() };
80+
_initialized = true;
81+
return;
82+
}
83+
7584
var predictedAccount =
7685
smartWalletOverride
7786
?? (
@@ -95,18 +104,33 @@ internal async Task Initialize(string smartWalletOverride = null)
95104

96105
internal async Task UpdateDeploymentStatus()
97106
{
107+
if (IsZkSync)
108+
{
109+
_deployed = true;
110+
return;
111+
}
112+
98113
var web3 = Utils.GetWeb3(_sdk.Session.ChainId, _sdk.Session.Options.clientId, _sdk.Session.Options.bundleId);
99114
var bytecode = await web3.Eth.GetCode.SendRequestAsync(Accounts[0]);
100115
_deployed = bytecode != "0x";
101116
}
102117

103118
internal async Task<TransactionResult> SetPermissionsForSigner(SignerPermissionRequest signerPermissionRequest, byte[] signature)
104119
{
120+
if (IsZkSync)
121+
{
122+
throw new NotImplementedException("SetPermissionsForSigner is not supported on zkSync");
123+
}
105124
return await TransactionManager.ThirdwebWrite(_sdk, Accounts[0], new SetPermissionsForSignerFunction() { Req = signerPermissionRequest, Signature = signature });
106125
}
107126

108127
internal async Task ForceDeploy()
109128
{
129+
if (IsZkSync)
130+
{
131+
return;
132+
}
133+
110134
if (_deployed)
111135
return;
112136

@@ -118,6 +142,11 @@ internal async Task ForceDeploy()
118142

119143
internal async Task<bool> VerifySignature(byte[] hash, byte[] signature)
120144
{
145+
if (IsZkSync)
146+
{
147+
throw new NotImplementedException("VerifySignature is not supported on zkSync");
148+
}
149+
121150
try
122151
{
123152
var verifyRes = await TransactionManager.ThirdwebRead<AccountContract.IsValidSignatureFunction, AccountContract.IsValidSignatureOutputDTO>(
@@ -136,6 +165,11 @@ internal async Task<bool> VerifySignature(byte[] hash, byte[] signature)
136165

137166
internal async Task<(byte[] initCode, BigInteger gas)> GetInitCode()
138167
{
168+
if (IsZkSync)
169+
{
170+
throw new NotImplementedException("GetInitCode is not supported on zkSync");
171+
}
172+
139173
if (_deployed)
140174
return (new byte[] { }, 0);
141175

@@ -153,13 +187,25 @@ internal async Task<RpcResponseMessage> Request(RpcRequestMessage requestMessage
153187

154188
if (requestMessage.Method == "eth_signTransaction")
155189
{
190+
if (IsZkSync)
191+
{
192+
throw new NotImplementedException("eth_signTransaction is not supported on zkSync");
193+
}
194+
156195
var parameters = JsonConvert.DeserializeObject<object[]>(JsonConvert.SerializeObject(requestMessage.RawParameters));
157196
var txInput = JsonConvert.DeserializeObject<TransactionInput>(JsonConvert.SerializeObject(parameters[0]));
158197
var partialUserOp = await SignTransactionAsUserOp(txInput, requestMessage.Id);
159198
return new RpcResponseMessage(requestMessage.Id, JsonConvert.SerializeObject(EncodeUserOperation(partialUserOp)));
160199
}
161200
else if (requestMessage.Method == "eth_sendTransaction")
162201
{
202+
if (IsZkSync)
203+
{
204+
var paramList = JsonConvert.DeserializeObject<List<object>>(JsonConvert.SerializeObject(requestMessage.RawParameters));
205+
var transactionInput = JsonConvert.DeserializeObject<TransactionInput>(JsonConvert.SerializeObject(paramList[0]));
206+
var hash = await SendZkSyncAATransaction(transactionInput);
207+
return new RpcResponseMessage(requestMessage.Id, hash);
208+
}
163209
return await CreateUserOpAndSend(requestMessage);
164210
}
165211
else if (requestMessage.Method == "eth_chainId")
@@ -188,6 +234,82 @@ internal async Task<RpcResponseMessage> Request(RpcRequestMessage requestMessage
188234
}
189235
}
190236

237+
private async Task<string> SendZkSyncAATransaction(TransactionInput transactionInput)
238+
{
239+
var transaction = new Transaction(_sdk, transactionInput);
240+
var web3 = Utils.GetWeb3(_sdk.Session.ChainId, _sdk.Session.Options.clientId, _sdk.Session.Options.bundleId);
241+
242+
if (transactionInput.Nonce == null)
243+
{
244+
var nonce = await web3.Client.SendRequestAsync<HexBigInteger>(method: "eth_getTransactionCount", route: null, paramList: new object[] { Accounts[0], "latest" });
245+
_ = transaction.SetNonce(nonce.Value.ToString());
246+
}
247+
248+
var feeData = await web3.Client.SendRequestAsync<JToken>(method: "zks_estimateFee", route: null, paramList: new object[] { transactionInput, "latest" });
249+
var maxFee = feeData["max_fee_per_gas"].ToObject<HexBigInteger>().Value * 10 / 5;
250+
var maxPriorityFee = feeData["max_priority_fee_per_gas"].ToObject<HexBigInteger>().Value * 10 / 5;
251+
var gasPerPubData = feeData["gas_per_pubdata_limit"].ToObject<HexBigInteger>().Value;
252+
var gasLimit = feeData["gas_limit"].ToObject<HexBigInteger>().Value * 10 / 5;
253+
254+
if (_sdk.Session.Options.smartWalletConfig?.gasless == true)
255+
{
256+
var pmDataResult = await BundlerClient.ZkPaymasterData(
257+
_sdk.Session.Options.smartWalletConfig?.paymasterUrl,
258+
_sdk.Session.Options.clientId,
259+
_sdk.Session.Options.bundleId,
260+
1,
261+
transactionInput
262+
);
263+
264+
var zkTx = new AccountAbstraction.ZkSyncAATransaction
265+
{
266+
TxType = 0x71,
267+
From = new HexBigInteger(transaction.Input.From).Value,
268+
To = new HexBigInteger(transaction.Input.To).Value,
269+
GasLimit = gasLimit,
270+
GasPerPubdataByteLimit = gasPerPubData,
271+
MaxFeePerGas = maxFee,
272+
MaxPriorityFeePerGas = maxPriorityFee,
273+
Paymaster = new HexBigInteger(pmDataResult.paymaster).Value,
274+
Nonce = transaction.Input.Nonce.Value,
275+
Value = transaction.Input.Value?.Value ?? 0,
276+
Data = transaction.Input.Data?.HexToByteArray() ?? new byte[0],
277+
FactoryDeps = new List<byte[]>(),
278+
PaymasterInput = pmDataResult.paymasterInput?.HexToByteArray() ?? new byte[0]
279+
};
280+
281+
var zkTxSigned = await EIP712.GenerateSignature_ZkSyncTransaction(_sdk, "zkSync", "2", _sdk.Session.ChainId, zkTx);
282+
283+
// Match bundler ZkTransactionInput type without recreating
284+
var zkBroadcastResult = await BundlerClient.ZkBroadcastTransaction(
285+
_sdk.Session.Options.smartWalletConfig?.paymasterUrl,
286+
_sdk.Session.Options.clientId,
287+
_sdk.Session.Options.bundleId,
288+
1,
289+
new
290+
{
291+
nonce = zkTx.Nonce.ToString(),
292+
from = zkTx.From,
293+
to = zkTx.To,
294+
gas = zkTx.GasLimit.ToString(),
295+
gasPrice = string.Empty,
296+
value = zkTx.Value.ToString(),
297+
data = Utils.ByteArrayToHexString(zkTx.Data),
298+
maxFeePerGas = zkTx.MaxFeePerGas.ToString(),
299+
maxPriorityFeePerGas = zkTx.MaxPriorityFeePerGas.ToString(),
300+
chainId = _sdk.Session.ChainId.ToString(),
301+
signedTransaction = zkTxSigned,
302+
paymaster = pmDataResult.paymaster,
303+
}
304+
);
305+
return zkBroadcastResult.transactionHash;
306+
}
307+
else
308+
{
309+
throw new NotImplementedException("ZkSync Smart Wallet transactions are not supported without gasless mode");
310+
}
311+
}
312+
191313
private async Task<EntryPointContract.UserOperation> SignTransactionAsUserOp(TransactionInput transactionInput, object requestId = null)
192314
{
193315
requestId ??= SmartWalletClient.GenerateRpcId();

Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/Types.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using System.Collections.Generic;
2+
using System.Numerics;
3+
using Nethereum.ABI.FunctionEncoding.Attributes;
14
using Nethereum.RPC.Eth.DTOs;
25

36
namespace Thirdweb.AccountAbstraction
@@ -37,4 +40,58 @@ public class ThirdwebGetUserOperationGasPriceResponse
3740
public string maxFeePerGas { get; set; }
3841
public string maxPriorityFeePerGas { get; set; }
3942
}
43+
44+
public class ZkPaymasterDataResponse
45+
{
46+
public string paymaster { get; set; }
47+
public string paymasterInput { get; set; }
48+
}
49+
50+
public class ZkBroadcastTransactionResponse
51+
{
52+
public string transactionHash { get; set; }
53+
}
54+
55+
[Struct("Transaction")]
56+
public class ZkSyncAATransaction
57+
{
58+
[Parameter("uint256", "txType", 1)]
59+
public virtual BigInteger TxType { get; set; }
60+
61+
[Parameter("uint256", "from", 2)]
62+
public virtual BigInteger From { get; set; }
63+
64+
[Parameter("uint256", "to", 3)]
65+
public virtual BigInteger To { get; set; }
66+
67+
[Parameter("uint256", "gasLimit", 4)]
68+
public virtual BigInteger GasLimit { get; set; }
69+
70+
[Parameter("uint256", "gasPerPubdataByteLimit", 5)]
71+
public virtual BigInteger GasPerPubdataByteLimit { get; set; }
72+
73+
[Parameter("uint256", "maxFeePerGas", 6)]
74+
public virtual BigInteger MaxFeePerGas { get; set; }
75+
76+
[Parameter("uint256", "maxPriorityFeePerGas", 7)]
77+
public virtual BigInteger MaxPriorityFeePerGas { get; set; }
78+
79+
[Parameter("uint256", "paymaster", 8)]
80+
public virtual BigInteger Paymaster { get; set; }
81+
82+
[Parameter("uint256", "nonce", 9)]
83+
public virtual BigInteger Nonce { get; set; }
84+
85+
[Parameter("uint256", "value", 10)]
86+
public virtual BigInteger Value { get; set; }
87+
88+
[Parameter("bytes", "data", 11)]
89+
public virtual byte[] Data { get; set; }
90+
91+
[Parameter("bytes32[]", "factoryDeps", 12)]
92+
public virtual List<byte[]> FactoryDeps { get; set; }
93+
94+
[Parameter("bytes", "paymasterInput", 13)]
95+
public virtual byte[] PaymasterInput { get; set; }
96+
}
4097
}

Assets/Thirdweb/Core/Scripts/EIP712.cs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@
88
using TokenERC1155Contract = Thirdweb.Contracts.TokenERC1155.ContractDefinition;
99
using MinimalForwarder = Thirdweb.Contracts.Forwarder.ContractDefinition;
1010
using AccountContract = Thirdweb.Contracts.Account.ContractDefinition;
11+
using System;
12+
using System.Collections.Generic;
13+
using Nethereum.Model;
14+
using Nethereum.Hex.HexConvertors.Extensions;
15+
using Nethereum.RLP;
16+
using System.Linq;
1117

1218
namespace Thirdweb
1319
{
1420
public static class EIP712
1521
{
16-
/// SIGNATURE GENERATION ///
22+
#region Signature Generation
1723

1824
public async static Task<string> GenerateSignature_MinimalForwarder(
1925
ThirdwebSDK sdk,
@@ -147,6 +153,16 @@ public async static Task<string> GenerateSignature_SmartAccount_AccountMessage(T
147153
return await sdk.Wallet.SignTypedDataV4(accountMessage, typedData);
148154
}
149155

156+
public static async Task<string> GenerateSignature_ZkSyncTransaction(ThirdwebSDK sdk, string domainName, string version, BigInteger chainId, AccountAbstraction.ZkSyncAATransaction transaction)
157+
{
158+
var typedData = GetTypedDefinition_ZkSyncTransaction(domainName, version, chainId);
159+
var signatureHex = await sdk.Wallet.SignTypedDataV4(transaction, typedData);
160+
var signatureRaw = EthECDSASignatureFactory.ExtractECDSASignature(signatureHex);
161+
return SerializeEip712(transaction, signatureRaw, chainId);
162+
}
163+
164+
#endregion
165+
150166
#region Typed Data Definitions
151167

152168
public static TypedData<Domain> GetTypedDefinition_TokenERC20(string domainName, string version, BigInteger chainId, string verifyingContract)
@@ -244,10 +260,63 @@ public static TypedData<Domain> GetTypedDefinition_SmartAccount_AccountMessage(s
244260
PrimaryType = nameof(AccountMessage),
245261
};
246262
}
247-
}
263+
264+
public static TypedData<DomainWithNameVersionAndChainId> GetTypedDefinition_ZkSyncTransaction(string domainName, string version, BigInteger chainId)
265+
{
266+
return new TypedData<DomainWithNameVersionAndChainId>
267+
{
268+
Domain = new DomainWithNameVersionAndChainId
269+
{
270+
Name = domainName,
271+
Version = version,
272+
ChainId = chainId,
273+
},
274+
Types = MemberDescriptionFactory.GetTypesMemberDescription(typeof(DomainWithNameVersionAndChainId), typeof(AccountAbstraction.ZkSyncAATransaction)),
275+
PrimaryType = "Transaction",
276+
};
277+
}
248278

249279
#endregion
250280

281+
#region Helpers
282+
283+
private static string SerializeEip712(AccountAbstraction.ZkSyncAATransaction transaction, EthECDSASignature signature, BigInteger chainId)
284+
{
285+
if (chainId == 0)
286+
{
287+
throw new ArgumentException("Chain ID must be provided for EIP712 transactions!");
288+
}
289+
290+
var fields = new List<byte[]>
291+
{
292+
transaction.Nonce == 0 ? new byte[0] : transaction.Nonce.ToByteArray(isUnsigned: true, isBigEndian: true),
293+
transaction.MaxPriorityFeePerGas == 0 ? new byte[0] : transaction.MaxPriorityFeePerGas.ToByteArray(isUnsigned: true, isBigEndian: true),
294+
transaction.MaxFeePerGas.ToByteArray(isUnsigned: true, isBigEndian: true),
295+
transaction.GasLimit.ToByteArray(isUnsigned: true, isBigEndian: true),
296+
transaction.To.ToByteArray(isUnsigned: true, isBigEndian: true),
297+
transaction.Value == 0 ? new byte[0] : transaction.Value.ToByteArray(isUnsigned: true, isBigEndian: true),
298+
transaction.Data == null ? new byte[0] : transaction.Data,
299+
};
300+
301+
fields.Add(signature.IsVSignedForYParity() ? new byte[] { 0x1b } : new byte[] { 0x1c });
302+
fields.Add(signature.R);
303+
fields.Add(signature.S);
304+
305+
fields.Add(chainId.ToByteArray(isUnsigned: true, isBigEndian: true));
306+
fields.Add(transaction.From.ToByteArray(isUnsigned: true, isBigEndian: true));
307+
308+
// Add meta
309+
fields.Add(transaction.GasPerPubdataByteLimit.ToByteArray(isUnsigned: true, isBigEndian: true));
310+
fields.Add(new byte[] { }); // TODO: FactoryDeps
311+
fields.Add(signature.CreateStringSignature().HexToByteArray());
312+
// add array of rlp encoded paymaster/paymasterinput
313+
fields.Add(RLP.EncodeElement(transaction.Paymaster.ToByteArray(isUnsigned: true, isBigEndian: true)).Concat(RLP.EncodeElement(transaction.PaymasterInput)).ToArray());
314+
315+
return "0x71" + RLP.EncodeDataItemsAsElementOrListAndCombineAsList(fields.ToArray(), new int[] { 13, 15 }).ToHex();
316+
}
317+
318+
#endregion
319+
}
251320

252321
public partial class AccountMessage : AccountMessageBase { }
253322

0 commit comments

Comments
 (0)