Skip to content

Commit

Permalink
Managed ZkSync Paymaster (#24)
Browse files Browse the repository at this point in the history
* Managed ZkSync Paymaster

* cleanup

* Working example

* Update BundlerClient.cs

* Support normal tx flow w/ sw zk aa

* Update SmartWallet.cs

* Broadcast from engine

* fix tests

* Fix RLP indirection
  • Loading branch information
0xFirekeeper authored Jun 1, 2024
1 parent fa39694 commit 4c74bee
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 141 deletions.
116 changes: 61 additions & 55 deletions Thirdweb.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
var privateKeyWallet = await PrivateKeyWallet.Create(client: client, privateKeyHex: privateKey);

// var inAppWallet = await InAppWallet.Create(client: client, email: "[email protected]"); // or email: null, phoneNumber: "+1234567890"
var inAppWallet = await InAppWallet.Create(client: client, authprovider: AuthProvider.Google); // or email: null, phoneNumber: "+1234567890"
// var inAppWallet = await InAppWallet.Create(client: client, authprovider: AuthProvider.Google); // or email: null, phoneNumber: "+1234567890"

// Reset InAppWallet (optional step for testing login flow)
// if (await inAppWallet.IsConnected())
Expand All @@ -34,62 +34,68 @@
// }

// Relog if InAppWallet not logged in
if (!await inAppWallet.IsConnected())
{
var address = await inAppWallet.LoginWithOauth(
isMobile: false,
(url) =>
{
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
_ = Process.Start(psi);
},
"thirdweb://",
new InAppWalletBrowser()
);
Console.WriteLine($"InAppWallet address: {address}");
// await inAppWallet.SendOTP();
// Console.WriteLine("Please submit the OTP.");
// var otp = Console.ReadLine();
// (var inAppWalletAddress, var canRetry) = await inAppWallet.SubmitOTP(otp);
// if (inAppWalletAddress == null && canRetry)
// {
// Console.WriteLine("Please submit the OTP again.");
// otp = Console.ReadLine();
// (inAppWalletAddress, _) = await inAppWallet.SubmitOTP(otp);
// }
// if (inAppWalletAddress == null)
// {
// Console.WriteLine("OTP login failed. Please try again.");
// return;
// }
}
// if (!await inAppWallet.IsConnected())
// {
// var address = await inAppWallet.LoginWithOauth(
// isMobile: false,
// (url) =>
// {
// var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
// _ = Process.Start(psi);
// },
// "thirdweb://",
// new InAppWalletBrowser()
// );
// Console.WriteLine($"InAppWallet address: {address}");
// await inAppWallet.SendOTP();
// Console.WriteLine("Please submit the OTP.");
// var otp = Console.ReadLine();
// (var inAppWalletAddress, var canRetry) = await inAppWallet.SubmitOTP(otp);
// if (inAppWalletAddress == null && canRetry)
// {
// Console.WriteLine("Please submit the OTP again.");
// otp = Console.ReadLine();
// (inAppWalletAddress, _) = await inAppWallet.SubmitOTP(otp);
// }
// if (inAppWalletAddress == null)
// {
// Console.WriteLine("OTP login failed. Please try again.");
// return;
// }
// }

// Prepare a transaction directly, or with Contract.Prepare
var tx = await ThirdwebTransaction.Create(
client: client,
wallet: privateKeyWallet,
txInput: new ThirdwebTransactionInput()
{
From = await privateKeyWallet.GetAddress(),
To = await privateKeyWallet.GetAddress(),
Value = new HexBigInteger(BigInteger.Zero),
},
chainId: 300
);

// Set zkSync options
tx.SetZkSyncOptions(
new ZkSyncOptions(
// Paymaster contract address
paymaster: "0xbA226d47Cbb2731CBAA67C916c57d68484AA269F",
// IPaymasterFlow interface encoded data
paymasterInput: "0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"
)
);

// Send as usual, it's now gasless!
var txHash = await ThirdwebTransaction.Send(transaction: tx);
Console.WriteLine($"Transaction hash: {txHash}");
// var tx = await ThirdwebTransaction.Create(
// client: client,
// wallet: privateKeyWallet,
// txInput: new ThirdwebTransactionInput()
// {
// From = await privateKeyWallet.GetAddress(),
// To = await privateKeyWallet.GetAddress(),
// Value = new HexBigInteger(BigInteger.Zero),
// },
// chainId: 300
// );

// // Set zkSync options
// tx.SetZkSyncOptions(
// new ZkSyncOptions(
// // Paymaster contract address
// paymaster: "0xbA226d47Cbb2731CBAA67C916c57d68484AA269F",
// // IPaymasterFlow interface encoded data
// paymasterInput: "0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"
// )
// );

// // Send as usual, it's now gasless!
// var txHash = await ThirdwebTransaction.Send(transaction: tx);
// Console.WriteLine($"Transaction hash: {txHash}");

var zkSmartWallet = await SmartWallet.Create(client: client, personalWallet: privateKeyWallet, chainId: 324, gasless: true);
Console.WriteLine($"Smart wallet address: {await zkSmartWallet.GetAddress()}");
var zkAaTx = await ThirdwebTransaction.Create(client, zkSmartWallet, new ThirdwebTransactionInput() { From = await zkSmartWallet.GetAddress(), To = await zkSmartWallet.GetAddress(), }, 324);
var zkSyncSignatureBasedAaTxHash = await ThirdwebTransaction.Send(zkAaTx);
Console.WriteLine($"Transaction hash: {zkSyncSignatureBasedAaTxHash}");



Expand Down
41 changes: 28 additions & 13 deletions Thirdweb.Tests/Thirdweb.Transactions.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ private async Task<ThirdwebTransaction> CreateSampleTransaction()
var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey);
var chainId = new BigInteger(421614);

