Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Managed ZkSync Paymaster #24

Merged
merged 9 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading