diff --git a/Thirdweb.Tests/Thirdweb.SmartAccount.Tests.cs b/Thirdweb.Tests/Thirdweb.SmartAccount.Tests.cs new file mode 100644 index 0000000..1785141 --- /dev/null +++ b/Thirdweb.Tests/Thirdweb.SmartAccount.Tests.cs @@ -0,0 +1,174 @@ +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_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 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/Thirdweb.Wallets/SmartAccount/SmartAccount.cs b/Thirdweb/Thirdweb.Wallets/SmartAccount/SmartAccount.cs index d7742bf..4b3628b 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); } @@ -334,6 +338,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);