From b22d405eb13de3e53a7bfc3ed207295d468044c4 Mon Sep 17 00:00:00 2001 From: Firekeeper <0xFirekeeper@gmail.com> Date: Wed, 12 Jun 2024 21:21:15 +0300 Subject: [PATCH] Unity Compatibility + Customizability + Various Improvements (#25) * Thirdweb.Http Refactor to a cross-platform http client wrapper, must test thoroughly with Unity use case. May require additional conditional compilation. * clear out temp auth header * support oldschool ews and fix header * Unity config, support and wrappers Needs cleanup specially config and build pipeline * Generic APIs, move Unity-specific code back to Unity * dupe header fix * zkcandy support * HttpTests * HttpContent * Update ThirdwebHttpContent.cs * final http tests * improved tx tests, bump gas per pubdata * Create Thirdweb.ZkSmartWallet.Tests.cs * Update Thirdweb.PrivateKeyWallet.Tests.cs * Update Thirdweb.Transactions.Tests.cs * improve estimations --- Thirdweb.Console/Program.cs | 4 +- Thirdweb.Tests/Thirdweb.Contracts.Tests.cs | 2 +- Thirdweb.Tests/Thirdweb.Http.Tests.cs | 358 ++++++++++++++++++ .../Thirdweb.PrivateKeyWallet.Tests.cs | 53 ++- Thirdweb.Tests/Thirdweb.Storage.Tests.cs | 4 +- Thirdweb.Tests/Thirdweb.Transactions.Tests.cs | 89 ++++- Thirdweb.Tests/Thirdweb.Wallets.Tests.cs | 3 +- .../Thirdweb.ZkSmartWallet.Tests.cs | 67 ++++ Thirdweb/Thirdweb.Client/ThirdwebClient.cs | 37 +- .../Thirdweb.Contracts/ThirdwebContract.cs | 14 +- Thirdweb/Thirdweb.Http/IThirdwebHttpClient.cs | 15 + Thirdweb/Thirdweb.Http/ThirdwebHttpClient.cs | 94 +++++ Thirdweb/Thirdweb.Http/ThirdwebHttpContent.cs | 65 ++++ .../ThirdwebHttpResponseMessage.cs | 26 ++ Thirdweb/Thirdweb.RPC/ThirdwebRPC.cs | 36 +- Thirdweb/Thirdweb.Storage/ThirdwebStorage.cs | 23 +- .../ThirdwebTransaction.cs | 88 ++--- Thirdweb/Thirdweb.Utils/Utils.cs | 33 +- Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs | 14 +- .../EmbeddedWallet.Authentication/Server.cs | 245 ++++++------ .../EmbeddedWallet.Cryptography.cs | 2 +- .../EmbeddedWallet.Encryption/IvGenerator.cs | 11 +- .../EmbeddedWallet.Storage/LocalStorage.cs | 12 - .../EmbeddedWallet/EmbeddedWallet.cs | 25 +- .../InAppWallet/InAppWallet.cs | 1 + .../PrivateKeyWallet/PrivateKeyWallet.cs | 40 +- .../SmartWallet/SmartWallet.cs | 62 +-- .../BundlerClient.cs | 29 +- thirdweb.sln | 5 +- 29 files changed, 1058 insertions(+), 399 deletions(-) create mode 100644 Thirdweb.Tests/Thirdweb.Http.Tests.cs create mode 100644 Thirdweb.Tests/Thirdweb.ZkSmartWallet.Tests.cs create mode 100644 Thirdweb/Thirdweb.Http/IThirdwebHttpClient.cs create mode 100644 Thirdweb/Thirdweb.Http/ThirdwebHttpClient.cs create mode 100644 Thirdweb/Thirdweb.Http/ThirdwebHttpContent.cs create mode 100644 Thirdweb/Thirdweb.Http/ThirdwebHttpResponseMessage.cs diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs index be365e4..ef7ca33 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -91,9 +91,9 @@ // 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); +var zkSmartWallet = await SmartWallet.Create(client: client, personalWallet: privateKeyWallet, chainId: 302, 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 zkAaTx = await ThirdwebTransaction.Create(client, zkSmartWallet, new ThirdwebTransactionInput() { From = await zkSmartWallet.GetAddress(), To = await zkSmartWallet.GetAddress(), }, 302); var zkSyncSignatureBasedAaTxHash = await ThirdwebTransaction.Send(zkAaTx); Console.WriteLine($"Transaction hash: {zkSyncSignatureBasedAaTxHash}"); diff --git a/Thirdweb.Tests/Thirdweb.Contracts.Tests.cs b/Thirdweb.Tests/Thirdweb.Contracts.Tests.cs index cdd7d26..9f7e9c5 100644 --- a/Thirdweb.Tests/Thirdweb.Contracts.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Contracts.Tests.cs @@ -11,7 +11,7 @@ public ContractsTests(ITestOutputHelper output) [Fact] public async Task FetchAbi() { - var abi = await ThirdwebContract.FetchAbi(address: "0x1320Cafa93fb53Ed9068E3272cb270adbBEf149C", chainId: 84532); + var abi = await ThirdwebContract.FetchAbi(client: ThirdwebClient.Create(secretKey: _secretKey), address: "0x1320Cafa93fb53Ed9068E3272cb270adbBEf149C", chainId: 84532); Assert.NotNull(abi); Assert.NotEmpty(abi); } diff --git a/Thirdweb.Tests/Thirdweb.Http.Tests.cs b/Thirdweb.Tests/Thirdweb.Http.Tests.cs new file mode 100644 index 0000000..24f5d09 --- /dev/null +++ b/Thirdweb.Tests/Thirdweb.Http.Tests.cs @@ -0,0 +1,358 @@ +using System.Numerics; +using System.Text; + +namespace Thirdweb.Tests +{ + public class HttpTests : BaseTests + { + public HttpTests(ITestOutputHelper output) + : base(output) { } + + #region ThirdwebHttpClient + + [Fact] + public async Task GetAsync_ShouldReturnSuccessResponse() + { + // Arrange + var httpClient = new ThirdwebHttpClient(); + var requestUri = "https://jsonplaceholder.typicode.com/posts/1"; + + // Act + var response = await httpClient.GetAsync(requestUri); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(200, response.StatusCode); + } + + [Fact] + public async Task PostAsync_ShouldReturnSuccessResponse() + { + // Arrange + var httpClient = new ThirdwebHttpClient(); + var requestUri = "https://jsonplaceholder.typicode.com/posts"; + var content = new StringContent("{\"title\": \"foo\", \"body\": \"bar\", \"userId\": 1}", System.Text.Encoding.UTF8, "application/json"); + + // Act + var response = await httpClient.PostAsync(requestUri, content); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(201, response.StatusCode); + } + + [Fact] + public void SetHeaders_ShouldAddHeaders() + { + // Arrange + var httpClient = new ThirdwebHttpClient(); + var headers = new Dictionary { { "Authorization", "Bearer token" } }; + + // Act + httpClient.SetHeaders(headers); + + // Assert + _ = Assert.Single(httpClient.Headers); + Assert.Equal("Bearer token", httpClient.Headers["Authorization"]); + } + + [Fact] + public void ClearHeaders_ShouldRemoveAllHeaders() + { + // Arrange + var httpClient = new ThirdwebHttpClient(); + var headers = new Dictionary { { "Authorization", "Bearer token" } }; + httpClient.SetHeaders(headers); + + // Act + httpClient.ClearHeaders(); + + // Assert + Assert.Empty(httpClient.Headers); + } + + [Fact] + public void AddHeader_ShouldAddHeader() + { + // Arrange + var httpClient = new ThirdwebHttpClient(); + + // Act + httpClient.AddHeader("Authorization", "Bearer token"); + + // Assert + _ = Assert.Single(httpClient.Headers); + Assert.Equal("Bearer token", httpClient.Headers["Authorization"]); + } + + [Fact] + public void RemoveHeader_ShouldRemoveHeader() + { + // Arrange + var httpClient = new ThirdwebHttpClient(); + httpClient.AddHeader("Authorization", "Bearer token"); + + // Act + httpClient.RemoveHeader("Authorization"); + + // Assert + Assert.Empty(httpClient.Headers); + } + + [Fact] + public async Task PutAsync_ShouldThrowNotImplementedException() + { + // Arrange + var httpClient = new ThirdwebHttpClient(); + var requestUri = "https://jsonplaceholder.typicode.com/posts/1"; + var content = new StringContent("{\"title\": \"foo\", \"body\": \"bar\", \"userId\": 1}", System.Text.Encoding.UTF8, "application/json"); + + // Act & Assert + _ = await Assert.ThrowsAsync(() => httpClient.PutAsync(requestUri, content)); + } + + [Fact] + public async Task DeleteAsync_ShouldThrowNotImplementedException() + { + // Arrange + var httpClient = new ThirdwebHttpClient(); + var requestUri = "https://jsonplaceholder.typicode.com/posts/1"; + + // Act & Assert + _ = await Assert.ThrowsAsync(() => httpClient.DeleteAsync(requestUri)); + } + + [Fact] + public void Dispose_ShouldDisposeHttpClient() + { + // Arrange + var httpClient = new ThirdwebHttpClient(); + + // Act + httpClient.Dispose(); + + // Assert + // Check that disposing twice does not throw an exception + var exception = Record.Exception(() => httpClient.Dispose()); + Assert.Null(exception); + } + + #endregion + + #region ThirdwebHttpContent + + [Fact] + public async Task Constructor_WithString_ShouldInitializeContent() + { + // Arrange + var contentString = "Hello, World!"; + var expectedBytes = Encoding.UTF8.GetBytes(contentString); + + // Act + var content = new ThirdwebHttpContent(contentString); + var resultBytes = await content.ReadAsByteArrayAsync(); + + // Assert + Assert.Equal(expectedBytes, resultBytes); + } + + [Fact] + public async Task Constructor_WithByteArray_ShouldInitializeContent() + { + // Arrange + var contentBytes = Encoding.UTF8.GetBytes("Hello, World!"); + + // Act + var content = new ThirdwebHttpContent(contentBytes); + var resultBytes = await content.ReadAsByteArrayAsync(); + + // Assert + Assert.Equal(contentBytes, resultBytes); + } + + [Fact] + public async Task Constructor_WithStream_ShouldInitializeContent() + { + // Arrange + var contentString = "Hello, World!"; + var contentStream = new MemoryStream(Encoding.UTF8.GetBytes(contentString)); + var expectedBytes = Encoding.UTF8.GetBytes(contentString); + + // Act + var content = new ThirdwebHttpContent(contentStream); + var resultBytes = await content.ReadAsByteArrayAsync(); + + // Assert + Assert.Equal(expectedBytes, resultBytes); + } + + [Fact] + public async Task ReadAsStringAsync_ShouldReturnContentAsString() + { + // Arrange + var contentString = "Hello, World!"; + var content = new ThirdwebHttpContent(contentString); + + // Act + var resultString = await content.ReadAsStringAsync(); + + // Assert + Assert.Equal(contentString, resultString); + } + + [Fact] + public async Task ReadAsByteArrayAsync_ShouldReturnContentAsByteArray() + { + // Arrange + var contentBytes = Encoding.UTF8.GetBytes("Hello, World!"); + var content = new ThirdwebHttpContent(contentBytes); + + // Act + var resultBytes = await content.ReadAsByteArrayAsync(); + + // Assert + Assert.Equal(contentBytes, resultBytes); + } + + [Fact] + public async Task ReadAsStreamAsync_ShouldReturnContentAsStream() + { + // Arrange + var contentString = "Hello, World!"; + var content = new ThirdwebHttpContent(contentString); + var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes(contentString)); + + // Act + var resultStream = await content.ReadAsStreamAsync(); + + // Assert + using (var reader = new StreamReader(resultStream)) + using (var expectedReader = new StreamReader(expectedStream)) + { + var resultString = await reader.ReadToEndAsync(); + var expectedString = await expectedReader.ReadToEndAsync(); + Assert.Equal(expectedString, resultString); + } + } + +#nullable disable + + [Fact] + public void Constructor_WithNullString_ShouldThrowArgumentNullException() + { + // Arrange, Act & Assert + _ = Assert.Throws(() => new ThirdwebHttpContent((string)null)); + } + + [Fact] + public void Constructor_WithNullByteArray_ShouldThrowArgumentNullException() + { + // Arrange, Act & Assert + _ = Assert.Throws(() => new ThirdwebHttpContent((byte[])null)); + } + + [Fact] + public void Constructor_WithNullStream_ShouldThrowArgumentNullException() + { + // Arrange, Act & Assert + _ = Assert.Throws(() => new ThirdwebHttpContent((Stream)null)); + } + +#nullable restore + + #endregion + + #region ThirdwebHttpResponseMessage + + [Fact] + public void Constructor_ShouldInitializeProperties() + { + // Arrange + var statusCode = 200; + var content = new ThirdwebHttpContent("Test Content"); + var isSuccessStatusCode = true; + + // Act + var responseMessage = new ThirdwebHttpResponseMessage(statusCode, content, isSuccessStatusCode); + + // Assert + Assert.Equal(statusCode, responseMessage.StatusCode); + Assert.Equal(content, responseMessage.Content); + Assert.Equal(isSuccessStatusCode, responseMessage.IsSuccessStatusCode); + } + + [Fact] + public void EnsureSuccessStatusCode_ShouldReturnSelfOnSuccess() + { + // Arrange + var statusCode = 200; + var content = new ThirdwebHttpContent("Test Content"); + var isSuccessStatusCode = true; + var responseMessage = new ThirdwebHttpResponseMessage(statusCode, content, isSuccessStatusCode); + + // Act + var result = responseMessage.EnsureSuccessStatusCode(); + + // Assert + Assert.Equal(responseMessage, result); + } + + [Fact] + public async Task EnsureSuccessStatusCode_ShouldThrowExceptionOnFailure() + { + // Arrange + var statusCode = 400; + var content = new ThirdwebHttpContent("Error Content"); + var isSuccessStatusCode = false; + var responseMessage = new ThirdwebHttpResponseMessage(statusCode, content, isSuccessStatusCode); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => Task.FromResult(responseMessage.EnsureSuccessStatusCode())); + var contentString = await content.ReadAsStringAsync(); + Assert.Equal($"Request failed with status code {statusCode} and content: {contentString}", exception.Message); + } + + [Fact] + public void StatusCode_ShouldSetAndGet() + { + // Arrange + var responseMessage = new ThirdwebHttpResponseMessage(200, new ThirdwebHttpContent("Test Content"), true); + + // Act + responseMessage.StatusCode = 404; + + // Assert + Assert.Equal(404, responseMessage.StatusCode); + } + + [Fact] + public void Content_ShouldSetAndGet() + { + // Arrange + var initialContent = new ThirdwebHttpContent("Initial Content"); + var newContent = new ThirdwebHttpContent("New Content"); + var responseMessage = new ThirdwebHttpResponseMessage(200, initialContent, true); + + // Act + responseMessage.Content = newContent; + + // Assert + Assert.Equal(newContent, responseMessage.Content); + } + + [Fact] + public void IsSuccessStatusCode_ShouldSetAndGet() + { + // Arrange + var responseMessage = new ThirdwebHttpResponseMessage(200, new ThirdwebHttpContent("Test Content"), true); + + // Act + responseMessage.IsSuccessStatusCode = false; + + // Assert + Assert.False(responseMessage.IsSuccessStatusCode); + } + + #endregion + } +} diff --git a/Thirdweb.Tests/Thirdweb.PrivateKeyWallet.Tests.cs b/Thirdweb.Tests/Thirdweb.PrivateKeyWallet.Tests.cs index 9596da1..952920c 100644 --- a/Thirdweb.Tests/Thirdweb.PrivateKeyWallet.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.PrivateKeyWallet.Tests.cs @@ -180,9 +180,10 @@ public async Task SignTransaction_Success() Gas = new HexBigInteger(21000), Data = "0x", Nonce = new HexBigInteger(99999999999), - GasPrice = new HexBigInteger(10000000000) + GasPrice = new HexBigInteger(10000000000), + ChainId = new HexBigInteger(421614) }; - var signature = await account.SignTransaction(transaction, 421614); + var signature = await account.SignTransaction(transaction); Assert.NotNull(signature); } @@ -197,9 +198,10 @@ public async Task SignTransaction_NoFrom_Success() Gas = new HexBigInteger(21000), Data = "0x", Nonce = new HexBigInteger(99999999999), - GasPrice = new HexBigInteger(10000000000) + GasPrice = new HexBigInteger(10000000000), + ChainId = new HexBigInteger(421614) }; - var signature = await account.SignTransaction(transaction, 421614); + var signature = await account.SignTransaction(transaction); Assert.NotNull(signature); } @@ -207,7 +209,7 @@ public async Task SignTransaction_NoFrom_Success() public async Task SignTransaction_NullTransaction() { var account = await GetAccount(); - var ex = await Assert.ThrowsAsync(() => account.SignTransaction(null, 421614)); + var ex = await Assert.ThrowsAsync(() => account.SignTransaction(null)); Assert.Equal("Value cannot be null. (Parameter 'transaction')", ex.Message); } @@ -223,7 +225,7 @@ public async Task SignTransaction_NoNonce() Gas = new HexBigInteger(21000), Data = "0x" }; - var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction, 421614)); + var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction)); Assert.Equal("Transaction nonce has not been set (Parameter 'transaction')", ex.Message); } @@ -238,9 +240,10 @@ public async Task SignTransaction_WrongFrom() Value = new HexBigInteger(0), Gas = new HexBigInteger(21000), Data = "0x", - Nonce = new HexBigInteger(99999999999) + Nonce = new HexBigInteger(99999999999), + ChainId = new HexBigInteger(421614) }; - var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction, 421614)); + var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction)); Assert.Equal("Transaction 'From' address does not match the wallet address", ex.Message); } @@ -255,9 +258,10 @@ public async Task SignTransaction_NoGasPrice() Value = new HexBigInteger(0), Gas = new HexBigInteger(21000), Data = "0x", - Nonce = new HexBigInteger(99999999999) + Nonce = new HexBigInteger(99999999999), + ChainId = new HexBigInteger(421614) }; - var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction, 421614)); + var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction)); Assert.Equal("Transaction MaxPriorityFeePerGas and MaxFeePerGas must be set for EIP-1559 transactions", ex.Message); } @@ -274,9 +278,10 @@ public async Task SignTransaction_1559_Success() Data = "0x", Nonce = new HexBigInteger(99999999999), MaxFeePerGas = new HexBigInteger(10000000000), - MaxPriorityFeePerGas = new HexBigInteger(10000000000) + MaxPriorityFeePerGas = new HexBigInteger(10000000000), + ChainId = new HexBigInteger(421614) }; - var signature = await account.SignTransaction(transaction, 421614); + var signature = await account.SignTransaction(transaction); Assert.NotNull(signature); } @@ -292,9 +297,10 @@ public async Task SignTransaction_1559_NoMaxFeePerGas() Gas = new HexBigInteger(21000), Data = "0x", Nonce = new HexBigInteger(99999999999), - MaxPriorityFeePerGas = new HexBigInteger(10000000000) + MaxPriorityFeePerGas = new HexBigInteger(10000000000), + ChainId = new HexBigInteger(421614) }; - var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction, 421614)); + var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction)); Assert.Equal("Transaction MaxPriorityFeePerGas and MaxFeePerGas must be set for EIP-1559 transactions", ex.Message); } @@ -310,9 +316,10 @@ public async Task SignTransaction_1559_NoMaxPriorityFeePerGas() Gas = new HexBigInteger(21000), Data = "0x", Nonce = new HexBigInteger(99999999999), - MaxFeePerGas = new HexBigInteger(10000000000) + MaxFeePerGas = new HexBigInteger(10000000000), + ChainId = new HexBigInteger(421614) }; - var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction, 421614)); + var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction)); Assert.Equal("Transaction MaxPriorityFeePerGas and MaxFeePerGas must be set for EIP-1559 transactions", ex.Message); } @@ -354,4 +361,18 @@ public async Task Disconnect_Connected() await account.Disconnect(); Assert.False(await account.IsConnected()); } + + [Fact] + public async Task SendTransaction_InvalidOperation() + { + var account = await GetAccount(); + var transaction = new ThirdwebTransactionInput + { + From = await account.GetAddress(), + To = Constants.ADDRESS_ZERO, + Value = new HexBigInteger(0), + Data = "0x", + }; + _ = await Assert.ThrowsAsync(() => account.SendTransaction(transaction)); + } } diff --git a/Thirdweb.Tests/Thirdweb.Storage.Tests.cs b/Thirdweb.Tests/Thirdweb.Storage.Tests.cs index 297ebf3..7af57af 100644 --- a/Thirdweb.Tests/Thirdweb.Storage.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Storage.Tests.cs @@ -52,7 +52,7 @@ public async Task DownloadTest_400() var client = ThirdwebClient.Create(secretKey: _secretKey); var exception = await Assert.ThrowsAsync(() => ThirdwebStorage.Download(client, "https://0.rpc.thirdweb.com/")); Assert.Contains("Failed to download", exception.Message); - Assert.Contains("BadRequest", exception.Message); + Assert.Contains("400", exception.Message); } [Fact] @@ -99,6 +99,6 @@ public async Task UploadTest_401() File.WriteAllText(path, "{\"test\": \"test\"}"); var exception = await Assert.ThrowsAsync(() => ThirdwebStorage.Upload(client, path)); Assert.Contains("Failed to upload", exception.Message); - Assert.Contains("Unauthorized", exception.Message); + Assert.Contains("401", exception.Message); } } diff --git a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs index de3e146..a39c9df 100644 --- a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs @@ -36,7 +36,7 @@ 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(() => ThirdwebTransaction.Create(client, wallet, txInput, BigInteger.Zero)); + var ex = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(client, wallet, txInput, 421614)); Assert.Contains("Transaction recipient (to) must be provided", ex.Message); } @@ -45,18 +45,39 @@ public async Task Create_ThrowsOnInvalidAddress() { var client = ThirdwebClient.Create(secretKey: _secretKey); var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey); - var txInput = new ThirdwebTransactionInput() { From = "0x123", To = Constants.ADDRESS_ZERO }; - var ex = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(client, wallet, txInput, BigInteger.Zero)); + var txInput = new ThirdwebTransactionInput() { From = "0xHello", To = Constants.ADDRESS_ZERO }; + var ex = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(client, wallet, txInput, 421614)); Assert.Contains("Transaction sender (from) must match wallet address", ex.Message); } [Fact] - public async Task Create_ThrowsOnInvalidChainId() + public async Task Create_ThrowsOnNoClient() + { + var client = ThirdwebClient.Create(secretKey: _secretKey); + var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey); + var txInput = new ThirdwebTransactionInput() { From = await wallet.GetAddress(), To = Constants.ADDRESS_ZERO }; + var ex = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(null, wallet, txInput, 421614)); + Assert.Contains("Client must be provided", ex.Message); + } + + [Fact] + public async Task Create_ThrowsOnNoWallet() { var client = ThirdwebClient.Create(secretKey: _secretKey); var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey); - var txInput = new ThirdwebTransactionInput(); - _ = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(client, wallet, txInput, BigInteger.Zero)); + var txInput = new ThirdwebTransactionInput() { From = await wallet.GetAddress(), To = Constants.ADDRESS_ZERO }; + var ex = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(client, null, txInput, 421614)); + Assert.Contains("Wallet must be provided", ex.Message); + } + + [Fact] + public async Task Create_ThrowsOnChainIdZero() + { + var client = ThirdwebClient.Create(secretKey: _secretKey); + var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey); + var txInput = new ThirdwebTransactionInput() { From = await wallet.GetAddress(), To = Constants.ADDRESS_ZERO }; + var ex = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(client, wallet, txInput, BigInteger.Zero)); + Assert.Contains("Invalid Chain ID", ex.Message); } [Fact] @@ -173,6 +194,34 @@ public async Task Send_CorrectlyHandlesNonce() Assert.Equal("123", transaction.Input.Nonce.Value.ToString()); } + [Fact] + public async Task SetZkSyncOptions_DefaultsToZeroNull() + { + // Both null + var transaction = await CreateSampleTransaction(); + _ = transaction.SetZkSyncOptions(new ZkSyncOptions()); + Assert.Equal(0, transaction.Input.ZkSync?.Paymaster); + Assert.Null(transaction.Input.ZkSync?.PaymasterInput); + Assert.Null(transaction.Input.ZkSync?.GasPerPubdataByteLimit); + Assert.Null(transaction.Input.ZkSync?.FactoryDeps); + + // Paymaster null + transaction = await CreateSampleTransaction(); + _ = transaction.SetZkSyncOptions(new ZkSyncOptions(paymaster: null, paymasterInput: "0x")); + Assert.Equal(0, transaction.Input.ZkSync?.Paymaster); + Assert.Null(transaction.Input.ZkSync?.PaymasterInput); + Assert.Null(transaction.Input.ZkSync?.GasPerPubdataByteLimit); + Assert.Null(transaction.Input.ZkSync?.FactoryDeps); + + // PaymasterInput null + transaction = await CreateSampleTransaction(); + _ = transaction.SetZkSyncOptions(new ZkSyncOptions(paymaster: "0x", paymasterInput: null)); + Assert.Equal(0, transaction.Input.ZkSync?.Paymaster); + Assert.Null(transaction.Input.ZkSync?.PaymasterInput); + Assert.Null(transaction.Input.ZkSync?.GasPerPubdataByteLimit); + Assert.Null(transaction.Input.ZkSync?.FactoryDeps); + } + [Fact] public async Task Send_ZkSync_TransfersGaslessly() { @@ -308,14 +357,23 @@ public async Task EstimateTotalCosts_HigherThanGasCostsByValue() [Fact] public async Task EstimateGasFees_ReturnsCorrectly() { - var transaction = await CreateSampleTransaction(); - _ = transaction.SetValue(new BigInteger(1000)); - _ = transaction.SetTo(Constants.ADDRESS_ZERO); + var transaction = await ThirdwebTransaction.Create( + ThirdwebClient.Create(secretKey: _secretKey), + await PrivateKeyWallet.Create(ThirdwebClient.Create(secretKey: _secretKey), _testPrivateKey), + new ThirdwebTransactionInput() + { + To = Constants.ADDRESS_ZERO, + Value = new HexBigInteger(0), + Data = "0x", + }, + 250 // fantom for 1559 non zero prio + ); (var maxFee, var maxPrio) = await ThirdwebTransaction.EstimateGasFees(transaction); Assert.NotEqual(BigInteger.Zero, maxFee); Assert.NotEqual(BigInteger.Zero, maxPrio); + Assert.NotEqual(maxFee, maxPrio); } [Fact] @@ -340,7 +398,7 @@ public async Task Simulate_ThrowsInsufficientFunds() } [Fact] - public async Task Simulate_ReturnsData() + public async Task Simulate_ReturnsDataOrThrowsIntrinsic() { var client = ThirdwebClient.Create(secretKey: _secretKey); var privateKeyAccount = await PrivateKeyWallet.Create(client, _testPrivateKey); @@ -358,8 +416,15 @@ public async Task Simulate_ReturnsData() 421614 ); - var data = await ThirdwebTransaction.Simulate(transaction); - Assert.NotNull(data); + try + { + var data = await ThirdwebTransaction.Simulate(transaction); + Assert.NotNull(data); + } + catch (Exception ex) + { + Assert.Contains("intrinsic gas too low", ex.Message); + } } [Fact] diff --git a/Thirdweb.Tests/Thirdweb.Wallets.Tests.cs b/Thirdweb.Tests/Thirdweb.Wallets.Tests.cs index 048749f..568ed0d 100644 --- a/Thirdweb.Tests/Thirdweb.Wallets.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Wallets.Tests.cs @@ -121,9 +121,10 @@ public async Task SignTransaction() Gas = new HexBigInteger(21000), GasPrice = new HexBigInteger(10000000000), Nonce = new HexBigInteger(9999999999999), + ChainId = new HexBigInteger(421614), }; var rpc = ThirdwebRPC.GetRpcInstance(ThirdwebClient.Create(secretKey: _secretKey), 421614); - var signature = await wallet.SignTransaction(transaction, 421614); + var signature = await wallet.SignTransaction(transaction); Assert.NotNull(signature); } } diff --git a/Thirdweb.Tests/Thirdweb.ZkSmartWallet.Tests.cs b/Thirdweb.Tests/Thirdweb.ZkSmartWallet.Tests.cs new file mode 100644 index 0000000..9c6ca00 --- /dev/null +++ b/Thirdweb.Tests/Thirdweb.ZkSmartWallet.Tests.cs @@ -0,0 +1,67 @@ +namespace Thirdweb.Tests; + +public class ZkSmartWalletTests : BaseTests +{ + private readonly ThirdwebClient _zkClient; + + public ZkSmartWalletTests(ITestOutputHelper output) + : base(output) + { + _zkClient = ThirdwebClient.Create(secretKey: _secretKey); + } + + private async Task GetSmartAccount(int zkChainId = 300, bool gasless = true) + { + var privateKeyAccount = await PrivateKeyWallet.Create(_zkClient, _testPrivateKey); + var smartAccount = await SmartWallet.Create(_zkClient, personalWallet: privateKeyAccount, gasless: gasless, chainId: zkChainId); + return smartAccount; + } + + [Fact] + public async Task GetAddress_Success() + { + var account = await GetSmartAccount(); + Assert.NotNull(await account.GetAddress()); + } + + [Fact] + public async Task IsDeployed_ReturnsTrue() + { + var account = await GetSmartAccount(); + Assert.True(await account.IsDeployed()); + } + + [Fact] + public async Task SendGaslessZkTx_Success() + { + var account = await GetSmartAccount(); + 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] + 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); + } +} diff --git a/Thirdweb/Thirdweb.Client/ThirdwebClient.cs b/Thirdweb/Thirdweb.Client/ThirdwebClient.cs index 6294d49..a91cca0 100644 --- a/Thirdweb/Thirdweb.Client/ThirdwebClient.cs +++ b/Thirdweb/Thirdweb.Client/ThirdwebClient.cs @@ -4,12 +4,21 @@ namespace Thirdweb { public class ThirdwebClient { + public IThirdwebHttpClient HttpClient { get; } + internal string SecretKey { get; } internal string ClientId { get; } internal string BundleId { get; } internal ITimeoutOptions FetchTimeoutOptions { get; } - private ThirdwebClient(string clientId = null, string secretKey = null, string bundleId = null, ITimeoutOptions fetchTimeoutOptions = null) + private ThirdwebClient( + string clientId = null, + string secretKey = null, + string bundleId = null, + ITimeoutOptions fetchTimeoutOptions = null, + IThirdwebHttpClient httpClient = null, + Dictionary headers = null + ) { if (string.IsNullOrEmpty(clientId) && string.IsNullOrEmpty(secretKey)) { @@ -29,11 +38,33 @@ private ThirdwebClient(string clientId = null, string secretKey = null, string b BundleId = bundleId; FetchTimeoutOptions = fetchTimeoutOptions ?? new TimeoutOptions(); + + HttpClient = httpClient ?? new ThirdwebHttpClient(); + HttpClient.SetHeaders( + headers + ?? new Dictionary + { + { "x-sdk-name", "Thirdweb.NET" }, + { "x-sdk-os", System.Runtime.InteropServices.RuntimeInformation.OSDescription }, + { "x-sdk-platform", "dotnet" }, + { "x-sdk-version", Constants.VERSION }, + { "x-client-id", ClientId }, + { "x-secret-key", SecretKey }, + { "x-bundle-id", BundleId } + } + ); } - public static ThirdwebClient Create(string clientId = null, string secretKey = null, string bundleId = null, ITimeoutOptions fetchTimeoutOptions = null) + public static ThirdwebClient Create( + string clientId = null, + string secretKey = null, + string bundleId = null, + ITimeoutOptions fetchTimeoutOptions = null, + IThirdwebHttpClient httpClient = null, + Dictionary headers = null + ) { - return new ThirdwebClient(clientId, secretKey, bundleId, fetchTimeoutOptions); + return new ThirdwebClient(clientId, secretKey, bundleId, fetchTimeoutOptions, httpClient, headers); } } } diff --git a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs index cb45ef6..aba30aa 100644 --- a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs +++ b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs @@ -36,19 +36,17 @@ public static async Task Create(ThirdwebClient client, string throw new ArgumentException("Chain must be provided"); } - abi ??= await FetchAbi(address, chain); + abi ??= await FetchAbi(client, address, chain); return new ThirdwebContract(client, address, chain, abi); } - public static async Task FetchAbi(string address, BigInteger chainId) + public static async Task FetchAbi(ThirdwebClient client, string address, BigInteger chainId) { var url = $"https://contract.thirdweb.com/abi/{chainId}/{address}"; - using (var client = new HttpClient()) - { - var response = await client.GetAsync(url); - _ = response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(); - } + var httpClient = client.HttpClient; + var response = await httpClient.GetAsync(url).ConfigureAwait(false); + _ = response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); } public static async Task Read(ThirdwebContract contract, string method, params object[] parameters) diff --git a/Thirdweb/Thirdweb.Http/IThirdwebHttpClient.cs b/Thirdweb/Thirdweb.Http/IThirdwebHttpClient.cs new file mode 100644 index 0000000..946aeca --- /dev/null +++ b/Thirdweb/Thirdweb.Http/IThirdwebHttpClient.cs @@ -0,0 +1,15 @@ +namespace Thirdweb +{ + public interface IThirdwebHttpClient : IDisposable + { + Dictionary Headers { get; } + void SetHeaders(Dictionary headers); + void ClearHeaders(); + void AddHeader(string key, string value); + void RemoveHeader(string key); + Task GetAsync(string requestUri, CancellationToken cancellationToken = default); + Task PostAsync(string requestUri, HttpContent content, CancellationToken cancellationToken = default); + Task PutAsync(string requestUri, HttpContent content, CancellationToken cancellationToken = default); + Task DeleteAsync(string requestUri, CancellationToken cancellationToken = default); + } +} diff --git a/Thirdweb/Thirdweb.Http/ThirdwebHttpClient.cs b/Thirdweb/Thirdweb.Http/ThirdwebHttpClient.cs new file mode 100644 index 0000000..ab8fcf9 --- /dev/null +++ b/Thirdweb/Thirdweb.Http/ThirdwebHttpClient.cs @@ -0,0 +1,94 @@ +namespace Thirdweb +{ + public class ThirdwebHttpClient : IThirdwebHttpClient + { + public Dictionary Headers { get; private set; } + + private readonly HttpClient _httpClient; + private bool _disposed; + + public ThirdwebHttpClient() + { + _httpClient = new HttpClient(); + Headers = new Dictionary(); + } + + public void SetHeaders(Dictionary headers) + { + Headers = new Dictionary(headers); + } + + public void ClearHeaders() + { + Headers.Clear(); + } + + public void AddHeader(string key, string value) + { + Headers.Add(key, value); + } + + public void RemoveHeader(string key) + { + _ = Headers.Remove(key); + } + + private void AddHeaders(HttpRequestMessage request) + { + foreach (var header in Headers) + { + _ = request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } + + public async Task GetAsync(string requestUri, CancellationToken cancellationToken = default) + { + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + AddHeaders(request); + var result = await _httpClient.SendAsync(request, cancellationToken); +#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods + var resultContent = new ThirdwebHttpContent(await result.Content.ReadAsByteArrayAsync()); +#pragma warning restore CA2016 // Forward the 'CancellationToken' parameter to methods + return new ThirdwebHttpResponseMessage((long)result.StatusCode, resultContent, result.IsSuccessStatusCode); + } + + public async Task PostAsync(string requestUri, HttpContent content, CancellationToken cancellationToken = default) + { + var request = new HttpRequestMessage(HttpMethod.Post, requestUri) { Content = content }; + AddHeaders(request); + var result = await _httpClient.SendAsync(request, cancellationToken); +#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods + var resultContent = new ThirdwebHttpContent(await result.Content.ReadAsByteArrayAsync()); +#pragma warning restore CA2016 // Forward the 'CancellationToken' parameter to methods + return new ThirdwebHttpResponseMessage((long)result.StatusCode, resultContent, result.IsSuccessStatusCode); + } + + public Task PutAsync(string requestUri, HttpContent content, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(string requestUri, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _httpClient.Dispose(); + } + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Thirdweb/Thirdweb.Http/ThirdwebHttpContent.cs b/Thirdweb/Thirdweb.Http/ThirdwebHttpContent.cs new file mode 100644 index 0000000..825f4dd --- /dev/null +++ b/Thirdweb/Thirdweb.Http/ThirdwebHttpContent.cs @@ -0,0 +1,65 @@ +using System.Text; + +namespace Thirdweb +{ + public class ThirdwebHttpContent + { + private readonly byte[] content; + + // Constructor to initialize from a string + public ThirdwebHttpContent(string content) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + this.content = Encoding.UTF8.GetBytes(content); + } + + // Constructor to initialize from a byte array + public ThirdwebHttpContent(byte[] content) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + this.content = content; + } + + // Constructor to initialize from a stream + public ThirdwebHttpContent(Stream content) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + using (var memoryStream = new MemoryStream()) + { + content.CopyTo(memoryStream); + this.content = memoryStream.ToArray(); + } + } + + // Read the content as a string + public Task ReadAsStringAsync() + { + return Task.FromResult(Encoding.UTF8.GetString(content)); + } + + // Read the content as a byte array + public Task ReadAsByteArrayAsync() + { + return Task.FromResult(content); + } + + // Read the content as a stream + public Task ReadAsStreamAsync() + { + var stream = new MemoryStream(content); + return Task.FromResult(stream); + } + } +} diff --git a/Thirdweb/Thirdweb.Http/ThirdwebHttpResponseMessage.cs b/Thirdweb/Thirdweb.Http/ThirdwebHttpResponseMessage.cs new file mode 100644 index 0000000..f9b64a4 --- /dev/null +++ b/Thirdweb/Thirdweb.Http/ThirdwebHttpResponseMessage.cs @@ -0,0 +1,26 @@ +namespace Thirdweb +{ + public class ThirdwebHttpResponseMessage + { + public long StatusCode { get; set; } + public ThirdwebHttpContent Content { get; set; } + public bool IsSuccessStatusCode { get; set; } + + public ThirdwebHttpResponseMessage(long statusCode, ThirdwebHttpContent content, bool isSuccessStatusCode) + { + StatusCode = statusCode; + Content = content; + IsSuccessStatusCode = isSuccessStatusCode; + } + + public ThirdwebHttpResponseMessage EnsureSuccessStatusCode() + { + if (!IsSuccessStatusCode) + { + // TODO: Custom exception + throw new Exception($"Request failed with status code {StatusCode} and content: {Content.ReadAsStringAsync().Result}"); + } + return this; + } + } +} diff --git a/Thirdweb/Thirdweb.RPC/ThirdwebRPC.cs b/Thirdweb/Thirdweb.RPC/ThirdwebRPC.cs index c38f7b4..988b5c9 100644 --- a/Thirdweb/Thirdweb.RPC/ThirdwebRPC.cs +++ b/Thirdweb/Thirdweb.RPC/ThirdwebRPC.cs @@ -17,12 +17,9 @@ public class ThirdwebRPC private Dictionary> _responseCompletionSources = new Dictionary>(); private int _requestIdCounter = 1; - private static readonly HttpClient _httpClient = new HttpClient(); private static readonly Dictionary _rpcs = new Dictionary(); - private readonly string _clientId; - private readonly string _secretKey; - private readonly string _bundleId; + private readonly IThirdwebHttpClient _httpClient; public static ThirdwebRPC GetRpcInstance(ThirdwebClient client, BigInteger chainId) { @@ -94,19 +91,9 @@ public async Task SendRequestAsync(string method, params o } } - static ThirdwebRPC() - { - _httpClient.DefaultRequestHeaders.Add("x-sdk-name", "Thirdweb.NET"); - _httpClient.DefaultRequestHeaders.Add("x-sdk-os", System.Runtime.InteropServices.RuntimeInformation.OSDescription); - _httpClient.DefaultRequestHeaders.Add("x-sdk-platform", "dotnet"); - _httpClient.DefaultRequestHeaders.Add("x-sdk-version", Constants.VERSION); - } - private ThirdwebRPC(ThirdwebClient client, BigInteger chainId) { - _clientId = client.ClientId; - _secretKey = client.SecretKey; - _bundleId = client.BundleId; + _httpClient = client.HttpClient; _rpcUrl = new Uri($"https://{chainId}.rpc.thirdweb.com/"); _rpcTimeout = TimeSpan.FromMilliseconds(client.FetchTimeoutOptions.GetTimeout(TimeoutType.Rpc)); _batchSendInterval = TimeSpan.FromMilliseconds(100); @@ -133,27 +120,12 @@ private void SendBatchNow() private async Task SendBatchAsync(List batch) { var batchJson = JsonConvert.SerializeObject(batch); - - var requestMessage = new HttpRequestMessage(HttpMethod.Post, _rpcUrl) { Content = new StringContent(batchJson, Encoding.UTF8, "application/json") }; - if (!string.IsNullOrEmpty(_clientId)) - { - requestMessage.Headers.Add("x-client-id", _clientId); - } - - if (!string.IsNullOrEmpty(_secretKey)) - { - requestMessage.Headers.Add("x-secret-key", _secretKey); - } - - if (!string.IsNullOrEmpty(_bundleId)) - { - requestMessage.Headers.Add("x-bundle-id", _bundleId); - } + var content = new StringContent(batchJson, Encoding.UTF8, "application/json"); try { using var cts = new CancellationTokenSource(_rpcTimeout); - var response = await _httpClient.SendAsync(requestMessage, cts.Token); + var response = await _httpClient.PostAsync(_rpcUrl.ToString(), content, cts.Token).ConfigureAwait(false); if (!response.IsSuccessStatusCode) { diff --git a/Thirdweb/Thirdweb.Storage/ThirdwebStorage.cs b/Thirdweb/Thirdweb.Storage/ThirdwebStorage.cs index 1240bec..a801ae2 100644 --- a/Thirdweb/Thirdweb.Storage/ThirdwebStorage.cs +++ b/Thirdweb/Thirdweb.Storage/ThirdwebStorage.cs @@ -13,17 +13,7 @@ public static async Task Download(ThirdwebClient client, string uri, int? uri = uri.ReplaceIPFS($"https://{client.ClientId}.ipfscdn.io/ipfs/"); - using var httpClient = new HttpClient(); - - var isThirdwebRequest = new Uri(uri).Host.EndsWith(".ipfscdn.io"); - if (isThirdwebRequest) - { - var headers = Utils.GetThirdwebHeaders(client); - foreach (var header in headers) - { - httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); - } - } + var httpClient = client.HttpClient; requestTimeout ??= client.FetchTimeoutOptions.GetTimeout(TimeoutType.Storage); @@ -31,7 +21,7 @@ public static async Task Download(ThirdwebClient client, string uri, int? if (!response.IsSuccessStatusCode) { - throw new Exception($"Failed to download {uri}: {response.StatusCode} | {response.ReasonPhrase} | {await response.Content.ReadAsStringAsync()}"); + throw new Exception($"Failed to download {uri}: {response.StatusCode} | {await response.Content.ReadAsStringAsync()}"); } var content = await response.Content.ReadAsStringAsync(); @@ -46,20 +36,15 @@ public static async Task Upload(ThirdwebClient client, string throw new ArgumentNullException(nameof(path)); } - using var httpClient = new HttpClient(); using var form = new MultipartFormDataContent { { new ByteArrayContent(File.ReadAllBytes(path)), "file", Path.GetFileName(path) } }; - var headers = Utils.GetThirdwebHeaders(client); - foreach (var header in headers) - { - httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); - } + var httpClient = client.HttpClient; var response = await httpClient.PostAsync(Constants.PIN_URI, form); if (!response.IsSuccessStatusCode) { - throw new Exception($"Failed to upload {path}: {response.StatusCode} | {response.ReasonPhrase} | {await response.Content.ReadAsStringAsync()}"); + throw new Exception($"Failed to upload {path}: {response.StatusCode} | {await response.Content.ReadAsStringAsync()}"); } var result = await response.Content.ReadAsStringAsync(); diff --git a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs index 24d092e..5d2805d 100644 --- a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs +++ b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs @@ -32,33 +32,33 @@ private ThirdwebTransaction(ThirdwebClient client, IThirdwebWallet wallet, Third public static async Task Create(ThirdwebClient client, IThirdwebWallet wallet, ThirdwebTransactionInput txInput, BigInteger chainId) { - var address = await wallet.GetAddress(); - txInput.From ??= address; - txInput.Data ??= "0x"; - - if (txInput.To == null) + if (client == null) { - throw new ArgumentException("Transaction recipient (to) must be provided"); + throw new ArgumentException("Client must be provided", nameof(client)); } - if (address != txInput.From) + if (wallet == null) { - throw new ArgumentException("Transaction sender (from) must match wallet address"); + throw new ArgumentException("Wallet must be provided", nameof(wallet)); } - if (client == null) + if (chainId == 0) { - throw new ArgumentNullException(nameof(client)); + throw new ArgumentException("Invalid Chain ID", nameof(chainId)); } - if (wallet == null) + if (txInput.To == null) { - throw new ArgumentNullException(nameof(wallet)); + throw new ArgumentException("Transaction recipient (to) must be provided", nameof(txInput)); } - if (chainId == 0) + var address = await wallet.GetAddress(); + txInput.From ??= address; + txInput.Data ??= "0x"; + + if (address != txInput.From) { - throw new ArgumentException("Invalid Chain ID"); + throw new ArgumentException("Transaction sender (from) must match wallet address", nameof(txInput)); } return new ThirdwebTransaction(client, wallet, txInput, chainId); @@ -156,7 +156,7 @@ public static async Task EstimateGasPrice(ThirdwebTransaction transa var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value); var chainId = transaction.Input.ChainId.Value; - if (IsZkSyncTransaction(transaction)) + if (Utils.IsZkSync(transaction.Input.ChainId.Value)) { var fees = await rpc.SendRequestAsync("zks_estimateFee", transaction.Input, "latest"); var maxFee = fees["max_fee_per_gas"].ToObject().Value; @@ -166,31 +166,31 @@ public static async Task EstimateGasPrice(ThirdwebTransaction transa var gasPrice = await EstimateGasPrice(transaction, withBump); - try + // Polygon Mainnet & Amoy + if (chainId == 137 || chainId == 80002) { - var block = await rpc.SendRequestAsync("eth_getBlockByNumber", "latest", true); - var maxPriorityFeePerGas = await rpc.SendRequestAsync("eth_maxPriorityFeePerGas"); - var baseBlockFee = block["result"]?["baseFeePerGas"]?.ToObject() ?? new BigInteger(100); - - if (chainId == 42220 || chainId == 44787 || chainId == 62320) - { - return (gasPrice, gasPrice); - } + return new(gasPrice * 3 / 2, gasPrice * 4 / 3); + } - if (!maxPriorityFeePerGas.HasValue) - { - maxPriorityFeePerGas = new BigInteger(2); - } + // Celo Mainnet, Alfajores & Baklava + if (chainId == 42220 || chainId == 44787 || chainId == 62320) + { + return new(gasPrice, gasPrice); + } - var preferredMaxPriorityFeePerGas = maxPriorityFeePerGas.Value * 10 / 9; - var maxFeePerGas = (baseBlockFee * 2) + preferredMaxPriorityFeePerGas; + try + { + var block = await rpc.SendRequestAsync(method: "eth_getBlockByNumber", "latest", true); + var baseBlockFee = block["baseFeePerGas"]?.ToObject(); + var maxFeePerGas = baseBlockFee.Value * 2; + var maxPriorityFeePerGas = ((await rpc.SendRequestAsync("eth_maxPriorityFeePerGas"))?.Value) ?? maxFeePerGas / 2; - if (withBump) + if (maxPriorityFeePerGas > maxFeePerGas) { - maxFeePerGas *= 10 / 9; + maxPriorityFeePerGas = maxFeePerGas / 2; } - return (maxFeePerGas, preferredMaxPriorityFeePerGas); + return new((maxFeePerGas + maxPriorityFeePerGas) * 10 / 9, maxPriorityFeePerGas * 10 / 9); } catch { @@ -209,7 +209,7 @@ public static async Task EstimateGasLimit(ThirdwebTransaction transa { var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value); - if (IsZkSyncTransaction(transaction)) + if (Utils.IsZkSync(transaction.Input.ChainId.Value)) { var hex = (await rpc.SendRequestAsync("zks_estimateFee", transaction.Input, "latest"))["gas_limit"].ToString(); return new HexBigInteger(hex).Value * 10 / 5; @@ -237,12 +237,12 @@ private static async Task GetGasPerPubData(ThirdwebTransaction trans { var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value); var hex = (await rpc.SendRequestAsync("zks_estimateFee", transaction.Input, "latest"))["gas_per_pubdata_limit"].ToString(); - return new HexBigInteger(hex).Value; + return new HexBigInteger(hex).Value * 3 / 2; } public static async Task Sign(ThirdwebTransaction transaction) { - return await transaction._wallet.SignTransaction(transaction.Input, transaction.Input.ChainId.Value); + return await transaction._wallet.SignTransaction(transaction.Input); } public static async Task Send(ThirdwebTransaction transaction) @@ -275,7 +275,12 @@ public static async Task Send(ThirdwebTransaction transaction) var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value); string hash; - if (IsZkSyncTransaction(transaction) && transaction.Input.ZkSync.HasValue && transaction.Input.ZkSync.Value.Paymaster != 0 && transaction.Input.ZkSync.Value.PaymasterInput != null) + if ( + Utils.IsZkSync(transaction.Input.ChainId.Value) + && transaction.Input.ZkSync.HasValue + && transaction.Input.ZkSync.Value.Paymaster != 0 + && transaction.Input.ZkSync.Value.PaymasterInput != null + ) { var zkTx = await ConvertToZkSyncTransaction(transaction); var zkTxSigned = await EIP712.GenerateSignature_ZkSyncTransaction("zkSync", "2", transaction.Input.ChainId.Value, zkTx, transaction._wallet); @@ -291,8 +296,8 @@ public static async Task Send(ThirdwebTransaction transaction) hash = await rpc.SendRequestAsync("eth_sendRawTransaction", signedTx); break; case ThirdwebAccountType.SmartAccount: - var smartAccount = transaction._wallet as SmartWallet; - hash = await smartAccount.SendTransaction(transaction.Input); + case ThirdwebAccountType.ExternalAccount: + hash = await transaction._wallet.SendTransaction(transaction.Input); break; default: throw new NotImplementedException("Account type not supported"); @@ -371,10 +376,5 @@ public static async Task WaitForTransactionReceipt(ThirdwebC 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); - } } } diff --git a/Thirdweb/Thirdweb.Utils/Utils.cs b/Thirdweb/Thirdweb.Utils/Utils.cs index f32bcbf..aa06160 100644 --- a/Thirdweb/Thirdweb.Utils/Utils.cs +++ b/Thirdweb/Thirdweb.Utils/Utils.cs @@ -88,34 +88,6 @@ public static string ReplaceIPFS(this string uri, string gateway = null) return !string.IsNullOrEmpty(uri) && uri.StartsWith("ipfs://") ? uri.Replace("ipfs://", gateway) : uri; } - public static Dictionary GetThirdwebHeaders(ThirdwebClient client) - { - var headers = new Dictionary - { - { "x-sdk-name", "Thirdweb.NET" }, - { "x-sdk-os", System.Runtime.InteropServices.RuntimeInformation.OSDescription }, - { "x-sdk-platform", "dotnet" }, - { "x-sdk-version", Constants.VERSION } - }; - - if (!string.IsNullOrEmpty(client.ClientId)) - { - headers.Add("x-client-id", client.ClientId); - } - - if (!string.IsNullOrEmpty(client.SecretKey)) - { - headers.Add("x-secret-key", client.SecretKey); - } - - if (!string.IsNullOrEmpty(client.BundleId)) - { - headers.Add("x-bundle-id", client.BundleId); - } - - return headers; - } - public static string ToWei(this string eth) { if (!double.TryParse(eth, NumberStyles.Number, CultureInfo.InvariantCulture, out var ethDouble)) @@ -196,5 +168,10 @@ public static string GenerateSIWE(LoginPayloadData loginPayloadData) + resourcesString; return payloadToSign; } + + public static bool IsZkSync(BigInteger chainId) + { + return chainId.Equals(324) || chainId.Equals(300) || chainId.Equals(302); + } } } diff --git a/Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs b/Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs index 03f7dee..1fad815 100644 --- a/Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs @@ -17,14 +17,22 @@ public interface IThirdwebWallet public Task SignTypedDataV4(T data, TypedData typedData) where TDomain : IDomain; public Task IsConnected(); - public Task SignTransaction(ThirdwebTransactionInput transaction, BigInteger chainId); - public Task Authenticate(string domain, BigInteger chainId, string authPayloadPath = "/auth/payload", string authLoginPath = "/auth/login", HttpClient httpClient = null); + public Task SignTransaction(ThirdwebTransactionInput transaction); + public Task SendTransaction(ThirdwebTransactionInput transaction); + public Task Authenticate( + string domain, + BigInteger chainId, + string authPayloadPath = "/auth/payload", + string authLoginPath = "/auth/login", + IThirdwebHttpClient httpClientOverride = null + ); } public enum ThirdwebAccountType { PrivateKeyAccount, - SmartAccount + SmartAccount, + ExternalAccount } [Serializable] diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Authentication/Server.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Authentication/Server.cs index bf8ce01..8fd29b1 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Authentication/Server.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Authentication/Server.cs @@ -46,49 +46,25 @@ internal partial class Server : ServerBase private const string ROOT_URL_LEGACY = "https://ews.thirdweb.com"; private const string API_ROOT_PATH = "/api/2023-10-20"; private const string API_ROOT_PATH_LEGACY = "/api/2022-08-12"; - private const string BUNDLE_ID_HEADER = "x-bundle-id"; - private const string THIRDWEB_CLIENT_ID_HEADER = "x-thirdweb-client-id"; - private const string THIRDWEB_SECRET_KEY_HEADER = "x-thirdweb-client-id"; - private const string SESSION_NONCE_HEADER = "x-session-nonce"; - private const string EMBEDDED_WALLET_VERSION_HEADER = "x-embedded-wallet-version"; private static readonly MediaTypeHeaderValue jsonContentType = MediaTypeHeaderValue.Parse("application/json"); - private static readonly HttpClient httpClient = new(); + private readonly IThirdwebHttpClient httpClient; private readonly string clientId; - internal Server(string clientId, string bundleId, string platform, string version, string secretKey) + internal Server(ThirdwebClient client, IThirdwebHttpClient httpClient) { - this.clientId = clientId; - - httpClient.DefaultRequestHeaders.Clear(); - - if (!string.IsNullOrEmpty(clientId)) - { - httpClient.DefaultRequestHeaders.Add(THIRDWEB_CLIENT_ID_HEADER, clientId); - } - - if (!string.IsNullOrEmpty(bundleId)) - { - httpClient.DefaultRequestHeaders.Add(BUNDLE_ID_HEADER, bundleId); - } - - if (!string.IsNullOrEmpty(secretKey)) - { - httpClient.DefaultRequestHeaders.Add(THIRDWEB_SECRET_KEY_HEADER, secretKey); - } - - httpClient.DefaultRequestHeaders.Add(SESSION_NONCE_HEADER, Guid.NewGuid().ToString()); - httpClient.DefaultRequestHeaders.Add(EMBEDDED_WALLET_VERSION_HEADER, $"{platform}:{version}"); + this.clientId = client.ClientId; + this.httpClient = httpClient; } // embedded-wallet/verify-thirdweb-client-id internal override async Task VerifyThirdwebClientIdAsync(string parentDomain) { Dictionary queryParams = new() { { "clientId", clientId }, { "parentDomain", parentDomain } }; - Uri uri = MakeUri("/embedded-wallet/verify-thirdweb-client-id", queryParams); - StringContent content = MakeHttpContent(new { clientId, parentDomain }); - HttpResponseMessage response = await httpClient.PostAsync(uri, content); + var uri = MakeUri("/embedded-wallet/verify-thirdweb-client-id", queryParams); + var content = MakeHttpContent(new { clientId, parentDomain }); + var response = await httpClient.PostAsync(uri.ToString(), content); await CheckStatusCodeAsync(response); var error = await DeserializeAsync(response); return error.Error; @@ -100,8 +76,8 @@ internal override async Task FetchDeveloperWalletSettings() try { Dictionary queryParams = new() { { "clientId", clientId }, }; - Uri uri = MakeUri("/embedded-wallet/developer-wallet-settings", queryParams); - HttpResponseMessage response = await httpClient.GetAsync(uri); + var uri = MakeUri("/embedded-wallet/developer-wallet-settings", queryParams); + ThirdwebHttpResponseMessage response = await httpClient.GetAsync(uri.ToString()); var responseContent = await DeserializeAsync(response); return responseContent.Value ?? "AWS_MANAGED"; } @@ -124,8 +100,8 @@ internal override async Task FetchUserDetailsAsync(string emailAddre queryParams.Add("email", emailAddress ?? "uninitialized"); queryParams.Add("clientId", clientId); - Uri uri = MakeUri("/embedded-wallet/embedded-wallet-user-details", queryParams); - HttpResponseMessage response = await SendHttpWithAuthAsync(uri, authToken ?? ""); + var uri = MakeUri("/embedded-wallet/embedded-wallet-user-details", queryParams); + var response = await SendHttpWithAuthAsync(uri, authToken ?? ""); await CheckStatusCodeAsync(response); var rv = await DeserializeAsync(response); return rv; @@ -151,23 +127,23 @@ internal override async Task StoreAddressAndSharesAsync(string walletAddress, st } ), }; - HttpResponseMessage response = await SendHttpWithAuthAsync(httpRequestMessage, authToken); + var response = await SendHttpWithAuthAsync(httpRequestMessage, authToken); await CheckStatusCodeAsync(response); } // embedded-wallet/embedded-wallet-shares GET internal override async Task<(string authShare, string recoveryShare)> FetchAuthAndRecoverySharesAsync(string authToken) { - SharesGetResponse sharesGetResponse = await FetchRemoteSharesAsync(authToken, true); - string authShare = sharesGetResponse.AuthShare ?? throw new InvalidOperationException("Server failed to return auth share"); - string encryptedRecoveryShare = sharesGetResponse.MaybeEncryptedRecoveryShares?.FirstOrDefault() ?? throw new InvalidOperationException("Server failed to return recovery share"); + var sharesGetResponse = await FetchRemoteSharesAsync(authToken, true); + var authShare = sharesGetResponse.AuthShare ?? throw new InvalidOperationException("Server failed to return auth share"); + var encryptedRecoveryShare = sharesGetResponse.MaybeEncryptedRecoveryShares?.FirstOrDefault() ?? throw new InvalidOperationException("Server failed to return recovery share"); return (authShare, encryptedRecoveryShare); } // embedded-wallet/embedded-wallet-shares GET internal override async Task FetchAuthShareAsync(string authToken) { - SharesGetResponse sharesGetResponse = await FetchRemoteSharesAsync(authToken, false); + var sharesGetResponse = await FetchRemoteSharesAsync(authToken, false); return sharesGetResponse.AuthShare ?? throw new InvalidOperationException("Server failed to return auth share"); } @@ -181,8 +157,8 @@ private async Task FetchRemoteSharesAsync(string authToken, b { "getEncryptedRecoveryShare", wantsRecoveryShare ? "true" : "false" }, { "useSealedSecret", "false" } }; - Uri uri = MakeUri("/embedded-wallet/embedded-wallet-shares", queryParams); - HttpResponseMessage response = await SendHttpWithAuthAsync(uri, authToken); + var uri = MakeUri("/embedded-wallet/embedded-wallet-shares", queryParams); + var response = await SendHttpWithAuthAsync(uri, authToken); await CheckStatusCodeAsync(response); var rv = await DeserializeAsync(response); return rv; @@ -191,8 +167,8 @@ private async Task FetchRemoteSharesAsync(string authToken, b // embedded-wallet/cognito-id-token private async Task FetchCognitoIdTokenAsync(string authToken) { - Uri uri = MakeUri("/embedded-wallet/cognito-id-token"); - HttpResponseMessage response = await SendHttpWithAuthAsync(uri, authToken); + var uri = MakeUri("/embedded-wallet/cognito-id-token"); + var response = await SendHttpWithAuthAsync(uri, authToken); await CheckStatusCodeAsync(response); return await DeserializeAsync(response); } @@ -201,17 +177,18 @@ private async Task FetchCognitoIdTokenAsync(string authToken) internal override async Task FetchHeadlessOauthLoginLinkAsync(string authProvider) { // based on above unity implementation, adapt to this class - Uri uri = MakeUri( + var uri = MakeUri( "/embedded-wallet/headless-oauth-login-link", new Dictionary { + // TODO: Use this for login link when it's ready in backend { "platform", "unity" }, { "authProvider", authProvider }, { "baseUrl", "https://embedded-wallet.thirdweb.com" } } ); - HttpResponseMessage response = await httpClient.GetAsync(uri); + ThirdwebHttpResponseMessage response = await httpClient.GetAsync(uri.ToString()); await CheckStatusCodeAsync(response); var rv = await DeserializeAsync(response); return rv.PlatformLoginLink; @@ -220,7 +197,7 @@ internal override async Task FetchHeadlessOauthLoginLinkAsync(string aut // /embedded-wallet/is-cognito-otp-valid internal override async Task CheckIsEmailKmsOtpValidAsync(string email, string otp) { - Uri uri = MakeUriLegacy( + var uri = MakeUriLegacy( "/embedded-wallet/is-cognito-otp-valid", new Dictionary { @@ -229,7 +206,7 @@ internal override async Task CheckIsEmailKmsOtpValidAsync(string email, st { "clientId", clientId } } ); - HttpResponseMessage response = await httpClient.GetAsync(uri); + ThirdwebHttpResponseMessage response = await httpClient.GetAsync(uri.ToString()); await CheckStatusCodeAsync(response); var result = await DeserializeAsync(response); return result.IsOtpValid; @@ -238,8 +215,8 @@ internal override async Task CheckIsEmailKmsOtpValidAsync(string email, st // embedded-wallet/is-thirdweb-email-otp-valid internal override async Task CheckIsEmailUserOtpValidAsync(string email, string otp) { - Uri uri = MakeUri("/embedded-wallet/is-thirdweb-email-otp-valid"); - StringContent content = MakeHttpContent( + var uri = MakeUri("/embedded-wallet/is-thirdweb-email-otp-valid"); + var content = MakeHttpContent( new { email, @@ -247,7 +224,7 @@ internal override async Task CheckIsEmailUserOtpValidAsync(string email, s clientId, } ); - HttpResponseMessage response = await httpClient.PostAsync(uri, content); + ThirdwebHttpResponseMessage response = await httpClient.PostAsync(uri.ToString(), content); await CheckStatusCodeAsync(response); var result = await DeserializeAsync(response); return result.IsValid; @@ -256,9 +233,9 @@ internal override async Task CheckIsEmailUserOtpValidAsync(string email, s // embedded-wallet/send-user-managed-email-otp internal override async Task SendUserOtpEmailAsync(string emailAddress) { - Uri uri = MakeUri("/embedded-wallet/send-user-managed-email-otp"); - StringContent content = MakeHttpContent(new { clientId, email = emailAddress }); - HttpResponseMessage response = await httpClient.PostAsync(uri, content); + var uri = MakeUri("/embedded-wallet/send-user-managed-email-otp"); + var content = MakeHttpContent(new { clientId, email = emailAddress }); + ThirdwebHttpResponseMessage response = await httpClient.PostAsync(uri.ToString(), content); await CheckStatusCodeAsync(response); } @@ -280,7 +257,7 @@ internal override async Task SendRecoveryCodeEmailAsync(string authToken, string }; try { - HttpResponseMessage response = await SendHttpWithAuthAsync(httpRequestMessage, authToken); + var response = await SendHttpWithAuthAsync(httpRequestMessage, authToken); await CheckStatusCodeAsync(response); } catch (Exception ex) @@ -292,8 +269,8 @@ internal override async Task SendRecoveryCodeEmailAsync(string authToken, string // embedded-wallet/validate-thirdweb-email-otp internal override async Task VerifyUserOtpAsync(string emailAddress, string otp) { - Uri uri = MakeUri("/embedded-wallet/validate-thirdweb-email-otp"); - StringContent content = MakeHttpContent( + var uri = MakeUri("/embedded-wallet/validate-thirdweb-email-otp"); + var content = MakeHttpContent( new { clientId, @@ -301,7 +278,7 @@ internal override async Task VerifyUserOtpAsync(string emailAddres otp } ); - HttpResponseMessage response = await httpClient.PostAsync(uri, content); + ThirdwebHttpResponseMessage response = await httpClient.PostAsync(uri.ToString(), content); await CheckStatusCodeAsync(response); var authVerifiedToken = await DeserializeAsync(response); return new VerifyResult( @@ -316,12 +293,12 @@ internal override async Task VerifyUserOtpAsync(string emailAddres // KMS Send internal override async Task SendKmsOtpEmailAsync(string emailAddress) { - string userName = MakeCognitoUserName(emailAddress, "email"); - string sessionId = await AWS.StartCognitoUserAuth(userName); + var userName = MakeCognitoUserName(emailAddress, "email"); + var sessionId = await AWS.StartCognitoUserAuth(userName); if (sessionId == null) { await AWS.SignUpCognitoUserAsync(emailAddress, userName); - for (int i = 0; i < 3; ++i) + for (var i = 0; i < 3; ++i) { await Task.Delay(3333 * i); sessionId = await AWS.StartCognitoUserAuth(userName); @@ -341,9 +318,9 @@ internal override async Task SendKmsOtpEmailAsync(string emailAddress) // embedded-wallet/validate-cognito-email-otp internal override async Task VerifyKmsOtpAsync(string emailAddress, string otp, string sessionId) { - string userName = MakeCognitoUserName(emailAddress, "email"); - TokenCollection tokens = await AWS.FinishCognitoUserAuth(userName, otp, sessionId); - Uri uri = MakeUri("/embedded-wallet/validate-cognito-email-otp"); + var userName = MakeCognitoUserName(emailAddress, "email"); + var tokens = await AWS.FinishCognitoUserAuth(userName, otp, sessionId); + var uri = MakeUri("/embedded-wallet/validate-cognito-email-otp"); ByteArrayContent content = MakeHttpContent( new { @@ -354,16 +331,16 @@ internal override async Task VerifyKmsOtpAsync(string emailAddress otpMethod = "email", } ); - HttpResponseMessage response = await httpClient.PostAsync(uri, content); + ThirdwebHttpResponseMessage response = await httpClient.PostAsync(uri.ToString(), content); await CheckStatusCodeAsync(response); var authVerifiedToken = await DeserializeAsync(response); - bool isNewUser = authVerifiedToken.VerifiedToken.IsNewUser; - string authToken = authVerifiedToken.VerifiedTokenJwtString; - string walletUserId = authVerifiedToken.VerifiedToken.AuthDetails.WalletUserId; + var isNewUser = authVerifiedToken.VerifiedToken.IsNewUser; + var authToken = authVerifiedToken.VerifiedTokenJwtString; + var walletUserId = authVerifiedToken.VerifiedToken.AuthDetails.WalletUserId; var idTokenResponse = await FetchCognitoIdTokenAsync(authToken); - string idToken = idTokenResponse.IdToken; - string invokePayload = Serialize(new { accessToken = idTokenResponse.AccessToken, idToken = idTokenResponse.IdToken }); - MemoryStream responsePayload = await AWS.InvokeRecoverySharePasswordLambdaAsync(idToken, invokePayload); + var idToken = idTokenResponse.IdToken; + var invokePayload = Serialize(new { accessToken = idTokenResponse.AccessToken, idToken = idTokenResponse.IdToken }); + var responsePayload = await AWS.InvokeRecoverySharePasswordLambdaAsync(idToken, invokePayload); JsonSerializer jsonSerializer = new(); var payload = jsonSerializer.Deserialize(new JsonTextReader(new StreamReader(responsePayload))); payload = jsonSerializer.Deserialize(new JsonTextReader(new StringReader(payload.Body))); @@ -372,12 +349,12 @@ internal override async Task VerifyKmsOtpAsync(string emailAddress internal override async Task SendKmsPhoneOtpAsync(string phoneNumber) { - string userName = MakeCognitoUserName(phoneNumber, "sms"); - string sessionId = await AWS.StartCognitoUserAuth(userName); + var userName = MakeCognitoUserName(phoneNumber, "sms"); + var sessionId = await AWS.StartCognitoUserAuth(userName); if (sessionId == null) { await AWS.SignUpCognitoUserAsync(null, userName); - for (int i = 0; i < 3; ++i) + for (var i = 0; i < 3; ++i) { await Task.Delay(3333 * i); sessionId = await AWS.StartCognitoUserAuth(userName); @@ -397,9 +374,9 @@ internal override async Task SendKmsPhoneOtpAsync(string phoneNumber) // embedded-wallet/validate-cognito-email-otp internal override async Task VerifyKmsPhoneOtpAsync(string phoneNumber, string otp, string sessionId) { - string userName = MakeCognitoUserName(phoneNumber, "sms"); - TokenCollection tokens = await AWS.FinishCognitoUserAuth(userName, otp, sessionId); - Uri uri = MakeUri("/embedded-wallet/validate-cognito-email-otp"); + var userName = MakeCognitoUserName(phoneNumber, "sms"); + var tokens = await AWS.FinishCognitoUserAuth(userName, otp, sessionId); + var uri = MakeUri("/embedded-wallet/validate-cognito-email-otp"); ByteArrayContent content = MakeHttpContent( new { @@ -410,16 +387,16 @@ internal override async Task VerifyKmsPhoneOtpAsync(string phoneNu otpMethod = "email", } ); - HttpResponseMessage response = await httpClient.PostAsync(uri, content); + ThirdwebHttpResponseMessage response = await httpClient.PostAsync(uri.ToString(), content); await CheckStatusCodeAsync(response); var authVerifiedToken = await DeserializeAsync(response); - bool isNewUser = authVerifiedToken.VerifiedToken.IsNewUser; - string authToken = authVerifiedToken.VerifiedTokenJwtString; - string walletUserId = authVerifiedToken.VerifiedToken.AuthDetails.WalletUserId; + var isNewUser = authVerifiedToken.VerifiedToken.IsNewUser; + var authToken = authVerifiedToken.VerifiedTokenJwtString; + var walletUserId = authVerifiedToken.VerifiedToken.AuthDetails.WalletUserId; var idTokenResponse = await FetchCognitoIdTokenAsync(authToken); - string idToken = idTokenResponse.IdToken; - string invokePayload = Serialize(new { accessToken = idTokenResponse.AccessToken, idToken = idTokenResponse.IdToken }); - MemoryStream responsePayload = await AWS.InvokeRecoverySharePasswordLambdaAsync(idToken, invokePayload); + var idToken = idTokenResponse.IdToken; + var invokePayload = Serialize(new { accessToken = idTokenResponse.AccessToken, idToken = idTokenResponse.IdToken }); + var responsePayload = await AWS.InvokeRecoverySharePasswordLambdaAsync(idToken, invokePayload); JsonSerializer jsonSerializer = new(); var payload = jsonSerializer.Deserialize(new JsonTextReader(new StreamReader(responsePayload))); payload = jsonSerializer.Deserialize(new JsonTextReader(new StringReader(payload.Body))); @@ -430,16 +407,16 @@ internal override async Task VerifyKmsPhoneOtpAsync(string phoneNu internal override async Task VerifyJwtAsync(string jwtToken) { var requestContent = new { jwt = jwtToken, developerClientId = clientId }; - StringContent content = MakeHttpContent(requestContent); - Uri uri = MakeUri("/embedded-wallet/validate-custom-jwt"); - HttpResponseMessage response = await httpClient.PostAsync(uri, content); + var content = MakeHttpContent(requestContent); + var uri = MakeUri("/embedded-wallet/validate-custom-jwt"); + ThirdwebHttpResponseMessage response = await httpClient.PostAsync(uri.ToString(), content); await CheckStatusCodeAsync(response); var authVerifiedToken = await DeserializeAsync(response); - bool isNewUser = authVerifiedToken.VerifiedToken.IsNewUser; - string authToken = authVerifiedToken.VerifiedTokenJwtString; - string walletUserId = authVerifiedToken.VerifiedToken.AuthDetails.WalletUserId; - string email = authVerifiedToken.VerifiedToken.AuthDetails.Email; - string recoveryCode = authVerifiedToken.VerifiedToken.AuthDetails.RecoveryCode; + var isNewUser = authVerifiedToken.VerifiedToken.IsNewUser; + var authToken = authVerifiedToken.VerifiedTokenJwtString; + var walletUserId = authVerifiedToken.VerifiedToken.AuthDetails.WalletUserId; + var email = authVerifiedToken.VerifiedToken.AuthDetails.Email; + var recoveryCode = authVerifiedToken.VerifiedToken.AuthDetails.RecoveryCode; return new VerifyResult(isNewUser, authToken, walletUserId, recoveryCode, email); } @@ -447,33 +424,33 @@ internal override async Task VerifyJwtAsync(string jwtToken) internal override async Task VerifyAuthEndpointAsync(string payload) { var requestContent = new { payload, developerClientId = clientId }; - StringContent content = MakeHttpContent(requestContent); - Uri uri = MakeUri("/embedded-wallet/validate-custom-auth-endpoint"); - HttpResponseMessage response = await httpClient.PostAsync(uri, content); + var content = MakeHttpContent(requestContent); + var uri = MakeUri("/embedded-wallet/validate-custom-auth-endpoint"); + ThirdwebHttpResponseMessage response = await httpClient.PostAsync(uri.ToString(), content); await CheckStatusCodeAsync(response); var authVerifiedToken = await DeserializeAsync(response); - bool isNewUser = authVerifiedToken.VerifiedToken.IsNewUser; - string authToken = authVerifiedToken.VerifiedTokenJwtString; - string walletUserId = authVerifiedToken.VerifiedToken.AuthDetails.WalletUserId; - string email = authVerifiedToken.VerifiedToken.AuthDetails.Email; - string recoveryCode = authVerifiedToken.VerifiedToken.AuthDetails.RecoveryCode; + var isNewUser = authVerifiedToken.VerifiedToken.IsNewUser; + var authToken = authVerifiedToken.VerifiedTokenJwtString; + var walletUserId = authVerifiedToken.VerifiedToken.AuthDetails.WalletUserId; + var email = authVerifiedToken.VerifiedToken.AuthDetails.Email; + var recoveryCode = authVerifiedToken.VerifiedToken.AuthDetails.RecoveryCode; return new VerifyResult(isNewUser, authToken, walletUserId, recoveryCode, email); } internal override async Task VerifyOAuthAsync(string authResultStr) { var authResult = JsonConvert.DeserializeObject(authResultStr); - bool isNewUser = authResult.StoredToken.IsNewUser; - string authToken = authResult.StoredToken.CookieString; - string walletUserId = authResult.StoredToken.AuthDetails.UserWalletId; - bool isUserManaged = (await FetchUserDetailsAsync(authResult.StoredToken.AuthDetails.Email, authToken)).RecoveryShareManagement == "USER_MANAGED"; + var isNewUser = authResult.StoredToken.IsNewUser; + var authToken = authResult.StoredToken.CookieString; + var walletUserId = authResult.StoredToken.AuthDetails.UserWalletId; + var isUserManaged = (await FetchUserDetailsAsync(authResult.StoredToken.AuthDetails.Email, authToken)).RecoveryShareManagement == "USER_MANAGED"; string recoveryCode = null; if (!isUserManaged) { var idTokenResponse = await FetchCognitoIdTokenAsync(authToken); - string idToken = idTokenResponse.IdToken; - string invokePayload = Serialize(new { accessToken = idTokenResponse.AccessToken, idToken = idTokenResponse.IdToken }); - MemoryStream responsePayload = await AWS.InvokeRecoverySharePasswordLambdaAsync(idToken, invokePayload); + var idToken = idTokenResponse.IdToken; + var invokePayload = Serialize(new { accessToken = idTokenResponse.AccessToken, idToken = idTokenResponse.IdToken }); + var responsePayload = await AWS.InvokeRecoverySharePasswordLambdaAsync(idToken, invokePayload); JsonSerializer jsonSerializer = new(); var payload = jsonSerializer.Deserialize(new JsonTextReader(new StreamReader(responsePayload))); payload = jsonSerializer.Deserialize(new JsonTextReader(new StringReader(payload.Body))); @@ -484,29 +461,47 @@ internal override async Task VerifyOAuthAsync(string authResultStr #region Misc - private Task SendHttpWithAuthAsync(HttpRequestMessage httpRequestMessage, string authToken) + private Task SendHttpWithAuthAsync(HttpRequestMessage httpRequestMessage, string authToken) { - httpRequestMessage.Headers.Add("Authorization", $"Bearer embedded-wallet-token:{authToken}"); -#if DEBUG - Console.WriteLine($"Request: {JsonConvert.SerializeObject(httpRequestMessage)}"); -#endif - return httpClient.SendAsync(httpRequestMessage); + httpClient.AddHeader("Authorization", $"Bearer embedded-wallet-token:{authToken}"); + + try + { + if (httpRequestMessage.Method == HttpMethod.Get) + { + return httpClient.GetAsync(httpRequestMessage.RequestUri.ToString()); + } + else if (httpRequestMessage.Method == HttpMethod.Post) + { + return httpClient.PostAsync(httpRequestMessage.RequestUri.ToString(), httpRequestMessage.Content); + } + else if (httpRequestMessage.Method == HttpMethod.Put) + { + return httpClient.PutAsync(httpRequestMessage.RequestUri.ToString(), httpRequestMessage.Content); + } + else if (httpRequestMessage.Method == HttpMethod.Delete) + { + return httpClient.DeleteAsync(httpRequestMessage.RequestUri.ToString()); + } + else + { + throw new InvalidOperationException("Unsupported HTTP method"); + } + } + finally + { + httpClient.RemoveHeader("Authorization"); + } } - private Task SendHttpWithAuthAsync(Uri uri, string authToken) + private Task SendHttpWithAuthAsync(Uri uri, string authToken) { HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, uri); -#if DEBUG - Console.WriteLine($"Request: {JsonConvert.SerializeObject(httpRequestMessage)}"); -#endif return SendHttpWithAuthAsync(httpRequestMessage, authToken); } - private static async Task CheckStatusCodeAsync(HttpResponseMessage response) + private static async Task CheckStatusCodeAsync(ThirdwebHttpResponseMessage response) { -#if DEBUG - Console.WriteLine($"Response: {await response.Content.ReadAsStringAsync()}"); -#endif if (!response.IsSuccessStatusCode) { var error = await DeserializeAsync(response); @@ -514,7 +509,7 @@ private static async Task CheckStatusCodeAsync(HttpResponseMessage response) } } - private static async Task DeserializeAsync(HttpResponseMessage response) + private static async Task DeserializeAsync(ThirdwebHttpResponseMessage response) { JsonSerializer jsonSerializer = new(); TextReader textReader = new StreamReader(await response.Content.ReadAsStreamAsync()); @@ -527,7 +522,7 @@ private static Uri MakeUri(string path, IDictionary parameters = UriBuilder b = new(ROOT_URL) { Path = API_ROOT_PATH + path, }; if (parameters != null && parameters.Any()) { - string queryString = string.Join('&', parameters.Select((p) => $"{p.Key}={Uri.EscapeDataString(p.Value)}")); + var queryString = string.Join('&', parameters.Select((p) => $"{p.Key}={Uri.EscapeDataString(p.Value)}")); b.Query = queryString; } return b.Uri; @@ -538,7 +533,7 @@ private static Uri MakeUriLegacy(string path, IDictionary parame UriBuilder b = new(ROOT_URL_LEGACY) { Path = API_ROOT_PATH_LEGACY + path, }; if (parameters != null && parameters.Any()) { - string queryString = string.Join('&', parameters.Select((p) => $"{p.Key}={Uri.EscapeDataString(p.Value)}")); + var queryString = string.Join('&', parameters.Select((p) => $"{p.Key}={Uri.EscapeDataString(p.Value)}")); b.Query = queryString; } return b.Uri; @@ -556,7 +551,7 @@ private static string Serialize(object data) JsonSerializer jsonSerializer = new() { NullValueHandling = NullValueHandling.Ignore, }; StringWriter stringWriter = new(); jsonSerializer.Serialize(stringWriter, data); - string rv = stringWriter.ToString(); + var rv = stringWriter.ToString(); return rv; } diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Encryption/EmbeddedWallet.Cryptography.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Encryption/EmbeddedWallet.Cryptography.cs index 005baa3..e5d0787 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Encryption/EmbeddedWallet.Cryptography.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Encryption/EmbeddedWallet.Cryptography.cs @@ -27,7 +27,7 @@ private string DecryptShare(string encryptedShare, string password) } else { - iterationCount = CURRENT_ITERATION_COUNT; + iterationCount = DEPRECATED_ITERATION_COUNT; } byte[] key = GetEncryptionKey(password, salt, iterationCount); diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Encryption/IvGenerator.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Encryption/IvGenerator.cs index 50754fc..e876010 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Encryption/IvGenerator.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Encryption/IvGenerator.cs @@ -1,7 +1,4 @@ using System.Security.Cryptography; -#if UNITY_5_3_OR_NEWER -using UnityEngine; -#endif namespace Thirdweb.EWS { @@ -18,14 +15,10 @@ internal class IvGenerator : IvGeneratorBase private const long prbsPeriod = (1L << nPrbsBits) - 1; private static readonly long taps = new int[] { nPrbsBits, 47, 21, 20 }.Aggregate(0L, (a, b) => a + (1L << (nPrbsBits - b))); // https://docs.xilinx.com/v/u/en-US/xapp052, page 5 - internal IvGenerator() + internal IvGenerator(string storageDirectoryPath = null) { string directory; -#if UNITY_5_3_OR_NEWER - directory = Application.persistentDataPath; -#else - directory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); -#endif + directory = storageDirectoryPath ?? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); directory = Path.Combine(directory, "EWS"); Directory.CreateDirectory(directory); ivFilePath = Path.Combine(directory, "iv.txt"); diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Storage/LocalStorage.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Storage/LocalStorage.cs index 191e4b5..2d6106d 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Storage/LocalStorage.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Storage/LocalStorage.cs @@ -1,11 +1,4 @@ -using System; -using System.IO; using System.Runtime.Serialization.Json; -using System.Security; -using System.Threading.Tasks; -#if UNITY_5_3_OR_NEWER -using UnityEngine; -#endif namespace Thirdweb.EWS { @@ -30,12 +23,7 @@ internal partial class LocalStorage : LocalStorageBase internal LocalStorage(string clientId, string storageDirectoryPath = null) { string directory; -#if UNITY_5_3_OR_NEWER - directory = Application.persistentDataPath; -#else directory = storageDirectoryPath ?? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - // Console.WriteLine($"Embedded Wallet Storage: Using '{directory}'"); -#endif directory = Path.Combine(directory, "EWS"); Directory.CreateDirectory(directory); filePath = Path.Combine(directory, $"{clientId}.txt"); diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.cs index 3c6e290..e1c9bee 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.cs @@ -11,14 +11,35 @@ internal partial class EmbeddedWallet private const int KEY_SIZE = 256 / 8; private const int TAG_SIZE = 16; private const int CURRENT_ITERATION_COUNT = 650_000; + private const int DEPRECATED_ITERATION_COUNT = 5_000_000; private const string WALLET_PRIVATE_KEY_PREFIX = "thirdweb_"; private const string ENCRYPTION_SEPARATOR = ":"; public EmbeddedWallet(ThirdwebClient client, string storageDirectoryPath = null) { localStorage = new LocalStorage(client.ClientId, storageDirectoryPath); - server = new Server(client.ClientId, client.BundleId, "dotnet", Constants.VERSION, client.SecretKey); - ivGenerator = new IvGenerator(); + + // Create a new client of same type with extra needed headers for EWS + var thirdwebHttpClientType = client.HttpClient.GetType(); + var ewsHttpClient = thirdwebHttpClientType.GetConstructor(Type.EmptyTypes).Invoke(null) as IThirdwebHttpClient; + var headers = client.HttpClient.Headers.ToDictionary(entry => entry.Key, entry => entry.Value); + var platform = client.HttpClient.Headers["x-sdk-platform"]; + var version = client.HttpClient.Headers["x-sdk-version"]; + if (client.ClientId != null) + { + headers.Add("x-thirdweb-client-id", client.ClientId); + } + if (client.SecretKey != null) + { + headers.Add("x-thirdweb-secret-key", client.SecretKey); + } + headers.Add("x-session-nonce", Guid.NewGuid().ToString()); + headers.Add("x-embedded-wallet-version", $"{platform}:{version}"); + ewsHttpClient.SetHeaders(headers); + + server = new Server(client, ewsHttpClient); + + ivGenerator = new IvGenerator(storageDirectoryPath); } } } diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.cs index b5129de..9ac7f59 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.cs @@ -89,6 +89,7 @@ public virtual async Task LoginWithOauth( throw new ArgumentNullException(nameof(mobileRedirectScheme), "Mobile redirect scheme cannot be null or empty on this platform."); } + // TODO: Use this for login link when it's ready in backend var platform = "unity"; var redirectUrl = isMobile ? mobileRedirectScheme : "http://localhost:8789/"; var loginUrl = await _embeddedWallet.FetchHeadlessOauthLoginLinkAsync(_authProvider); diff --git a/Thirdweb/Thirdweb.Wallets/PrivateKeyWallet/PrivateKeyWallet.cs b/Thirdweb/Thirdweb.Wallets/PrivateKeyWallet/PrivateKeyWallet.cs index 3fd85fe..1292a7c 100644 --- a/Thirdweb/Thirdweb.Wallets/PrivateKeyWallet/PrivateKeyWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/PrivateKeyWallet/PrivateKeyWallet.cs @@ -110,7 +110,7 @@ public virtual Task SignTypedDataV4(T data, TypedData SignTransaction(ThirdwebTransactionInput transaction, BigInteger chainId) + public virtual async Task SignTransaction(ThirdwebTransactionInput transaction) { if (transaction == null) { @@ -137,7 +137,16 @@ public virtual async Task SignTransaction(ThirdwebTransactionInput trans { var gasPrice = transaction.GasPrice; var legacySigner = new LegacyTransactionSigner(); - signedTransaction = legacySigner.SignTransaction(_ecKey.GetPrivateKey(), chainId, transaction.To, value.Value, nonce, gasPrice.Value, gasLimit.Value, transaction.Data); + signedTransaction = legacySigner.SignTransaction( + _ecKey.GetPrivateKey(), + transaction.ChainId.Value, + transaction.To, + value.Value, + nonce, + gasPrice.Value, + gasLimit.Value, + transaction.Data + ); } else { @@ -147,7 +156,7 @@ public virtual async Task SignTransaction(ThirdwebTransactionInput trans } var maxPriorityFeePerGas = transaction.MaxPriorityFeePerGas.Value; var maxFeePerGas = transaction.MaxFeePerGas.Value; - var transaction1559 = new Transaction1559(chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, transaction.To, value, transaction.Data, null); + var transaction1559 = new Transaction1559(transaction.ChainId.Value, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, transaction.To, value, transaction.Data, null); var signer = new Transaction1559Signer(); signer.SignTransaction(_ecKey, transaction1559); @@ -168,7 +177,13 @@ public virtual Task Disconnect() return Task.CompletedTask; } - public virtual async Task Authenticate(string domain, BigInteger chainId, string authPayloadPath = "/auth/payload", string authLoginPath = "/auth/login", HttpClient httpClient = null) + public virtual async Task Authenticate( + string domain, + BigInteger chainId, + string authPayloadPath = "/auth/payload", + string authLoginPath = "/auth/login", + IThirdwebHttpClient httpClientOverride = null + ) { var payloadURL = domain + authPayloadPath; var loginURL = domain + authLoginPath; @@ -176,11 +191,10 @@ public virtual async Task Authenticate(string domain, BigInteger chainId var payloadBodyRaw = new { address = await GetAddress(), chainId = chainId.ToString() }; var payloadBody = JsonConvert.SerializeObject(payloadBodyRaw); - httpClient ??= new HttpClient(); + using var httpClient = httpClientOverride ?? _client.HttpClient; - using var payloadRequest = new HttpRequestMessage(HttpMethod.Post, payloadURL); - payloadRequest.Content = new StringContent(payloadBody, Encoding.UTF8, "application/json"); - var payloadResponse = await httpClient.SendAsync(payloadRequest); + var payloadContent = new StringContent(payloadBody, Encoding.UTF8, "application/json"); + var payloadResponse = await httpClient.PostAsync(payloadURL, payloadContent); _ = payloadResponse.EnsureSuccessStatusCode(); var payloadString = await payloadResponse.Content.ReadAsStringAsync(); @@ -190,12 +204,16 @@ public virtual async Task Authenticate(string domain, BigInteger chainId loginBodyRaw.signature = await PersonalSign(payloadToSign); var loginBody = JsonConvert.SerializeObject(new { payload = loginBodyRaw }); - using var loginRequest = new HttpRequestMessage(HttpMethod.Post, loginURL); - loginRequest.Content = new StringContent(loginBody, Encoding.UTF8, "application/json"); - var loginResponse = await httpClient.SendAsync(loginRequest); + var loginContent = new StringContent(loginBody, Encoding.UTF8, "application/json"); + var loginResponse = await httpClient.PostAsync(loginURL, loginContent); _ = loginResponse.EnsureSuccessStatusCode(); var responseString = await loginResponse.Content.ReadAsStringAsync(); return responseString; } + + public Task SendTransaction(ThirdwebTransactionInput transaction) + { + throw new InvalidOperationException("SendTransaction is not supported for private key wallets, please use the unified Contract or ThirdwebTransaction APIs."); + } } } diff --git a/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs b/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs index 6b2a721..ead5701 100644 --- a/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs @@ -24,7 +24,6 @@ public class SmartWallet : IThirdwebWallet protected BigInteger _chainId; protected string _bundlerUrl; protected string _paymasterUrl; - protected bool IsZkSync => _chainId == 324 || _chainId == 300; protected SmartWallet( ThirdwebClient client, @@ -74,7 +73,7 @@ public static async Task Create( ThirdwebContract factoryContract = null; ThirdwebContract accountContract = null; - if (chainId != 324 && chainId != 300) + if (!Utils.IsZkSync(chainId)) { entryPointContract = await ThirdwebContract.Create( client, @@ -102,7 +101,7 @@ public static async Task Create( public async Task IsDeployed() { - if (IsZkSync) + if (Utils.IsZkSync(_chainId)) { return true; } @@ -118,7 +117,7 @@ public async Task SendTransaction(ThirdwebTransactionInput transactionIn throw new InvalidOperationException("SmartAccount.SendTransaction: Transaction input is required."); } - if (IsZkSync) + if (Utils.IsZkSync(_chainId)) { var transaction = await ThirdwebTransaction.Create(_client, _personalAccount, transactionInput, _chainId); @@ -331,7 +330,7 @@ private UserOperationHexified EncodeUserOperation(UserOperation userOperation) public async Task ForceDeploy() { - if (IsZkSync) + if (Utils.IsZkSync(_chainId)) { return; } @@ -353,7 +352,7 @@ public Task GetPersonalAccount() public async Task GetAddress() { - return IsZkSync ? await _personalAccount.GetAddress() : _accountContract.Address; + return Utils.IsZkSync(_chainId) ? await _personalAccount.GetAddress() : _accountContract.Address; } public Task EthSign(byte[] rawMessage) @@ -373,7 +372,7 @@ public Task PersonalSign(byte[] rawMessage) public async Task PersonalSign(string message) { - if (IsZkSync) + if (Utils.IsZkSync(_chainId)) { return await _personalAccount.PersonalSign(message); } @@ -434,7 +433,7 @@ public async Task CreateSessionKey( string reqValidityEndTimestamp ) { - if (IsZkSync) + if (Utils.IsZkSync(_chainId)) { throw new Exception("Account Permissions are not supported in ZkSync"); } @@ -467,7 +466,7 @@ string reqValidityEndTimestamp public async Task AddAdmin(string admin) { - if (IsZkSync) + if (Utils.IsZkSync(_chainId)) { throw new Exception("Account Permissions are not supported in ZkSync"); } @@ -500,7 +499,7 @@ public async Task AddAdmin(string admin) public async Task RemoveAdmin(string admin) { - if (IsZkSync) + if (Utils.IsZkSync(_chainId)) { throw new Exception("Account Permissions are not supported in ZkSync"); } @@ -544,7 +543,7 @@ public Task SignTypedDataV4(T data, TypedData typed public async Task EstimateUserOperationGas(ThirdwebTransactionInput transaction, BigInteger chainId) { - if (IsZkSync) + if (Utils.IsZkSync(_chainId)) { throw new Exception("User Operations are not supported in ZkSync"); } @@ -554,9 +553,9 @@ public async Task EstimateUserOperationGas(ThirdwebTransactionInput return cost; } - public async Task SignTransaction(ThirdwebTransactionInput transaction, BigInteger chainId) + public async Task SignTransaction(ThirdwebTransactionInput transaction) { - if (IsZkSync) + if (Utils.IsZkSync(_chainId)) { throw new Exception("Offline Signing is not supported in ZkSync"); } @@ -566,7 +565,7 @@ public async Task SignTransaction(ThirdwebTransactionInput transaction, public async Task IsConnected() { - return IsZkSync ? await _personalAccount.IsConnected() : _accountContract != null; + return Utils.IsZkSync(_chainId) ? await _personalAccount.IsConnected() : _accountContract != null; } public Task Disconnect() @@ -575,34 +574,15 @@ public Task Disconnect() return Task.CompletedTask; } - public virtual async Task Authenticate(string domain, BigInteger chainId, string authPayloadPath = "/auth/payload", string authLoginPath = "/auth/login", HttpClient httpClient = null) + public async Task Authenticate( + string domain, + BigInteger chainId, + string authPayloadPath = "/auth/payload", + string authLoginPath = "/auth/login", + IThirdwebHttpClient httpClientOverride = null + ) { - var payloadURL = domain + authPayloadPath; - var loginURL = domain + authLoginPath; - - var payloadBodyRaw = new { address = await GetAddress(), chainId = chainId.ToString() }; - var payloadBody = JsonConvert.SerializeObject(payloadBodyRaw); - - httpClient ??= new HttpClient(); - - using var payloadRequest = new HttpRequestMessage(HttpMethod.Post, payloadURL); - payloadRequest.Content = new StringContent(payloadBody, Encoding.UTF8, "application/json"); - var payloadResponse = await httpClient.SendAsync(payloadRequest); - _ = payloadResponse.EnsureSuccessStatusCode(); - var payloadString = await payloadResponse.Content.ReadAsStringAsync(); - - var loginBodyRaw = JsonConvert.DeserializeObject(payloadString); - var payloadToSign = Utils.GenerateSIWE(loginBodyRaw.payload); - - loginBodyRaw.signature = await PersonalSign(payloadToSign); - var loginBody = JsonConvert.SerializeObject(new { payload = loginBodyRaw }); - - using var loginRequest = new HttpRequestMessage(HttpMethod.Post, loginURL); - loginRequest.Content = new StringContent(loginBody, Encoding.UTF8, "application/json"); - var loginResponse = await httpClient.SendAsync(loginRequest); - _ = loginResponse.EnsureSuccessStatusCode(); - var responseString = await loginResponse.Content.ReadAsStringAsync(); - return responseString; + return await _personalAccount.Authenticate(domain, chainId, authPayloadPath, authLoginPath, httpClientOverride); } } } diff --git a/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/BundlerClient.cs b/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/BundlerClient.cs index 041a4f5..3f9ddfd 100644 --- a/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/BundlerClient.cs +++ b/Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/BundlerClient.cs @@ -78,41 +78,20 @@ public static async Task ZkBroadcastTransaction( private static async Task BundlerRequest(ThirdwebClient client, string url, object requestId, string method, params object[] args) { - using var httpClient = new HttpClient(); + var httpClient = client.HttpClient; #if DEBUG Console.WriteLine($"Bundler Request: {method}({JsonConvert.SerializeObject(args)}"); #endif var requestMessage = new RpcRequestMessage(requestId, method, args); var requestMessageJson = JsonConvert.SerializeObject(requestMessage); - var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url) { Content = new StringContent(requestMessageJson, System.Text.Encoding.UTF8, "application/json") }; - if (new Uri(url).Host.EndsWith(".thirdweb.com")) - { - httpRequestMessage.Headers.Add("x-sdk-name", "Thirdweb.NET"); - httpRequestMessage.Headers.Add("x-sdk-os", System.Runtime.InteropServices.RuntimeInformation.OSDescription); - httpRequestMessage.Headers.Add("x-sdk-platform", "dotnet"); - httpRequestMessage.Headers.Add("x-sdk-version", Constants.VERSION); - if (!string.IsNullOrEmpty(client.ClientId)) - { - httpRequestMessage.Headers.Add("x-client-id", client.ClientId); - } - - if (!string.IsNullOrEmpty(client.SecretKey)) - { - httpRequestMessage.Headers.Add("x-secret-key", client.SecretKey); - } - - if (!string.IsNullOrEmpty(client.BundleId)) - { - httpRequestMessage.Headers.Add("x-bundle-id", client.BundleId); - } - } + var httpContent = new StringContent(requestMessageJson, System.Text.Encoding.UTF8, "application/json"); - var httpResponse = await httpClient.SendAsync(httpRequestMessage); + var httpResponse = await httpClient.PostAsync(url, httpContent); if (!httpResponse.IsSuccessStatusCode) { - throw new Exception($"Bundler Request Failed. Error: {httpResponse.StatusCode} - {httpResponse.ReasonPhrase} - {await httpResponse.Content.ReadAsStringAsync()}"); + throw new Exception($"Bundler Request Failed. Error: {httpResponse.StatusCode} - {await httpResponse.Content.ReadAsStringAsync()}"); } var httpResponseJson = await httpResponse.Content.ReadAsStringAsync(); diff --git a/thirdweb.sln b/thirdweb.sln index 27239a2..ae00ee2 100644 --- a/thirdweb.sln +++ b/thirdweb.sln @@ -1,5 +1,4 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 @@ -34,6 +33,7 @@ Global {98BA8071-A8BF-44A5-9DDC-7BBDE4E732E8}.Release|x64.Build.0 = Release|Any CPU {98BA8071-A8BF-44A5-9DDC-7BBDE4E732E8}.Release|x86.ActiveCfg = Release|Any CPU {98BA8071-A8BF-44A5-9DDC-7BBDE4E732E8}.Release|x86.Build.0 = Release|Any CPU + {D78B4271-7DE9-4C54-BB97-31FBBD25A093}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D78B4271-7DE9-4C54-BB97-31FBBD25A093}.Debug|Any CPU.Build.0 = Debug|Any CPU {D78B4271-7DE9-4C54-BB97-31FBBD25A093}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -46,6 +46,7 @@ Global {D78B4271-7DE9-4C54-BB97-31FBBD25A093}.Release|x64.Build.0 = Release|Any CPU {D78B4271-7DE9-4C54-BB97-31FBBD25A093}.Release|x86.ActiveCfg = Release|Any CPU {D78B4271-7DE9-4C54-BB97-31FBBD25A093}.Release|x86.Build.0 = Release|Any CPU + {7CEBE316-4F2E-433B-8B1D-CBE8F8EE328F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7CEBE316-4F2E-433B-8B1D-CBE8F8EE328F}.Debug|Any CPU.Build.0 = Debug|Any CPU {7CEBE316-4F2E-433B-8B1D-CBE8F8EE328F}.Debug|x64.ActiveCfg = Debug|Any CPU