From 371170ecae20e0861151e702d15f5d1ed9d725ff Mon Sep 17 00:00:00 2001 From: Firekeeper <0xFirekeeper@gmail.com> Date: Tue, 27 Aug 2024 03:36:05 +0300 Subject: [PATCH] Automatic Smart Wallet network switching // Require chain id lowest level (#60) --- .../Thirdweb.Extensions.Tests.cs | 53 ++++++++++ .../Thirdweb.Transactions.Tests.cs | 100 ++++++++---------- .../Thirdweb.ZkSmartWallet.Tests.cs | 51 +++++---- .../Thirdweb.PrivateKeyWallet.Tests.cs | 23 ++-- .../Thirdweb.SmartWallet.Tests.cs | 50 +++++++-- .../Thirdweb.Wallets.Tests.cs | 11 +- .../Thirdweb.Contracts/ThirdwebContract.cs | 4 +- .../Thirdweb.Extensions/ThirdwebExtensions.cs | 75 ++++++++++++- .../Thirdweb.Pay/ThirdwebPay.BuyWithCrypto.cs | 4 +- .../ThirdwebTransaction.cs | 16 +-- .../ThirdwebTransactionInput.cs | 9 +- Thirdweb/Thirdweb.Utils/Utils.cs | 5 + .../SmartWallet/SmartWallet.cs | 63 ++++++++--- 13 files changed, 332 insertions(+), 132 deletions(-) diff --git a/Thirdweb.Tests/Thirdweb.Extensions/Thirdweb.Extensions.Tests.cs b/Thirdweb.Tests/Thirdweb.Extensions/Thirdweb.Extensions.Tests.cs index 2c9d380..1aaa9ff 100644 --- a/Thirdweb.Tests/Thirdweb.Extensions/Thirdweb.Extensions.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Extensions/Thirdweb.Extensions.Tests.cs @@ -197,6 +197,59 @@ public async Task GetBalance_Wallet_WithERC20() Assert.True(balance >= 0); } + [Fact(Timeout = 120000)] + public async Task GetTransactionCountRaw() + { + var address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; // vitalik.eth + var chainId = BigInteger.One; + var transactionCount = await ThirdwebExtensions.GetTransactionCountRaw(this._client, chainId, address); + Assert.True(transactionCount >= 0); + } + + [Fact(Timeout = 120000)] + public async Task GetTransactionCountRaw_WithBlockTag() + { + var address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; // vitalik.eth + var chainId = this._chainId; + var blockTag = "latest"; + var transactionCount = await ThirdwebExtensions.GetTransactionCountRaw(this._client, chainId, address, blockTag); + Assert.True(transactionCount >= 0); + } + + [Fact(Timeout = 120000)] + public async Task GetTransactionCount_Contract() + { + var contract = await this.GetTokenERC20Contract(); + var transactionCount = await contract.GetTransactionCount(); + Assert.True(transactionCount >= 0); + } + + [Fact(Timeout = 120000)] + public async Task GetTransactionCount_Contract_WithBlockTag() + { + var contract = await this.GetTokenERC20Contract(); + var blockTag = "latest"; + var transactionCount = await contract.GetTransactionCount(blockTag); + Assert.True(transactionCount >= 0); + } + + [Fact(Timeout = 120000)] + public async Task GetTransactionCount_Wallet() + { + var wallet = await this.GetSmartWallet(); + var transactionCount = await wallet.GetTransactionCount(this._chainId); + Assert.True(transactionCount >= 0); + } + + [Fact(Timeout = 120000)] + public async Task GetTransactionCount_Wallet_WithBlockTag() + { + var wallet = await this.GetSmartWallet(); + var blockTag = "latest"; + var transactionCount = await wallet.GetTransactionCount(this._chainId, blockTag); + Assert.True(transactionCount >= 0); + } + [Fact(Timeout = 120000)] public async Task Transfer() { diff --git a/Thirdweb.Tests/Thirdweb.Transactions/Thirdweb.Transactions.Tests.cs b/Thirdweb.Tests/Thirdweb.Transactions/Thirdweb.Transactions.Tests.cs index 8d62632..4a64bbd 100644 --- a/Thirdweb.Tests/Thirdweb.Transactions/Thirdweb.Transactions.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Transactions/Thirdweb.Transactions.Tests.cs @@ -9,9 +9,7 @@ private async Task CreateSampleTransaction() { var client = ThirdwebClient.Create(secretKey: this.SecretKey); var wallet = await PrivateKeyWallet.Generate(client); - var chainId = new BigInteger(421614); - - var transaction = await ThirdwebTransaction.Create(wallet, new ThirdwebTransactionInput() { To = await wallet.GetAddress(), }, chainId); + var transaction = await ThirdwebTransaction.Create(wallet, new ThirdwebTransactionInput(421614) { To = await wallet.GetAddress(), }); return transaction; } @@ -50,9 +48,8 @@ public async Task Create_ValidatesInputParameters() { var client = ThirdwebClient.Create(secretKey: this.SecretKey); var wallet = await PrivateKeyWallet.Generate(client); - var txInput = new ThirdwebTransactionInput() { To = Constants.ADDRESS_ZERO }; - var chainId = new BigInteger(421614); - var transaction = await ThirdwebTransaction.Create(wallet, txInput, chainId); + var txInput = new ThirdwebTransactionInput(421614) { To = Constants.ADDRESS_ZERO }; + var transaction = await ThirdwebTransaction.Create(wallet, txInput); Assert.NotNull(transaction); } @@ -61,8 +58,8 @@ public async Task Create_ThrowsOnNoTo() { var client = ThirdwebClient.Create(secretKey: this.SecretKey); var wallet = await PrivateKeyWallet.Generate(client); - var txInput = new ThirdwebTransactionInput() { }; - var ex = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(wallet, txInput, 421614)); + var txInput = new ThirdwebTransactionInput(421614) { }; + var ex = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(wallet, txInput)); Assert.Contains("Transaction recipient (to) must be provided", ex.Message); } @@ -71,8 +68,8 @@ public async Task Create_ThrowsOnNoWallet() { var client = ThirdwebClient.Create(secretKey: this.SecretKey); var wallet = await PrivateKeyWallet.Generate(client); - var txInput = new ThirdwebTransactionInput() { To = Constants.ADDRESS_ZERO }; - var ex = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(null, txInput, 421614)); + var txInput = new ThirdwebTransactionInput(421614) { To = Constants.ADDRESS_ZERO }; + var ex = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(null, txInput)); Assert.Contains("Wallet must be provided", ex.Message); } @@ -81,8 +78,7 @@ public async Task Create_ThrowsOnChainIdZero() { var client = ThirdwebClient.Create(secretKey: this.SecretKey); var wallet = await PrivateKeyWallet.Generate(client); - var txInput = new ThirdwebTransactionInput() { To = Constants.ADDRESS_ZERO }; - var ex = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(wallet, txInput, BigInteger.Zero)); + var ex = Assert.Throws(() => new ThirdwebTransactionInput(0) { To = Constants.ADDRESS_ZERO }); Assert.Contains("Invalid Chain ID", ex.Message); } @@ -166,7 +162,7 @@ public async Task Sign_SmartWallet_SignsTransaction() var client = ThirdwebClient.Create(secretKey: this.SecretKey); var privateKeyAccount = await PrivateKeyWallet.Generate(client); var smartAccount = await SmartWallet.Create(personalWallet: privateKeyAccount, factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", gasless: true, chainId: 421614); - var transaction = await ThirdwebTransaction.Create(smartAccount, new ThirdwebTransactionInput() { To = Constants.ADDRESS_ZERO, }, 421614); + var transaction = await ThirdwebTransaction.Create(smartAccount, new ThirdwebTransactionInput(421614) { To = Constants.ADDRESS_ZERO, }); var signed = await ThirdwebTransaction.Sign(transaction); Assert.NotNull(signed); } @@ -218,43 +214,43 @@ public async Task SetZkSyncOptions_DefaultsToZeroNull() Assert.Null(transaction.Input.ZkSync?.FactoryDeps); } - // [Fact(Timeout = 120000)] - // public async Task Send_ZkSync_TransfersGaslessly() - // { - // var transaction = await CreateSampleTransaction(); - // _ = transaction.SetChainId(300); - // _ = transaction.SetTo("0xbA226d47Cbb2731CBAA67C916c57d68484AA269F"); - // _ = transaction.SetValue(BigInteger.Zero); - // _ = transaction.SetZkSyncOptions( - // new ZkSyncOptions( - // paymaster: "0xbA226d47Cbb2731CBAA67C916c57d68484AA269F", - // paymasterInput: "0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000", - // gasPerPubdataByteLimit: 50000, - // factoryDeps: new List() - // ) - // ); - // var receipt = await ThirdwebTransaction.SendAndWaitForTransactionReceipt(transaction); - // Assert.NotNull(receipt); - // Assert.StartsWith("0x", receipt.TransactionHash); - // } + [Fact(Timeout = 120000)] + public async Task Send_ZkSync_TransfersGaslessly() + { + var transaction = await this.CreateSampleTransaction(); + _ = transaction.SetChainId(300); + _ = transaction.SetTo("0xbA226d47Cbb2731CBAA67C916c57d68484AA269F"); + _ = transaction.SetValue(BigInteger.Zero); + _ = transaction.SetZkSyncOptions( + new ZkSyncOptions( + paymaster: "0xbA226d47Cbb2731CBAA67C916c57d68484AA269F", + paymasterInput: "0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000", + gasPerPubdataByteLimit: 50000, + factoryDeps: new List() + ) + ); + var receipt = await ThirdwebTransaction.SendAndWaitForTransactionReceipt(transaction); + Assert.NotNull(receipt); + Assert.StartsWith("0x", receipt.TransactionHash); + } - // [Fact(Timeout = 120000)] - // public async Task Send_ZkSync_NoGasPerPubFactoryDepsTransfersGaslessly() - // { - // var transaction = await CreateSampleTransaction(); - // _ = transaction.SetChainId(300); - // _ = transaction.SetTo("0xbA226d47Cbb2731CBAA67C916c57d68484AA269F"); - // _ = transaction.SetValue(BigInteger.Zero); - // _ = transaction.SetZkSyncOptions( - // new ZkSyncOptions( - // paymaster: "0xbA226d47Cbb2731CBAA67C916c57d68484AA269F", - // paymasterInput: "0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" - // ) - // ); - // var receipt = await ThirdwebTransaction.SendAndWaitForTransactionReceipt(transaction); - // Assert.NotNull(receipt); - // Assert.StartsWith("0x", receipt.TransactionHash); - // } + [Fact(Timeout = 120000)] + public async Task Send_ZkSync_NoGasPerPubFactoryDepsTransfersGaslessly() + { + var transaction = await this.CreateSampleTransaction(); + _ = transaction.SetChainId(300); + _ = transaction.SetTo("0xbA226d47Cbb2731CBAA67C916c57d68484AA269F"); + _ = transaction.SetValue(BigInteger.Zero); + _ = transaction.SetZkSyncOptions( + new ZkSyncOptions( + paymaster: "0xbA226d47Cbb2731CBAA67C916c57d68484AA269F", + paymasterInput: "0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" + ) + ); + var receipt = await ThirdwebTransaction.SendAndWaitForTransactionReceipt(transaction); + Assert.NotNull(receipt); + Assert.StartsWith("0x", receipt.TransactionHash); + } [Fact(Timeout = 120000)] public async Task EstimateTotalCosts_CalculatesCostsCorrectly() @@ -355,9 +351,7 @@ public async Task EstimateGasFees_ReturnsCorrectly() { var transaction = await ThirdwebTransaction.Create( await PrivateKeyWallet.Generate(ThirdwebClient.Create(secretKey: this.SecretKey)), - new ThirdwebTransactionInput() { To = Constants.ADDRESS_ZERO, }, - 250 // fantom for 1559 non zero prio - ); + new ThirdwebTransactionInput(250) { To = Constants.ADDRESS_ZERO, }); (var maxFee, var maxPrio) = await ThirdwebTransaction.EstimateGasFees(transaction); @@ -393,7 +387,7 @@ public async Task Simulate_ReturnsDataOrThrowsIntrinsic() var client = ThirdwebClient.Create(secretKey: this.SecretKey); var privateKeyAccount = await PrivateKeyWallet.Generate(client); var smartAccount = await SmartWallet.Create(personalWallet: privateKeyAccount, factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", gasless: true, chainId: 421614); - var transaction = await ThirdwebTransaction.Create(smartAccount, new ThirdwebTransactionInput() { To = Constants.ADDRESS_ZERO, Gas = new HexBigInteger(250000), }, 421614); + var transaction = await ThirdwebTransaction.Create(smartAccount, new ThirdwebTransactionInput(421614) { To = Constants.ADDRESS_ZERO, Gas = new HexBigInteger(250000), }); try { diff --git a/Thirdweb.Tests/Thirdweb.Transactions/Thirdweb.ZkSmartWallet.Tests.cs b/Thirdweb.Tests/Thirdweb.Transactions/Thirdweb.ZkSmartWallet.Tests.cs index ff6840a..129b03d 100644 --- a/Thirdweb.Tests/Thirdweb.Transactions/Thirdweb.ZkSmartWallet.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Transactions/Thirdweb.ZkSmartWallet.Tests.cs @@ -78,7 +78,7 @@ public async Task SendGaslessZkTx_Success() { var account = await this.GetSmartAccount(); var hash = await account.SendTransaction( - new ThirdwebTransactionInput() + new ThirdwebTransactionInput(300) { From = await account.GetAddress(), To = await account.GetAddress(), @@ -90,29 +90,29 @@ public async Task SendGaslessZkTx_Success() Assert.True(hash.Length == 66); } - // [Fact(Timeout = 120000)] - // public async Task SendGaslessZkTx_ZkCandy_Success() - // { - // var account = await GetSmartAccount(zkChainId: 302); - // var hash = await account.SendTransaction( - // new ThirdwebTransactionInput() - // { - // From = await account.GetAddress(), - // To = await account.GetAddress(), - // Value = new Nethereum.Hex.HexTypes.HexBigInteger(0), - // Data = "0x" - // } - // ); - // Assert.NotNull(hash); - // Assert.True(hash.Length == 66); - // } + [Fact(Timeout = 120000)] + public async Task SendGaslessZkTx_ZkCandy_Success() + { + var account = await this.GetSmartAccount(zkChainId: 302); + var hash = await account.SendTransaction( + new ThirdwebTransactionInput(302) + { + From = await account.GetAddress(), + To = await account.GetAddress(), + Value = new Nethereum.Hex.HexTypes.HexBigInteger(0), + Data = "0x" + } + ); + Assert.NotNull(hash); + Assert.True(hash.Length == 66); + } [Fact(Timeout = 120000)] public async Task SendGaslessZkTx_Abstract_Success() { var account = await this.GetSmartAccount(zkChainId: 11124); var hash = await account.SendTransaction( - new ThirdwebTransactionInput() + new ThirdwebTransactionInput(11124) { From = await account.GetAddress(), To = await account.GetAddress(), @@ -123,4 +123,19 @@ public async Task SendGaslessZkTx_Abstract_Success() Assert.NotNull(hash); Assert.True(hash.Length == 66); } + + [Fact(Timeout = 120000)] + public async Task ZkSync_Switch() + { + var account = await this.GetSmartAccount(zkChainId: 300); + _ = await account.SendTransaction( + new ThirdwebTransactionInput(302) + { + From = await account.GetAddress(), + To = await account.GetAddress(), + Value = new Nethereum.Hex.HexTypes.HexBigInteger(0), + Data = "0x" + } + ); + } } diff --git a/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.PrivateKeyWallet.Tests.cs b/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.PrivateKeyWallet.Tests.cs index 843fc79..f1c8499 100644 --- a/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.PrivateKeyWallet.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.PrivateKeyWallet.Tests.cs @@ -168,7 +168,7 @@ public async Task SignTypedDataV4_Typed_NullData() public async Task SignTransaction_Success() { var account = await this.GetAccount(); - var transaction = new ThirdwebTransactionInput + var transaction = new ThirdwebTransactionInput(421614) { From = await account.GetAddress(), To = Constants.ADDRESS_ZERO, @@ -177,7 +177,6 @@ public async Task SignTransaction_Success() // Data = "0x", Nonce = new HexBigInteger(99999999999), GasPrice = new HexBigInteger(10000000000), - ChainId = new HexBigInteger(421614) }; var signature = await account.SignTransaction(transaction); Assert.NotNull(signature); @@ -187,7 +186,7 @@ public async Task SignTransaction_Success() public async Task SignTransaction_NoFrom_Success() { var account = await this.GetAccount(); - var transaction = new ThirdwebTransactionInput + var transaction = new ThirdwebTransactionInput(421614) { To = Constants.ADDRESS_ZERO, // Value = new HexBigInteger(0), @@ -195,7 +194,6 @@ public async Task SignTransaction_NoFrom_Success() Data = "0x", Nonce = new HexBigInteger(99999999999), GasPrice = new HexBigInteger(10000000000), - ChainId = new HexBigInteger(421614) }; var signature = await account.SignTransaction(transaction); Assert.NotNull(signature); @@ -213,7 +211,7 @@ public async Task SignTransaction_NullTransaction() public async Task SignTransaction_NoNonce() { var account = await this.GetAccount(); - var transaction = new ThirdwebTransactionInput + var transaction = new ThirdwebTransactionInput(421614) { From = await account.GetAddress(), To = Constants.ADDRESS_ZERO, @@ -229,7 +227,7 @@ public async Task SignTransaction_NoNonce() public async Task SignTransaction_NoGasPrice() { var account = await this.GetAccount(); - var transaction = new ThirdwebTransactionInput + var transaction = new ThirdwebTransactionInput(421614) { From = await account.GetAddress(), To = Constants.ADDRESS_ZERO, @@ -247,7 +245,7 @@ public async Task SignTransaction_NoGasPrice() public async Task SignTransaction_1559_Success() { var account = await this.GetAccount(); - var transaction = new ThirdwebTransactionInput + var transaction = new ThirdwebTransactionInput(421614) { From = await account.GetAddress(), To = Constants.ADDRESS_ZERO, @@ -257,7 +255,6 @@ public async Task SignTransaction_1559_Success() Nonce = new HexBigInteger(99999999999), MaxFeePerGas = new HexBigInteger(10000000000), MaxPriorityFeePerGas = new HexBigInteger(10000000000), - ChainId = new HexBigInteger(421614) }; var signature = await account.SignTransaction(transaction); Assert.NotNull(signature); @@ -267,7 +264,7 @@ public async Task SignTransaction_1559_Success() public async Task SignTransaction_1559_NoMaxFeePerGas() { var account = await this.GetAccount(); - var transaction = new ThirdwebTransactionInput + var transaction = new ThirdwebTransactionInput(421614) { From = await account.GetAddress(), To = Constants.ADDRESS_ZERO, @@ -276,7 +273,6 @@ public async Task SignTransaction_1559_NoMaxFeePerGas() Data = "0x", Nonce = new HexBigInteger(99999999999), MaxPriorityFeePerGas = new HexBigInteger(10000000000), - ChainId = new HexBigInteger(421614) }; var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction)); Assert.Equal("Transaction MaxPriorityFeePerGas and MaxFeePerGas must be set for EIP-1559 transactions", ex.Message); @@ -286,7 +282,7 @@ public async Task SignTransaction_1559_NoMaxFeePerGas() public async Task SignTransaction_1559_NoMaxPriorityFeePerGas() { var account = await this.GetAccount(); - var transaction = new ThirdwebTransactionInput + var transaction = new ThirdwebTransactionInput(421614) { From = await account.GetAddress(), To = Constants.ADDRESS_ZERO, @@ -295,7 +291,6 @@ public async Task SignTransaction_1559_NoMaxPriorityFeePerGas() Data = "0x", Nonce = new HexBigInteger(99999999999), MaxFeePerGas = new HexBigInteger(10000000000), - ChainId = new HexBigInteger(421614) }; var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction)); Assert.Equal("Transaction MaxPriorityFeePerGas and MaxFeePerGas must be set for EIP-1559 transactions", ex.Message); @@ -344,7 +339,7 @@ public async Task Disconnect_Connected() public async Task SendTransaction_InvalidOperation() { var account = await this.GetAccount(); - var transaction = new ThirdwebTransactionInput + var transaction = new ThirdwebTransactionInput(421614) { From = await account.GetAddress(), To = Constants.ADDRESS_ZERO, @@ -358,7 +353,7 @@ public async Task SendTransaction_InvalidOperation() public async Task ExecuteTransaction_InvalidOperation() { var account = await this.GetAccount(); - var transaction = new ThirdwebTransactionInput + var transaction = new ThirdwebTransactionInput(421614) { From = await account.GetAddress(), To = Constants.ADDRESS_ZERO, diff --git a/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.SmartWallet.Tests.cs b/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.SmartWallet.Tests.cs index 6b722a2..bd14c51 100644 --- a/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.SmartWallet.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.SmartWallet.Tests.cs @@ -82,7 +82,7 @@ public async Task IsDeployed_False() public async Task ExecuteTransaction_Success() { var account = await this.GetSmartAccount(); - var tx = await account.ExecuteTransaction(new ThirdwebTransactionInput() { To = await account.GetAddress() }); + var tx = await account.ExecuteTransaction(new ThirdwebTransactionInput(421614) { To = await account.GetAddress() }); Assert.NotNull(tx); } @@ -90,7 +90,7 @@ public async Task ExecuteTransaction_Success() public async Task SendTransaction_Success() { var account = await this.GetSmartAccount(); - var tx = await account.SendTransaction(new ThirdwebTransactionInput() { To = await account.GetAddress(), }); + var tx = await account.SendTransaction(new ThirdwebTransactionInput(421614) { To = await account.GetAddress(), }); Assert.NotNull(tx); } @@ -100,7 +100,7 @@ public async Task SendTransaction_ClientBundleId_Success() var client = ThirdwebClient.Create(clientId: this.ClientIdBundleIdOnly, bundleId: this.BundleIdBundleIdOnly); var privateKeyAccount = await PrivateKeyWallet.Generate(client); var smartAccount = await SmartWallet.Create(personalWallet: privateKeyAccount, factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", gasless: true, chainId: 421614); - var tx = await smartAccount.SendTransaction(new ThirdwebTransactionInput() { To = await smartAccount.GetAddress(), }); + var tx = await smartAccount.SendTransaction(new ThirdwebTransactionInput(421614) { To = await smartAccount.GetAddress(), }); Assert.NotNull(tx); } @@ -170,7 +170,7 @@ public async Task CreateSessionKey() var account = await this.GetSmartAccount(); var receipt = await account.CreateSessionKey( signerAddress: "0x253d077C45A3868d0527384e0B34e1e3088A3908", - approvedTargets: [Constants.ADDRESS_ZERO], + approvedTargets: new List { Constants.ADDRESS_ZERO }, nativeTokenLimitPerTransactionInWei: "0", permissionStartTimestamp: "0", permissionEndTimestamp: (Utils.GetUnixTimeStampNow() + 86400).ToString(), @@ -229,7 +229,7 @@ public async Task GetAllActiveSigners() var randomSigner = await (await PrivateKeyWallet.Generate(this._client)).GetAddress(); _ = await account.CreateSessionKey( signerAddress: randomSigner, - approvedTargets: [Constants.ADDRESS_ZERO], + approvedTargets: new List() { Constants.ADDRESS_ZERO }, nativeTokenLimitPerTransactionInWei: "0", permissionStartTimestamp: "0", permissionEndTimestamp: (Utils.GetUnixTimeStampNow() + 86400).ToString(), @@ -285,9 +285,47 @@ public async Task SendTransaction_07_Success() entryPoint: Constants.ENTRYPOINT_ADDRESS_V07 ); - var hash07 = await smartWallet07.SendTransaction(new ThirdwebTransactionInput() { To = await smartWallet07.GetAddress(), }); + var hash07 = await smartWallet07.SendTransaction(new ThirdwebTransactionInput(11155111) { To = await smartWallet07.GetAddress(), }); Assert.NotNull(hash07); Assert.True(hash07.Length == 66); } + + [Fact(Timeout = 120000)] + public async Task MultiChainTransaction_Success() + { + var chainId1 = 11155111; + var chainId2 = 421614; + + var smartWallet = await SmartWallet.Create( + personalWallet: await PrivateKeyWallet.Generate(this._client), + chainId: chainId1, + gasless: true, + factoryAddress: Constants.DEFAULT_FACTORY_ADDRESS_V06, + entryPoint: Constants.ENTRYPOINT_ADDRESS_V06 + ); + + var address1 = await smartWallet.GetAddress(); + var receipt1 = await smartWallet.ExecuteTransaction(new ThirdwebTransactionInput(chainId1) { To = address1, }); + var nonce1 = await smartWallet.GetTransactionCount(chainId: chainId1, blocktag: "latest"); + + var address2 = await smartWallet.GetAddress(); + var receipt2 = await smartWallet.ExecuteTransaction(new ThirdwebTransactionInput(chainId2) { To = address2, }); + var nonce2 = await smartWallet.GetTransactionCount(chainId: chainId2, blocktag: "latest"); + + Assert.NotNull(address1); + Assert.NotNull(address2); + Assert.Equal(address1, address2); + + Assert.NotNull(receipt1); + Assert.NotNull(receipt2); + + Assert.True(receipt1.TransactionHash.Length == 66); + Assert.True(receipt2.TransactionHash.Length == 66); + + Assert.Equal(receipt1.To, receipt2.To); + + Assert.Equal(nonce1, 1); + Assert.Equal(nonce2, 1); + } } diff --git a/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.Wallets.Tests.cs b/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.Wallets.Tests.cs index 20bd94b..826b59c 100644 --- a/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.Wallets.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Wallets/Thirdweb.Wallets.Tests.cs @@ -61,7 +61,7 @@ public async Task PersonalSignRaw() [Fact(Timeout = 120000)] public async Task PersonalSign() { - var wallet = await this.GetSmartAccount(); + var wallet = await this.GetPrivateKeyAccount(); var message = "Hello, world!"; var signature = await wallet.PersonalSign(message); Assert.NotNull(signature); @@ -122,7 +122,7 @@ await wallet.GetAddress(), public async Task SignTransaction() { var wallet = await this.GetSmartAccount(); - var transaction = new ThirdwebTransactionInput + var transaction = new ThirdwebTransactionInput(421614) { To = await wallet.GetAddress(), Data = "0x", @@ -130,7 +130,6 @@ public async Task SignTransaction() Gas = new HexBigInteger(21000), GasPrice = new HexBigInteger(10000000000), Nonce = new HexBigInteger(9999999999999), - ChainId = new HexBigInteger(421614), }; _ = ThirdwebRPC.GetRpcInstance(ThirdwebClient.Create(secretKey: this.SecretKey), 421614); var signature = await wallet.SignTransaction(transaction); @@ -174,7 +173,7 @@ public async Task RecoverAddressFromSignTypedDataV4_ReturnsSameAddress() var typedData = EIP712.GetTypedDefinition_SmartAccount_AccountMessage("Account", "1", 421614, await wallet.GetAddress()); var accountMessage = new AccountAbstraction.AccountMessage { Message = System.Text.Encoding.UTF8.GetBytes("Hello, world!").HashPrefixedMessage() }; var signature = await wallet.SignTypedDataV4(accountMessage, typedData); - var recoveredAddress = await wallet.RecoverAddressFromTypedDataV4(accountMessage, typedData, signature); + var recoveredAddress = await wallet.RecoverAddressFromTypedDataV4(accountMessage, typedData, signature); Assert.Equal(await wallet.GetAddress(), recoveredAddress); } @@ -232,7 +231,7 @@ public async Task RecoverAddress_AllVariants_NullTests() ); _ = await Assert.ThrowsAsync( async () => - await wallet.RecoverAddressFromTypedDataV4( + await wallet.RecoverAddressFromTypedDataV4( new AccountAbstraction.SignerPermissionRequest(), nullTypedData, nullSig @@ -240,7 +239,7 @@ public async Task RecoverAddress_AllVariants_NullTests() ); _ = await Assert.ThrowsAsync( async () => - await wallet.RecoverAddressFromTypedDataV4( + await wallet.RecoverAddressFromTypedDataV4( new AccountAbstraction.SignerPermissionRequest(), new Nethereum.ABI.EIP712.TypedData(), nullSig diff --git a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs index 081bdfd..3f7a45e 100644 --- a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs +++ b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs @@ -150,14 +150,14 @@ public static async Task Prepare(IThirdwebWallet wallet, Th } var data = function.GetData(parameters); - var transaction = new ThirdwebTransactionInput + var transaction = new ThirdwebTransactionInput(chainId: contract.Chain) { To = contract.Address, Data = data, Value = new HexBigInteger(weiValue), }; - return await ThirdwebTransaction.Create(wallet, transaction, contract.Chain).ConfigureAwait(false); + return await ThirdwebTransaction.Create(wallet, transaction).ConfigureAwait(false); } /// diff --git a/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs b/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs index b6d7275..40a272d 100644 --- a/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs +++ b/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs @@ -181,10 +181,79 @@ public static async Task GetBalance(this IThirdwebWallet wallet, Big } var address = await wallet.GetAddress().ConfigureAwait(false); - return await GetBalanceRaw(wallet.Client, chainId, address, erc20ContractAddress).ConfigureAwait(false); } + /// + /// Retrieves the transaction count (i.e. nonce) of the specified address on the specified chain. + /// + /// The client used to retrieve the transaction count. + /// The chain ID to retrieve the transaction count from. + /// The address to retrieve the transaction count for. + /// The block tag to retrieve the transaction count at. Defaults to "pending". + /// A task that represents the asynchronous operation. The task result contains the transaction count. + /// Thrown when the client is null. + /// Thrown when the chain ID is less than or equal to 0. + /// Thrown when the address is null or empty. + public static async Task GetTransactionCountRaw(ThirdwebClient client, BigInteger chainId, string address, string blocktag = "pending") + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (chainId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(chainId), "Chain ID must be greater than 0."); + } + + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentException("Address must be provided"); + } + + var rpc = ThirdwebRPC.GetRpcInstance(client, chainId); + var balanceHex = await rpc.SendRequestAsync("eth_getTransactionCount", address, blocktag).ConfigureAwait(false); + return new HexBigInteger(balanceHex).Value; + } + + /// + /// Retrieves the transaction count (i.e. nonce) of the specified contract. + /// + /// The contract to retrieve the transaction count for. + /// The block tag to retrieve the transaction count at. Defaults to "pending". + /// A task that represents the asynchronous operation. The task result contains the transaction count. + /// Thrown when the contract is null. + public static async Task GetTransactionCount(this ThirdwebContract contract, string blocktag = "pending") + { + return contract == null ? throw new ArgumentNullException(nameof(contract)) : await GetTransactionCountRaw(contract.Client, contract.Chain, contract.Address, blocktag).ConfigureAwait(false); + } + + /// + /// Retrieves the transaction count (i.e. nonce) of the specified wallet on the specified chain. + /// + /// The wallet to retrieve the transaction count for. + /// The chain ID to retrieve the transaction count from. + /// The block tag to retrieve the transaction count at. Defaults to "pending". + /// A task that represents the asynchronous operation. The task result contains the transaction count. + /// Thrown when the wallet is null. + /// Thrown when the chain ID is less than or equal to 0. + public static async Task GetTransactionCount(this IThirdwebWallet wallet, BigInteger chainId, string blocktag = "pending") + { + if (wallet == null) + { + throw new ArgumentNullException(nameof(wallet)); + } + + if (chainId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(chainId), "Chain ID must be greater than 0."); + } + + var address = await wallet.GetAddress().ConfigureAwait(false); + return await GetTransactionCountRaw(wallet.Client, chainId, address, blocktag).ConfigureAwait(false); + } + /// /// Transfers the specified amount of Wei to the specified address. /// @@ -218,8 +287,8 @@ public static async Task Transfer(this IThirdwebWall throw new ArgumentOutOfRangeException(nameof(weiAmount), "Amount must be 0 or greater."); } - var txInput = new ThirdwebTransactionInput() { To = toAddress, Value = new HexBigInteger(weiAmount) }; - var tx = await ThirdwebTransaction.Create(wallet, txInput, chainId).ConfigureAwait(false); + var txInput = new ThirdwebTransactionInput(chainId) { To = toAddress, Value = new HexBigInteger(weiAmount) }; + var tx = await ThirdwebTransaction.Create(wallet, txInput).ConfigureAwait(false); return await ThirdwebTransaction.SendAndWaitForTransactionReceipt(tx).ConfigureAwait(false); } diff --git a/Thirdweb/Thirdweb.Pay/ThirdwebPay.BuyWithCrypto.cs b/Thirdweb/Thirdweb.Pay/ThirdwebPay.BuyWithCrypto.cs index bfb053b..492a611 100644 --- a/Thirdweb/Thirdweb.Pay/ThirdwebPay.BuyWithCrypto.cs +++ b/Thirdweb/Thirdweb.Pay/ThirdwebPay.BuyWithCrypto.cs @@ -26,7 +26,7 @@ public static async Task BuyWithCrypto(IThirdwebWallet wallet, BuyWithCr } } - var txInput = new ThirdwebTransactionInput() + var txInput = new ThirdwebTransactionInput(chainId: buyWithCryptoQuote.TransactionRequest.ChainId) { To = buyWithCryptoQuote.TransactionRequest.To, Data = buyWithCryptoQuote.TransactionRequest.Data, @@ -35,7 +35,7 @@ public static async Task BuyWithCrypto(IThirdwebWallet wallet, BuyWithCr GasPrice = new HexBigInteger(BigInteger.Parse(buyWithCryptoQuote.TransactionRequest.GasPrice)), }; - var tx = await ThirdwebTransaction.Create(wallet, txInput, buyWithCryptoQuote.TransactionRequest.ChainId); + var tx = await ThirdwebTransaction.Create(wallet, txInput); var hash = await ThirdwebTransaction.Send(tx); diff --git a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs index bd0b8e1..f54e9c8 100644 --- a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs +++ b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs @@ -32,11 +32,10 @@ public class ThirdwebTransaction private readonly IThirdwebWallet _wallet; - private ThirdwebTransaction(IThirdwebWallet wallet, ThirdwebTransactionInput txInput, BigInteger chainId) + private ThirdwebTransaction(IThirdwebWallet wallet, ThirdwebTransactionInput txInput) { this.Input = txInput; this._wallet = wallet; - this.Input.ChainId = chainId.ToHexBigInteger(); } /// @@ -44,20 +43,14 @@ private ThirdwebTransaction(IThirdwebWallet wallet, ThirdwebTransactionInput txI /// /// The wallet to use for the transaction. /// The transaction input. - /// The chain ID. /// A new Thirdweb transaction. - public static async Task Create(IThirdwebWallet wallet, ThirdwebTransactionInput txInput, BigInteger chainId) + public static async Task Create(IThirdwebWallet wallet, ThirdwebTransactionInput txInput) { if (wallet == null) { throw new ArgumentException("Wallet must be provided", nameof(wallet)); } - if (chainId == 0) - { - throw new ArgumentException("Invalid Chain ID", nameof(chainId)); - } - if (txInput.To == null) { throw new ArgumentException("Transaction recipient (to) must be provided", nameof(txInput)); @@ -67,7 +60,7 @@ public static async Task Create(IThirdwebWallet wallet, Thi txInput.Data ??= "0x"; txInput.Value ??= new HexBigInteger(0); - return new ThirdwebTransaction(wallet, txInput, chainId); + return new ThirdwebTransaction(wallet, txInput); } /// @@ -325,8 +318,7 @@ public static async Task EstimateGasLimit(ThirdwebTransaction transa /// The nonce. public static async Task GetNonce(ThirdwebTransaction transaction) { - var rpc = ThirdwebRPC.GetRpcInstance(transaction._wallet.Client, transaction.Input.ChainId.Value); - return new HexBigInteger(await rpc.SendRequestAsync("eth_getTransactionCount", transaction.Input.From, "pending").ConfigureAwait(false)).Value; + return await transaction._wallet.GetTransactionCount(chainId: transaction.Input.ChainId, blocktag: "pending").ConfigureAwait(false); } private static async Task GetGasPerPubData(ThirdwebTransaction transaction) diff --git a/Thirdweb/Thirdweb.Transactions/ThirdwebTransactionInput.cs b/Thirdweb/Thirdweb.Transactions/ThirdwebTransactionInput.cs index ca0789d..e30f286 100644 --- a/Thirdweb/Thirdweb.Transactions/ThirdwebTransactionInput.cs +++ b/Thirdweb/Thirdweb.Transactions/ThirdwebTransactionInput.cs @@ -10,9 +10,13 @@ namespace Thirdweb; /// public class ThirdwebTransactionInput { - public ThirdwebTransactionInput() { } + public ThirdwebTransactionInput(BigInteger chainId) + { + this.ChainId = chainId > 0 ? new HexBigInteger(chainId) : throw new ArgumentException("Invalid Chain ID"); + } public ThirdwebTransactionInput( + BigInteger chainId, string from = null, string to = null, BigInteger? nonce = null, @@ -20,12 +24,12 @@ public ThirdwebTransactionInput( BigInteger? gasPrice = null, BigInteger? value = null, string data = null, - BigInteger? chainId = null, BigInteger? maxFeePerGas = null, BigInteger? maxPriorityFeePerGas = null, ZkSyncOptions? zkSync = null ) { + this.ChainId = chainId > 0 ? new HexBigInteger(chainId) : throw new ArgumentException("Invalid Chain ID"); this.From = string.IsNullOrEmpty(from) ? Constants.ADDRESS_ZERO : from; this.To = string.IsNullOrEmpty(to) ? Constants.ADDRESS_ZERO : to; this.Nonce = nonce == null ? null : new HexBigInteger(nonce.Value); @@ -33,7 +37,6 @@ public ThirdwebTransactionInput( this.GasPrice = gasPrice == null ? null : new HexBigInteger(gasPrice.Value); this.Value = value == null ? null : new HexBigInteger(value.Value); this.Data = string.IsNullOrEmpty(data) ? "0x" : data; - this.ChainId = chainId == null ? null : new HexBigInteger(chainId.Value); this.MaxFeePerGas = maxFeePerGas == null ? null : new HexBigInteger(maxFeePerGas.Value); this.MaxPriorityFeePerGas = maxPriorityFeePerGas == null ? null : new HexBigInteger(maxPriorityFeePerGas.Value); this.ZkSync = zkSync; diff --git a/Thirdweb/Thirdweb.Utils/Utils.cs b/Thirdweb/Thirdweb.Utils/Utils.cs index 78cc6c4..6cc5f2f 100644 --- a/Thirdweb/Thirdweb.Utils/Utils.cs +++ b/Thirdweb/Thirdweb.Utils/Utils.cs @@ -392,6 +392,11 @@ public static async Task GetChainMetadata(ThirdwebClient clie public static int GetEntryPointVersion(string address) { + if (address == null) + { + return 6; // non-4337 + } + address = address.ToChecksumAddress(); return address switch { diff --git a/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs b/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs index 5f4ab1a..d9354b8 100644 --- a/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs @@ -26,18 +26,19 @@ public class SmartWallet : IThirdwebWallet public bool IsDeploying { get; private set; } private readonly IThirdwebWallet _personalAccount; - private readonly bool _gasless; - private readonly ThirdwebContract _factoryContract; + private ThirdwebContract _factoryContract; private ThirdwebContract _accountContract; - private readonly ThirdwebContract _entryPointContract; - private readonly BigInteger _chainId; - private readonly string _bundlerUrl; - private readonly string _paymasterUrl; + private ThirdwebContract _entryPointContract; + private BigInteger _chainId; + private string _bundlerUrl; + private string _paymasterUrl; + private bool _isApproving; + private bool _isApproved; + private readonly string _erc20PaymasterAddress; private readonly string _erc20PaymasterToken; private readonly BigInteger _erc20PaymasterStorageSlot; - private bool _isApproving; - private bool _isApproved; + private readonly bool _gasless; private struct TokenPaymasterConfig { @@ -179,6 +180,36 @@ public static async Task Create( ); } + /// + /// Attempts to set the active network to the specified chain ID. Requires related contracts to be deterministically deployed on the chain. + /// + /// The chain ID to switch to. + /// + public async Task SwitchNetwork(BigInteger chainId) + { + if (this._chainId == chainId) + { + return; + } + + if (this.UseERC20Paymaster) + { + throw new InvalidOperationException("You cannot switch networks when using an ERC20 paymaster yet."); + } + + this._chainId = chainId; + + var entryPointVersion = Utils.GetEntryPointVersion(this._entryPointContract?.Address); + this._bundlerUrl = entryPointVersion == 6 ? $"https://{chainId}.bundler.thirdweb.com" : $"https://{chainId}.bundler.thirdweb.com/v2"; + this._paymasterUrl = entryPointVersion == 6 ? $"https://{chainId}.bundler.thirdweb.com" : $"https://{chainId}.bundler.thirdweb.com/v2"; + if (!Utils.IsZkSync(chainId)) + { + this._entryPointContract = await ThirdwebContract.Create(this.Client, this._entryPointContract.Address, chainId, this._entryPointContract.Abi); + this._factoryContract = await ThirdwebContract.Create(this.Client, this._factoryContract.Address, chainId, this._factoryContract.Abi); + this._accountContract = await ThirdwebContract.Create(this.Client, this._accountContract.Address, chainId, this._accountContract.Abi); + } + } + public async Task IsDeployed() { if (Utils.IsZkSync(this._chainId)) @@ -197,7 +228,9 @@ public async Task SendTransaction(ThirdwebTransactionInput transactionIn throw new InvalidOperationException("SmartAccount.SendTransaction: Transaction input is required."); } - var transaction = await ThirdwebTransaction.Create(Utils.IsZkSync(this._chainId) ? this._personalAccount : this, transactionInput, this._chainId); + await this.SwitchNetwork(transactionInput.ChainId.Value); + + var transaction = await ThirdwebTransaction.Create(Utils.IsZkSync(this._chainId) ? this._personalAccount : this, transactionInput); transaction = await ThirdwebTransaction.Prepare(transaction); transactionInput = transaction.Input; @@ -643,7 +676,7 @@ public async Task ForceDeploy() throw new InvalidOperationException("SmartAccount.ForceDeploy: Account is already deploying."); } - var input = new ThirdwebTransactionInput() + var input = new ThirdwebTransactionInput(this._chainId) { Data = "0x", To = this._accountContract.Address, @@ -805,7 +838,7 @@ string reqValidityEndTimestamp var signature = await EIP712.GenerateSignature_SmartAccount("Account", "1", this._chainId, await this.GetAddress(), request, this._personalAccount); // Do it this way to avoid triggering an extra sig from estimation var data = new Contract(null, this._accountContract.Abi, this._accountContract.Address).GetFunction("setPermissionsForSigner").GetData(request, signature.HexToBytes()); - var txInput = new ThirdwebTransactionInput() + var txInput = new ThirdwebTransactionInput(this._chainId) { To = this._accountContract.Address, Value = new HexBigInteger(0), @@ -844,7 +877,7 @@ public async Task AddAdmin(string admin) var signature = await EIP712.GenerateSignature_SmartAccount("Account", "1", this._chainId, await this.GetAddress(), request, this._personalAccount); var data = new Contract(null, this._accountContract.Abi, this._accountContract.Address).GetFunction("setPermissionsForSigner").GetData(request, signature.HexToBytes()); - var txInput = new ThirdwebTransactionInput() + var txInput = new ThirdwebTransactionInput(this._chainId) { To = this._accountContract.Address, Value = new HexBigInteger(0), @@ -876,7 +909,7 @@ public async Task RemoveAdmin(string admin) var signature = await EIP712.GenerateSignature_SmartAccount("Account", "1", this._chainId, await this.GetAddress(), request, this._personalAccount); var data = new Contract(null, this._accountContract.Abi, this._accountContract.Address).GetFunction("setPermissionsForSigner").GetData(request, signature.HexToBytes()); - var txInput = new ThirdwebTransactionInput() + var txInput = new ThirdwebTransactionInput(this._chainId) { To = this._accountContract.Address, Value = new HexBigInteger(0), @@ -905,6 +938,8 @@ public Task RecoverAddressFromTypedDataV4(T data, TypedData< public async Task EstimateUserOperationGas(ThirdwebTransactionInput transaction) { + await this.SwitchNetwork(transaction.ChainId.Value); + if (Utils.IsZkSync(this._chainId)) { throw new Exception("User Operations are not supported in ZkSync"); @@ -932,6 +967,8 @@ public async Task EstimateUserOperationGas(ThirdwebTransactionInput public async Task SignTransaction(ThirdwebTransactionInput transaction) { + await this.SwitchNetwork(transaction.ChainId.Value); + if (Utils.IsZkSync(this._chainId)) { throw new Exception("Offline Signing is not supported in ZkSync");