diff --git a/Thirdweb.Tests/Thirdweb.PrivateKeyAccount.Tests.cs b/Thirdweb.Tests/Thirdweb.PrivateKeyAccount.Tests.cs new file mode 100644 index 0000000..3a69189 --- /dev/null +++ b/Thirdweb.Tests/Thirdweb.PrivateKeyAccount.Tests.cs @@ -0,0 +1,349 @@ +using System.Numerics; +using Nethereum.Hex.HexTypes; +using Nethereum.RPC.Eth.DTOs; + +namespace Thirdweb.Tests; + +public class PrivateKeyAccountTests : BaseTests +{ + public PrivateKeyAccountTests(ITestOutputHelper output) + : base(output) { } + + private PrivateKeyAccount GetAccount() + { + var client = new ThirdwebClient(secretKey: _secretKey); + var privateKeyAccount = new PrivateKeyAccount(client: client, privateKeyHex: _testPrivateKey); + return privateKeyAccount; + } + + [Fact] + public void Initialization_Success() + { + var account = GetAccount(); + Assert.NotNull(account); + } + + [Fact] + public void Initialization_NullPrivateKey() + { + var client = new ThirdwebClient(secretKey: _secretKey); + var ex = Assert.Throws(() => new PrivateKeyAccount(client, null)); + Assert.Equal("Private key cannot be null or empty. (Parameter 'privateKeyHex')", ex.Message); + } + + [Fact] + public async Task Connect() + { + var account = GetAccount(); + await account.Connect(); + Assert.True(await account.IsConnected()); + } + + [Fact] + public async Task GetAddress() + { + var account = GetAccount(); + await account.Connect(); + var address = await account.GetAddress(); + Assert.True(address.Length == 42); + } + + [Fact] + public async Task EthSign_Success() + { + var account = GetAccount(); + await account.Connect(); + var message = "Hello, World!"; + var signature = await account.EthSign(message); + Assert.True(signature.Length == 132); + } + + [Fact] + public async Task EthSign_NullMessage() + { + var account = GetAccount(); + await account.Connect(); + var ex = await Assert.ThrowsAsync(() => account.EthSign(null)); + Assert.Equal("Message to sign cannot be null. (Parameter 'message')", ex.Message); + } + + [Fact] + public async Task PersonalSign_Success() + { + var account = GetAccount(); + await account.Connect(); + var message = "Hello, World!"; + var signature = await account.PersonalSign(message); + Assert.True(signature.Length == 132); + } + + [Fact] + public async Task PersonalSign_EmptyMessage() + { + var account = GetAccount(); + await account.Connect(); + var ex = await Assert.ThrowsAsync(() => account.PersonalSign(string.Empty)); + Assert.Equal("Message to sign cannot be null. (Parameter 'message')", ex.Message); + } + + [Fact] + public async Task PersonalSign_NullyMessage() + { + var account = GetAccount(); + await account.Connect(); + var ex = await Assert.ThrowsAsync(() => account.PersonalSign(null as string)); + Assert.Equal("Message to sign cannot be null. (Parameter 'message')", ex.Message); + } + + [Fact] + public async Task PersonalSignRaw_Success() + { + var account = GetAccount(); + await account.Connect(); + var message = System.Text.Encoding.UTF8.GetBytes("Hello, World!"); + var signature = await account.PersonalSign(message); + Assert.True(signature.Length == 132); + } + + [Fact] + public async Task PersonalSignRaw_NullMessage() + { + var account = GetAccount(); + await account.Connect(); + var ex = await Assert.ThrowsAsync(() => account.PersonalSign(null as byte[])); + Assert.Equal("Message to sign cannot be null. (Parameter 'rawMessage')", ex.Message); + } + + [Fact] + public async Task SignTypedDataV4_Success() + { + var account = GetAccount(); + await account.Connect(); + var json = + "{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallet\",\"type\":\"address\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person\"},{\"name\":\"contents\",\"type\":\"string\"}]},\"primaryType\":\"Mail\",\"domain\":{\"name\":\"Ether Mail\",\"version\":\"1\",\"chainId\":1,\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\"},\"message\":{\"from\":{\"name\":\"Cow\",\"wallet\":\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\"},\"to\":{\"name\":\"Bob\",\"wallet\":\"0xbBbBBBBbbBBBbbbBbbBbbBBbBbbBbBbBbBbbBBbB\"},\"contents\":\"Hello, Bob!\"}}"; + var signature = await account.SignTypedDataV4(json); + Assert.True(signature.Length == 132); + } + + [Fact] + public async Task SignTypedDataV4_NullJson() + { + var account = GetAccount(); + await account.Connect(); + var ex = await Assert.ThrowsAsync(() => account.SignTypedDataV4(null)); + Assert.Equal("Json to sign cannot be null. (Parameter 'json')", ex.Message); + } + + [Fact] + public async Task SignTypedDataV4_EmptyJson() + { + var account = GetAccount(); + await account.Connect(); + var ex = await Assert.ThrowsAsync(() => account.SignTypedDataV4(string.Empty)); + Assert.Equal("Json to sign cannot be null. (Parameter 'json')", ex.Message); + } + + [Fact] + public async Task SignTypedDataV4_Typed() + { + var account = GetAccount(); + await account.Connect(); + var typedData = EIP712.GetTypedDefinition_SmartAccount_AccountMessage("Account", "1", 421614, await account.GetAddress()); + var accountMessage = new AccountAbstraction.AccountMessage { Message = System.Text.Encoding.UTF8.GetBytes("Hello, world!") }; + var signature = await account.SignTypedDataV4(accountMessage, typedData); + Assert.True(signature.Length == 132); + } + + [Fact] + public async Task SignTypedDataV4_Typed_NullData() + { + var account = GetAccount(); + await account.Connect(); + var typedData = EIP712.GetTypedDefinition_SmartAccount_AccountMessage("Account", "1", 421614, await account.GetAddress()); + var ex = await Assert.ThrowsAsync(() => account.SignTypedDataV4(null as string, typedData)); + Assert.Equal("Data to sign cannot be null. (Parameter 'data')", ex.Message); + } + + [Fact] + public async Task SignTransaction_Success() + { + var account = GetAccount(); + await account.Connect(); + var transaction = new TransactionInput + { + From = await account.GetAddress(), + To = Constants.ADDRESS_ZERO, + // Value = new HexBigInteger(0), + Gas = new HexBigInteger(21000), + Data = "0x", + Nonce = new HexBigInteger(99999999999), + GasPrice = new HexBigInteger(10000000000) + }; + var signature = await account.SignTransaction(transaction, 421614); + Assert.NotNull(signature); + } + + [Fact] + public async Task SignTransaction_NullTransaction() + { + var account = GetAccount(); + await account.Connect(); + var ex = await Assert.ThrowsAsync(() => account.SignTransaction(null, 421614)); + Assert.Equal("Value cannot be null. (Parameter 'transaction')", ex.Message); + } + + [Fact] + public async Task SignTransaction_NoNonce() + { + var account = GetAccount(); + await account.Connect(); + var transaction = new TransactionInput + { + From = await account.GetAddress(), + To = Constants.ADDRESS_ZERO, + Value = new HexBigInteger(0), + Gas = new HexBigInteger(21000), + Data = "0x" + }; + var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction, 421614)); + Assert.Equal("Transaction nonce has not been set (Parameter 'transaction')", ex.Message); + } + + [Fact] + public async Task SignTransaction_WrongFrom() + { + var account = GetAccount(); + await account.Connect(); + var transaction = new TransactionInput + { + From = Constants.ADDRESS_ZERO, + To = Constants.ADDRESS_ZERO, + Value = new HexBigInteger(0), + Gas = new HexBigInteger(21000), + Data = "0x", + Nonce = new HexBigInteger(99999999999) + }; + var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction, 421614)); + Assert.Equal("Transaction 'From' address does not match the wallet address", ex.Message); + } + + [Fact] + public async Task SignTransaction_NoGasPrice() + { + var account = GetAccount(); + await account.Connect(); + var transaction = new TransactionInput + { + From = await account.GetAddress(), + To = Constants.ADDRESS_ZERO, + Value = new HexBigInteger(0), + Gas = new HexBigInteger(21000), + Data = "0x", + Nonce = new HexBigInteger(99999999999) + }; + var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction, 421614)); + Assert.Equal("Transaction gas price must be set for legacy transactions", ex.Message); + } + + [Fact] + public async Task SignTransaction_1559_Success() + { + var account = GetAccount(); + await account.Connect(); + var transaction = new TransactionInput + { + From = await account.GetAddress(), + To = Constants.ADDRESS_ZERO, + Value = new HexBigInteger(0), + Gas = new HexBigInteger(21000), + Data = "0x", + Nonce = new HexBigInteger(99999999999), + Type = new HexBigInteger(2), + MaxFeePerGas = new HexBigInteger(10000000000), + MaxPriorityFeePerGas = new HexBigInteger(10000000000) + }; + var signature = await account.SignTransaction(transaction, 421614); + Assert.NotNull(signature); + } + + [Fact] + public async Task SignTransaction_1559_NoMaxFeePerGas() + { + var account = GetAccount(); + await account.Connect(); + var transaction = new TransactionInput + { + From = await account.GetAddress(), + To = Constants.ADDRESS_ZERO, + Value = new HexBigInteger(0), + Gas = new HexBigInteger(21000), + Data = "0x", + Nonce = new HexBigInteger(99999999999), + Type = new HexBigInteger(2), + MaxPriorityFeePerGas = new HexBigInteger(10000000000) + }; + var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction, 421614)); + Assert.Equal("Transaction MaxPriorityFeePerGas and MaxFeePerGas must be set for EIP-1559 transactions", ex.Message); + } + + [Fact] + public async Task SignTransaction_1559_NoMaxPriorityFeePerGas() + { + var account = GetAccount(); + await account.Connect(); + var transaction = new TransactionInput + { + From = await account.GetAddress(), + To = Constants.ADDRESS_ZERO, + Value = new HexBigInteger(0), + Gas = new HexBigInteger(21000), + Data = "0x", + Nonce = new HexBigInteger(99999999999), + Type = new HexBigInteger(2), + MaxFeePerGas = new HexBigInteger(10000000000) + }; + var ex = await Assert.ThrowsAsync(() => account.SignTransaction(transaction, 421614)); + Assert.Equal("Transaction MaxPriorityFeePerGas and MaxFeePerGas must be set for EIP-1559 transactions", ex.Message); + } + + [Fact] + public async Task IsConnected_True() + { + var account = GetAccount(); + await account.Connect(); + Assert.True(await account.IsConnected()); + } + + [Fact] + public async Task IsConnected_False() + { + var account = GetAccount(); + Assert.False(await account.IsConnected()); + } + + [Fact] + public async Task Disconnect() + { + var account = GetAccount(); + await account.Connect(); + await account.Disconnect(); + Assert.False(await account.IsConnected()); + } + + [Fact] + public async Task Disconnect_NotConnected() + { + var account = GetAccount(); + await account.Disconnect(); + Assert.False(await account.IsConnected()); + } + + [Fact] + public async Task Disconnect_Connected() + { + var account = GetAccount(); + await account.Connect(); + await account.Disconnect(); + Assert.False(await account.IsConnected()); + } +} diff --git a/Thirdweb/Thirdweb.Wallets/PrivateKeyAccount/PrivateKeyAccount.cs b/Thirdweb/Thirdweb.Wallets/PrivateKeyAccount/PrivateKeyAccount.cs index 6025277..4fa9b7d 100644 --- a/Thirdweb/Thirdweb.Wallets/PrivateKeyAccount/PrivateKeyAccount.cs +++ b/Thirdweb/Thirdweb.Wallets/PrivateKeyAccount/PrivateKeyAccount.cs @@ -68,7 +68,7 @@ public Task PersonalSign(byte[] rawMessage) public Task PersonalSign(string message) { - if (message == null) + if (string.IsNullOrEmpty(message)) { throw new ArgumentNullException(nameof(message), "Message to sign cannot be null."); } @@ -80,7 +80,7 @@ public Task PersonalSign(string message) public Task SignTypedDataV4(string json) { - if (json == null) + if (string.IsNullOrEmpty(json)) { throw new ArgumentNullException(nameof(json), "Json to sign cannot be null."); } @@ -127,6 +127,10 @@ public async Task SignTransaction(TransactionInput transaction, BigInteg string signedTransaction; if (transaction.Type != null && transaction.Type.Value == TransactionType.EIP1559.AsByte()) { + if (transaction.MaxPriorityFeePerGas == null || transaction.MaxFeePerGas == null) + { + throw new InvalidOperationException("Transaction MaxPriorityFeePerGas and MaxFeePerGas must be set for EIP-1559 transactions"); + } var maxPriorityFeePerGas = transaction.MaxPriorityFeePerGas.Value; var maxFeePerGas = transaction.MaxFeePerGas.Value; var transaction1559 = new Transaction1559( @@ -147,6 +151,10 @@ public async Task SignTransaction(TransactionInput transaction, BigInteg } else { + if (transaction.GasPrice == null) + { + throw new InvalidOperationException("Transaction gas price must be set for legacy transactions"); + } 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); diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..cf1c89e --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "Thirdweb/Thirdweb.Wallets/EmbeddedAccount" \ No newline at end of file