diff --git a/Thirdweb.Tests/Thirdweb.SmartAccount.Tests.cs b/Thirdweb.Tests/Thirdweb.SmartAccount.Tests.cs new file mode 100644 index 0000000..4d2c508 --- /dev/null +++ b/Thirdweb.Tests/Thirdweb.SmartAccount.Tests.cs @@ -0,0 +1,202 @@ +using System.Numerics; +using Nethereum.Hex.HexTypes; +using Nethereum.RPC.Eth.DTOs; + +namespace Thirdweb.Tests; + +public class SmartAccountTests : BaseTests +{ + public SmartAccountTests(ITestOutputHelper output) + : base(output) { } + + private async Task GetSmartAccount() + { + var client = new ThirdwebClient(secretKey: _secretKey); + var privateKeyAccount = new PrivateKeyAccount(client, _testPrivateKey); + await privateKeyAccount.Connect(); + var smartAccount = new SmartAccount(client, personalAccount: privateKeyAccount, factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", gasless: true, chainId: 421614); + await smartAccount.Connect(); + return smartAccount; + } + + [Fact] + public async Task Initialization_Success() + { + var account = await GetSmartAccount(); + Assert.NotNull(await account.GetAddress()); + } + + [Fact] + public async Task Initialization_Fail() + { + var client = new ThirdwebClient(secretKey: _secretKey); + var privateKeyAccount = new PrivateKeyAccount(client, _testPrivateKey); + var smartAccount = new SmartAccount(client, personalAccount: privateKeyAccount, factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", gasless: true, chainId: 421614); + var ex = await Assert.ThrowsAsync(smartAccount.Connect); + Assert.Equal("SmartAccount.Connect: Personal account must be connected.", ex.Message); + } + + [Fact] + public async Task IsDeployed_True() + { + var account = await GetSmartAccount(); + Assert.True(await account.IsDeployed()); + } + + [Fact] + public async Task IsDeployed_False() + { + var client = new ThirdwebClient(secretKey: _secretKey); + var privateKeyAccount = new PrivateKeyAccount(client, _testPrivateKey); + await privateKeyAccount.Connect(); + var smartAccount = new SmartAccount( + client, + personalAccount: privateKeyAccount, + factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", + gasless: true, + chainId: 421614, + accountAddressOverride: "0x75A4e181286F5767c38dFBE65fe1Ad4793aCB642" // vanity + ); + await smartAccount.Connect(); + Assert.False(await smartAccount.IsDeployed()); + } + + [Fact] + public async Task SendTransaction_Success() + { + var account = await GetSmartAccount(); + var tx = await account.SendTransaction( + new TransactionInput() + { + From = await account.GetAddress(), + To = await account.GetAddress(), + Value = new HexBigInteger(BigInteger.Parse("0")), + } + ); + Assert.NotNull(tx); + } + + [Fact] + public async Task SendTransaction_ClientBundleId_Success() + { + var client = new ThirdwebClient(clientId: _clientIdBundleIdOnly, bundleId: _bundleIdBundleIdOnly); + var privateKeyAccount = new PrivateKeyAccount(client, _testPrivateKey); + await privateKeyAccount.Connect(); + var smartAccount = new SmartAccount(client, personalAccount: privateKeyAccount, factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", gasless: true, chainId: 421614); + await smartAccount.Connect(); + var tx = await smartAccount.SendTransaction( + new TransactionInput() + { + From = await smartAccount.GetAddress(), + To = await smartAccount.GetAddress(), + Value = new HexBigInteger(BigInteger.Parse("0")), + } + ); + Assert.NotNull(tx); + } + + [Fact] + public async Task SendTransaction_Fail() + { + var account = await GetSmartAccount(); + var ex = await Assert.ThrowsAsync(async () => await account.SendTransaction(null)); + Assert.Equal("SmartAccount.SendTransaction: Transaction input is required.", ex.Message); + } + + [Fact] + public async Task GetAddress() + { + var account = await GetSmartAccount(); + var address = await account.GetAddress(); + Assert.NotNull(address); + } + + [Fact] + public async Task GetPersonalAccount() + { + var account = await GetSmartAccount(); + var personalAccount = await account.GetPersonalAccount(); + Assert.NotNull(personalAccount); + _ = Assert.IsType(personalAccount); + } + + [Fact] + public async Task GetAddress_WithOverride() + { + var client = new ThirdwebClient(secretKey: _secretKey); + var privateKeyAccount = new PrivateKeyAccount(client, _testPrivateKey); + await privateKeyAccount.Connect(); + var smartAccount = new SmartAccount( + client, + personalAccount: privateKeyAccount, + factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", + gasless: true, + chainId: 421614, + accountAddressOverride: "0x75A4e181286F5767c38dFBE65fe1Ad4793aCB642" // vanity + ); + await smartAccount.Connect(); + var address = await smartAccount.GetAddress(); + Assert.Equal("0x75A4e181286F5767c38dFBE65fe1Ad4793aCB642", address); + } + + [Fact] + public async Task PersonalSign() // This is the only different signing mechanism for smart wallets, also tests isValidSignature + { + var account = await GetSmartAccount(); + var sig = await account.PersonalSign("Hello, world!"); + Assert.NotNull(sig); + } + + [Fact] + public async Task CreateSessionKey() + { + var account = await GetSmartAccount(); + var receipt = await account.CreateSessionKey( + signerAddress: "0x253d077C45A3868d0527384e0B34e1e3088A3908", + approvedTargets: new List() { Constants.ADDRESS_ZERO }, + nativeTokenLimitPerTransactionInWei: "0", + permissionStartTimestamp: "0", + permissionEndTimestamp: (Utils.GetUnixTimeStampNow() + 86400).ToString(), + reqValidityStartTimestamp: "0", + reqValidityEndTimestamp: Utils.GetUnixTimeStampIn10Years().ToString() + ); + Assert.NotNull(receipt); + Assert.NotNull(receipt.TransactionHash); + } + + [Fact] + public async Task AddAdmin() + { + var account = await GetSmartAccount(); + var receipt = await account.AddAdmin("0x039d7D195f6f8537003fFC19e86cd91De5e9C431"); + Assert.NotNull(receipt); + Assert.NotNull(receipt.TransactionHash); + } + + [Fact] + public async Task RemoveAdmin() + { + var account = await GetSmartAccount(); + var receipt = await account.RemoveAdmin("0x039d7D195f6f8537003fFC19e86cd91De5e9C431"); + Assert.NotNull(receipt); + Assert.NotNull(receipt.TransactionHash); + } + + [Fact] + public async Task IsConnected() + { + var account = await GetSmartAccount(); + Assert.True(await account.IsConnected()); + + await account.Disconnect(); + Assert.False(await account.IsConnected()); + } + + [Fact] + public async Task Disconnect() + { + var account = await GetSmartAccount(); + await account.Disconnect(); + Assert.False(await account.IsConnected()); + } +} diff --git a/Thirdweb.Tests/Thirdweb.Wallets.Tests.cs b/Thirdweb.Tests/Thirdweb.Wallets.Tests.cs index 478ea04..f103573 100644 --- a/Thirdweb.Tests/Thirdweb.Wallets.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Wallets.Tests.cs @@ -105,9 +105,39 @@ public async Task SignTypedDataV4_Typed() { var wallet = await GetWallet(); var typedData = EIP712.GetTypedDefinition_SmartAccount_AccountMessage("Account", "1", 421614, await wallet.GetAddress()); - var accountMessage = new AccountAbstraction.AccountMessage { Message = System.Text.Encoding.UTF8.GetBytes("Hello, world!") }; + var accountMessage = new AccountAbstraction.AccountMessage { Message = System.Text.Encoding.UTF8.GetBytes("Hello, world!").HashPrefixedMessage() }; var signature = await wallet.SignTypedDataV4(accountMessage, typedData); Assert.NotNull(signature); + + var signerAcc = await ((SmartAccount)wallet.ActiveAccount).GetPersonalAccount(); + var gen1 = await EIP712.GenerateSignature_SmartAccount_AccountMessage( + "Account", + "1", + 421614, + await wallet.GetAddress(), + System.Text.Encoding.UTF8.GetBytes("Hello, world!").HashPrefixedMessage(), + signerAcc + ); + Assert.Equal(gen1, signature); + + var req = new AccountAbstraction.SignerPermissionRequest() + { + Signer = await wallet.GetAddress(), + IsAdmin = 0, + ApprovedTargets = new List() { Constants.ADDRESS_ZERO }, + NativeTokenLimitPerTransaction = 0, + PermissionStartTimestamp = 0, + ReqValidityStartTimestamp = 0, + PermissionEndTimestamp = 0, + Uid = new byte[32] + }; + + var typedData2 = EIP712.GetTypedDefinition_SmartAccount("Account", "1", 421614, await wallet.GetAddress()); + var signature2 = await wallet.SignTypedDataV4(req, typedData2); + Assert.NotNull(signature2); + + var gen2 = await EIP712.GenerateSignature_SmartAccount("Account", "1", 421614, await wallet.GetAddress(), req, signerAcc); + Assert.Equal(gen2, signature2); } [Fact] diff --git a/Thirdweb/Thirdweb.Wallets/SmartAccount/SmartAccount.cs b/Thirdweb/Thirdweb.Wallets/SmartAccount/SmartAccount.cs index d7742bf..5ac0ec5 100644 --- a/Thirdweb/Thirdweb.Wallets/SmartAccount/SmartAccount.cs +++ b/Thirdweb/Thirdweb.Wallets/SmartAccount/SmartAccount.cs @@ -56,7 +56,7 @@ public async Task Connect() { if (!await _personalAccount.IsConnected()) { - throw new Exception("SmartAccount.Connect: Personal account must be connected."); + throw new InvalidOperationException("SmartAccount.Connect: Personal account must be connected."); } _entryPointContract = new ThirdwebContract( @@ -88,6 +88,10 @@ public async Task IsDeployed() public async Task SendTransaction(TransactionInput transaction) { + if (transaction == null) + { + throw new InvalidOperationException("SmartAccount.SendTransaction: Transaction input is required."); + } var signedOp = await SignUserOp(transaction); return await SendUserOp(signedOp); } @@ -235,6 +239,11 @@ internal async Task ForceDeploy() _ = await Utils.GetTransactionReceipt(_client, _chainId, txHash); } + public Task GetPersonalAccount() + { + return Task.FromResult(_personalAccount); + } + public Task GetAddress() { return Task.FromResult(_accountContract.Address); @@ -334,6 +343,62 @@ string reqValidityEndTimestamp return await Utils.GetTransactionReceipt(_client, _chainId, txHash); } + public async Task AddAdmin(string admin) + { + var request = new SignerPermissionRequest() + { + Signer = admin, + IsAdmin = 1, + ApprovedTargets = new List(), + NativeTokenLimitPerTransaction = 0, + PermissionStartTimestamp = Utils.GetUnixTimeStampNow() - 3600, + PermissionEndTimestamp = Utils.GetUnixTimeStampIn10Years(), + ReqValidityStartTimestamp = Utils.GetUnixTimeStampNow() - 3600, + ReqValidityEndTimestamp = Utils.GetUnixTimeStampIn10Years(), + Uid = Guid.NewGuid().ToByteArray() + }; + + var signature = await EIP712.GenerateSignature_SmartAccount("Account", "1", _chainId, await GetAddress(), request, _personalAccount); + var data = new Contract(null, _accountContract.Abi, _accountContract.Address).GetFunction("setPermissionsForSigner").GetData(request, signature.HexToByteArray()); + var txInput = new TransactionInput() + { + From = await GetAddress(), + To = _accountContract.Address, + Value = new HexBigInteger(0), + Data = data + }; + var txHash = await SendTransaction(txInput); + return await Utils.GetTransactionReceipt(_client, _chainId, txHash); + } + + public async Task RemoveAdmin(string admin) + { + var request = new SignerPermissionRequest() + { + Signer = admin, + IsAdmin = 2, + ApprovedTargets = new List(), + NativeTokenLimitPerTransaction = 0, + PermissionStartTimestamp = Utils.GetUnixTimeStampNow() - 3600, + PermissionEndTimestamp = Utils.GetUnixTimeStampIn10Years(), + ReqValidityStartTimestamp = Utils.GetUnixTimeStampNow() - 3600, + ReqValidityEndTimestamp = Utils.GetUnixTimeStampIn10Years(), + Uid = Guid.NewGuid().ToByteArray() + }; + + var signature = await EIP712.GenerateSignature_SmartAccount("Account", "1", _chainId, await GetAddress(), request, _personalAccount); + var data = new Contract(null, _accountContract.Abi, _accountContract.Address).GetFunction("setPermissionsForSigner").GetData(request, signature.HexToByteArray()); + var txInput = new TransactionInput() + { + From = await GetAddress(), + To = _accountContract.Address, + Value = new HexBigInteger(0), + Data = data + }; + var txHash = await SendTransaction(txInput); + return await Utils.GetTransactionReceipt(_client, _chainId, txHash); + } + public Task SignTypedDataV4(string json) { return _personalAccount.SignTypedDataV4(json); diff --git a/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/AATypes.cs b/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/AATypes.cs index 2570745..3e9bf72 100644 --- a/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/AATypes.cs +++ b/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/AATypes.cs @@ -69,16 +69,6 @@ public class ExecuteFunction : FunctionMessage public virtual byte[] Calldata { get; set; } } - [Function("createAccount", "address")] - public class CreateAccountFunction : FunctionMessage - { - [Parameter("address", "_admin", 1)] - public virtual string Admin { get; set; } - - [Parameter("bytes", "_data", 2)] - public virtual byte[] Data { get; set; } - } - public class EthEstimateUserOperationGasResponse { public string PreVerificationGas { get; set; } @@ -86,14 +76,6 @@ public class EthEstimateUserOperationGasResponse public string CallGasLimit { get; set; } } - public class EthGetUserOperationByHashResponse - { - public string entryPoint { get; set; } - public string transactionHash { get; set; } - public string blockHash { get; set; } - public string blockNumber { get; set; } - } - public class EthGetUserOperationReceiptResponse { public TransactionReceipt receipt { get; set; } diff --git a/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/BundlerClient.cs b/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/BundlerClient.cs index 30615c5..0a00caa 100644 --- a/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/BundlerClient.cs +++ b/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/BundlerClient.cs @@ -10,12 +10,6 @@ public static class BundlerClient { // Bundler requests - public static async Task EthGetUserOperationByHash(ThirdwebClient client, string bundlerUrl, object requestId, string userOpHash) - { - var response = await BundlerRequest(client, bundlerUrl, requestId, "eth_getUserOperationByHash", userOpHash); - return JsonConvert.DeserializeObject(response.Result.ToString()); - } - public static async Task EthGetUserOperationReceipt(ThirdwebClient client, string bundlerUrl, object requestId, string userOpHash) { var response = await BundlerRequest(client, bundlerUrl, requestId, "eth_getUserOperationReceipt", userOpHash);