var transaction = await ThirdwebTransaction.Create(client, wallet, new ThirdwebTransactionInput(), chainId);
var transaction = await ThirdwebTransaction.Create(client, wallet, new ThirdwebTransactionInput() { From = await wallet.GetAddress(), To = await wallet.GetAddress(), }, chainId);
return transaction;
}

Expand All @@ -24,18 +24,28 @@ public async Task Create_ValidatesInputParameters()
{
var client = ThirdwebClient.Create(secretKey: _secretKey);
var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey);
var txInput = new ThirdwebTransactionInput() { From = await wallet.GetAddress() };
var txInput = new ThirdwebTransactionInput() { From = await wallet.GetAddress(), To = Constants.ADDRESS_ZERO };
var chainId = new BigInteger(421614);
var transaction = await ThirdwebTransaction.Create(client, wallet, txInput, chainId);
Assert.NotNull(transaction);
}

[Fact]
public async Task Create_ThrowsOnNoTo()
{
var client = ThirdwebClient.Create(secretKey: _secretKey);
var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey);
var txInput = new ThirdwebTransactionInput() { From = await wallet.GetAddress() };
var ex = await Assert.ThrowsAsync<ArgumentException>(() => ThirdwebTransaction.Create(client, wallet, txInput, BigInteger.Zero));
Assert.Contains("Transaction recipient (to) must be provided", ex.Message);
}

[Fact]
public async Task Create_ThrowsOnInvalidAddress()
{
var client = ThirdwebClient.Create(secretKey: _secretKey);
var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey);
var txInput = new ThirdwebTransactionInput() { From = "0x123" };
var txInput = new ThirdwebTransactionInput() { From = "0x123", To = Constants.ADDRESS_ZERO };
var ex = await Assert.ThrowsAsync<ArgumentException>(() => ThirdwebTransaction.Create(client, wallet, txInput, BigInteger.Zero));
Assert.Contains("Transaction sender (from) must match wallet address", ex.Message);
}
Expand Down Expand Up @@ -150,7 +160,7 @@ public async Task Send_ThrowsIfToAddressNotProvided()
var transaction = await CreateSampleTransaction();
_ = transaction.SetTo(null);

_ = await Assert.ThrowsAsync<ArgumentException>(() => ThirdwebTransaction.Send(transaction));
_ = await Assert.ThrowsAsync<InvalidOperationException>(() => ThirdwebTransaction.Send(transaction));
}

[Fact]
Expand Down Expand Up @@ -268,15 +278,11 @@ public async Task EstimateGasCosts_SmartWalletHigherThanPrivateKeyWallet()
var privateKeyAccount = await PrivateKeyWallet.Create(client, _testPrivateKey);
var smartAccount = await SmartWallet.Create(client, personalWallet: privateKeyAccount, factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", gasless: true, chainId: 421614);

var transaction = await ThirdwebTransaction.Create(client, smartAccount, new ThirdwebTransactionInput(), 421614);
_ = transaction.SetTo(Constants.ADDRESS_ZERO);
_ = transaction.SetValue(new BigInteger(1000));
var transaction = await ThirdwebTransaction.Create(client, smartAccount, new ThirdwebTransactionInput() { To = Constants.ADDRESS_ZERO, Value = new HexBigInteger(1000), }, 421614);

var smartCosts = await ThirdwebTransaction.EstimateGasCosts(transaction);

transaction = await ThirdwebTransaction.Create(client, privateKeyAccount, new ThirdwebTransactionInput(), 421614);
_ = transaction.SetTo(Constants.ADDRESS_ZERO);
_ = transaction.SetValue(new BigInteger(1000));
transaction = await ThirdwebTransaction.Create(client, privateKeyAccount, new ThirdwebTransactionInput() { To = Constants.ADDRESS_ZERO, Value = new HexBigInteger(1000), }, 421614);

var privateCosts = await ThirdwebTransaction.EstimateGasCosts(transaction);

Expand Down Expand Up @@ -339,9 +345,18 @@ public async Task Simulate_ReturnsData()
var client = ThirdwebClient.Create(secretKey: _secretKey);
var privateKeyAccount = await PrivateKeyWallet.Create(client, _testPrivateKey);
var smartAccount = await SmartWallet.Create(client, personalWallet: privateKeyAccount, factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", gasless: true, chainId: 421614);
var transaction = await ThirdwebTransaction.Create(client, smartAccount, new ThirdwebTransactionInput(), 421614);
_ = transaction.SetValue(new BigInteger(0));
_ = transaction.SetGasLimit(250000);
var transaction = await ThirdwebTransaction.Create(
client,
smartAccount,
new ThirdwebTransactionInput()
{
To = Constants.ADDRESS_ZERO,
Value = new HexBigInteger(0),
Data = "0x",
Gas = new HexBigInteger(250000),
},
421614
);

var data = await ThirdwebTransaction.Simulate(transaction);
Assert.NotNull(data);
Expand Down
93 changes: 56 additions & 37 deletions Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public static async Task<ThirdwebTransaction> Create(ThirdwebClient client, IThi
txInput.From ??= address;
txInput.Data ??= "0x";

if (txInput.To == null)
{
throw new ArgumentException("Transaction recipient (to) must be provided");
}

if (address != txInput.From)
{
throw new ArgumentException("Transaction sender (from) must match wallet address");
Expand Down Expand Up @@ -156,8 +161,7 @@ public static async Task<BigInteger> EstimateGasPrice(ThirdwebTransaction transa
var fees = await rpc.SendRequestAsync<JToken>("zks_estimateFee", transaction.Input, "latest");
var maxFee = fees["max_fee_per_gas"].ToObject<HexBigInteger>().Value;
var maxPriorityFee = fees["max_priority_fee_per_gas"].ToObject<HexBigInteger>().Value;
maxPriorityFee = maxPriorityFee == 0 ? maxFee : maxPriorityFee;
return withBump ? (maxFee * 10 / 9, maxPriorityFee * 10 / 9) : (maxFee, maxPriorityFee);
return withBump ? (maxFee * 10 / 5, maxPriorityFee * 10 / 5) : (maxFee, maxPriorityFee);
}

var gasPrice = await EstimateGasPrice(transaction, withBump);
Expand Down Expand Up @@ -203,27 +207,39 @@ public static async Task<string> Simulate(ThirdwebTransaction transaction)

public static async Task<BigInteger> EstimateGasLimit(ThirdwebTransaction transaction)
{
var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value);

if (IsZkSyncTransaction(transaction))
{
var hex = (await rpc.SendRequestAsync<JToken>("zks_estimateFee", transaction.Input, "latest"))["gas_limit"].ToString();
return new HexBigInteger(hex).Value * 10 / 5;
}

if (transaction._wallet.AccountType == ThirdwebAccountType.SmartAccount)
{
var smartAccount = transaction._wallet as SmartWallet;
return await smartAccount.EstimateUserOperationGas(transaction.Input, transaction.Input.ChainId.Value);
}
else
{
var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value);
if (IsZkSyncTransaction(transaction))
{
var hex = (await rpc.SendRequestAsync<JToken>("zks_estimateFee", transaction.Input, "latest"))["gas_limit"].ToString();
return new HexBigInteger(hex).Value * 10 / 7;
}
else
{
var hex = await rpc.SendRequestAsync<string>("eth_estimateGas", transaction.Input, "latest");
return new HexBigInteger(hex).Value;
}
var hex = await rpc.SendRequestAsync<string>("eth_estimateGas", transaction.Input, "latest");
return new HexBigInteger(hex).Value;
}
}

public static async Task<BigInteger> GetNonce(ThirdwebTransaction transaction)
{
var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value);
return new HexBigInteger(await rpc.SendRequestAsync<string>("eth_getTransactionCount", transaction.Input.From, "latest")).Value;
}

private static async Task<BigInteger> GetGasPerPubData(ThirdwebTransaction transaction)
{
var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value);
var hex = (await rpc.SendRequestAsync<JToken>("zks_estimateFee", transaction.Input, "latest"))["gas_per_pubdata_limit"].ToString();
return new HexBigInteger(hex).Value;
}

public static async Task<string> Sign(ThirdwebTransaction transaction)
{
return await transaction._wallet.SignTransaction(transaction.Input, transaction.Input.ChainId.Value);
Expand All @@ -233,7 +249,7 @@ public static async Task<string> Send(ThirdwebTransaction transaction)
{
if (transaction.Input.To == null)
{
throw new ArgumentException("To address must be provided");
throw new InvalidOperationException("Transaction recipient (to) must be provided");
}

if (transaction.Input.GasPrice != null && (transaction.Input.MaxFeePerGas != null || transaction.Input.MaxPriorityFeePerGas != null))
Expand All @@ -259,24 +275,9 @@ public static async Task<string> Send(ThirdwebTransaction transaction)

var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value);
string hash;
if (IsZkSyncTransaction(transaction))
if (IsZkSyncTransaction(transaction) && transaction.Input.ZkSync.HasValue && transaction.Input.ZkSync.Value.Paymaster != 0 && transaction.Input.ZkSync.Value.PaymasterInput != null)
{
var zkTx = new AccountAbstraction.ZkSyncAATransaction
{
TxType = 0x71,
From = new HexBigInteger(transaction.Input.From).Value,
To = new HexBigInteger(transaction.Input.To).Value,
GasLimit = transaction.Input.Gas.Value,
GasPerPubdataByteLimit = transaction.Input.ZkSync?.GasPerPubdataByteLimit ?? 50000,
MaxFeePerGas = transaction.Input.MaxFeePerGas?.Value ?? transaction.Input.GasPrice.Value,
MaxPriorityFeePerGas = transaction.Input.MaxPriorityFeePerGas?.Value ?? transaction.Input.GasPrice.Value,
Paymaster = transaction.Input.ZkSync.Value.Paymaster,
Nonce = transaction.Input.Nonce ?? new HexBigInteger(await rpc.SendRequestAsync<string>("eth_getTransactionCount", transaction.Input.From, "latest")),
Value = transaction.Input.Value.Value,
Data = transaction.Input.Data.HexToByteArray(),
FactoryDeps = transaction.Input.ZkSync.Value.FactoryDeps,
PaymasterInput = transaction.Input.ZkSync.Value.PaymasterInput
};
var zkTx = await ConvertToZkSyncTransaction(transaction);
var zkTxSigned = await EIP712.GenerateSignature_ZkSyncTransaction("zkSync", "2", transaction.Input.ChainId.Value, zkTx, transaction._wallet);
hash = await rpc.SendRequestAsync<string>("eth_sendRawTransaction", zkTxSigned);
}
Expand All @@ -285,12 +286,11 @@ public static async Task<string> Send(ThirdwebTransaction transaction)
switch (transaction._wallet.AccountType)
{
case ThirdwebAccountType.PrivateKeyAccount:
transaction.Input.Nonce ??= new HexBigInteger(await rpc.SendRequestAsync<string>("eth_getTransactionCount", await transaction._wallet.GetAddress(), "latest"));
transaction.Input.Nonce ??= new HexBigInteger(await GetNonce(transaction));
var signedTx = await Sign(transaction);
hash = await rpc.SendRequestAsync<string>("eth_sendRawTransaction", signedTx);
break;
case ThirdwebAccountType.SmartAccount:

var smartAccount = transaction._wallet as SmartWallet;
hash = await smartAccount.SendTransaction(transaction.Input);
break;
Expand Down Expand Up @@ -350,12 +350,31 @@ public static async Task<TransactionReceipt> WaitForTransactionReceipt(ThirdwebC
return receipt;
}

public static async Task<AccountAbstraction.ZkSyncAATransaction> ConvertToZkSyncTransaction(ThirdwebTransaction transaction)
{
var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value);
Console.WriteLine("Current TX: " + JsonConvert.SerializeObject(transaction.Input));
return new AccountAbstraction.ZkSyncAATransaction
{
TxType = 0x71,
From = new HexBigInteger(transaction.Input.From).Value,
To = new HexBigInteger(transaction.Input.To).Value,
GasLimit = transaction.Input.Gas.Value,
GasPerPubdataByteLimit = transaction.Input.ZkSync?.GasPerPubdataByteLimit ?? await GetGasPerPubData(transaction),
MaxFeePerGas = transaction.Input.MaxFeePerGas?.Value ?? transaction.Input.GasPrice.Value,
MaxPriorityFeePerGas = transaction.Input.MaxPriorityFeePerGas?.Value ?? 0,
Paymaster = transaction.Input.ZkSync.Value.Paymaster,
Nonce = transaction.Input.Nonce ?? new HexBigInteger(await GetNonce(transaction)),
Value = transaction.Input.Value?.Value ?? 0,
Data = transaction.Input.Data?.HexToByteArray() ?? new byte[0],
FactoryDeps = transaction.Input.ZkSync.Value.FactoryDeps,
PaymasterInput = transaction.Input.ZkSync.Value.PaymasterInput
};
}

private static bool IsZkSyncTransaction(ThirdwebTransaction transaction)
{
return (transaction.Input.ChainId.Value.Equals(324) || transaction.Input.ChainId.Value.Equals(300))
&& transaction.Input.ZkSync.HasValue
&& transaction.Input.ZkSync.Value.Paymaster != 0
&& transaction.Input.ZkSync.Value.PaymasterInput != null;
return transaction.Input.ChainId.Value.Equals(324) || transaction.Input.ChainId.Value.Equals(300);
}
}
}
Loading

0 comments on commit 4c74bee

Please sign in to comment.