diff --git a/Directory.Build.props b/Directory.Build.props
index 5968e5f..e120acd 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -2,7 +2,7 @@
0.0.1
- netstandard2.0;netstandard2.1;net6.0;net7.0;
+ netstandard2.1;net6.0;net7.0;
latest
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0da372c..049fdeb 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -13,5 +13,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs
index d501da1..31b4776 100644
--- a/Thirdweb.Console/Program.cs
+++ b/Thirdweb.Console/Program.cs
@@ -1,24 +1,80 @@
-using dotenv.net;
+using System.Numerics;
+using dotenv.net;
using Newtonsoft.Json;
using Thirdweb;
-DotEnv.Load();
+internal class Program
+{
+ private static async Task Main(string[] args)
+ {
+ DotEnv.Load();
-var secretKey = Environment.GetEnvironmentVariable("THIRDWEB_SECRET_KEY");
+ var secretKey = Environment.GetEnvironmentVariable("THIRDWEB_SECRET_KEY");
+ var privateKey = Environment.GetEnvironmentVariable("PRIVATE_KEY");
-var clientOptions = new ThirdwebClientOptions(secretKey: secretKey, fetchTimeoutOptions: new TimeoutOptions(storage: 30000, rpc: 10000));
-var client = new ThirdwebClient(clientOptions);
+ var clientOptions = new ThirdwebClientOptions(secretKey: secretKey, fetchTimeoutOptions: new TimeoutOptions(storage: 30000, rpc: 60000));
+ var client = new ThirdwebClient(clientOptions);
+ Console.WriteLine($"Initialized ThirdwebClient: {JsonConvert.SerializeObject(clientOptions, Formatting.Indented)}");
-Console.WriteLine($"Initialized ThirdwebClient: {JsonConvert.SerializeObject(clientOptions, Formatting.Indented)}");
+ // var rpc = ThirdwebRPC.GetRpcInstance(client, 421614);
+ // var blockNumber = await rpc.SendRequestAsync("eth_blockNumber");
+ // Console.WriteLine($"Block number: {blockNumber}");
-var rpc = ThirdwebRPC.GetRpcInstance(client, 1);
-var blockNumber = await rpc.SendRequestAsync("eth_blockNumber");
-Console.WriteLine($"Block number: {blockNumber}");
+ var contract = new ThirdwebContract(
+ client: client,
+ address: "0x81ebd23aA79bCcF5AaFb9c9c5B0Db4223c39102e",
+ chain: 421614,
+ abi: "[{\"type\": \"constructor\",\"name\": \"\",\"inputs\": [],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"event\",\"name\": \"Approval\",\"inputs\": [{\"type\": \"address\",\"name\": \"owner\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"spender\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"value\",\"indexed\": false,\"internalType\": \"uint256\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"DelegateChanged\",\"inputs\": [{\"type\": \"address\",\"name\": \"delegator\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"fromDelegate\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"toDelegate\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"DelegateVotesChanged\",\"inputs\": [{\"type\": \"address\",\"name\": \"delegate\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"previousBalance\",\"indexed\": false,\"internalType\": \"uint256\"},{\"type\": \"uint256\",\"name\": \"newBalance\",\"indexed\": false,\"internalType\": \"uint256\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"EIP712DomainChanged\",\"inputs\": [],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"FlatPlatformFeeUpdated\",\"inputs\": [{\"type\": \"address\",\"name\": \"platformFeeRecipient\",\"indexed\": false,\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"flatFee\",\"indexed\": false,\"internalType\": \"uint256\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"Initialized\",\"inputs\": [{\"type\": \"uint8\",\"name\": \"version\",\"indexed\": false,\"internalType\": \"uint8\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"PlatformFeeInfoUpdated\",\"inputs\": [{\"type\": \"address\",\"name\": \"platformFeeRecipient\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"platformFeeBps\",\"indexed\": false,\"internalType\": \"uint256\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"PlatformFeeTypeUpdated\",\"inputs\": [{\"type\": \"uint8\",\"name\": \"feeType\",\"indexed\": false,\"internalType\": \"enum IPlatformFee.PlatformFeeType\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"PrimarySaleRecipientUpdated\",\"inputs\": [{\"type\": \"address\",\"name\": \"recipient\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"RoleAdminChanged\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"indexed\": true,\"internalType\": \"bytes32\"},{\"type\": \"bytes32\",\"name\": \"previousAdminRole\",\"indexed\": true,\"internalType\": \"bytes32\"},{\"type\": \"bytes32\",\"name\": \"newAdminRole\",\"indexed\": true,\"internalType\": \"bytes32\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"RoleGranted\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"indexed\": true,\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"sender\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"RoleRevoked\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"indexed\": true,\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"sender\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"TokensMinted\",\"inputs\": [{\"type\": \"address\",\"name\": \"mintedTo\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"quantityMinted\",\"indexed\": false,\"internalType\": \"uint256\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"TokensMintedWithSignature\",\"inputs\": [{\"type\": \"address\",\"name\": \"signer\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"mintedTo\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"tuple\",\"name\": \"mintRequest\",\"components\": [{\"type\": \"address\",\"name\": \"to\",\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"primarySaleRecipient\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"quantity\",\"internalType\": \"uint256\"},{\"type\": \"uint256\",\"name\": \"price\",\"internalType\": \"uint256\"},{\"type\": \"address\",\"name\": \"currency\",\"internalType\": \"address\"},{\"type\": \"uint128\",\"name\": \"validityStartTimestamp\",\"internalType\": \"uint128\"},{\"type\": \"uint128\",\"name\": \"validityEndTimestamp\",\"internalType\": \"uint128\"},{\"type\": \"bytes32\",\"name\": \"uid\",\"internalType\": \"bytes32\"}],\"indexed\": false,\"internalType\": \"struct ITokenERC20.MintRequest\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"Transfer\",\"inputs\": [{\"type\": \"address\",\"name\": \"from\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"to\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"value\",\"indexed\": false,\"internalType\": \"uint256\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"function\",\"name\": \"CLOCK_MODE\",\"inputs\": [],\"outputs\": [{\"type\": \"string\",\"name\": \"\",\"internalType\": \"string\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"DEFAULT_ADMIN_ROLE\",\"inputs\": [],\"outputs\": [{\"type\": \"bytes32\",\"name\": \"\",\"internalType\": \"bytes32\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"DOMAIN_SEPARATOR\",\"inputs\": [],\"outputs\": [{\"type\": \"bytes32\",\"name\": \"\",\"internalType\": \"bytes32\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"allowance\",\"inputs\": [{\"type\": \"address\",\"name\": \"owner\",\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"spender\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"uint256\",\"name\": \"\",\"internalType\": \"uint256\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"approve\",\"inputs\": [{\"type\": \"address\",\"name\": \"spender\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"amount\",\"internalType\": \"uint256\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"balanceOf\",\"inputs\": [{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"uint256\",\"name\": \"\",\"internalType\": \"uint256\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"burn\",\"inputs\": [{\"type\": \"uint256\",\"name\": \"amount\",\"internalType\": \"uint256\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"burnFrom\",\"inputs\": [{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"amount\",\"internalType\": \"uint256\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"checkpoints\",\"inputs\": [{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"},{\"type\": \"uint32\",\"name\": \"pos\",\"internalType\": \"uint32\"}],\"outputs\": [{\"type\": \"tuple\",\"name\": \"\",\"components\": [{\"type\": \"uint32\",\"name\": \"fromBlock\",\"internalType\": \"uint32\"},{\"type\": \"uint224\",\"name\": \"votes\",\"internalType\": \"uint224\"}],\"internalType\": \"struct ERC20VotesUpgradeable.Checkpoint\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"clock\",\"inputs\": [],\"outputs\": [{\"type\": \"uint48\",\"name\": \"\",\"internalType\": \"uint48\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"contractType\",\"inputs\": [],\"outputs\": [{\"type\": \"bytes32\",\"name\": \"\",\"internalType\": \"bytes32\"}],\"stateMutability\": \"pure\"},{\"type\": \"function\",\"name\": \"contractURI\",\"inputs\": [],\"outputs\": [{\"type\": \"string\",\"name\": \"\",\"internalType\": \"string\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"contractVersion\",\"inputs\": [],\"outputs\": [{\"type\": \"uint8\",\"name\": \"\",\"internalType\": \"uint8\"}],\"stateMutability\": \"pure\"},{\"type\": \"function\",\"name\": \"decimals\",\"inputs\": [],\"outputs\": [{\"type\": \"uint8\",\"name\": \"\",\"internalType\": \"uint8\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"decreaseAllowance\",\"inputs\": [{\"type\": \"address\",\"name\": \"spender\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"subtractedValue\",\"internalType\": \"uint256\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"delegate\",\"inputs\": [{\"type\": \"address\",\"name\": \"delegatee\",\"internalType\": \"address\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"delegateBySig\",\"inputs\": [{\"type\": \"address\",\"name\": \"delegatee\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"nonce\",\"internalType\": \"uint256\"},{\"type\": \"uint256\",\"name\": \"expiry\",\"internalType\": \"uint256\"},{\"type\": \"uint8\",\"name\": \"v\",\"internalType\": \"uint8\"},{\"type\": \"bytes32\",\"name\": \"r\",\"internalType\": \"bytes32\"},{\"type\": \"bytes32\",\"name\": \"s\",\"internalType\": \"bytes32\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"delegates\",\"inputs\": [{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"eip712Domain\",\"inputs\": [],\"outputs\": [{\"type\": \"bytes1\",\"name\": \"fields\",\"internalType\": \"bytes1\"},{\"type\": \"string\",\"name\": \"name\",\"internalType\": \"string\"},{\"type\": \"string\",\"name\": \"version\",\"internalType\": \"string\"},{\"type\": \"uint256\",\"name\": \"chainId\",\"internalType\": \"uint256\"},{\"type\": \"address\",\"name\": \"verifyingContract\",\"internalType\": \"address\"},{\"type\": \"bytes32\",\"name\": \"salt\",\"internalType\": \"bytes32\"},{\"type\": \"uint256[]\",\"name\": \"extensions\",\"internalType\": \"uint256[]\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getPastTotalSupply\",\"inputs\": [{\"type\": \"uint256\",\"name\": \"timepoint\",\"internalType\": \"uint256\"}],\"outputs\": [{\"type\": \"uint256\",\"name\": \"\",\"internalType\": \"uint256\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getPastVotes\",\"inputs\": [{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"timepoint\",\"internalType\": \"uint256\"}],\"outputs\": [{\"type\": \"uint256\",\"name\": \"\",\"internalType\": \"uint256\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getPlatformFeeInfo\",\"inputs\": [],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"},{\"type\": \"uint16\",\"name\": \"\",\"internalType\": \"uint16\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getRoleAdmin\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"}],\"outputs\": [{\"type\": \"bytes32\",\"name\": \"\",\"internalType\": \"bytes32\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getRoleMember\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"uint256\",\"name\": \"index\",\"internalType\": \"uint256\"}],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getRoleMemberCount\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"}],\"outputs\": [{\"type\": \"uint256\",\"name\": \"\",\"internalType\": \"uint256\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getVotes\",\"inputs\": [{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"uint256\",\"name\": \"\",\"internalType\": \"uint256\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"grantRole\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"hasRole\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"increaseAllowance\",\"inputs\": [{\"type\": \"address\",\"name\": \"spender\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"addedValue\",\"internalType\": \"uint256\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"initialize\",\"inputs\": [{\"type\": \"address\",\"name\": \"_defaultAdmin\",\"internalType\": \"address\"},{\"type\": \"string\",\"name\": \"_name\",\"internalType\": \"string\"},{\"type\": \"string\",\"name\": \"_symbol\",\"internalType\": \"string\"},{\"type\": \"string\",\"name\": \"_contractURI\",\"internalType\": \"string\"},{\"type\": \"address[]\",\"name\": \"_trustedForwarders\",\"internalType\": \"address[]\"},{\"type\": \"address\",\"name\": \"_primarySaleRecipient\",\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"_platformFeeRecipient\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"_platformFeeBps\",\"internalType\": \"uint256\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"isTrustedForwarder\",\"inputs\": [{\"type\": \"address\",\"name\": \"forwarder\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"mintTo\",\"inputs\": [{\"type\": \"address\",\"name\": \"to\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"amount\",\"internalType\": \"uint256\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"mintWithSignature\",\"inputs\": [{\"type\": \"tuple\",\"name\": \"_req\",\"components\": [{\"type\": \"address\",\"name\": \"to\",\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"primarySaleRecipient\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"quantity\",\"internalType\": \"uint256\"},{\"type\": \"uint256\",\"name\": \"price\",\"internalType\": \"uint256\"},{\"type\": \"address\",\"name\": \"currency\",\"internalType\": \"address\"},{\"type\": \"uint128\",\"name\": \"validityStartTimestamp\",\"internalType\": \"uint128\"},{\"type\": \"uint128\",\"name\": \"validityEndTimestamp\",\"internalType\": \"uint128\"},{\"type\": \"bytes32\",\"name\": \"uid\",\"internalType\": \"bytes32\"}],\"internalType\": \"struct ITokenERC20.MintRequest\"},{\"type\": \"bytes\",\"name\": \"_signature\",\"internalType\": \"bytes\"}],\"outputs\": [],\"stateMutability\": \"payable\"},{\"type\": \"function\",\"name\": \"multicall\",\"inputs\": [{\"type\": \"bytes[]\",\"name\": \"data\",\"internalType\": \"bytes[]\"}],\"outputs\": [{\"type\": \"bytes[]\",\"name\": \"results\",\"internalType\": \"bytes[]\"}],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"name\",\"inputs\": [],\"outputs\": [{\"type\": \"string\",\"name\": \"\",\"internalType\": \"string\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"nonces\",\"inputs\": [{\"type\": \"address\",\"name\": \"owner\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"uint256\",\"name\": \"\",\"internalType\": \"uint256\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"numCheckpoints\",\"inputs\": [{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"uint32\",\"name\": \"\",\"internalType\": \"uint32\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"permit\",\"inputs\": [{\"type\": \"address\",\"name\": \"owner\",\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"spender\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"value\",\"internalType\": \"uint256\"},{\"type\": \"uint256\",\"name\": \"deadline\",\"internalType\": \"uint256\"},{\"type\": \"uint8\",\"name\": \"v\",\"internalType\": \"uint8\"},{\"type\": \"bytes32\",\"name\": \"r\",\"internalType\": \"bytes32\"},{\"type\": \"bytes32\",\"name\": \"s\",\"internalType\": \"bytes32\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"primarySaleRecipient\",\"inputs\": [],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"renounceRole\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"revokeRole\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"setContractURI\",\"inputs\": [{\"type\": \"string\",\"name\": \"_uri\",\"internalType\": \"string\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"setPlatformFeeInfo\",\"inputs\": [{\"type\": \"address\",\"name\": \"_platformFeeRecipient\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"_platformFeeBps\",\"internalType\": \"uint256\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"setPrimarySaleRecipient\",\"inputs\": [{\"type\": \"address\",\"name\": \"_saleRecipient\",\"internalType\": \"address\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"supportsInterface\",\"inputs\": [{\"type\": \"bytes4\",\"name\": \"interfaceId\",\"internalType\": \"bytes4\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"symbol\",\"inputs\": [],\"outputs\": [{\"type\": \"string\",\"name\": \"\",\"internalType\": \"string\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"totalSupply\",\"inputs\": [],\"outputs\": [{\"type\": \"uint256\",\"name\": \"\",\"internalType\": \"uint256\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"transfer\",\"inputs\": [{\"type\": \"address\",\"name\": \"to\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"amount\",\"internalType\": \"uint256\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"transferFrom\",\"inputs\": [{\"type\": \"address\",\"name\": \"from\",\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"to\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"amount\",\"internalType\": \"uint256\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"verify\",\"inputs\": [{\"type\": \"tuple\",\"name\": \"_req\",\"components\": [{\"type\": \"address\",\"name\": \"to\",\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"primarySaleRecipient\",\"internalType\": \"address\"},{\"type\": \"uint256\",\"name\": \"quantity\",\"internalType\": \"uint256\"},{\"type\": \"uint256\",\"name\": \"price\",\"internalType\": \"uint256\"},{\"type\": \"address\",\"name\": \"currency\",\"internalType\": \"address\"},{\"type\": \"uint128\",\"name\": \"validityStartTimestamp\",\"internalType\": \"uint128\"},{\"type\": \"uint128\",\"name\": \"validityEndTimestamp\",\"internalType\": \"uint128\"},{\"type\": \"bytes32\",\"name\": \"uid\",\"internalType\": \"bytes32\"}],\"internalType\": \"struct ITokenERC20.MintRequest\"},{\"type\": \"bytes\",\"name\": \"_signature\",\"internalType\": \"bytes\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"},{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"}]"
+ );
+ var readResult = await ThirdwebContract.ReadContract(contract, "name");
+ Console.WriteLine($"Contract read result: {readResult}");
-var contractOptions = new ThirdwebContractOptions(client: client, address: "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D", chain: 1, abi: "function name() view returns (string)");
-var contract = new ThirdwebContract(contractOptions);
-var readResult = await ThirdwebContract.ReadContract(contract, "name");
+ var privateKeyAccount = new PrivateKeyAccount(client, privateKey);
+ var embeddedAccount = new EmbeddedAccount(client, "firekeeper@thirdweb.com");
+ var smartAccount = new SmartAccount(client, privateKeyAccount, "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", true, 421614);
-Console.WriteLine($"Contract read result: {readResult}");
+ var accounts = new List { privateKeyAccount, embeddedAccount, smartAccount };
-Console.ReadLine();
+ foreach (var account in accounts)
+ {
+ await account.Connect();
+ }
+
+ if (!await embeddedAccount.IsConnected())
+ {
+ await embeddedAccount.SendOTP();
+ Console.WriteLine("Please submit the OTP.");
+ var otp = Console.ReadLine();
+ (var embeddedAccountAddress, var canRetry) = await embeddedAccount.SubmitOTP(otp);
+ if (embeddedAccountAddress == null && canRetry)
+ {
+ Console.WriteLine("Please submit the OTP again.");
+ otp = Console.ReadLine();
+ _ = await embeddedAccount.SubmitOTP(otp);
+ }
+ if (embeddedAccountAddress == null)
+ {
+ Console.WriteLine("OTP login failed. Please try again.");
+ return;
+ }
+ }
+
+ var thirdwebWallet = new ThirdwebWallet();
+ await thirdwebWallet.Initialize(accounts);
+ thirdwebWallet.SetActive(await smartAccount.GetAddress());
+ Console.WriteLine($"Active account: {await thirdwebWallet.GetAddress()}");
+
+ var message = "Hello, Thirdweb!";
+ var signature = await thirdwebWallet.PersonalSign(message);
+ Console.WriteLine($"Signed message: {signature}");
+
+ var balanceBefore = await ThirdwebContract.ReadContract(contract, "balanceOf", await thirdwebWallet.GetAddress());
+ Console.WriteLine($"Balance before mint: {balanceBefore}");
+
+ var writeResult = await ThirdwebContract.WriteContract(thirdwebWallet, contract, "mintTo", 0, await thirdwebWallet.GetAddress(), 100);
+ Console.WriteLine($"Contract write result: {writeResult}");
+
+ var balanceAfter = await ThirdwebContract.ReadContract(contract, "balanceOf", await thirdwebWallet.GetAddress());
+ Console.WriteLine($"Balance after mint: {balanceAfter}");
+ }
+}
diff --git a/Thirdweb.Tests/ClientTests.cs b/Thirdweb.Tests/ClientTests.cs
index c3fadb0..e288976 100644
--- a/Thirdweb.Tests/ClientTests.cs
+++ b/Thirdweb.Tests/ClientTests.cs
@@ -94,7 +94,7 @@ public void NoTimeoutOptions()
{
var client = new ThirdwebClient(new ThirdwebClientOptions(secretKey: _secretKey));
Assert.NotNull(client.FetchTimeoutOptions);
- Assert.Equal(Constants.DefaultFetchTimeout, client.FetchTimeoutOptions.GetTimeout(TimeoutType.Storage));
- Assert.Equal(Constants.DefaultFetchTimeout, client.FetchTimeoutOptions.GetTimeout(TimeoutType.Rpc));
+ Assert.Equal(Constants.DEFAULT_FETCH_TIMEOUT, client.FetchTimeoutOptions.GetTimeout(TimeoutType.Storage));
+ Assert.Equal(Constants.DEFAULT_FETCH_TIMEOUT, client.FetchTimeoutOptions.GetTimeout(TimeoutType.Rpc));
}
}
diff --git a/Thirdweb/Thirdweb.Client/ITimeoutOptions.cs b/Thirdweb/Thirdweb.Client/ITimeoutOptions.cs
index 297baaa..2e2d0b6 100644
--- a/Thirdweb/Thirdweb.Client/ITimeoutOptions.cs
+++ b/Thirdweb/Thirdweb.Client/ITimeoutOptions.cs
@@ -2,6 +2,6 @@
{
public interface ITimeoutOptions
{
- int GetTimeout(TimeoutType type, int fallback = Constants.DefaultFetchTimeout);
+ int GetTimeout(TimeoutType type, int fallback = Constants.DEFAULT_FETCH_TIMEOUT);
}
}
diff --git a/Thirdweb/Thirdweb.Client/TimeoutOptions.cs b/Thirdweb/Thirdweb.Client/TimeoutOptions.cs
index 70b43ca..7f541c5 100644
--- a/Thirdweb/Thirdweb.Client/TimeoutOptions.cs
+++ b/Thirdweb/Thirdweb.Client/TimeoutOptions.cs
@@ -13,7 +13,7 @@ public TimeoutOptions(int? storage = null, int? rpc = null, int? other = null)
Other = other;
}
- public int GetTimeout(TimeoutType type, int fallback = Constants.DefaultFetchTimeout)
+ public int GetTimeout(TimeoutType type, int fallback = Constants.DEFAULT_FETCH_TIMEOUT)
{
return type switch
{
diff --git a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs
index 5a3f857..56c99f6 100644
--- a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs
+++ b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs
@@ -1,4 +1,6 @@
using System.Numerics;
+using Nethereum.Hex.HexTypes;
+using Nethereum.RPC.Eth.DTOs;
namespace Thirdweb
{
@@ -9,24 +11,32 @@ public class ThirdwebContract
internal BigInteger Chain { get; private set; }
internal string Abi { get; private set; }
- public ThirdwebContract(ThirdwebContractOptions options)
+ public ThirdwebContract(ThirdwebClient client, string address, BigInteger chain, string abi)
{
- if (options.Client == null)
+ if (client == null)
+ {
throw new ArgumentException("Client must be provided");
+ }
- if (string.IsNullOrEmpty(options.Address))
+ if (string.IsNullOrEmpty(address))
+ {
throw new ArgumentException("Address must be provided");
+ }
- if (options.Chain == 0)
+ if (chain == 0)
+ {
throw new ArgumentException("Chain must be provided");
+ }
- if (string.IsNullOrEmpty(options.Abi))
+ if (string.IsNullOrEmpty(abi))
+ {
throw new ArgumentException("Abi must be provided");
+ }
- Client = options.Client;
- Address = options.Address;
- Chain = options.Chain;
- Abi = options.Abi;
+ Client = client;
+ Address = address;
+ Chain = chain;
+ Abi = abi;
}
public static async Task ReadContract(ThirdwebContract contract, string method, params object[] parameters)
@@ -40,5 +50,45 @@ public static async Task ReadContract(ThirdwebContract contract, string me
var resultData = await rpc.SendRequestAsync("eth_call", new { to = contract.Address, data = data, }, "latest");
return function.DecodeTypeOutput(resultData);
}
+
+ public static async Task WriteContract(ThirdwebWallet wallet, ThirdwebContract contract, string method, BigInteger weiValue, params object[] parameters)
+ {
+ var rpc = ThirdwebRPC.GetRpcInstance(contract.Client, contract.Chain);
+
+ var service = new Nethereum.Contracts.Contract(null, contract.Abi, contract.Address);
+ var function = service.GetFunction(method);
+ var data = function.GetData(parameters);
+
+ var transaction = new TransactionInput
+ {
+ From = await wallet.GetAddress(),
+ To = contract.Address,
+ Data = data,
+ };
+
+ // TODO: Implement 1559
+ transaction.Gas = new HexBigInteger(await rpc.SendRequestAsync("eth_estimateGas", transaction));
+ transaction.GasPrice = new HexBigInteger(await rpc.SendRequestAsync("eth_gasPrice"));
+ transaction.Value = new HexBigInteger(weiValue);
+
+ string hash;
+ if (wallet.ActiveAccount.AccountType is ThirdwebAccountType.PrivateKeyAccount)
+ {
+ transaction.Nonce = new HexBigInteger(await rpc.SendRequestAsync("eth_getTransactionCount", wallet.GetAddress(), "latest"));
+ var signedTx = wallet.SignTransaction(transaction, contract.Chain);
+ Console.WriteLine($"Signed transaction: {signedTx}");
+ hash = await rpc.SendRequestAsync("eth_sendRawTransaction", signedTx);
+ }
+ else if (wallet.ActiveAccount.AccountType is ThirdwebAccountType.SmartAccount)
+ {
+ var smartAccount = wallet.ActiveAccount as SmartAccount;
+ hash = await smartAccount.SendTransaction(transaction);
+ }
+ else
+ {
+ throw new NotImplementedException("Account type not supported");
+ }
+ return hash;
+ }
}
}
diff --git a/Thirdweb/Thirdweb.Contracts/ThirdwebContractOptions.cs b/Thirdweb/Thirdweb.Contracts/ThirdwebContractOptions.cs
deleted file mode 100644
index 6d5d6d4..0000000
--- a/Thirdweb/Thirdweb.Contracts/ThirdwebContractOptions.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System.Numerics;
-
-namespace Thirdweb
-{
- public class ThirdwebContractOptions
- {
- internal ThirdwebClient Client { get; private set; }
- internal string Address { get; private set; }
- internal BigInteger Chain { get; private set; }
- internal string Abi { get; private set; }
-
- public ThirdwebContractOptions(ThirdwebClient client, string address, BigInteger chain, string abi)
- {
- Client = client;
- Address = address;
- Chain = chain;
- Abi = abi;
- }
- }
-}
diff --git a/Thirdweb/Thirdweb.RPC/ThirdwebRPC.cs b/Thirdweb/Thirdweb.RPC/ThirdwebRPC.cs
index b1c69e8..4dcf7b4 100644
--- a/Thirdweb/Thirdweb.RPC/ThirdwebRPC.cs
+++ b/Thirdweb/Thirdweb.RPC/ThirdwebRPC.cs
@@ -1,5 +1,10 @@
-using System.Numerics;
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Numerics;
using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Thirdweb
@@ -75,7 +80,7 @@ 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);
+ _httpClient.DefaultRequestHeaders.Add("x-sdk-version", Constants.VERSION);
}
private ThirdwebRPC(ThirdwebClient client, BigInteger chainId)
@@ -112,7 +117,7 @@ private void SendBatchNow()
private async Task SendBatchAsync(List batch)
{
var batchJson = JsonConvert.SerializeObject(batch);
- Console.WriteLine($"Sending batch: {batchJson}");
+
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);
diff --git a/Thirdweb/Thirdweb.Utils/Constants.cs b/Thirdweb/Thirdweb.Utils/Constants.cs
index 278920e..0a621d8 100644
--- a/Thirdweb/Thirdweb.Utils/Constants.cs
+++ b/Thirdweb/Thirdweb.Utils/Constants.cs
@@ -2,7 +2,11 @@
{
public static class Constants
{
- internal const string Version = "0.0.1";
- internal const int DefaultFetchTimeout = 60000;
+ internal const string VERSION = "0.0.1";
+ internal const int DEFAULT_FETCH_TIMEOUT = 60000;
+ internal const string DEFAULT_ENTRYPOINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; // v0.6
+ internal const string DUMMY_SIG = "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c";
+ internal const string DUMMY_PAYMASTER_AND_DATA_HEX =
+ "0x0101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000001010101010100000000000000000000000000000000000000000000000000000000000000000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101";
}
}
diff --git a/Thirdweb/Thirdweb.Utils/Utils.cs b/Thirdweb/Thirdweb.Utils/Utils.cs
index 480f664..c69f262 100644
--- a/Thirdweb/Thirdweb.Utils/Utils.cs
+++ b/Thirdweb/Thirdweb.Utils/Utils.cs
@@ -11,5 +11,17 @@ public static string ComputeClientIdFromSecretKey(string secretKey)
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(secretKey));
return BitConverter.ToString(hash).Replace("-", "").ToLower().Substring(0, 32);
}
+
+ public static string HexConcat(params string[] hexStrings)
+ {
+ var hex = new StringBuilder("0x");
+
+ foreach (var hexStr in hexStrings)
+ {
+ _ = hex.Append(hexStr[2..]);
+ }
+
+ return hex.ToString();
+ }
}
}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedAccount.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedAccount.cs
new file mode 100644
index 0000000..c7753fa
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedAccount.cs
@@ -0,0 +1,234 @@
+using System.Numerics;
+using System.Text;
+using Nethereum.ABI.EIP712;
+using Nethereum.Hex.HexConvertors.Extensions;
+using Nethereum.Hex.HexTypes;
+using Nethereum.Model;
+using Nethereum.RPC.Eth.DTOs;
+using Nethereum.RPC.Eth.Mappers;
+using Nethereum.Signer;
+using Nethereum.Signer.EIP712;
+using Thirdweb.EWS;
+
+namespace Thirdweb
+{
+ public class EmbeddedAccount : IThirdwebAccount
+ {
+ public ThirdwebAccountType AccountType => ThirdwebAccountType.PrivateKeyAccount;
+
+ private ThirdwebClient _client;
+ private EmbeddedWallet _embeddedWallet;
+ private User _user;
+ private EthECKey _ecKey;
+ private string _email;
+
+ public EmbeddedAccount(ThirdwebClient client, string email)
+ {
+ if (string.IsNullOrEmpty(email))
+ {
+ throw new ArgumentException("Email must be provided to use Embedded Wallets.");
+ }
+
+ _embeddedWallet = new EmbeddedWallet(client);
+ _email = email;
+ _client = client;
+ }
+
+ public async Task Connect()
+ {
+ try
+ {
+ _user = await _embeddedWallet.GetUserAsync(_email, "EmailOTP");
+ _ecKey = new EthECKey(_user.Account.PrivateKey);
+ }
+ catch
+ {
+ Console.WriteLine("User not found. Please call EmbeddedAccount.SendOTP() to initialize the login process.");
+ _user = null;
+ _ecKey = null;
+ }
+ }
+
+ #region Email OTP Flow
+
+ public async Task SendOTP()
+ {
+ if (string.IsNullOrEmpty(_email))
+ {
+ throw new Exception("Email is required for OTP login");
+ }
+
+ try
+ {
+ (bool isNewUser, bool isNewDevice, bool needsRecoveryCode) = await _embeddedWallet.SendOtpEmailAsync(_email);
+ Console.WriteLine("OTP sent to email. Please call EmbeddedAccount.SubmitOTP to login.");
+ }
+ catch (Exception e)
+ {
+ throw new Exception("Failed to send OTP email", e);
+ }
+ }
+
+ public async Task<(string, bool)> SubmitOTP(string otp)
+ {
+ var res = await _embeddedWallet.VerifyOtpAsync(_email, otp, null);
+ if (res.User == null)
+ {
+ var canRetry = res.CanRetry;
+ if (canRetry)
+ {
+ Console.WriteLine("Invalid OTP. Please try again.");
+ }
+ else
+ {
+ Console.WriteLine("Invalid OTP. Please request a new OTP.");
+ }
+ return (null, canRetry);
+ }
+ else
+ {
+ _user = res.User;
+ _ecKey = new EthECKey(_user.Account.PrivateKey);
+ return (await GetAddress(), false);
+ }
+ }
+
+ #endregion
+
+ public Task GetAddress()
+ {
+ return Task.FromResult(_ecKey.GetPublicAddress());
+ }
+
+ public Task EthSign(string message)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message), "Message to sign cannot be null.");
+ }
+
+ var signer = new MessageSigner();
+ var signature = signer.Sign(Encoding.UTF8.GetBytes(message), _ecKey);
+ return Task.FromResult(signature);
+ }
+
+ public Task PersonalSign(byte[] rawMessage)
+ {
+ if (rawMessage == null)
+ {
+ throw new ArgumentNullException(nameof(rawMessage), "Message to sign cannot be null.");
+ }
+
+ var signer = new EthereumMessageSigner();
+ var signature = signer.Sign(rawMessage, _ecKey);
+ return Task.FromResult(signature);
+ }
+
+ public Task PersonalSign(string message)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message), "Message to sign cannot be null.");
+ }
+
+ var signer = new EthereumMessageSigner();
+ var signature = signer.EncodeUTF8AndSign(message, _ecKey);
+ return Task.FromResult(signature);
+ }
+
+ public Task SignTypedDataV4(string json)
+ {
+ if (json == null)
+ {
+ throw new ArgumentNullException(nameof(json), "Json to sign cannot be null.");
+ }
+
+ var signer = new Eip712TypedDataSigner();
+ var signature = signer.SignTypedDataV4(json, _ecKey);
+ return Task.FromResult(signature);
+ }
+
+ public Task SignTypedDataV4(T data, TypedData typedData)
+ {
+ if (data == null)
+ {
+ throw new ArgumentNullException(nameof(data), "Data to sign cannot be null.");
+ }
+
+ var signer = new Eip712TypedDataSigner();
+ var signature = signer.SignTypedDataV4(data, typedData, _ecKey);
+ return Task.FromResult(signature);
+ }
+
+ public async Task SignTransaction(TransactionInput transaction, BigInteger chainId)
+ {
+ if (transaction == null)
+ {
+ throw new ArgumentNullException(nameof(transaction));
+ }
+
+ if (string.IsNullOrWhiteSpace(transaction.From))
+ {
+ transaction.From = await GetAddress();
+ }
+ else if (transaction.From != await GetAddress())
+ {
+ throw new Exception("Transaction 'From' address does not match the wallet address");
+ }
+
+ var nonce = transaction.Nonce ?? throw new ArgumentNullException(nameof(transaction), "Transaction nonce has not been set");
+
+ var gasLimit = transaction.Gas;
+ var value = transaction.Value ?? new HexBigInteger(0);
+
+ string signedTransaction;
+ if (transaction.Type != null && transaction.Type.Value == TransactionType.EIP1559.AsByte())
+ {
+ var maxPriorityFeePerGas = transaction.MaxPriorityFeePerGas.Value;
+ var maxFeePerGas = transaction.MaxFeePerGas.Value;
+ var transaction1559 = new Transaction1559(
+ chainId,
+ nonce,
+ maxPriorityFeePerGas,
+ maxFeePerGas,
+ gasLimit,
+ transaction.To,
+ value,
+ transaction.Data,
+ transaction.AccessList.ToSignerAccessListItemArray()
+ );
+
+ var signer = new Transaction1559Signer();
+ signer.SignTransaction(_ecKey, transaction1559);
+ signedTransaction = transaction1559.GetRLPEncoded().ToHex();
+ }
+ else
+ {
+ 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);
+ }
+
+ return "0x" + signedTransaction;
+ }
+
+ public Task IsConnected()
+ {
+ return Task.FromResult(_ecKey != null);
+ }
+
+ public async Task Disconnect()
+ {
+ try
+ {
+ await _embeddedWallet.SignOutAsync();
+ }
+ catch
+ {
+ Console.WriteLine("Failed to sign out user. Proceeding anyway.");
+ }
+ _user = null;
+ _ecKey = null;
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Authentication/AWS.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Authentication/AWS.cs
new file mode 100644
index 0000000..b1624f1
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Authentication/AWS.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using Amazon;
+using Amazon.CognitoIdentity;
+using Amazon.CognitoIdentityProvider;
+using Amazon.CognitoIdentityProvider.Model;
+using Amazon.Extensions.CognitoAuthentication;
+using Amazon.Lambda;
+using Amazon.Lambda.Model;
+using Amazon.Runtime;
+
+namespace Thirdweb.EWS
+{
+ internal class AWS
+ {
+ private static readonly RegionEndpoint awsRegion = RegionEndpoint.USWest2;
+ private const string cognitoAppClientId = "2e02ha2ce6du13ldk8pai4h3d0";
+ private static readonly string cognitoIdentityPoolId = $"{awsRegion.SystemName}:2ad7ab1e-f48b-48a6-adfa-ac1090689c26";
+ private static readonly string cognitoUserPoolId = $"{awsRegion.SystemName}_UFwLcZIpq";
+ private static readonly string recoverySharePasswordLambdaFunctionName =
+ $"arn:aws:lambda:{awsRegion.SystemName}:324457261097:function:recovery-share-password-GenerateRecoverySharePassw-bbE5ZbVAToil";
+
+ internal static async Task SignUpCognitoUserAsync(string emailAddress, string userName)
+ {
+ emailAddress ??= "cognito@thirdweb.com";
+ AmazonCognitoIdentityProviderClient provider = new(new AnonymousAWSCredentials(), awsRegion);
+ CognitoUserPool userPool = new(cognitoUserPoolId, cognitoAppClientId, provider);
+ Dictionary userAttributes = new() { { "email", emailAddress }, };
+ await userPool.SignUpAsync(userName, Secrets.Random(12), userAttributes, new Dictionary());
+ }
+
+ internal static async Task StartCognitoUserAuth(string userName)
+ {
+ // https://stackoverflow.com/questions/66258459/how-to-get-aws-cognito-access-token-with-username-and-password-in-net-core-3-1
+ AmazonCognitoIdentityProviderClient provider = new(new AnonymousAWSCredentials(), awsRegion);
+ CognitoUserPool userPool = new(cognitoUserPoolId, cognitoAppClientId, provider);
+ CognitoUser user = new(userName, cognitoAppClientId, userPool, provider);
+ InitiateCustomAuthRequest customRequest =
+ new()
+ {
+ AuthParameters = new Dictionary() { { "USERNAME", userName }, },
+ ClientMetadata = new Dictionary(),
+ };
+ try
+ {
+ AuthFlowResponse authResponse = await user.StartWithCustomAuthAsync(customRequest);
+ return authResponse.SessionID;
+ }
+ catch (UserNotFoundException)
+ {
+ return null;
+ }
+ }
+
+ internal static async Task FinishCognitoUserAuth(string userName, string otp, string sessionId)
+ {
+ AmazonCognitoIdentityProviderClient provider = new(new AnonymousAWSCredentials(), awsRegion);
+ CognitoUserPool userPool = new(cognitoUserPoolId, cognitoAppClientId, provider);
+ CognitoUser user = new(userName, cognitoAppClientId, userPool, provider);
+ RespondToCustomChallengeRequest challengeRequest =
+ new()
+ {
+ ChallengeParameters = new Dictionary() { { "USERNAME", userName }, { "ANSWER", otp }, },
+ ClientMetadata = new Dictionary(),
+ SessionID = sessionId,
+ };
+ try
+ {
+ AuthFlowResponse authResponse = await user.RespondToCustomAuthAsync(challengeRequest);
+ AuthenticationResultType result = authResponse.AuthenticationResult ?? throw new VerificationException("The OTP is incorrect", true);
+ return new TokenCollection(result.AccessToken, result.IdToken, result.RefreshToken);
+ }
+ catch (NotAuthorizedException)
+ {
+ throw new VerificationException("The session expired", false);
+ }
+ catch (UserNotFoundException)
+ {
+ throw new InvalidOperationException("The user was not found");
+ }
+ }
+
+ internal static async Task InvokeRecoverySharePasswordLambdaAsync(string idToken, string invokePayload)
+ {
+ InvokeRequest request = new() { FunctionName = recoverySharePasswordLambdaFunctionName, Payload = invokePayload, };
+ CognitoAWSCredentials credentials = new(cognitoIdentityPoolId, awsRegion);
+ string providerName = $"cognito-idp.{awsRegion.SystemName}.amazonaws.com/{cognitoUserPoolId}";
+ credentials.AddLogin(providerName, idToken);
+ AmazonLambdaClient client = new(credentials, awsRegion);
+ InvokeResponse lambdaResponse = await client.InvokeAsync(request);
+ return lambdaResponse.Payload;
+ }
+ }
+
+ internal class TokenCollection
+ {
+ internal TokenCollection(string accessToken, string idToken, string refreshToken)
+ {
+ AccessToken = accessToken;
+ IdToken = idToken;
+ RefreshToken = refreshToken;
+ }
+
+ public string AccessToken { get; }
+ public string IdToken { get; }
+ public string RefreshToken { get; }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Authentication/Server.Types.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Authentication/Server.Types.cs
new file mode 100644
index 0000000..3f4f39b
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Authentication/Server.Types.cs
@@ -0,0 +1,293 @@
+using System.Linq;
+using System.Runtime.Serialization;
+
+namespace Thirdweb.EWS
+{
+ internal partial class Server
+ {
+ internal class VerifyResult
+ {
+ internal VerifyResult(bool isNewUser, string authToken, string walletUserId, string recoveryCode, string email)
+ {
+ IsNewUser = isNewUser;
+ AuthToken = authToken;
+ WalletUserId = walletUserId;
+ RecoveryCode = recoveryCode;
+ Email = email;
+ }
+
+ internal bool IsNewUser { get; }
+ internal string AuthToken { get; }
+ internal string WalletUserId { get; }
+ internal string RecoveryCode { get; }
+ internal string Email { get; }
+ }
+
+#pragma warning disable CS0169, CS8618, IDE0051 // Deserialization will construct the following classes.
+ [DataContract]
+ private class AuthVerifiedTokenReturnType
+ {
+ [DataMember(Name = "verifiedToken")]
+ internal VerifiedTokenType VerifiedToken { get; set; }
+
+ [DataMember(Name = "verifiedTokenJwtString")]
+ internal string VerifiedTokenJwtString { get; set; }
+
+ [DataContract]
+ internal class VerifiedTokenType
+ {
+ [DataMember(Name = "authDetails")]
+ internal UserAuthDetails AuthDetails { get; set; }
+
+ [DataMember]
+ private string authProvider;
+
+ [DataMember]
+ private string developerClientId;
+
+ [DataMember(Name = "isNewUser")]
+ internal bool IsNewUser { get; set; }
+
+ [DataMember]
+ private string rawToken;
+
+ [DataMember]
+ private string userId;
+ }
+ }
+
+ [DataContract]
+ private class GetUserStatusApiReturnType
+ {
+ [DataMember]
+#pragma warning disable CS0649 // Deserialization will populate this field.
+ private string status;
+#pragma warning restore CS0649 // Field 'Server.GetUserStatusApiReturnType.status' is never assigned to, and will always have its default value null
+ internal UserStatus Status => (UserStatus)status.Length;
+
+ [DataMember]
+ private StoredTokenType storedToken;
+
+ [DataMember(Name = "user")]
+ internal UserType User { get; set; }
+
+ [DataContract]
+ internal class UserType
+ {
+ [DataMember(Name = "authDetails")]
+ internal UserAuthDetails AuthDetails { get; set; }
+
+ [DataMember]
+ private string walletAddress;
+ }
+ }
+
+ [DataContract]
+ private class HttpErrorWithMessage
+ {
+ [DataMember(Name = "error")]
+ internal string Error { get; set; } = "";
+
+ [DataMember(Name = "message")]
+ internal string Message { get; set; } = "";
+ }
+
+ [DataContract]
+ private class SharesGetResponse
+ {
+ [DataMember(Name = "authShare")]
+ internal string AuthShare { get; set; }
+
+ [DataMember(Name = "maybeEncryptedRecoveryShares")]
+ internal string[] MaybeEncryptedRecoveryShares { get; set; }
+ }
+
+ [DataContract]
+ private class IsEmailUserOtpValidResponse
+ {
+ [DataMember(Name = "isValid")]
+ internal bool IsValid { get; set; }
+ }
+
+ [DataContract]
+ private class IsEmailKmsOtpValidResponse
+ {
+ [DataMember(Name = "isOtpValid")]
+ internal bool IsOtpValid { get; set; }
+ }
+
+ [DataContract]
+ private class HeadlessOauthLoginLinkResponse
+ {
+ [DataMember(Name = "googleLoginLink")]
+ internal string GoogleLoginLink { get; set; }
+
+ [DataMember(Name = "platformLoginLink")]
+ internal string PlatformLoginLink { get; set; }
+
+ [DataMember(Name = "oauthLoginLink")]
+ internal string OauthLoginLink { get; set; }
+ }
+
+ [DataContract]
+ internal class StoredTokenType
+ {
+ [DataMember]
+ private string jwtToken;
+
+ [DataMember]
+ private string authProvider;
+
+ [DataMember(Name = "authDetails")]
+ internal UserAuthDetails AuthDetails { get; set; }
+
+ [DataMember]
+ private string developerClientId;
+
+ [DataMember]
+ private string cookieString;
+
+ [DataMember]
+ private bool isNewUser;
+ }
+
+ [DataContract]
+ internal class UserAuthDetails
+ {
+ [DataMember(Name = "email")]
+ internal string Email { get; set; }
+
+ [DataMember(Name = "userWalletId")]
+ internal string WalletUserId { get; set; }
+
+ [DataMember(Name = "recoveryShareManagement")]
+ internal string RecoveryShareManagement { get; set; }
+
+ [DataMember(Name = "recoveryCode")]
+ internal string RecoveryCode { get; set; }
+
+ [DataMember(Name = "backupRecoveryCodes")]
+ internal string[] BackupRecoveryCodes { get; set; }
+ }
+
+ [DataContract]
+ internal class UserWallet
+ {
+ [DataMember(Name = "status")]
+ internal string Status { get; set; }
+
+ [DataMember(Name = "isNewUser")]
+ internal bool IsNewUser { get; set; }
+
+ [DataMember(Name = "walletUserId")]
+ internal string WalletUserId { get; set; }
+
+ [DataMember(Name = "recoveryShareManagement")]
+ internal string RecoveryShareManagement { get; set; }
+
+ [DataMember(Name = "storedToken")]
+ internal StoredTokenType StoredToken { get; set; }
+ }
+
+ [DataContract]
+ private class IdTokenResponse
+ {
+ [DataMember(Name = "accessToken")]
+ internal string AccessToken { get; set; }
+
+ [DataMember(Name = "idToken")]
+ internal string IdToken { get; set; }
+ }
+
+ [DataContract]
+ private class RecoverySharePasswordResponse
+ {
+ [DataMember(Name = "body")]
+ internal string Body { get; set; }
+
+ [DataMember(Name = "recoveryShareEncKey")]
+ internal string RecoverySharePassword { get; set; }
+ }
+
+ [DataContract]
+ internal class RecoveryShareManagementResponse
+ {
+ internal string Value => data.oauth.FirstOrDefault()?.recovery_share_management;
+#pragma warning disable CS0649 // Deserialization will populate these fields.
+ [DataMember]
+ private RecoveryShareManagementResponse data;
+
+ [DataMember]
+ private RecoveryShareManagementResponse[] oauth;
+
+ [DataMember]
+ private string recovery_share_management;
+#pragma warning restore CS0649 // Field 'Server.RecoveryShareManagementResponse.*' is never assigned to, and will always have its default value null
+ }
+
+ [DataContract]
+ internal class AuthResultType_OAuth
+ {
+ [DataMember(Name = "storedToken")]
+ internal StoredTokenType_OAuth StoredToken { get; set; }
+
+ [DataMember(Name = "walletDetails")]
+ internal WalletDetailsType_OAuth WalletDetails { get; set; }
+ }
+
+ [DataContract]
+ internal class StoredTokenType_OAuth
+ {
+ [DataMember(Name = "jwtToken")]
+ internal string JwtToken { get; set; }
+
+ [DataMember(Name = "authProvider")]
+ internal string AuthProvider { get; set; }
+
+ [DataMember(Name = "authDetails")]
+ internal AuthDetailsType_OAuth AuthDetails { get; set; }
+
+ [DataMember(Name = "developerClientId")]
+ internal string DeveloperClientId { get; set; }
+
+ [DataMember(Name = "cookieString")]
+ internal string CookieString { get; set; }
+
+ [DataMember(Name = "shouldStoreCookieString")]
+ internal bool ShouldStoreCookieString { get; set; }
+
+ [DataMember(Name = "isNewUser")]
+ internal bool IsNewUser { get; set; }
+
+ [DataContract]
+ internal class AuthDetailsType_OAuth
+ {
+ [DataMember(Name = "email")]
+ internal string Email { get; set; }
+
+ [DataMember(Name = "userWalletId")]
+ internal string UserWalletId { get; set; }
+
+ [DataMember(Name = "recoveryCode")]
+ internal string RecoveryCode { get; set; }
+ }
+ }
+
+ [DataContract]
+ internal class WalletDetailsType_OAuth
+ {
+ [DataMember(Name = "deviceShareStored")]
+ internal string DeviceShareStored { get; set; }
+
+ [DataMember(Name = "isIframeStorageEnabled")]
+ internal bool IsIframeStorageEnabled { get; set; }
+
+ [DataMember(Name = "walletAddress")]
+ internal string WalletAddress { get; set; }
+ }
+
+#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+#pragma warning restore CS0169 // The field 'Server.*' is never used
+#pragma warning restore IDE0051 // The field 'Server.*' is unused
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Authentication/Server.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Authentication/Server.cs
new file mode 100644
index 0000000..bf8ce01
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Authentication/Server.cs
@@ -0,0 +1,571 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+
+namespace Thirdweb.EWS
+{
+ internal abstract class ServerBase
+ {
+ internal abstract Task VerifyThirdwebClientIdAsync(string domain);
+ internal abstract Task FetchDeveloperWalletSettings();
+ internal abstract Task FetchUserDetailsAsync(string emailAddress, string authToken);
+ internal abstract Task StoreAddressAndSharesAsync(string walletAddress, string authShare, string encryptedRecoveryShare, string authToken, string[] backupRecoveryShares);
+
+ internal abstract Task<(string authShare, string recoveryShare)> FetchAuthAndRecoverySharesAsync(string authToken);
+ internal abstract Task FetchAuthShareAsync(string authToken);
+ internal abstract Task FetchHeadlessOauthLoginLinkAsync(string authProvider);
+
+ internal abstract Task CheckIsEmailKmsOtpValidAsync(string userName, string otp);
+ internal abstract Task CheckIsEmailUserOtpValidAsync(string emailAddress, string otp);
+
+ internal abstract Task SendUserOtpEmailAsync(string emailAddress);
+ internal abstract Task SendRecoveryCodeEmailAsync(string authToken, string recoveryCode, string email);
+ internal abstract Task VerifyUserOtpAsync(string emailAddress, string otp);
+
+ internal abstract Task SendKmsOtpEmailAsync(string emailAddress);
+ internal abstract Task VerifyKmsOtpAsync(string emailAddress, string otp, string sessionId);
+
+ internal abstract Task SendKmsPhoneOtpAsync(string phoneNumber);
+ internal abstract Task VerifyKmsPhoneOtpAsync(string phoneNumber, string otp, string sessionId);
+
+ internal abstract Task VerifyJwtAsync(string jwtToken);
+
+ internal abstract Task VerifyOAuthAsync(string authVerifiedToken);
+
+ internal abstract Task VerifyAuthEndpointAsync(string payload);
+ }
+
+ internal partial class Server : ServerBase
+ {
+ private const string ROOT_URL = "https://embedded-wallet.thirdweb.com";
+ 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 string clientId;
+
+ internal Server(string clientId, string bundleId, string platform, string version, string secretKey)
+ {
+ 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}");
+ }
+
+ // 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);
+ await CheckStatusCodeAsync(response);
+ var error = await DeserializeAsync(response);
+ return error.Error;
+ }
+
+ // embedded-wallet/developer-wallet-settings
+ 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 responseContent = await DeserializeAsync(response);
+ return responseContent.Value ?? "AWS_MANAGED";
+ }
+ catch (System.Exception e)
+ {
+ Console.WriteLine("Could not fetch recovery share management type, defaulting to managed: " + e.Message);
+ return "AWS_MANAGED";
+ }
+ }
+
+ // embedded-wallet/embedded-wallet-user-details
+ internal override async Task FetchUserDetailsAsync(string emailAddress, string authToken)
+ {
+ Dictionary queryParams = new();
+ if (emailAddress == null && authToken == null)
+ {
+ throw new InvalidOperationException("Must provide either email address or auth token");
+ }
+
+ 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 ?? "");
+ await CheckStatusCodeAsync(response);
+ var rv = await DeserializeAsync(response);
+ return rv;
+ }
+
+ // embedded-wallet/embedded-wallet-shares POST
+ internal override async Task StoreAddressAndSharesAsync(string walletAddress, string authShare, string encryptedRecoveryShare, string authToken, string[] backupRecoveryShares)
+ {
+ var encryptedRecoveryShares =
+ backupRecoveryShares == null
+ ? new[] { new { share = encryptedRecoveryShare, isClientEncrypted = "true" } }
+ : new[] { new { share = encryptedRecoveryShare, isClientEncrypted = "true" } }.Concat(backupRecoveryShares.Select((s) => new { share = s, isClientEncrypted = "true" })).ToArray();
+
+ HttpRequestMessage httpRequestMessage =
+ new(HttpMethod.Post, MakeUri("/embedded-wallet/embedded-wallet-shares"))
+ {
+ Content = MakeHttpContent(
+ new
+ {
+ authShare,
+ maybeEncryptedRecoveryShares = encryptedRecoveryShares,
+ walletAddress,
+ }
+ ),
+ };
+ HttpResponseMessage 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");
+ return (authShare, encryptedRecoveryShare);
+ }
+
+ // embedded-wallet/embedded-wallet-shares GET
+ internal override async Task FetchAuthShareAsync(string authToken)
+ {
+ SharesGetResponse sharesGetResponse = await FetchRemoteSharesAsync(authToken, false);
+ return sharesGetResponse.AuthShare ?? throw new InvalidOperationException("Server failed to return auth share");
+ }
+
+ // embedded-wallet/embedded-wallet-shares GET
+ private async Task FetchRemoteSharesAsync(string authToken, bool wantsRecoveryShare)
+ {
+ Dictionary queryParams =
+ new()
+ {
+ { "getEncryptedAuthShare", "true" },
+ { "getEncryptedRecoveryShare", wantsRecoveryShare ? "true" : "false" },
+ { "useSealedSecret", "false" }
+ };
+ Uri uri = MakeUri("/embedded-wallet/embedded-wallet-shares", queryParams);
+ HttpResponseMessage response = await SendHttpWithAuthAsync(uri, authToken);
+ await CheckStatusCodeAsync(response);
+ var rv = await DeserializeAsync(response);
+ return rv;
+ }
+
+ // 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);
+ await CheckStatusCodeAsync(response);
+ return await DeserializeAsync(response);
+ }
+
+ // embedded-wallet/headless-oauth-login-link
+ internal override async Task FetchHeadlessOauthLoginLinkAsync(string authProvider)
+ {
+ // based on above unity implementation, adapt to this class
+ Uri uri = MakeUri(
+ "/embedded-wallet/headless-oauth-login-link",
+ new Dictionary
+ {
+ { "platform", "unity" },
+ { "authProvider", authProvider },
+ { "baseUrl", "https://embedded-wallet.thirdweb.com" }
+ }
+ );
+
+ HttpResponseMessage response = await httpClient.GetAsync(uri);
+ await CheckStatusCodeAsync(response);
+ var rv = await DeserializeAsync(response);
+ return rv.PlatformLoginLink;
+ }
+
+ // /embedded-wallet/is-cognito-otp-valid
+ internal override async Task CheckIsEmailKmsOtpValidAsync(string email, string otp)
+ {
+ Uri uri = MakeUriLegacy(
+ "/embedded-wallet/is-cognito-otp-valid",
+ new Dictionary
+ {
+ { "email", email },
+ { "code", otp },
+ { "clientId", clientId }
+ }
+ );
+ HttpResponseMessage response = await httpClient.GetAsync(uri);
+ await CheckStatusCodeAsync(response);
+ var result = await DeserializeAsync(response);
+ return result.IsOtpValid;
+ }
+
+ // 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(
+ new
+ {
+ email,
+ otp,
+ clientId,
+ }
+ );
+ HttpResponseMessage response = await httpClient.PostAsync(uri, content);
+ await CheckStatusCodeAsync(response);
+ var result = await DeserializeAsync(response);
+ return result.IsValid;
+ }
+
+ // 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);
+ await CheckStatusCodeAsync(response);
+ }
+
+ // embedded-wallet/send-wallet-recovery-code
+ internal override async Task SendRecoveryCodeEmailAsync(string authToken, string recoveryCode, string email)
+ {
+ HttpRequestMessage httpRequestMessage =
+ new(HttpMethod.Post, MakeUri("/embedded-wallet/send-wallet-recovery-code"))
+ {
+ Content = MakeHttpContent(
+ new
+ {
+ strategy = "email",
+ clientId,
+ email,
+ recoveryCode
+ }
+ ),
+ };
+ try
+ {
+ HttpResponseMessage response = await SendHttpWithAuthAsync(httpRequestMessage, authToken);
+ await CheckStatusCodeAsync(response);
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException("Error sending recovery code email", ex);
+ }
+ }
+
+ // 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(
+ new
+ {
+ clientId,
+ email = emailAddress,
+ otp
+ }
+ );
+ HttpResponseMessage response = await httpClient.PostAsync(uri, content);
+ await CheckStatusCodeAsync(response);
+ var authVerifiedToken = await DeserializeAsync(response);
+ return new VerifyResult(
+ authVerifiedToken.VerifiedToken.IsNewUser,
+ authVerifiedToken.VerifiedTokenJwtString,
+ authVerifiedToken.VerifiedToken.AuthDetails.WalletUserId,
+ authVerifiedToken.VerifiedToken.AuthDetails.RecoveryCode,
+ authVerifiedToken.VerifiedToken.AuthDetails.Email
+ );
+ }
+
+ // KMS Send
+ internal override async Task SendKmsOtpEmailAsync(string emailAddress)
+ {
+ string userName = MakeCognitoUserName(emailAddress, "email");
+ string sessionId = await AWS.StartCognitoUserAuth(userName);
+ if (sessionId == null)
+ {
+ await AWS.SignUpCognitoUserAsync(emailAddress, userName);
+ for (int i = 0; i < 3; ++i)
+ {
+ await Task.Delay(3333 * i);
+ sessionId = await AWS.StartCognitoUserAuth(userName);
+ if (sessionId != null)
+ {
+ break;
+ }
+ }
+ if (sessionId == null)
+ {
+ throw new InvalidOperationException("Cannot find user within timeout period");
+ }
+ }
+ return sessionId;
+ }
+
+ // 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");
+ ByteArrayContent content = MakeHttpContent(
+ new
+ {
+ developerClientId = clientId,
+ access_token = tokens.AccessToken,
+ id_token = tokens.IdToken,
+ refresh_token = tokens.RefreshToken,
+ otpMethod = "email",
+ }
+ );
+ HttpResponseMessage response = await httpClient.PostAsync(uri, 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 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);
+ JsonSerializer jsonSerializer = new();
+ var payload = jsonSerializer.Deserialize(new JsonTextReader(new StreamReader(responsePayload)));
+ payload = jsonSerializer.Deserialize(new JsonTextReader(new StringReader(payload.Body)));
+ return new VerifyResult(isNewUser, authToken, walletUserId, payload.RecoverySharePassword, authVerifiedToken.VerifiedToken.AuthDetails.Email);
+ }
+
+ internal override async Task SendKmsPhoneOtpAsync(string phoneNumber)
+ {
+ string userName = MakeCognitoUserName(phoneNumber, "sms");
+ string sessionId = await AWS.StartCognitoUserAuth(userName);
+ if (sessionId == null)
+ {
+ await AWS.SignUpCognitoUserAsync(null, userName);
+ for (int i = 0; i < 3; ++i)
+ {
+ await Task.Delay(3333 * i);
+ sessionId = await AWS.StartCognitoUserAuth(userName);
+ if (sessionId != null)
+ {
+ break;
+ }
+ }
+ if (sessionId == null)
+ {
+ throw new InvalidOperationException("Cannot find user within timeout period");
+ }
+ }
+ return sessionId;
+ }
+
+ // 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");
+ ByteArrayContent content = MakeHttpContent(
+ new
+ {
+ developerClientId = clientId,
+ access_token = tokens.AccessToken,
+ id_token = tokens.IdToken,
+ refresh_token = tokens.RefreshToken,
+ otpMethod = "email",
+ }
+ );
+ HttpResponseMessage response = await httpClient.PostAsync(uri, 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 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);
+ JsonSerializer jsonSerializer = new();
+ var payload = jsonSerializer.Deserialize(new JsonTextReader(new StreamReader(responsePayload)));
+ payload = jsonSerializer.Deserialize(new JsonTextReader(new StringReader(payload.Body)));
+ return new VerifyResult(isNewUser, authToken, walletUserId, payload.RecoverySharePassword, authVerifiedToken.VerifiedToken.AuthDetails.Email);
+ }
+
+ // embedded-wallet/validate-custom-jwt
+ 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);
+ 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;
+ return new VerifyResult(isNewUser, authToken, walletUserId, recoveryCode, email);
+ }
+
+ // embedded-wallet/validate-custom-auth-endpoint
+ 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);
+ 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;
+ 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";
+ 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);
+ JsonSerializer jsonSerializer = new();
+ var payload = jsonSerializer.Deserialize(new JsonTextReader(new StreamReader(responsePayload)));
+ payload = jsonSerializer.Deserialize(new JsonTextReader(new StringReader(payload.Body)));
+ recoveryCode = payload.RecoverySharePassword;
+ }
+ return new VerifyResult(isNewUser, authToken, walletUserId, recoveryCode, authResult.StoredToken.AuthDetails.Email);
+ }
+
+ #region Misc
+
+ 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);
+ }
+
+ 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)
+ {
+#if DEBUG
+ Console.WriteLine($"Response: {await response.Content.ReadAsStringAsync()}");
+#endif
+ if (!response.IsSuccessStatusCode)
+ {
+ var error = await DeserializeAsync(response);
+ throw new InvalidOperationException(string.IsNullOrEmpty(error.Error) ? error.Message : error.Error);
+ }
+ }
+
+ private static async Task DeserializeAsync(HttpResponseMessage response)
+ {
+ JsonSerializer jsonSerializer = new();
+ TextReader textReader = new StreamReader(await response.Content.ReadAsStreamAsync());
+ var rv = jsonSerializer.Deserialize(new JsonTextReader(textReader));
+ return rv;
+ }
+
+ private static Uri MakeUri(string path, IDictionary parameters = null)
+ {
+ 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)}"));
+ b.Query = queryString;
+ }
+ return b.Uri;
+ }
+
+ private static Uri MakeUriLegacy(string path, IDictionary parameters = null)
+ {
+ 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)}"));
+ b.Query = queryString;
+ }
+ return b.Uri;
+ }
+
+ private static StringContent MakeHttpContent(object data)
+ {
+ StringContent stringContent = new(Serialize(data));
+ stringContent.Headers.ContentType = jsonContentType;
+ return stringContent;
+ }
+
+ private static string Serialize(object data)
+ {
+ JsonSerializer jsonSerializer = new() { NullValueHandling = NullValueHandling.Ignore, };
+ StringWriter stringWriter = new();
+ jsonSerializer.Serialize(stringWriter, data);
+ string rv = stringWriter.ToString();
+
+ return rv;
+ }
+
+ private string MakeCognitoUserName(string userData, string type)
+ {
+ return $"{userData}:{type}:{clientId}";
+ }
+
+ #endregion
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Encryption/EmbeddedWallet.Cryptography.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Encryption/EmbeddedWallet.Cryptography.cs
new file mode 100644
index 0000000..e5d0787
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Encryption/EmbeddedWallet.Cryptography.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using Nethereum.Web3.Accounts;
+using Org.BouncyCastle.Crypto.Engines;
+using Org.BouncyCastle.Crypto.Modes;
+using Org.BouncyCastle.Crypto.Parameters;
+
+namespace Thirdweb.EWS
+{
+ internal partial class EmbeddedWallet
+ {
+ private string DecryptShare(string encryptedShare, string password)
+ {
+ string[] parts = encryptedShare.Split(ENCRYPTION_SEPARATOR);
+ byte[] ciphertextWithTag = Convert.FromBase64String(parts[0]);
+ byte[] iv = Convert.FromBase64String(parts[1]);
+ byte[] salt = Convert.FromBase64String(parts[2]);
+
+ int iterationCount;
+ if (parts.Length > 3 && int.TryParse(parts[3], out var parsedIterationCount))
+ {
+ iterationCount = parsedIterationCount;
+ }
+ else
+ {
+ iterationCount = DEPRECATED_ITERATION_COUNT;
+ }
+
+ byte[] key = GetEncryptionKey(password, salt, iterationCount);
+
+ byte[] encodedShare;
+ try
+ {
+ // Bouncy Castle expects the authentication tag after the ciphertext.
+ GcmBlockCipher cipher = new(new AesEngine());
+ cipher.Init(forEncryption: false, new AeadParameters(new KeyParameter(key), 8 * TAG_SIZE, iv));
+ encodedShare = new byte[cipher.GetOutputSize(ciphertextWithTag.Length)];
+ int offset = cipher.ProcessBytes(ciphertextWithTag, 0, ciphertextWithTag.Length, encodedShare, 0);
+ cipher.DoFinal(encodedShare, offset);
+ }
+ catch
+ {
+ try
+ {
+ int ciphertextSize = ciphertextWithTag.Length - TAG_SIZE;
+ var ciphertext = new byte[ciphertextSize];
+ Array.Copy(ciphertextWithTag, ciphertext, ciphertext.Length);
+ var tag = new byte[TAG_SIZE];
+ Array.Copy(ciphertextWithTag, ciphertextSize, tag, 0, tag.Length);
+ encodedShare = new byte[ciphertext.Length];
+ using AesGcm crypto = new(key);
+ crypto.Decrypt(iv, ciphertext, tag, encodedShare);
+ }
+ catch (CryptographicException)
+ {
+ throw new VerificationException("Invalid recovery code", true);
+ }
+ }
+ string share = Encoding.ASCII.GetString(encodedShare);
+ return share;
+ }
+
+ private async Task EncryptShareAsync(string share, string password)
+ {
+ const int saltSize = 16;
+ var salt = new byte[saltSize];
+ RandomNumberGenerator.Fill(salt);
+ byte[] key = GetEncryptionKey(password, salt, CURRENT_ITERATION_COUNT);
+ byte[] encodedShare = Encoding.ASCII.GetBytes(share);
+ const int ivSize = 12;
+ var iv = new byte[ivSize];
+ await ivGenerator.ComputeIvAsync(iv);
+ byte[] encryptedShare;
+ try
+ {
+ // Bouncy Castle includes the authentication tag after the ciphertext.
+ GcmBlockCipher cipher = new(new AesEngine());
+ cipher.Init(forEncryption: true, new AeadParameters(new KeyParameter(key), 8 * TAG_SIZE, iv));
+ encryptedShare = new byte[cipher.GetOutputSize(encodedShare.Length)];
+ int offset = cipher.ProcessBytes(encodedShare, 0, encodedShare.Length, encryptedShare, 0);
+ cipher.DoFinal(encryptedShare, offset);
+ }
+ catch
+ {
+ var tag = new byte[TAG_SIZE];
+ encryptedShare = new byte[encodedShare.Length];
+ using AesGcm crypto = new(key);
+ crypto.Encrypt(iv, encodedShare, encryptedShare, tag);
+ encryptedShare = encryptedShare.Concat(tag).ToArray();
+ }
+ string rv =
+ $"{Convert.ToBase64String(encryptedShare)}{ENCRYPTION_SEPARATOR}{Convert.ToBase64String(iv)}{ENCRYPTION_SEPARATOR}{Convert.ToBase64String(salt)}{ENCRYPTION_SEPARATOR}{CURRENT_ITERATION_COUNT}";
+ return rv;
+ }
+
+ private (string deviceShare, string recoveryShare, string authShare) CreateShares(string secret)
+ {
+ Secrets secrets = new();
+ secret = $"{WALLET_PRIVATE_KEY_PREFIX}{secret}";
+ string encodedSecret = Secrets.GetHexString(Encoding.ASCII.GetBytes(secret));
+ List shares = secrets.Share(encodedSecret, 3, 2);
+ return (shares[0], shares[1], shares[2]);
+ }
+
+ private static byte[] GetEncryptionKey(string password, byte[] salt, int iterationCount)
+ {
+ using Rfc2898DeriveBytes pbkdf2 = new(password, salt, iterationCount, HashAlgorithmName.SHA256);
+ byte[] keyMaterial = pbkdf2.GetBytes(KEY_SIZE);
+ return keyMaterial;
+ }
+
+ private Account MakeAccountFromShares(params string[] shares)
+ {
+ Secrets secrets = new();
+ string encodedSecret = secrets.Combine(shares);
+ string secret = Encoding.ASCII.GetString(Secrets.GetBytes(encodedSecret));
+ if (!secret.StartsWith(WALLET_PRIVATE_KEY_PREFIX))
+ {
+ throw new InvalidOperationException($"Corrupted share encountered {secret}");
+ }
+ return new Account(secret.Split(WALLET_PRIVATE_KEY_PREFIX)[1]);
+ }
+
+ private string MakeRecoveryCode()
+ {
+ const int codeSize = 16;
+ const string characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+ string recoveryCode = new(Enumerable.Range(0, codeSize).Select((_) => characters[RandomNumberGenerator.GetInt32(characters.Length)]).ToArray());
+ return recoveryCode;
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Encryption/IvGenerator.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Encryption/IvGenerator.cs
new file mode 100644
index 0000000..50754fc
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Encryption/IvGenerator.cs
@@ -0,0 +1,73 @@
+using System.Security.Cryptography;
+#if UNITY_5_3_OR_NEWER
+using UnityEngine;
+#endif
+
+namespace Thirdweb.EWS
+{
+ internal abstract class IvGeneratorBase
+ {
+ internal abstract Task ComputeIvAsync(byte[] iv);
+ }
+
+ internal class IvGenerator : IvGeneratorBase
+ {
+ private long prbsValue;
+ private readonly string ivFilePath;
+ private const int nPrbsBits = 48;
+ 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()
+ {
+ string directory;
+#if UNITY_5_3_OR_NEWER
+ directory = Application.persistentDataPath;
+#else
+ directory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+#endif
+ directory = Path.Combine(directory, "EWS");
+ Directory.CreateDirectory(directory);
+ ivFilePath = Path.Combine(directory, "iv.txt");
+ try
+ {
+ prbsValue = long.Parse(File.ReadAllText(ivFilePath));
+ }
+ catch (Exception)
+ {
+ prbsValue = (0x434a49445a27 ^ DateTime.Now.Ticks) & prbsPeriod;
+ }
+ }
+
+ ///
+ /// Compute IV using half LFSR-generated and half random bytes.
+ ///
+ /// https://crypto.stackexchange.com/questions/84357/what-are-the-rules-for-using-aes-gcm-correctly
+ /// The IV byte array to fill. This must be twelve bytes in size.
+ internal override async Task ComputeIvAsync(byte[] iv)
+ {
+ RandomNumberGenerator.Fill(iv);
+ prbsValue = ComputeNextPrbsValue(prbsValue);
+ await File.WriteAllTextAsync(ivFilePath, prbsValue.ToString());
+ byte[] prbsBytes = Enumerable.Range(0, nPrbsBits / 8).Select((i) => (byte)(prbsValue >> (8 * i))).ToArray();
+ Array.Copy(prbsBytes, iv, prbsBytes.Length);
+ }
+
+ ///
+ /// Compute the next value of a PRBS using a 48-bit Galois LFSR.
+ ///
+ /// https://en.wikipedia.org/wiki/Linear-feedback_shift_register
+ /// The current PRBS value.
+ /// The next value.
+ private static long ComputeNextPrbsValue(long prbsValue)
+ {
+ prbsValue <<= 1;
+ if ((prbsValue & (1L << nPrbsBits)) != 0)
+ {
+ prbsValue ^= taps;
+ prbsValue &= prbsPeriod;
+ }
+ return prbsValue;
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Encryption/Secrets.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Encryption/Secrets.cs
new file mode 100644
index 0000000..8c6266d
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Encryption/Secrets.cs
@@ -0,0 +1,476 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Thirdweb.EWS
+{
+ internal class Secrets
+ {
+ private Config config = new(Defaults.nBits);
+ private const int nHexDigitBits = 4;
+ private readonly Func GetRandomInt32 = (nBits) => RandomNumberGenerator.GetInt32(1, 1 << nBits);
+ private static readonly string padding = string.Join("", Enumerable.Repeat("0", Defaults.maxPaddingMultiple));
+ private static readonly string[] nybbles = { "0000", "0001", "0010", "0011", "0100", "0101", "0110", "0111", "1000", "1001", "1010", "1011", "1100", "1101", "1110", "1111", };
+
+ ///
+ /// Reconsitute a secret from .
+ ///
+ ///
+ /// The return value will not be the original secret if the number of shares provided is less than the threshold
+ /// number of shares.
+ /// Duplicate shares do not count toward the threshold.
+ ///
+ /// The shares used to reconstitute the secret.
+ /// The reconstituted secret.
+ public string Combine(IReadOnlyList shares)
+ {
+ return Combine(shares, 0);
+ }
+
+ ///
+ /// Convert a string of hexadecimal digits into a byte array.
+ ///
+ /// The string of hexadecimal digits to convert.
+ /// A byte array.
+ public static byte[] GetBytes(string s)
+ {
+ byte[] bytes = Enumerable.Range(0, s.Length / 2).Select((i) => byte.Parse(s.Substring(i * 2, 2), NumberStyles.HexNumber)).ToArray();
+ return bytes;
+ }
+
+ ///
+ /// Convert a byte array into a string of hexadecimal digits.
+ ///
+ /// The byte array to convert.
+ /// A string of hexadecimal digits.
+ public static string GetHexString(byte[] bytes)
+ {
+ return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
+ }
+
+ ///
+ /// Generate a new share identified as .
+ ///
+ ///
+ /// The return value will be invalid if the number of shares provided is less than the threshold number of shares.
+ /// If is the identifier of a share in and the number of shares
+ /// provided is at least the threshold number of shares, the return value will be the same as the identified share.
+ /// Duplicate shares do not count toward the threshold.
+ ///
+ /// The identifier of the share to generate.
+ /// The shares from which to generate the new share.
+ /// A hexadecimal string of the new share.
+ ///
+ ///
+ public string NewShare(int shareId, IReadOnlyList shares)
+ {
+ if (shareId <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(shareId), $"{nameof(shareId)} must be greater than zero.");
+ }
+ else if (shares == null || !shares.Any() || string.IsNullOrEmpty(shares[0]))
+ {
+ throw new ArgumentException($"{nameof(shares)} cannot be empty.", nameof(shares));
+ }
+ ShareComponents share = ExtractShareComponents(shares[0]);
+ return ConstructPublicShareString(share.nBits, Convert.ToString(shareId, Defaults.radix), Combine(shares, shareId));
+ }
+
+ ///
+ /// Generate a random value expressed as a string of hexadecimal digits that contains bytes using a
+ /// secure random number generator.
+ ///
+ /// The number of bytes of output.
+ /// A hexadecimal string of the value.
+ ///
+ public static string Random(int nBytes)
+ {
+ const int maxnBytes = (1 << 16) / 8;
+ if (nBytes < 1 || nBytes > maxnBytes)
+ {
+ throw new ArgumentOutOfRangeException(nameof(nBytes), $"{nameof(nBytes)} must be in the range [1, {maxnBytes}].");
+ }
+ var bytes = new byte[nBytes];
+ RandomNumberGenerator.Fill(bytes);
+ string rv = GetHexString(bytes);
+ return rv;
+ }
+
+ ///
+ /// Divide a into
+ /// shares, requiring shares to
+ /// reconstruct the secret. Optionally, initialize with . Optionally, zero-pad the secret to a length
+ /// that is a multiple of (default 128) before sharing.
+ ///
+ /// A secret value expressed as a string of hexadecimal digits.
+ /// The number of shares to produce.
+ /// The number of shares required to reconstruct the secret.
+ /// The number of bits to use to create the shares.
+ /// The amount of zero-padding to apply to the secret before sharing.
+ /// A list of strings of hexadecimal digits.
+ ///
+ ///
+ public List Share(string secret, int nShares, int threshold, int nBits = 0, int paddingMultiple = 128)
+ {
+ // Initialize based on nBits if it's specified.
+ if (nBits != 0)
+ {
+ if (nBits < Defaults.minnBits || nBits > Defaults.maxnBits)
+ {
+ throw new ArgumentOutOfRangeException(nameof(nBits), $"{nameof(nBits)} must be in the range [{Defaults.minnBits}, {Defaults.maxnBits}].");
+ }
+ config = new(nBits);
+ }
+
+ // Validate the parameters.
+ if (string.IsNullOrEmpty(secret))
+ {
+ throw new ArgumentException($"{nameof(secret)} cannot be empty.", nameof(secret));
+ }
+ else if (!secret.All((ch) => char.IsDigit(ch) || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f')))
+ {
+ throw new ArgumentException($"{nameof(secret)} must consist only of hexadecimal digits.", nameof(secret));
+ }
+ else if (nShares < 2 || nShares > Math.Min(config.maxnShares, Defaults.maxnShares))
+ {
+ if (nShares > Defaults.maxnShares)
+ {
+ throw new ArgumentOutOfRangeException(nameof(nShares), $"The maximum number of shares is {Defaults.maxnShares} since the maximum bit count is {Defaults.maxnBits}.");
+ }
+ else if (nShares > config.maxnShares)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(nShares),
+ $"{nameof(nShares)} must be in the range [2, {config.maxnShares}]. To create {nShares} shares, specify at least {Math.Ceiling(Math.Log(nShares + 1, 2))} bits."
+ );
+ }
+ throw new ArgumentOutOfRangeException(nameof(nShares), $"{nameof(nShares)} must be in the range [2, {config.maxnShares}].");
+ }
+ else if (threshold < 2 || threshold > nShares)
+ {
+ throw new ArgumentOutOfRangeException(nameof(threshold), $"{nameof(threshold)} must be in the range [2, {nShares}].");
+ }
+ else if (paddingMultiple < 0 || paddingMultiple > 1024)
+ {
+ throw new ArgumentOutOfRangeException(nameof(paddingMultiple), $"{nameof(paddingMultiple)} must be in the range [0, {Defaults.maxPaddingMultiple}].");
+ }
+
+ // Prepend a 1 as a marker to preserve the correct number of leading zeros in the secret.
+ secret = "1" + Hex2bin(secret);
+
+ // Create the shares. For additional security, pad in multiples of 128 bits by default. This is a small trade-off in larger
+ // share size to help prevent leakage of information about small secrets and increase the difficulty of attacking them.
+ List l = SplitNumStringToIntArray(secret, paddingMultiple);
+ var x = new string[nShares];
+ var y = new string[nShares];
+ foreach (int value in l)
+ {
+ var subShares = GetShares(value, nShares, threshold);
+ for (int i = 0; i < nShares; ++i)
+ {
+ x[i] = Convert.ToString(subShares[i].x, Defaults.radix);
+ y[i] = PadLeft(Convert.ToString(subShares[i].y, 2), config.nBits) + (y[i] ?? "");
+ }
+ }
+ for (int i = 0; i < nShares; ++i)
+ {
+ x[i] = ConstructPublicShareString(config.nBits, x[i], Bin2hex(y[i]));
+ }
+ return x.ToList();
+ }
+
+ private static string Bin2hex(string value)
+ {
+ value = PadLeft(value, nHexDigitBits);
+ StringBuilder sb = new();
+ for (int i = 0; i < value.Length; i += nHexDigitBits)
+ {
+ int num = Convert.ToInt32(value.Substring(i, nHexDigitBits), 2);
+ sb.Append(Convert.ToString(num, 16));
+ }
+ return sb.ToString();
+ }
+
+ private string Combine(IReadOnlyList shares, int shareId)
+ {
+ // Zip distinct shares. E.g.
+ // [ [ 193, 186, 29, 177, 196 ],
+ // [ 53, 105, 139, 127, 149 ],
+ // [ 146, 211, 249, 206, 81 ] ]
+ // becomes
+ // [ [ 193, 53, 146 ],
+ // [ 186, 105, 211 ],
+ // [ 29, 139, 249 ],
+ // [ 177, 127, 206 ],
+ // [ 196, 149, 81 ] ]
+ int nBits = 0;
+ List x = new();
+ List> y = new();
+ foreach (ShareComponents share in shares.Select((s) => ExtractShareComponents(s)))
+ {
+ // All shares must have the same bits settings.
+ if (nBits == 0)
+ {
+ nBits = share.nBits;
+
+ // Reconfigure based on the bits settings of the shares.
+ if (config.nBits != nBits)
+ {
+ config = new(nBits);
+ }
+ }
+ else if (share.nBits != nBits)
+ {
+ throw new ArgumentException("Shares are mismatched due to different bits settings.", nameof(shares));
+ }
+
+ // Spread the share across the arrays if the share.id is not already in array `x`.
+ if (x.IndexOf(share.id) == -1)
+ {
+ x.Add(share.id);
+ List splitShare = SplitNumStringToIntArray(Hex2bin(share.data));
+ for (int i = 0, n = splitShare.Count; i < n; ++i)
+ {
+ if (i >= y.Count)
+ {
+ y.Add(new List());
+ }
+ y[i].Add(splitShare[i]);
+ }
+ }
+ }
+
+ // Extract the secret from the zipped share data.
+ StringBuilder sb = new();
+ foreach (List y_ in y)
+ {
+ sb.Insert(0, PadLeft(Convert.ToString(Lagrange(shareId, x, y_), 2), nBits));
+ }
+ string result = sb.ToString();
+
+ // If `shareId` is not zero, NewShare invoked Combine. In this case, return the new share data directly. Otherwise, find
+ // the first '1' which was added in the Share method as a padding marker and return only the data after the padding and the
+ // marker. Convert the binary string, which is the derived secret, to hexadecimal.
+ return Bin2hex(shareId >= 1 ? result : result[(result.IndexOf("1") + 1)..]);
+ }
+
+ private static string ConstructPublicShareString(int nBits, string shareId, string data)
+ {
+ int id = Convert.ToInt32(shareId, Defaults.radix);
+ string base36Bits = char.ConvertFromUtf32(nBits > 9 ? nBits - 10 + 'A' : nBits + '0');
+ int idMax = (1 << nBits) - 1;
+ int paddingMultiple = Convert.ToString(idMax, Defaults.radix).Length;
+ string hexId = PadLeft(Convert.ToString(id, Defaults.radix), paddingMultiple);
+ if (id < 1 || id > idMax)
+ {
+ throw new ArgumentOutOfRangeException(nameof(shareId), $"{nameof(shareId)} must be in the range [1, {idMax}].");
+ }
+ string share = base36Bits + hexId + data;
+ return share;
+ }
+
+ private static ShareComponents ExtractShareComponents(string share)
+ {
+ // Extract the first character which represents the number of bits in base 36.
+ int nBits = GetLargeBaseValue(share[0]);
+ if (nBits < Defaults.minnBits || nBits > Defaults.maxnBits)
+ {
+ throw new ArgumentException($"Unexpected {nBits}-bit share outside of the range [{Defaults.minnBits}, {Defaults.maxnBits}].", nameof(share));
+ }
+
+ // Calculate the maximum number of shares allowed for the given number of bits.
+ int maxnShares = (1 << nBits) - 1;
+
+ // Derive the identifier length from the bit count.
+ int idLength = Convert.ToString(maxnShares, Defaults.radix).Length;
+
+ // Extract all the parts now that the segment sizes are known.
+ var rx = new Regex("^([3-9A-Ka-k]{1})([0-9A-Fa-f]{" + idLength + "})([0-9A-Fa-f]+)$");
+ MatchCollection shareComponents = rx.Matches(share);
+ GroupCollection groups = shareComponents.FirstOrDefault()?.Groups;
+ if (groups == null || groups.Count != 4)
+ {
+ throw new ArgumentException("Malformed share", nameof(share));
+ }
+
+ // Convert the identifier from a string of hexadecimal digits into an integer.
+ int id = Convert.ToInt32(groups[2].Value, Defaults.radix);
+
+ // Return the components of the share.
+ ShareComponents rv = new(nBits, id, groups[3].Value);
+ return rv;
+ }
+
+ private static int GetLargeBaseValue(char ch)
+ {
+ int rv =
+ ch >= 'a'
+ ? ch - 'a' + 10
+ : ch >= 'A'
+ ? ch - 'A' + 10
+ : ch - '0';
+ return rv;
+ }
+
+ private (int x, int y)[] GetShares(int secret, int nShares, int threshold)
+ {
+ int[] coefficients = Enumerable.Range(0, threshold - 1).Select((i) => GetRandomInt32(config.nBits)).Concat(new[] { secret }).ToArray();
+ var shares = Enumerable.Range(1, nShares).Select((i) => (i, Horner(i, coefficients))).ToArray();
+ return shares;
+ }
+
+ private static string Hex2bin(string value)
+ {
+ StringBuilder sb = new();
+ foreach (char ch in value)
+ {
+ sb.Append(nybbles[GetLargeBaseValue(ch)]);
+ }
+ return sb.ToString();
+ }
+
+ // Evaluate the polynomial at `x` using Horner's Method.
+ // NOTE: fx = fx * x + coefficients[i] -> exp(log(fx) + log(x)) + coefficients[i], so if fx is zero, set fx to coefficients[i]
+ // since using the exponential or logarithmic form will result in an incorrect value.
+ private int Horner(int x, IEnumerable coefficients)
+ {
+ int logx = config.logarithms[x];
+ int fx = 0;
+ foreach (int coefficient in coefficients)
+ {
+ fx = fx == 0 ? coefficient : config.exponents[(logx + config.logarithms[fx]) % config.maxnShares] ^ coefficient;
+ }
+ return fx;
+ }
+
+ // Evaluate the Lagrange interpolation polynomial at x = `shareId` using x and y arrays that are of the same length, with
+ // corresponding elements constituting points on the polynomial.
+ private int Lagrange(int shareId, IReadOnlyList x, IReadOnlyList y)
+ {
+ int sum = 0;
+ foreach (int i in Enumerable.Range(0, x.Count))
+ {
+ if (i < y.Count && y[i] != 0)
+ {
+ int product = config.logarithms[y[i]];
+ foreach (int j in Enumerable.Range(0, x.Count).Where((j) => i != j))
+ {
+ if (shareId == x[j])
+ {
+ // This happens when computing a share that is in the list of shares used to compute it.
+ product = -1;
+ break;
+ }
+
+ // Ensure it's not negative.
+ product = (product + config.logarithms[shareId ^ x[j]] - config.logarithms[x[i] ^ x[j]] + config.maxnShares) % config.maxnShares;
+ }
+ sum = product == -1 ? sum : sum ^ config.exponents[product];
+ }
+ }
+ return sum;
+ }
+
+ private static string PadLeft(string value, int paddingMultiple)
+ {
+ if (paddingMultiple == 1)
+ {
+ return value;
+ }
+ else if (paddingMultiple < 2 || paddingMultiple > Defaults.maxPaddingMultiple)
+ {
+ throw new ArgumentOutOfRangeException(nameof(paddingMultiple), $"{nameof(paddingMultiple)} must be in the range [0, {Defaults.maxPaddingMultiple}].");
+ }
+ if (value.Any())
+ {
+ int extra = value.Length % paddingMultiple;
+ if (extra > 0)
+ {
+ string s = padding + value;
+ value = s[^(paddingMultiple - extra + value.Length)..];
+ }
+ }
+ return value;
+ }
+
+ private List SplitNumStringToIntArray(string value, int paddingMultiple = 0)
+ {
+ if (paddingMultiple > 0)
+ {
+ value = PadLeft(value, paddingMultiple);
+ }
+ List parts = new();
+ int i;
+ for (i = value.Length; i > config.nBits; i -= config.nBits)
+ {
+ parts.Add(Convert.ToInt32(value.Substring(i - config.nBits, config.nBits), 2));
+ }
+ parts.Add(Convert.ToInt32(value[..i], 2));
+ return parts;
+ }
+
+ private class Config
+ {
+ internal readonly int[] exponents;
+ internal readonly int[] logarithms;
+ internal readonly int maxnShares;
+ internal readonly int nBits;
+
+ internal Config(int nBits)
+ {
+ // Set the scalar values.
+ this.nBits = nBits;
+ int size = 1 << nBits;
+ maxnShares = size - 1;
+
+ // Construct the exponent and logarithm tables for multiplication.
+ int primitive = Defaults.primitivePolynomialCoefficients[nBits];
+ exponents = new int[size];
+ logarithms = new int[size];
+ for (int x = 1, i = 0; i < size; ++i)
+ {
+ exponents[i] = x;
+ logarithms[x] = i;
+ x <<= 1;
+ if (x >= size)
+ {
+ x ^= primitive;
+ x &= maxnShares;
+ }
+ }
+ }
+ }
+
+ private class Defaults
+ {
+ internal const int minnBits = 3;
+ internal const int maxnBits = 20; // up to 1,048,575 shares
+ internal const int maxnShares = (1 << maxnBits) - 1;
+ internal const int maxPaddingMultiple = 1024;
+ internal const int nBits = 8;
+ internal const int radix = 16; // hexadecimal
+
+ // These are primitive polynomial coefficients for Galois Fields GF(2^n) for 2 <= n <= 20. The index of each term in the
+ // array corresponds to the n for that polynomial.
+ internal static readonly int[] primitivePolynomialCoefficients = { -1, -1, 1, 3, 3, 5, 3, 3, 29, 17, 9, 5, 83, 27, 43, 3, 45, 9, 39, 39, 9, };
+ }
+
+ private class ShareComponents
+ {
+ internal int nBits;
+ internal int id;
+ internal string data;
+
+ internal ShareComponents(int nBits, int id, string data)
+ {
+ this.nBits = nBits;
+ this.id = id;
+ this.data = data;
+ }
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Exceptions/VerificationException.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Exceptions/VerificationException.cs
new file mode 100644
index 0000000..8f1c987
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Exceptions/VerificationException.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Thirdweb.EWS
+{
+ [Serializable]
+ internal class VerificationException : Exception
+ {
+ internal bool CanRetry { get; }
+
+ public VerificationException(string message, bool canRetry)
+ : base(message)
+ {
+ CanRetry = canRetry;
+ }
+
+ protected VerificationException(SerializationInfo info, StreamingContext context)
+ : base(info, context) { }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Models/User.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Models/User.cs
new file mode 100644
index 0000000..b9acd7c
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Models/User.cs
@@ -0,0 +1,16 @@
+using Nethereum.Web3.Accounts;
+
+namespace Thirdweb.EWS
+{
+ internal class User
+ {
+ internal User(Account account, string emailAddress)
+ {
+ Account = account;
+ EmailAddress = emailAddress;
+ }
+
+ public Account Account { get; internal set; }
+ public string EmailAddress { get; internal set; }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Models/UserStatus.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Models/UserStatus.cs
new file mode 100644
index 0000000..a42e694
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Models/UserStatus.cs
@@ -0,0 +1,10 @@
+namespace Thirdweb.EWS
+{
+ internal enum UserStatus
+ {
+ SignedOut = 10,
+ SignedInWalletUninitialized = 31,
+ SignedInNewDevice = 21,
+ SignedInWalletInitialized = 29,
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Storage/LocalStorage.Types.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Storage/LocalStorage.Types.cs
new file mode 100644
index 0000000..8aa7eed
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Storage/LocalStorage.Types.cs
@@ -0,0 +1,72 @@
+using System.Runtime.Serialization;
+
+namespace Thirdweb.EWS
+{
+ internal partial class LocalStorage : LocalStorageBase
+ {
+ [DataContract]
+ internal class DataStorage
+ {
+ internal string AuthToken => authToken;
+ internal string DeviceShare => deviceShare;
+ internal string EmailAddress => emailAddress;
+ internal string WalletUserId => walletUserId;
+ internal string AuthProvider => authProvider;
+
+ [DataMember]
+ private string authToken;
+
+ [DataMember]
+ private string deviceShare;
+
+ [DataMember]
+ private string emailAddress;
+
+ [DataMember]
+ private string walletUserId;
+
+ [DataMember]
+ private string authProvider;
+
+ internal DataStorage(string authToken, string deviceShare, string emailAddress, string walletUserId, string authProvider)
+ {
+ this.authToken = authToken;
+ this.deviceShare = deviceShare;
+ this.emailAddress = emailAddress;
+ this.walletUserId = walletUserId;
+ this.authProvider = authProvider;
+ }
+
+ internal void ClearAuthToken() => authToken = null;
+ }
+
+ [DataContract]
+ internal class SessionStorage
+ {
+ internal string Id => id;
+ internal bool IsKmsWallet => isKmsWallet;
+
+ [DataMember]
+ private string id;
+
+ [DataMember]
+ private bool isKmsWallet;
+
+ internal SessionStorage(string id, bool isKmsWallet)
+ {
+ this.id = id;
+ this.isKmsWallet = isKmsWallet;
+ }
+ }
+
+ [DataContract]
+ private class Storage
+ {
+ [DataMember]
+ internal DataStorage Data { get; set; }
+
+ [DataMember]
+ internal SessionStorage Session { get; set; }
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Storage/LocalStorage.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Storage/LocalStorage.cs
new file mode 100644
index 0000000..2d60736
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet.Storage/LocalStorage.cs
@@ -0,0 +1,108 @@
+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
+{
+ internal abstract class LocalStorageBase
+ {
+ internal abstract LocalStorage.DataStorage Data { get; }
+ internal abstract LocalStorage.SessionStorage Session { get; }
+
+ internal abstract Task RemoveAuthTokenAsync();
+ internal abstract Task RemoveSessionAsync();
+ internal abstract Task SaveDataAsync(LocalStorage.DataStorage data);
+ internal abstract Task SaveSessionAsync(string sessionId, bool isKmsWallet);
+ }
+
+ internal partial class LocalStorage : LocalStorageBase
+ {
+ internal override DataStorage Data => storage.Data;
+ internal override SessionStorage Session => storage.Session;
+ private readonly Storage storage;
+ private readonly string filePath;
+
+ internal LocalStorage(string clientId)
+ {
+ string directory;
+#if UNITY_5_3_OR_NEWER
+ directory = Application.persistentDataPath;
+#else
+ directory = 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");
+ try
+ {
+ byte[] json = File.ReadAllBytes(filePath);
+ DataContractJsonSerializer serializer = new(typeof(Storage));
+ MemoryStream fin = new(json);
+ storage = (Storage)serializer.ReadObject(fin);
+ }
+ catch (Exception)
+ {
+ storage = new Storage();
+ }
+ }
+
+ internal override Task RemoveAuthTokenAsync()
+ {
+ return UpdateDataAsync(() =>
+ {
+ if (storage.Data?.AuthToken != null)
+ {
+ storage.Data.ClearAuthToken();
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private async Task UpdateDataAsync(Func fn)
+ {
+ if (fn())
+ {
+ DataContractJsonSerializer serializer = new(typeof(Storage));
+ MemoryStream fout = new();
+ serializer.WriteObject(fout, storage);
+ await File.WriteAllBytesAsync(filePath, fout.ToArray());
+ return true;
+ }
+ return false;
+ }
+
+ internal override Task SaveDataAsync(DataStorage data)
+ {
+ return UpdateDataAsync(() =>
+ {
+ storage.Data = data;
+ return true;
+ });
+ }
+
+ internal override Task SaveSessionAsync(string sessionId, bool isKmsWallet)
+ {
+ return UpdateDataAsync(() =>
+ {
+ storage.Session = new SessionStorage(sessionId, isKmsWallet);
+ return true;
+ });
+ }
+
+ internal override Task RemoveSessionAsync()
+ {
+ return UpdateDataAsync(() =>
+ {
+ storage.Session = null;
+ return true;
+ });
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.AuthEndpoint.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.AuthEndpoint.cs
new file mode 100644
index 0000000..1850cdd
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.AuthEndpoint.cs
@@ -0,0 +1,13 @@
+using System.Threading.Tasks;
+
+namespace Thirdweb.EWS
+{
+ internal partial class EmbeddedWallet
+ {
+ public async Task SignInWithAuthEndpointAsync(string payload, string encryptionKey, string recoveryCode)
+ {
+ Server.VerifyResult result = await server.VerifyAuthEndpointAsync(payload);
+ return await PostAuthSetup(result, recoveryCode, encryptionKey, "AuthEndpoint");
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.EmailOTP.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.EmailOTP.cs
new file mode 100644
index 0000000..ccd2f64
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.EmailOTP.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Thirdweb.EWS
+{
+ internal partial class EmbeddedWallet
+ {
+ public async Task<(bool isNewUser, bool isNewDevice, bool needsPassword)> SendOtpEmailAsync(string emailAddress)
+ {
+ Server.UserWallet userWallet = await server.FetchUserDetailsAsync(emailAddress, null);
+ bool isKmsWallet = userWallet.RecoveryShareManagement != "USER_MANAGED";
+ string sessionId = "";
+ if (isKmsWallet)
+ {
+ sessionId = await server.SendKmsOtpEmailAsync(emailAddress);
+ }
+ else
+ {
+ await server.SendUserOtpEmailAsync(emailAddress);
+ }
+ await localStorage.SaveSessionAsync(sessionId, isKmsWallet);
+ bool isNewDevice = userWallet.IsNewUser || localStorage.Data?.WalletUserId != userWallet.WalletUserId;
+ return (userWallet.IsNewUser, isNewDevice, !isKmsWallet);
+ }
+
+ public async Task VerifyOtpAsync(string emailAddress, string otp, string recoveryCode)
+ {
+ if (localStorage.Session == null)
+ {
+ throw new InvalidOperationException($"Must first invoke {nameof(SendOtpEmailAsync)}", new NullReferenceException());
+ }
+ try
+ {
+ if (localStorage.Session.IsKmsWallet)
+ {
+ if (!await server.CheckIsEmailKmsOtpValidAsync(emailAddress, otp))
+ {
+ throw new VerificationException("Invalid OTP", true);
+ }
+ Server.VerifyResult result = await server.VerifyKmsOtpAsync(emailAddress, otp, localStorage.Session.Id);
+ await localStorage.RemoveSessionAsync();
+ return await PostAuthSetup(result, recoveryCode, null, "EmailOTP");
+ }
+ else
+ {
+ if (!await server.CheckIsEmailUserOtpValidAsync(emailAddress, otp))
+ {
+ throw new VerificationException("Invalid OTP", true);
+ }
+ Server.VerifyResult result = await server.VerifyUserOtpAsync(emailAddress, otp);
+ await localStorage.RemoveSessionAsync();
+ return await PostAuthSetup(result, recoveryCode, null, "EmailOTP");
+ }
+ }
+ catch (VerificationException ex)
+ {
+ Console.WriteLine("VerifyOtpAsync Error: " + ex.Message);
+ return new VerifyResult(ex.CanRetry);
+ }
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.JWT.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.JWT.cs
new file mode 100644
index 0000000..fb4c31c
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.JWT.cs
@@ -0,0 +1,13 @@
+using System.Threading.Tasks;
+
+namespace Thirdweb.EWS
+{
+ internal partial class EmbeddedWallet
+ {
+ public async Task SignInWithJwtAsync(string jwt, string encryptionKey, string recoveryCode)
+ {
+ Server.VerifyResult result = await server.VerifyJwtAsync(jwt);
+ return await PostAuthSetup(result, recoveryCode, encryptionKey, "JWT");
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.Misc.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.Misc.cs
new file mode 100644
index 0000000..acd6013
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.Misc.cs
@@ -0,0 +1,223 @@
+using System;
+using System.Threading.Tasks;
+using Nethereum.Web3.Accounts;
+
+namespace Thirdweb.EWS
+{
+ internal partial class EmbeddedWallet
+ {
+ public async Task VerifyThirdwebClientIdAsync(string domain)
+ {
+ var error = await server.VerifyThirdwebClientIdAsync(domain);
+ if (error != "")
+ {
+ throw new InvalidOperationException($"Invalid thirdweb client id for domain {domain} | {error}");
+ }
+ }
+
+ private async Task PostAuthSetup(Server.VerifyResult result, string userRecoveryCode, string twManagedRecoveryCodeOverride, string authProvider)
+ {
+ // Define necessary variables from the result.
+ Account account;
+ string walletUserId = result.WalletUserId;
+ string authToken = result.AuthToken;
+ string emailAddress = result.Email;
+ string deviceShare = localStorage.Data?.DeviceShare;
+
+ // Fetch user details from the server.
+ Server.UserWallet userDetails = await server.FetchUserDetailsAsync(emailAddress, authToken);
+ bool isUserManaged = userDetails.RecoveryShareManagement == "USER_MANAGED";
+ bool isNewUser = userDetails.IsNewUser;
+ User user;
+
+ // Initialize variables related to recovery codes and email status.
+ string mainRecoveryCode = null;
+ string[] backupRecoveryCodes = null;
+ bool? wasEmailed = null;
+
+ if (!isUserManaged)
+ {
+ mainRecoveryCode = twManagedRecoveryCodeOverride ?? result.RecoveryCode;
+ if (mainRecoveryCode == null)
+ throw new InvalidOperationException("Server failed to return recovery code.");
+ (account, deviceShare) = result.IsNewUser ? await CreateAccountAsync(result.AuthToken, mainRecoveryCode) : await RecoverAccountAsync(result.AuthToken, mainRecoveryCode);
+ user = await MakeUserAsync(emailAddress, account, authToken, walletUserId, deviceShare, authProvider);
+ return new VerifyResult(user, mainRecoveryCode, backupRecoveryCodes, wasEmailed);
+ }
+
+ if (isNewUser)
+ {
+ // Create recovery code for user-managed accounts.
+ mainRecoveryCode = MakeRecoveryCode();
+
+ // Commented out section for future use: Generating multiple backup recovery codes.
+ /*
+ backupRecoveryCodes = new string[7];
+ for (int i = 0; i < backupRecoveryCodes.Length; i++)
+ backupRecoveryCodes[i] = MakeRecoveryCode();
+ */
+
+ // Create a new account and handle the recovery codes.
+ (account, deviceShare) = await CreateAccountAsync(authToken, mainRecoveryCode, backupRecoveryCodes);
+
+ // Attempt to send the recovery code via email and record the outcome.
+ try
+ {
+ if (emailAddress == null)
+ throw new ArgumentNullException(nameof(emailAddress));
+ await server.SendRecoveryCodeEmailAsync(authToken, mainRecoveryCode, emailAddress);
+ wasEmailed = true;
+ }
+ catch
+ {
+ wasEmailed = false;
+ }
+ }
+ else
+ {
+ // Handling for existing users.
+ if (userRecoveryCode == null)
+ {
+ if (deviceShare == null)
+ throw new ArgumentNullException(nameof(userRecoveryCode));
+
+ // Fetch the auth share and create an account from shares.
+ string authShare = await server.FetchAuthShareAsync(authToken);
+ account = MakeAccountFromShares(authShare, deviceShare);
+ }
+ else
+ {
+ // Recover the account using the provided recovery code.
+ (account, deviceShare) = await RecoverAccountAsync(authToken, userRecoveryCode);
+ }
+ }
+
+ // Validate the device share returned from server operations.
+ if (deviceShare == null)
+ {
+ throw new InvalidOperationException("Server failed to return account");
+ }
+
+ // Construct the user object and prepare the result.
+ user = await MakeUserAsync(emailAddress, account, authToken, walletUserId, deviceShare, authProvider);
+ return new VerifyResult(user, mainRecoveryCode, backupRecoveryCodes, wasEmailed);
+ }
+
+ public async Task SignOutAsync()
+ {
+ user = null;
+ await localStorage.RemoveAuthTokenAsync();
+ }
+
+ public async Task GetUserAsync(string email, string authProvider)
+ {
+ if (user != null)
+ {
+ return user;
+ }
+ else if (localStorage.Data?.AuthToken == null)
+ {
+ throw new InvalidOperationException("User is not signed in");
+ }
+
+ Server.UserWallet userWallet = await server.FetchUserDetailsAsync(null, localStorage.Data.AuthToken);
+ switch (userWallet.Status)
+ {
+ case "Logged Out":
+ await SignOutAsync();
+ throw new InvalidOperationException("User is logged out");
+ case "Logged In, Wallet Uninitialized":
+ await SignOutAsync();
+ throw new InvalidOperationException("User is logged in but wallet is uninitialized");
+ case "Logged In, Wallet Initialized":
+ if (string.IsNullOrEmpty(localStorage.Data?.DeviceShare))
+ {
+ await SignOutAsync();
+ throw new InvalidOperationException("User is logged in but wallet is uninitialized");
+ }
+
+ string authShare = await server.FetchAuthShareAsync(localStorage.Data.AuthToken);
+ string emailAddress = userWallet.StoredToken?.AuthDetails.Email;
+
+ if (email != null && email != emailAddress)
+ {
+ await SignOutAsync();
+ throw new InvalidOperationException("User email does not match");
+ }
+ else if (email == null && localStorage.Data.AuthProvider != authProvider)
+ {
+ await SignOutAsync();
+ throw new InvalidOperationException($"User auth provider does not match. Expected {localStorage.Data.AuthProvider}, got {authProvider}");
+ }
+ else if (authShare == null)
+ {
+ throw new InvalidOperationException("Server failed to return auth share");
+ }
+ user = new User(MakeAccountFromShares(new[] { authShare, localStorage.Data.DeviceShare }), emailAddress);
+ return user;
+ }
+ throw new InvalidOperationException($"Unexpected user status '{userWallet.Status}'");
+ }
+
+ private async Task MakeUserAsync(string emailAddress, Account account, string authToken, string walletUserId, string deviceShare, string authProvider)
+ {
+ var data = new LocalStorage.DataStorage(authToken, deviceShare, emailAddress ?? "", walletUserId, authProvider);
+ await localStorage.SaveDataAsync(data);
+ user = new User(account, emailAddress ?? "");
+ return user;
+ }
+
+ private async Task<(Account account, string deviceShare)> CreateAccountAsync(string authToken, string recoveryCode, string[] backupRecoveryCodes = null)
+ {
+ string secret = Secrets.Random(KEY_SIZE);
+ (string deviceShare, string recoveryShare, string authShare) = CreateShares(secret);
+ string encryptedRecoveryShare = await EncryptShareAsync(recoveryShare, recoveryCode);
+ Account account = new(secret);
+
+ string[] backupRecoveryShares = null;
+ if (backupRecoveryCodes != null)
+ {
+ backupRecoveryShares = new string[backupRecoveryCodes.Length];
+ for (int i = 0; i < backupRecoveryCodes.Length; i++)
+ {
+ backupRecoveryShares[i] = await EncryptShareAsync(recoveryShare, backupRecoveryCodes[i]);
+ }
+ }
+ await server.StoreAddressAndSharesAsync(account.Address, authShare, encryptedRecoveryShare, authToken, backupRecoveryShares);
+ return (account, deviceShare);
+ }
+
+ private async Task<(Account account, string deviceShare)> RecoverAccountAsync(string authToken, string recoveryCode)
+ {
+ (string authShare, string encryptedRecoveryShare) = await server.FetchAuthAndRecoverySharesAsync(authToken);
+ // make below async
+ string recoveryShare = await Task.Run(() => DecryptShare(encryptedRecoveryShare, recoveryCode));
+ Account account = MakeAccountFromShares(authShare, recoveryShare);
+ Secrets secrets = new();
+ string deviceShare = secrets.NewShare(DEVICE_SHARE_ID, new[] { authShare, recoveryShare });
+ return (account, deviceShare);
+ }
+
+ public class VerifyResult
+ {
+ public User User { get; }
+ public bool CanRetry { get; }
+ public string MainRecoveryCode { get; }
+ public string[] BackupRecoveryCodes { get; }
+ public bool? WasEmailed { get; }
+
+ public VerifyResult(User user, string mainRecoveryCode, string[] backupRecoveryCodes, bool? wasEmailed)
+ {
+ User = user;
+ MainRecoveryCode = mainRecoveryCode;
+ BackupRecoveryCodes = backupRecoveryCodes;
+ WasEmailed = wasEmailed;
+ }
+
+ public VerifyResult(bool canRetry)
+ {
+ CanRetry = canRetry;
+ }
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.OAuth.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.OAuth.cs
new file mode 100644
index 0000000..d3edd33
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.OAuth.cs
@@ -0,0 +1,27 @@
+using System.Threading.Tasks;
+using Nethereum.Web3.Accounts;
+using Newtonsoft.Json;
+
+namespace Thirdweb.EWS
+{
+ internal partial class EmbeddedWallet
+ {
+ public async Task SignInWithOauthAsync(string authProvider, string authResult, string recoveryCode)
+ {
+ Server.VerifyResult result = await server.VerifyOAuthAsync(authResult);
+ return await PostAuthSetup(result, recoveryCode, null, authProvider);
+ }
+
+ public async Task FetchHeadlessOauthLoginLinkAsync(string authProvider)
+ {
+ return await server.FetchHeadlessOauthLoginLinkAsync(authProvider);
+ }
+
+ public async Task IsRecoveryCodeNeededAsync(string authResultStr)
+ {
+ var authResult = JsonConvert.DeserializeObject(authResultStr);
+ Server.UserWallet userWallet = await server.FetchUserDetailsAsync(authResult.StoredToken.AuthDetails.Email, null);
+ return userWallet.RecoveryShareManagement == "USER_MANAGED" && !userWallet.IsNewUser && localStorage.Data?.DeviceShare == null;
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.PhoneOTP.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.PhoneOTP.cs
new file mode 100644
index 0000000..f079d1c
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.PhoneOTP.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Thirdweb.EWS
+{
+ internal partial class EmbeddedWallet
+ {
+ public async Task<(bool isNewUser, bool isNewDevice, bool needsPassword)> SendOtpPhoneAsync(string phoneNumber)
+ {
+ string sessionId = await server.SendKmsPhoneOtpAsync(phoneNumber);
+ bool isKmsWallet = true;
+ await localStorage.SaveSessionAsync(sessionId, isKmsWallet);
+ bool isNewUser = true;
+ bool isNewDevice = true;
+ return (isNewUser, isNewDevice, !isKmsWallet);
+ }
+
+ public async Task VerifyPhoneOtpAsync(string phoneNumber, string otp, string recoveryCode)
+ {
+ if (localStorage.Session == null)
+ {
+ throw new InvalidOperationException($"Must first invoke {nameof(SendOtpPhoneAsync)}", new NullReferenceException());
+ }
+ try
+ {
+ // if (!await server.CheckIsPhoneKmsOtpValidAsync(phoneNumber, otp))
+ // {
+ // throw new VerificationException("Invalid OTP", true);
+ // }
+ Server.VerifyResult result = await server.VerifyKmsPhoneOtpAsync(phoneNumber, otp, localStorage.Session.Id);
+ await localStorage.RemoveSessionAsync();
+ return await PostAuthSetup(result, recoveryCode, null, "PhoneOTP");
+ }
+ catch (VerificationException ex)
+ {
+ Console.WriteLine("VerifyPhoneOtpAsync Error: " + ex.Message);
+ return new VerifyResult(ex.CanRetry);
+ }
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.cs
new file mode 100644
index 0000000..e14ff66
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedWallet/EmbeddedWallet.cs
@@ -0,0 +1,25 @@
+namespace Thirdweb.EWS
+{
+ internal partial class EmbeddedWallet
+ {
+ private readonly LocalStorageBase localStorage;
+ private readonly ServerBase server;
+ private readonly IvGeneratorBase ivGenerator;
+ private User user;
+
+ private const int DEVICE_SHARE_ID = 1;
+ 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)
+ {
+ localStorage = new LocalStorage(client.ClientId);
+ server = new Server(client.ClientId, client.BundleId, "dotnet", Constants.VERSION, client.SecretKey);
+ ivGenerator = new IvGenerator();
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/IThirdwebAccount.cs b/Thirdweb/Thirdweb.Wallets/IThirdwebAccount.cs
new file mode 100644
index 0000000..86ede99
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/IThirdwebAccount.cs
@@ -0,0 +1,27 @@
+using System.Numerics;
+using Nethereum.ABI.EIP712;
+using Nethereum.RPC.Eth.DTOs;
+
+namespace Thirdweb
+{
+ public interface IThirdwebAccount
+ {
+ public ThirdwebAccountType AccountType { get; }
+ public Task Connect();
+ public Task GetAddress();
+ public Task EthSign(string message);
+ public Task PersonalSign(byte[] rawMessage);
+ public Task PersonalSign(string message);
+ public Task SignTypedDataV4(string json);
+ public Task SignTypedDataV4(T data, TypedData typedData);
+ public Task SignTransaction(TransactionInput transaction, BigInteger chainId);
+ public Task IsConnected();
+ public Task Disconnect();
+ }
+
+ public enum ThirdwebAccountType
+ {
+ PrivateKeyAccount,
+ SmartAccount
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/PrivateKeyAccount/PrivateKeyAccount.cs b/Thirdweb/Thirdweb.Wallets/PrivateKeyAccount/PrivateKeyAccount.cs
new file mode 100644
index 0000000..0947237
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/PrivateKeyAccount/PrivateKeyAccount.cs
@@ -0,0 +1,166 @@
+using System.Numerics;
+using System.Text;
+using Nethereum.ABI.EIP712;
+using Nethereum.Hex.HexConvertors.Extensions;
+using Nethereum.Hex.HexTypes;
+using Nethereum.Model;
+using Nethereum.RPC.Eth.DTOs;
+using Nethereum.RPC.Eth.Mappers;
+using Nethereum.Signer;
+using Nethereum.Signer.EIP712;
+
+namespace Thirdweb
+{
+ public class PrivateKeyAccount : IThirdwebAccount
+ {
+ public ThirdwebAccountType AccountType => ThirdwebAccountType.SmartAccount;
+
+ private ThirdwebClient _client;
+ private EthECKey _ecKey;
+
+ public PrivateKeyAccount(ThirdwebClient client, string privateKeyHex)
+ {
+ if (string.IsNullOrEmpty(privateKeyHex))
+ {
+ throw new ArgumentNullException(nameof(privateKeyHex), "Private key cannot be null or empty.");
+ }
+
+ _client = client;
+ _ecKey = new EthECKey(privateKeyHex);
+ }
+
+ public Task Connect()
+ {
+ // No initialization required for private key wallets
+ return Task.CompletedTask;
+ }
+
+ public Task GetAddress()
+ {
+ return Task.FromResult(_ecKey.GetPublicAddress());
+ }
+
+ public Task EthSign(string message)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message), "Message to sign cannot be null.");
+ }
+
+ var signer = new MessageSigner();
+ var signature = signer.Sign(Encoding.UTF8.GetBytes(message), _ecKey);
+ return Task.FromResult(signature);
+ }
+
+ public Task PersonalSign(byte[] rawMessage)
+ {
+ if (rawMessage == null)
+ {
+ throw new ArgumentNullException(nameof(rawMessage), "Message to sign cannot be null.");
+ }
+
+ var signer = new EthereumMessageSigner();
+ var signature = signer.Sign(rawMessage, _ecKey);
+ return Task.FromResult(signature);
+ }
+
+ public Task PersonalSign(string message)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message), "Message to sign cannot be null.");
+ }
+
+ var signer = new EthereumMessageSigner();
+ var signature = signer.EncodeUTF8AndSign(message, _ecKey);
+ return Task.FromResult(signature);
+ }
+
+ public Task SignTypedDataV4(string json)
+ {
+ if (json == null)
+ {
+ throw new ArgumentNullException(nameof(json), "Json to sign cannot be null.");
+ }
+
+ var signer = new Eip712TypedDataSigner();
+ var signature = signer.SignTypedDataV4(json, _ecKey);
+ return Task.FromResult(signature);
+ }
+
+ public Task SignTypedDataV4(T data, TypedData typedData)
+ {
+ if (data == null)
+ {
+ throw new ArgumentNullException(nameof(data), "Data to sign cannot be null.");
+ }
+
+ var signer = new Eip712TypedDataSigner();
+ var signature = signer.SignTypedDataV4(data, typedData, _ecKey);
+ return Task.FromResult(signature);
+ }
+
+ public async Task SignTransaction(TransactionInput transaction, BigInteger chainId)
+ {
+ if (transaction == null)
+ {
+ throw new ArgumentNullException(nameof(transaction));
+ }
+
+ if (string.IsNullOrWhiteSpace(transaction.From))
+ {
+ transaction.From = await GetAddress();
+ }
+ else if (transaction.From != await GetAddress())
+ {
+ throw new Exception("Transaction 'From' address does not match the wallet address");
+ }
+
+ var nonce = transaction.Nonce ?? throw new ArgumentNullException(nameof(transaction), "Transaction nonce has not been set");
+
+ var gasLimit = transaction.Gas;
+ var value = transaction.Value ?? new HexBigInteger(0);
+
+ string signedTransaction;
+ if (transaction.Type != null && transaction.Type.Value == TransactionType.EIP1559.AsByte())
+ {
+ var maxPriorityFeePerGas = transaction.MaxPriorityFeePerGas.Value;
+ var maxFeePerGas = transaction.MaxFeePerGas.Value;
+ var transaction1559 = new Transaction1559(
+ chainId,
+ nonce,
+ maxPriorityFeePerGas,
+ maxFeePerGas,
+ gasLimit,
+ transaction.To,
+ value,
+ transaction.Data,
+ transaction.AccessList.ToSignerAccessListItemArray()
+ );
+
+ var signer = new Transaction1559Signer();
+ signer.SignTransaction(_ecKey, transaction1559);
+ signedTransaction = transaction1559.GetRLPEncoded().ToHex();
+ }
+ else
+ {
+ 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);
+ }
+
+ return "0x" + signedTransaction;
+ }
+
+ public Task IsConnected()
+ {
+ return Task.FromResult(_ecKey != null);
+ }
+
+ public Task Disconnect()
+ {
+ _ecKey = null;
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/SmartAccount/SmartAccount.cs b/Thirdweb/Thirdweb.Wallets/SmartAccount/SmartAccount.cs
new file mode 100644
index 0000000..21e2cbd
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/SmartAccount/SmartAccount.cs
@@ -0,0 +1,275 @@
+using System.Numerics;
+using System.Security.Cryptography;
+using Nethereum.ABI.EIP712;
+using Nethereum.Contracts;
+using Nethereum.Hex.HexConvertors.Extensions;
+using Nethereum.Hex.HexTypes;
+using Nethereum.JsonRpc.Client.RpcMessages;
+using Nethereum.RPC.Eth.DTOs;
+using Nethereum.Signer;
+using Newtonsoft.Json;
+using Thirdweb.AccountAbstraction;
+
+namespace Thirdweb
+{
+ public class SmartAccount : IThirdwebAccount
+ {
+ public ThirdwebAccountType AccountType => ThirdwebAccountType.SmartAccount;
+
+ private ThirdwebClient _client;
+ private IThirdwebAccount _personalAccount;
+ private string _factoryAddress;
+ private bool _gasless;
+ private ThirdwebContract _factoryContract;
+ private ThirdwebContract _accountContract;
+ private ThirdwebContract _entryPointContract;
+ private BigInteger _chainId;
+ private string _bundlerUrl;
+ private string _paymasterUrl;
+ private string _entryPoint;
+
+ public SmartAccount(
+ ThirdwebClient client,
+ IThirdwebAccount personalAccount,
+ string factoryAddress,
+ bool gasless,
+ BigInteger chainId,
+ string entryPoint = null,
+ string bundlerUrl = null,
+ string paymasterUrl = null
+ )
+ {
+ _client = client;
+ _personalAccount = personalAccount;
+ _factoryAddress = factoryAddress;
+ _gasless = gasless;
+ _chainId = chainId;
+ _entryPoint ??= $"0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; // v0.6.0
+ _bundlerUrl ??= $"https://{chainId}.bundler.thirdweb.com";
+ _paymasterUrl ??= $"https://{chainId}.bundler.thirdweb.com";
+ }
+
+ public async Task Connect()
+ {
+ if (!await _personalAccount.IsConnected())
+ {
+ throw new Exception("SmartAccount.Connect: Personal account must be connected.");
+ }
+
+ _entryPointContract = new ThirdwebContract(
+ _client,
+ _entryPoint,
+ _chainId,
+ "[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"preOpGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"paid\",\"type\":\"uint256\"},{\"internalType\":\"uint48\",\"name\":\"validAfter\",\"type\":\"uint48\"},{\"internalType\":\"uint48\",\"name\":\"validUntil\",\"type\":\"uint48\"},{\"internalType\":\"bool\",\"name\":\"targetSuccess\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"targetResult\",\"type\":\"bytes\"}],\"name\":\"ExecutionResult\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"opIndex\",\"type\":\"uint256\"},{\"internalType\":\"string\",\"name\":\"reason\",\"type\":\"string\"}],\"name\":\"FailedOp\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"}],\"name\":\"SenderAddressResult\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"aggregator\",\"type\":\"address\"}],\"name\":\"SignatureValidationFailed\",\"type\":\"error\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"preOpGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"prefund\",\"type\":\"uint256\"},{\"internalType\":\"bool\",\"name\":\"sigFailed\",\"type\":\"bool\"},{\"internalType\":\"uint48\",\"name\":\"validAfter\",\"type\":\"uint48\"},{\"internalType\":\"uint48\",\"name\":\"validUntil\",\"type\":\"uint48\"},{\"internalType\":\"bytes\",\"name\":\"paymasterContext\",\"type\":\"bytes\"}],\"internalType\":\"struct IEntryPoint.ReturnInfo\",\"name\":\"returnInfo\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"stake\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"unstakeDelaySec\",\"type\":\"uint256\"}],\"internalType\":\"struct IStakeManager.StakeInfo\",\"name\":\"senderInfo\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"stake\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"unstakeDelaySec\",\"type\":\"uint256\"}],\"internalType\":\"struct IStakeManager.StakeInfo\",\"name\":\"factoryInfo\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"stake\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"unstakeDelaySec\",\"type\":\"uint256\"}],\"internalType\":\"struct IStakeManager.StakeInfo\",\"name\":\"paymasterInfo\",\"type\":\"tuple\"}],\"name\":\"ValidationResult\",\"type\":\"error\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"preOpGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"prefund\",\"type\":\"uint256\"},{\"internalType\":\"bool\",\"name\":\"sigFailed\",\"type\":\"bool\"},{\"internalType\":\"uint48\",\"name\":\"validAfter\",\"type\":\"uint48\"},{\"internalType\":\"uint48\",\"name\":\"validUntil\",\"type\":\"uint48\"},{\"internalType\":\"bytes\",\"name\":\"paymasterContext\",\"type\":\"bytes\"}],\"internalType\":\"struct IEntryPoint.ReturnInfo\",\"name\":\"returnInfo\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"stake\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"unstakeDelaySec\",\"type\":\"uint256\"}],\"internalType\":\"struct IStakeManager.StakeInfo\",\"name\":\"senderInfo\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"stake\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"unstakeDelaySec\",\"type\":\"uint256\"}],\"internalType\":\"struct IStakeManager.StakeInfo\",\"name\":\"factoryInfo\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"stake\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"unstakeDelaySec\",\"type\":\"uint256\"}],\"internalType\":\"struct IStakeManager.StakeInfo\",\"name\":\"paymasterInfo\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"aggregator\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"stake\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"unstakeDelaySec\",\"type\":\"uint256\"}],\"internalType\":\"struct IStakeManager.StakeInfo\",\"name\":\"stakeInfo\",\"type\":\"tuple\"}],\"internalType\":\"struct IEntryPoint.AggregatorStakeInfo\",\"name\":\"aggregatorInfo\",\"type\":\"tuple\"}],\"name\":\"ValidationResultWithAggregation\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"userOpHash\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"factory\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"paymaster\",\"type\":\"address\"}],\"name\":\"AccountDeployed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[],\"name\":\"BeforeExecution\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"totalDeposit\",\"type\":\"uint256\"}],\"name\":\"Deposited\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"aggregator\",\"type\":\"address\"}],\"name\":\"SignatureAggregatorChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"totalStaked\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"unstakeDelaySec\",\"type\":\"uint256\"}],\"name\":\"StakeLocked\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"withdrawTime\",\"type\":\"uint256\"}],\"name\":\"StakeUnlocked\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"withdrawAddress\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"StakeWithdrawn\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"userOpHash\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"paymaster\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"actualGasCost\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"actualGasUsed\",\"type\":\"uint256\"}],\"name\":\"UserOperationEvent\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"userOpHash\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"revertReason\",\"type\":\"bytes\"}],\"name\":\"UserOperationRevertReason\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"withdrawAddress\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Withdrawn\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"SIG_VALIDATION_FAILED\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"initCode\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"paymasterAndData\",\"type\":\"bytes\"}],\"name\":\"_validateSenderAndPaymaster\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"unstakeDelaySec\",\"type\":\"uint32\"}],\"name\":\"addStake\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"depositTo\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"deposits\",\"outputs\":[{\"internalType\":\"uint112\",\"name\":\"deposit\",\"type\":\"uint112\"},{\"internalType\":\"bool\",\"name\":\"staked\",\"type\":\"bool\"},{\"internalType\":\"uint112\",\"name\":\"stake\",\"type\":\"uint112\"},{\"internalType\":\"uint32\",\"name\":\"unstakeDelaySec\",\"type\":\"uint32\"},{\"internalType\":\"uint48\",\"name\":\"withdrawTime\",\"type\":\"uint48\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"getDepositInfo\",\"outputs\":[{\"components\":[{\"internalType\":\"uint112\",\"name\":\"deposit\",\"type\":\"uint112\"},{\"internalType\":\"bool\",\"name\":\"staked\",\"type\":\"bool\"},{\"internalType\":\"uint112\",\"name\":\"stake\",\"type\":\"uint112\"},{\"internalType\":\"uint32\",\"name\":\"unstakeDelaySec\",\"type\":\"uint32\"},{\"internalType\":\"uint48\",\"name\":\"withdrawTime\",\"type\":\"uint48\"}],\"internalType\":\"struct IStakeManager.DepositInfo\",\"name\":\"info\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint192\",\"name\":\"key\",\"type\":\"uint192\"}],\"name\":\"getNonce\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"initCode\",\"type\":\"bytes\"}],\"name\":\"getSenderAddress\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"initCode\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"callGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"verificationGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"preVerificationGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxFeePerGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxPriorityFeePerGas\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"paymasterAndData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"internalType\":\"struct UserOperation\",\"name\":\"userOp\",\"type\":\"tuple\"}],\"name\":\"getUserOpHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"initCode\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"callGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"verificationGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"preVerificationGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxFeePerGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxPriorityFeePerGas\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"paymasterAndData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"internalType\":\"struct UserOperation[]\",\"name\":\"userOps\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IAggregator\",\"name\":\"aggregator\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"internalType\":\"struct IEntryPoint.UserOpsPerAggregator[]\",\"name\":\"opsPerAggregator\",\"type\":\"tuple[]\"},{\"internalType\":\"address payable\",\"name\":\"beneficiary\",\"type\":\"address\"}],\"name\":\"handleAggregatedOps\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"initCode\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"callGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"verificationGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"preVerificationGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxFeePerGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxPriorityFeePerGas\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"paymasterAndData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"internalType\":\"struct UserOperation[]\",\"name\":\"ops\",\"type\":\"tuple[]\"},{\"internalType\":\"address payable\",\"name\":\"beneficiary\",\"type\":\"address\"}],\"name\":\"handleOps\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint192\",\"name\":\"key\",\"type\":\"uint192\"}],\"name\":\"incrementNonce\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"},{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"callGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"verificationGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"preVerificationGas\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"paymaster\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"maxFeePerGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxPriorityFeePerGas\",\"type\":\"uint256\"}],\"internalType\":\"struct EntryPoint.MemoryUserOp\",\"name\":\"mUserOp\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"userOpHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"prefund\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"contextOffset\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"preOpGas\",\"type\":\"uint256\"}],\"internalType\":\"struct EntryPoint.UserOpInfo\",\"name\":\"opInfo\",\"type\":\"tuple\"},{\"internalType\":\"bytes\",\"name\":\"context\",\"type\":\"bytes\"}],\"name\":\"innerHandleOp\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"actualGasCost\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"uint192\",\"name\":\"\",\"type\":\"uint192\"}],\"name\":\"nonceSequenceNumber\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"initCode\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"callGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"verificationGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"preVerificationGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxFeePerGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxPriorityFeePerGas\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"paymasterAndData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"internalType\":\"struct UserOperation\",\"name\":\"op\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"targetCallData\",\"type\":\"bytes\"}],\"name\":\"simulateHandleOp\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"initCode\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"callGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"verificationGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"preVerificationGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxFeePerGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxPriorityFeePerGas\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"paymasterAndData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"internalType\":\"struct UserOperation\",\"name\":\"userOp\",\"type\":\"tuple\"}],\"name\":\"simulateValidation\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"unlockStake\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address payable\",\"name\":\"withdrawAddress\",\"type\":\"address\"}],\"name\":\"withdrawStake\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address payable\",\"name\":\"withdrawAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"withdrawAmount\",\"type\":\"uint256\"}],\"name\":\"withdrawTo\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}]"
+ );
+ _factoryContract = new ThirdwebContract(
+ _client,
+ _factoryAddress,
+ _chainId,
+ "[{\"type\": \"constructor\",\"name\": \"\",\"inputs\": [{\"type\": \"address\",\"name\": \"_defaultAdmin\",\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"_entrypoint\",\"internalType\": \"contract IEntryPoint\"},{\"type\": \"tuple[]\",\"name\": \"_defaultExtensions\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"internalType\": \"struct IExtension.Extension[]\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"error\",\"name\": \"InvalidCodeAtRange\",\"inputs\": [{\"type\": \"uint256\",\"name\": \"_size\",\"internalType\": \"uint256\"},{\"type\": \"uint256\",\"name\": \"_start\",\"internalType\": \"uint256\"},{\"type\": \"uint256\",\"name\": \"_end\",\"internalType\": \"uint256\"}],\"outputs\": []},{\"type\": \"error\",\"name\": \"WriteError\",\"inputs\": [],\"outputs\": []},{\"type\": \"event\",\"name\": \"AccountCreated\",\"inputs\": [{\"type\": \"address\",\"name\": \"account\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"accountAdmin\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"ContractURIUpdated\",\"inputs\": [{\"type\": \"string\",\"name\": \"prevURI\",\"indexed\": false,\"internalType\": \"string\"},{\"type\": \"string\",\"name\": \"newURI\",\"indexed\": false,\"internalType\": \"string\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"ExtensionAdded\",\"inputs\": [{\"type\": \"string\",\"name\": \"name\",\"indexed\": true,\"internalType\": \"string\"},{\"type\": \"address\",\"name\": \"implementation\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"tuple\",\"name\": \"extension\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"indexed\": false,\"internalType\": \"struct IExtension.Extension\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"ExtensionRemoved\",\"inputs\": [{\"type\": \"string\",\"name\": \"name\",\"indexed\": true,\"internalType\": \"string\"},{\"type\": \"tuple\",\"name\": \"extension\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"indexed\": false,\"internalType\": \"struct IExtension.Extension\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"ExtensionReplaced\",\"inputs\": [{\"type\": \"string\",\"name\": \"name\",\"indexed\": true,\"internalType\": \"string\"},{\"type\": \"address\",\"name\": \"implementation\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"tuple\",\"name\": \"extension\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"indexed\": false,\"internalType\": \"struct IExtension.Extension\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"FunctionDisabled\",\"inputs\": [{\"type\": \"string\",\"name\": \"name\",\"indexed\": true,\"internalType\": \"string\"},{\"type\": \"bytes4\",\"name\": \"functionSelector\",\"indexed\": true,\"internalType\": \"bytes4\"},{\"type\": \"tuple\",\"name\": \"extMetadata\",\"components\": [{\"type\": \"string\",\"name\": \"name\",\"internalType\": \"string\"},{\"type\": \"string\",\"name\": \"metadataURI\",\"internalType\": \"string\"},{\"type\": \"address\",\"name\": \"implementation\",\"internalType\": \"address\"}],\"indexed\": false,\"internalType\": \"struct IExtension.ExtensionMetadata\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"FunctionEnabled\",\"inputs\": [{\"type\": \"string\",\"name\": \"name\",\"indexed\": true,\"internalType\": \"string\"},{\"type\": \"bytes4\",\"name\": \"functionSelector\",\"indexed\": true,\"internalType\": \"bytes4\"},{\"type\": \"tuple\",\"name\": \"extFunction\",\"components\": [{\"type\": \"bytes4\",\"name\": \"functionSelector\",\"internalType\": \"bytes4\"},{\"type\": \"string\",\"name\": \"functionSignature\",\"internalType\": \"string\"}],\"indexed\": false,\"internalType\": \"struct IExtension.ExtensionFunction\"},{\"type\": \"tuple\",\"name\": \"extMetadata\",\"components\": [{\"type\": \"string\",\"name\": \"name\",\"internalType\": \"string\"},{\"type\": \"string\",\"name\": \"metadataURI\",\"internalType\": \"string\"},{\"type\": \"address\",\"name\": \"implementation\",\"internalType\": \"address\"}],\"indexed\": false,\"internalType\": \"struct IExtension.ExtensionMetadata\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"RoleAdminChanged\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"indexed\": true,\"internalType\": \"bytes32\"},{\"type\": \"bytes32\",\"name\": \"previousAdminRole\",\"indexed\": true,\"internalType\": \"bytes32\"},{\"type\": \"bytes32\",\"name\": \"newAdminRole\",\"indexed\": true,\"internalType\": \"bytes32\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"RoleGranted\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"indexed\": true,\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"sender\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"RoleRevoked\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"indexed\": true,\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"sender\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"SignerAdded\",\"inputs\": [{\"type\": \"address\",\"name\": \"account\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"signer\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"SignerRemoved\",\"inputs\": [{\"type\": \"address\",\"name\": \"account\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"signer\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"fallback\",\"name\": \"\",\"inputs\": [],\"outputs\": [],\"stateMutability\": \"payable\"},{\"type\": \"function\",\"name\": \"DEFAULT_ADMIN_ROLE\",\"inputs\": [],\"outputs\": [{\"type\": \"bytes32\",\"name\": \"\",\"internalType\": \"bytes32\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"_disableFunctionInExtension\",\"inputs\": [{\"type\": \"string\",\"name\": \"_extensionName\",\"internalType\": \"string\"},{\"type\": \"bytes4\",\"name\": \"_functionSelector\",\"internalType\": \"bytes4\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"accountImplementation\",\"inputs\": [],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"addExtension\",\"inputs\": [{\"type\": \"tuple\",\"name\": \"_extension\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"internalType\": \"struct IExtension.Extension\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"contractURI\",\"inputs\": [],\"outputs\": [{\"type\": \"string\",\"name\": \"\",\"internalType\": \"string\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"createAccount\",\"inputs\": [{\"type\": \"address\",\"name\": \"_admin\",\"internalType\": \"address\"},{\"type\": \"bytes\",\"name\": \"_data\",\"internalType\": \"bytes\"}],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"defaultExtensions\",\"inputs\": [],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"disableFunctionInExtension\",\"inputs\": [{\"type\": \"string\",\"name\": \"_extensionName\",\"internalType\": \"string\"},{\"type\": \"bytes4\",\"name\": \"_functionSelector\",\"internalType\": \"bytes4\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"enableFunctionInExtension\",\"inputs\": [{\"type\": \"string\",\"name\": \"_extensionName\",\"internalType\": \"string\"},{\"type\": \"tuple\",\"name\": \"_function\",\"components\": [{\"type\": \"bytes4\",\"name\": \"functionSelector\",\"internalType\": \"bytes4\"},{\"type\": \"string\",\"name\": \"functionSignature\",\"internalType\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"entrypoint\",\"inputs\": [],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getAccounts\",\"inputs\": [{\"type\": \"uint256\",\"name\": \"_start\",\"internalType\": \"uint256\"},{\"type\": \"uint256\",\"name\": \"_end\",\"internalType\": \"uint256\"}],\"outputs\": [{\"type\": \"address[]\",\"name\": \"accounts\",\"internalType\": \"address[]\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getAccountsOfSigner\",\"inputs\": [{\"type\": \"address\",\"name\": \"signer\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"address[]\",\"name\": \"accounts\",\"internalType\": \"address[]\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getAddress\",\"inputs\": [{\"type\": \"address\",\"name\": \"_adminSigner\",\"internalType\": \"address\"},{\"type\": \"bytes\",\"name\": \"_data\",\"internalType\": \"bytes\"}],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getAllAccounts\",\"inputs\": [],\"outputs\": [{\"type\": \"address[]\",\"name\": \"\",\"internalType\": \"address[]\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getAllExtensions\",\"inputs\": [],\"outputs\": [{\"type\": \"tuple[]\",\"name\": \"allExtensions\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"internalType\": \"struct IExtension.Extension[]\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getExtension\",\"inputs\": [{\"type\": \"string\",\"name\": \"extensionName\",\"internalType\": \"string\"}],\"outputs\": [{\"type\": \"tuple\",\"name\": \"\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"internalType\": \"struct IExtension.Extension\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getImplementationForFunction\",\"inputs\": [{\"type\": \"bytes4\",\"name\": \"_functionSelector\",\"internalType\": \"bytes4\"}],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getMetadataForFunction\",\"inputs\": [{\"type\": \"bytes4\",\"name\": \"functionSelector\",\"internalType\": \"bytes4\"}],\"outputs\": [{\"type\": \"tuple\",\"name\": \"\",\"components\": [{\"type\": \"string\",\"name\": \"name\",\"internalType\": \"string\"},{\"type\": \"string\",\"name\": \"metadataURI\",\"internalType\": \"string\"},{\"type\": \"address\",\"name\": \"implementation\",\"internalType\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getRoleAdmin\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"}],\"outputs\": [{\"type\": \"bytes32\",\"name\": \"\",\"internalType\": \"bytes32\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getRoleMember\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"uint256\",\"name\": \"index\",\"internalType\": \"uint256\"}],\"outputs\": [{\"type\": \"address\",\"name\": \"member\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getRoleMemberCount\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"}],\"outputs\": [{\"type\": \"uint256\",\"name\": \"count\",\"internalType\": \"uint256\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"grantRole\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"hasRole\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"hasRoleWithSwitch\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"isRegistered\",\"inputs\": [{\"type\": \"address\",\"name\": \"_account\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"multicall\",\"inputs\": [{\"type\": \"bytes[]\",\"name\": \"data\",\"internalType\": \"bytes[]\"}],\"outputs\": [{\"type\": \"bytes[]\",\"name\": \"results\",\"internalType\": \"bytes[]\"}],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"onRegister\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"_salt\",\"internalType\": \"bytes32\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"onSignerAdded\",\"inputs\": [{\"type\": \"address\",\"name\": \"_signer\",\"internalType\": \"address\"},{\"type\": \"bytes32\",\"name\": \"_salt\",\"internalType\": \"bytes32\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"onSignerRemoved\",\"inputs\": [{\"type\": \"address\",\"name\": \"_signer\",\"internalType\": \"address\"},{\"type\": \"bytes32\",\"name\": \"_salt\",\"internalType\": \"bytes32\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"removeExtension\",\"inputs\": [{\"type\": \"string\",\"name\": \"_extensionName\",\"internalType\": \"string\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"renounceRole\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"replaceExtension\",\"inputs\": [{\"type\": \"tuple\",\"name\": \"_extension\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"internalType\": \"struct IExtension.Extension\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"revokeRole\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"setContractURI\",\"inputs\": [{\"type\": \"string\",\"name\": \"_uri\",\"internalType\": \"string\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"totalAccounts\",\"inputs\": [],\"outputs\": [{\"type\": \"uint256\",\"name\": \"\",\"internalType\": \"uint256\"}],\"stateMutability\": \"view\"}]"
+ );
+ var accountAddress = await ThirdwebContract.ReadContract(_factoryContract, "getAddress", await _personalAccount.GetAddress(), new byte[0]);
+ _accountContract = new ThirdwebContract(
+ _client,
+ accountAddress,
+ _chainId,
+ "[{\"type\": \"constructor\",\"name\": \"\",\"inputs\": [{\"type\": \"address\",\"name\": \"_defaultAdmin\",\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"_entrypoint\",\"internalType\": \"contract IEntryPoint\"},{\"type\": \"tuple[]\",\"name\": \"_defaultExtensions\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"internalType\": \"struct IExtension.Extension[]\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"error\",\"name\": \"InvalidCodeAtRange\",\"inputs\": [{\"type\": \"uint256\",\"name\": \"_size\",\"internalType\": \"uint256\"},{\"type\": \"uint256\",\"name\": \"_start\",\"internalType\": \"uint256\"},{\"type\": \"uint256\",\"name\": \"_end\",\"internalType\": \"uint256\"}],\"outputs\": []},{\"type\": \"error\",\"name\": \"WriteError\",\"inputs\": [],\"outputs\": []},{\"type\": \"event\",\"name\": \"AccountCreated\",\"inputs\": [{\"type\": \"address\",\"name\": \"account\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"accountAdmin\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"ContractURIUpdated\",\"inputs\": [{\"type\": \"string\",\"name\": \"prevURI\",\"indexed\": false,\"internalType\": \"string\"},{\"type\": \"string\",\"name\": \"newURI\",\"indexed\": false,\"internalType\": \"string\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"ExtensionAdded\",\"inputs\": [{\"type\": \"string\",\"name\": \"name\",\"indexed\": true,\"internalType\": \"string\"},{\"type\": \"address\",\"name\": \"implementation\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"tuple\",\"name\": \"extension\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"indexed\": false,\"internalType\": \"struct IExtension.Extension\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"ExtensionRemoved\",\"inputs\": [{\"type\": \"string\",\"name\": \"name\",\"indexed\": true,\"internalType\": \"string\"},{\"type\": \"tuple\",\"name\": \"extension\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"indexed\": false,\"internalType\": \"struct IExtension.Extension\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"ExtensionReplaced\",\"inputs\": [{\"type\": \"string\",\"name\": \"name\",\"indexed\": true,\"internalType\": \"string\"},{\"type\": \"address\",\"name\": \"implementation\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"tuple\",\"name\": \"extension\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"indexed\": false,\"internalType\": \"struct IExtension.Extension\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"FunctionDisabled\",\"inputs\": [{\"type\": \"string\",\"name\": \"name\",\"indexed\": true,\"internalType\": \"string\"},{\"type\": \"bytes4\",\"name\": \"functionSelector\",\"indexed\": true,\"internalType\": \"bytes4\"},{\"type\": \"tuple\",\"name\": \"extMetadata\",\"components\": [{\"type\": \"string\",\"name\": \"name\",\"internalType\": \"string\"},{\"type\": \"string\",\"name\": \"metadataURI\",\"internalType\": \"string\"},{\"type\": \"address\",\"name\": \"implementation\",\"internalType\": \"address\"}],\"indexed\": false,\"internalType\": \"struct IExtension.ExtensionMetadata\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"FunctionEnabled\",\"inputs\": [{\"type\": \"string\",\"name\": \"name\",\"indexed\": true,\"internalType\": \"string\"},{\"type\": \"bytes4\",\"name\": \"functionSelector\",\"indexed\": true,\"internalType\": \"bytes4\"},{\"type\": \"tuple\",\"name\": \"extFunction\",\"components\": [{\"type\": \"bytes4\",\"name\": \"functionSelector\",\"internalType\": \"bytes4\"},{\"type\": \"string\",\"name\": \"functionSignature\",\"internalType\": \"string\"}],\"indexed\": false,\"internalType\": \"struct IExtension.ExtensionFunction\"},{\"type\": \"tuple\",\"name\": \"extMetadata\",\"components\": [{\"type\": \"string\",\"name\": \"name\",\"internalType\": \"string\"},{\"type\": \"string\",\"name\": \"metadataURI\",\"internalType\": \"string\"},{\"type\": \"address\",\"name\": \"implementation\",\"internalType\": \"address\"}],\"indexed\": false,\"internalType\": \"struct IExtension.ExtensionMetadata\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"RoleAdminChanged\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"indexed\": true,\"internalType\": \"bytes32\"},{\"type\": \"bytes32\",\"name\": \"previousAdminRole\",\"indexed\": true,\"internalType\": \"bytes32\"},{\"type\": \"bytes32\",\"name\": \"newAdminRole\",\"indexed\": true,\"internalType\": \"bytes32\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"RoleGranted\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"indexed\": true,\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"sender\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"RoleRevoked\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"indexed\": true,\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"sender\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"SignerAdded\",\"inputs\": [{\"type\": \"address\",\"name\": \"account\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"signer\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"event\",\"name\": \"SignerRemoved\",\"inputs\": [{\"type\": \"address\",\"name\": \"account\",\"indexed\": true,\"internalType\": \"address\"},{\"type\": \"address\",\"name\": \"signer\",\"indexed\": true,\"internalType\": \"address\"}],\"outputs\": [],\"anonymous\": false},{\"type\": \"fallback\",\"name\": \"\",\"inputs\": [],\"outputs\": [],\"stateMutability\": \"payable\"},{\"type\": \"function\",\"name\": \"DEFAULT_ADMIN_ROLE\",\"inputs\": [],\"outputs\": [{\"type\": \"bytes32\",\"name\": \"\",\"internalType\": \"bytes32\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"_disableFunctionInExtension\",\"inputs\": [{\"type\": \"string\",\"name\": \"_extensionName\",\"internalType\": \"string\"},{\"type\": \"bytes4\",\"name\": \"_functionSelector\",\"internalType\": \"bytes4\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"accountImplementation\",\"inputs\": [],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"addExtension\",\"inputs\": [{\"type\": \"tuple\",\"name\": \"_extension\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"internalType\": \"struct IExtension.Extension\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"contractURI\",\"inputs\": [],\"outputs\": [{\"type\": \"string\",\"name\": \"\",\"internalType\": \"string\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"createAccount\",\"inputs\": [{\"type\": \"address\",\"name\": \"_admin\",\"internalType\": \"address\"},{\"type\": \"bytes\",\"name\": \"_data\",\"internalType\": \"bytes\"}],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"defaultExtensions\",\"inputs\": [],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"disableFunctionInExtension\",\"inputs\": [{\"type\": \"string\",\"name\": \"_extensionName\",\"internalType\": \"string\"},{\"type\": \"bytes4\",\"name\": \"_functionSelector\",\"internalType\": \"bytes4\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"enableFunctionInExtension\",\"inputs\": [{\"type\": \"string\",\"name\": \"_extensionName\",\"internalType\": \"string\"},{\"type\": \"tuple\",\"name\": \"_function\",\"components\": [{\"type\": \"bytes4\",\"name\": \"functionSelector\",\"internalType\": \"bytes4\"},{\"type\": \"string\",\"name\": \"functionSignature\",\"internalType\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"entrypoint\",\"inputs\": [],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getAccounts\",\"inputs\": [{\"type\": \"uint256\",\"name\": \"_start\",\"internalType\": \"uint256\"},{\"type\": \"uint256\",\"name\": \"_end\",\"internalType\": \"uint256\"}],\"outputs\": [{\"type\": \"address[]\",\"name\": \"accounts\",\"internalType\": \"address[]\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getAccountsOfSigner\",\"inputs\": [{\"type\": \"address\",\"name\": \"signer\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"address[]\",\"name\": \"accounts\",\"internalType\": \"address[]\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getAddress\",\"inputs\": [{\"type\": \"address\",\"name\": \"_adminSigner\",\"internalType\": \"address\"},{\"type\": \"bytes\",\"name\": \"_data\",\"internalType\": \"bytes\"}],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getAllAccounts\",\"inputs\": [],\"outputs\": [{\"type\": \"address[]\",\"name\": \"\",\"internalType\": \"address[]\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getAllExtensions\",\"inputs\": [],\"outputs\": [{\"type\": \"tuple[]\",\"name\": \"allExtensions\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"internalType\": \"struct IExtension.Extension[]\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getExtension\",\"inputs\": [{\"type\": \"string\",\"name\": \"extensionName\",\"internalType\": \"string\"}],\"outputs\": [{\"type\": \"tuple\",\"name\": \"\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"internalType\": \"struct IExtension.Extension\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getImplementationForFunction\",\"inputs\": [{\"type\": \"bytes4\",\"name\": \"_functionSelector\",\"internalType\": \"bytes4\"}],\"outputs\": [{\"type\": \"address\",\"name\": \"\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getMetadataForFunction\",\"inputs\": [{\"type\": \"bytes4\",\"name\": \"functionSelector\",\"internalType\": \"bytes4\"}],\"outputs\": [{\"type\": \"tuple\",\"name\": \"\",\"components\": [{\"type\": \"string\",\"name\": \"name\",\"internalType\": \"string\"},{\"type\": \"string\",\"name\": \"metadataURI\",\"internalType\": \"string\"},{\"type\": \"address\",\"name\": \"implementation\",\"internalType\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getRoleAdmin\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"}],\"outputs\": [{\"type\": \"bytes32\",\"name\": \"\",\"internalType\": \"bytes32\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getRoleMember\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"uint256\",\"name\": \"index\",\"internalType\": \"uint256\"}],\"outputs\": [{\"type\": \"address\",\"name\": \"member\",\"internalType\": \"address\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"getRoleMemberCount\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"}],\"outputs\": [{\"type\": \"uint256\",\"name\": \"count\",\"internalType\": \"uint256\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"grantRole\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"hasRole\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"hasRoleWithSwitch\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"isRegistered\",\"inputs\": [{\"type\": \"address\",\"name\": \"_account\",\"internalType\": \"address\"}],\"outputs\": [{\"type\": \"bool\",\"name\": \"\",\"internalType\": \"bool\"}],\"stateMutability\": \"view\"},{\"type\": \"function\",\"name\": \"multicall\",\"inputs\": [{\"type\": \"bytes[]\",\"name\": \"data\",\"internalType\": \"bytes[]\"}],\"outputs\": [{\"type\": \"bytes[]\",\"name\": \"results\",\"internalType\": \"bytes[]\"}],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"onRegister\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"_salt\",\"internalType\": \"bytes32\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"onSignerAdded\",\"inputs\": [{\"type\": \"address\",\"name\": \"_signer\",\"internalType\": \"address\"},{\"type\": \"bytes32\",\"name\": \"_salt\",\"internalType\": \"bytes32\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"onSignerRemoved\",\"inputs\": [{\"type\": \"address\",\"name\": \"_signer\",\"internalType\": \"address\"},{\"type\": \"bytes32\",\"name\": \"_salt\",\"internalType\": \"bytes32\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"removeExtension\",\"inputs\": [{\"type\": \"string\",\"name\": \"_extensionName\",\"internalType\": \"string\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"renounceRole\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"replaceExtension\",\"inputs\": [{\"type\": \"tuple\",\"name\": \"_extension\",\"components\": [{\"type\": \"tuple\",\"name\": \"metadata\",\"components\": [{\"internalType\": \"string\",\"name\": \"name\",\"type\": \"string\"},{\"internalType\": \"string\",\"name\": \"metadataURI\",\"type\": \"string\"},{\"internalType\": \"address\",\"name\": \"implementation\",\"type\": \"address\"}],\"internalType\": \"struct IExtension.ExtensionMetadata\"},{\"type\": \"tuple[]\",\"name\": \"functions\",\"components\": [{\"internalType\": \"bytes4\",\"name\": \"functionSelector\",\"type\": \"bytes4\"},{\"internalType\": \"string\",\"name\": \"functionSignature\",\"type\": \"string\"}],\"internalType\": \"struct IExtension.ExtensionFunction[]\"}],\"internalType\": \"struct IExtension.Extension\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"revokeRole\",\"inputs\": [{\"type\": \"bytes32\",\"name\": \"role\",\"internalType\": \"bytes32\"},{\"type\": \"address\",\"name\": \"account\",\"internalType\": \"address\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"setContractURI\",\"inputs\": [{\"type\": \"string\",\"name\": \"_uri\",\"internalType\": \"string\"}],\"outputs\": [],\"stateMutability\": \"nonpayable\"},{\"type\": \"function\",\"name\": \"totalAccounts\",\"inputs\": [],\"outputs\": [{\"type\": \"uint256\",\"name\": \"\",\"internalType\": \"uint256\"}],\"stateMutability\": \"view\"}]"
+ );
+ }
+
+ public async Task IsDeployed()
+ {
+ var code = await ThirdwebRPC.GetRpcInstance(_client, _chainId).SendRequestAsync("eth_getCode", _accountContract.Address, "latest");
+ return code != "0x";
+ }
+
+ public async Task SendTransaction(TransactionInput transaction)
+ {
+ var signedOp = await SignUserOp(transaction);
+ return await SendUserOp(signedOp);
+ }
+
+ private async Task GetInitCode()
+ {
+ if (await IsDeployed())
+ {
+ return new byte[0];
+ }
+
+ var rpc = ThirdwebRPC.GetRpcInstance(_factoryContract.Client, _factoryContract.Chain);
+ var service = new Contract(null, _factoryContract.Abi, _factoryContract.Address);
+ var function = service.GetFunction("createAccount");
+ var data = function.GetData(await _personalAccount.GetAddress(), new byte[0]);
+ data = Utils.HexConcat(_factoryAddress, data);
+ return data.HexToByteArray();
+ }
+
+ private async Task SignUserOp(TransactionInput transactionInput, int? requestId = null)
+ {
+ requestId ??= 1;
+
+ // Create the user operation and its safe (hexified) version
+
+ var executeFn = new ExecuteFunction
+ {
+ Target = transactionInput.To,
+ Value = transactionInput.Value.Value,
+ Calldata = transactionInput.Data.HexToByteArray(),
+ FromAddress = await GetAddress(),
+ };
+ var executeInput = executeFn.CreateTransactionInput(await GetAddress());
+
+ var fees = await BundlerClient.ThirdwebGetUserOperationGasPrice(_client, _bundlerUrl, requestId);
+ var maxFee = new HexBigInteger(fees.maxFeePerGas).Value;
+ var maxPriorityFee = new HexBigInteger(fees.maxPriorityFeePerGas).Value;
+
+ var partialUserOp = new UserOperation()
+ {
+ Sender = _accountContract.Address,
+ Nonce = await GetNonce(),
+ InitCode = await GetInitCode(),
+ CallData = executeInput.Data.HexToByteArray(),
+ CallGasLimit = 0,
+ VerificationGasLimit = 0,
+ PreVerificationGas = 0,
+ MaxFeePerGas = maxFee,
+ MaxPriorityFeePerGas = maxPriorityFee,
+ PaymasterAndData = new byte[] { },
+ Signature = Constants.DUMMY_SIG.HexToByteArray(),
+ };
+
+ // Update paymaster data if any
+
+ partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestId, EncodeUserOperation(partialUserOp));
+
+ // Estimate gas
+
+ var gasEstimates = await BundlerClient.EthEstimateUserOperationGas(_client, _bundlerUrl, requestId, EncodeUserOperation(partialUserOp), _entryPoint);
+ partialUserOp.CallGasLimit = 50000 + new HexBigInteger(gasEstimates.CallGasLimit).Value;
+ partialUserOp.VerificationGasLimit = new HexBigInteger(gasEstimates.VerificationGas).Value;
+ partialUserOp.PreVerificationGas = new HexBigInteger(gasEstimates.PreVerificationGas).Value;
+
+ // Update paymaster data if any
+
+ partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestId, EncodeUserOperation(partialUserOp));
+
+ // Hash, sign and encode the user operation
+
+ partialUserOp.Signature = await HashAndSignUserOp(partialUserOp, _entryPointContract);
+
+ return partialUserOp;
+ }
+
+ private async Task SendUserOp(UserOperation userOperation, int? requestId = null)
+ {
+ requestId ??= 1;
+
+ // Send the user operation
+
+ var userOpHash = await BundlerClient.EthSendUserOperation(_client, _bundlerUrl, requestId, EncodeUserOperation(userOperation), _entryPoint);
+
+ // Wait for the transaction to be mined
+
+ string txHash = null;
+ while (txHash == null)
+ {
+ var userOpReceipt = await BundlerClient.EthGetUserOperationReceipt(_client, _bundlerUrl, requestId, userOpHash);
+ txHash = userOpReceipt?.receipt?.TransactionHash;
+ await Task.Delay(1000);
+ }
+ return txHash;
+ }
+
+ private async Task GetNonce()
+ {
+ var randomBytes = new byte[24];
+ RandomNumberGenerator.Fill(randomBytes);
+ BigInteger randomInt192 = new(randomBytes);
+ randomInt192 = BigInteger.Abs(randomInt192) % (BigInteger.One << 192);
+ return await ThirdwebContract.ReadContract(_entryPointContract, "getNonce", await GetAddress(), randomInt192);
+ }
+
+ private async Task GetPaymasterAndData(object requestId, UserOperationHexified userOp)
+ {
+ if (_gasless)
+ {
+ var paymasterAndData = await BundlerClient.PMSponsorUserOperation(_client, _paymasterUrl, requestId, userOp, _entryPoint);
+ return paymasterAndData.paymasterAndData.HexToByteArray();
+ }
+ else
+ {
+ return new byte[] { };
+ }
+ }
+
+ private async Task HashAndSignUserOp(UserOperation userOp, ThirdwebContract entryPointContract)
+ {
+ var userOpHash = await ThirdwebContract.ReadContract(entryPointContract, "getUserOpHash", userOp);
+ var sig = await _personalAccount.PersonalSign(userOpHash);
+ return sig.HexToByteArray();
+ }
+
+ private UserOperationHexified EncodeUserOperation(UserOperation userOperation)
+ {
+ return new UserOperationHexified()
+ {
+ sender = userOperation.Sender,
+ nonce = userOperation.Nonce.ToHexBigInteger().HexValue,
+ initCode = userOperation.InitCode.ToHex(true),
+ callData = userOperation.CallData.ToHex(true),
+ callGasLimit = userOperation.CallGasLimit.ToHexBigInteger().HexValue,
+ verificationGasLimit = userOperation.VerificationGasLimit.ToHexBigInteger().HexValue,
+ preVerificationGas = userOperation.PreVerificationGas.ToHexBigInteger().HexValue,
+ maxFeePerGas = userOperation.MaxFeePerGas.ToHexBigInteger().HexValue,
+ maxPriorityFeePerGas = userOperation.MaxPriorityFeePerGas.ToHexBigInteger().HexValue,
+ paymasterAndData = userOperation.PaymasterAndData.ToHex(true),
+ signature = userOperation.Signature.ToHex(true)
+ };
+ }
+
+ public Task GetAddress()
+ {
+ return Task.FromResult(_accountContract.Address);
+ }
+
+ public Task EthSign(string message)
+ {
+ return _personalAccount.EthSign(message);
+ }
+
+ public Task PersonalSign(byte[] rawMessage)
+ {
+ return _personalAccount.PersonalSign(rawMessage);
+ }
+
+ public Task PersonalSign(string message)
+ {
+ return _personalAccount.PersonalSign(message);
+ }
+
+ public Task SignTypedDataV4(string json)
+ {
+ return _personalAccount.SignTypedDataV4(json);
+ }
+
+ public Task SignTypedDataV4(T data, TypedData typedData)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task SignTransaction(TransactionInput transaction, BigInteger chainId)
+ {
+ return _personalAccount.SignTransaction(transaction, chainId);
+ }
+
+ public Task IsConnected()
+ {
+ return Task.FromResult(_accountContract != null);
+ }
+
+ public Task Disconnect()
+ {
+ return _personalAccount.Disconnect();
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/AATypes.cs b/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/AATypes.cs
new file mode 100644
index 0000000..fe0bd2e
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/AATypes.cs
@@ -0,0 +1,117 @@
+using System.Numerics;
+using Nethereum.ABI.FunctionEncoding.Attributes;
+using Nethereum.Contracts;
+using Nethereum.RPC.Eth.DTOs;
+
+namespace Thirdweb.AccountAbstraction
+{
+ public class UserOperation
+ {
+ [Parameter("address", "sender", 1)]
+ public virtual string Sender { get; set; }
+
+ [Parameter("uint256", "nonce", 2)]
+ public virtual BigInteger Nonce { get; set; }
+
+ [Parameter("bytes", "initCode", 3)]
+ public virtual byte[] InitCode { get; set; }
+
+ [Parameter("bytes", "callData", 4)]
+ public virtual byte[] CallData { get; set; }
+
+ [Parameter("uint256", "callGasLimit", 5)]
+ public virtual BigInteger CallGasLimit { get; set; }
+
+ [Parameter("uint256", "verificationGasLimit", 6)]
+ public virtual BigInteger VerificationGasLimit { get; set; }
+
+ [Parameter("uint256", "preVerificationGas", 7)]
+ public virtual BigInteger PreVerificationGas { get; set; }
+
+ [Parameter("uint256", "maxFeePerGas", 8)]
+ public virtual BigInteger MaxFeePerGas { get; set; }
+
+ [Parameter("uint256", "maxPriorityFeePerGas", 9)]
+ public virtual BigInteger MaxPriorityFeePerGas { get; set; }
+
+ [Parameter("bytes", "paymasterAndData", 10)]
+ public virtual byte[] PaymasterAndData { get; set; }
+
+ [Parameter("bytes", "signature", 11)]
+ public virtual byte[] Signature { get; set; }
+ }
+
+ public class UserOperationHexified
+ {
+ public string sender { get; set; }
+ public string nonce { get; set; }
+ public string initCode { get; set; }
+ public string callData { get; set; }
+ public string callGasLimit { get; set; }
+ public string verificationGasLimit { get; set; }
+ public string preVerificationGas { get; set; }
+ public string maxFeePerGas { get; set; }
+ public string maxPriorityFeePerGas { get; set; }
+ public string paymasterAndData { get; set; }
+ public string signature { get; set; }
+ }
+
+ [Function("execute")]
+ public class ExecuteFunction : FunctionMessage
+ {
+ [Parameter("address", "_target", 1)]
+ public virtual string Target { get; set; }
+
+ [Parameter("uint256", "_value", 2)]
+ public virtual BigInteger Value { get; set; }
+
+ [Parameter("bytes", "_calldata", 3)]
+ 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; }
+ public string VerificationGas { get; set; }
+ 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; }
+ }
+
+ public class EntryPointWrapper
+ {
+ public string entryPoint { get; set; }
+ }
+
+ public class PMSponsorOperationResponse
+ {
+ public string paymasterAndData { get; set; }
+ }
+
+ public class ThirdwebGetUserOperationGasPriceResponse
+ {
+ public string maxFeePerGas { get; set; }
+ public string maxPriorityFeePerGas { get; set; }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/BundlerClient.cs b/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/BundlerClient.cs
new file mode 100644
index 0000000..30615c5
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/SmartAccount/Thirdweb.AccountAbstraction/BundlerClient.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Nethereum.JsonRpc.Client.RpcMessages;
+using Newtonsoft.Json;
+
+namespace Thirdweb.AccountAbstraction
+{
+ 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);
+ return JsonConvert.DeserializeObject(response.Result.ToString());
+ }
+
+ public static async Task EthSendUserOperation(ThirdwebClient client, string bundlerUrl, object requestId, UserOperationHexified userOp, string entryPoint)
+ {
+ var response = await BundlerRequest(client, bundlerUrl, requestId, "eth_sendUserOperation", userOp, entryPoint);
+ return response.Result.ToString();
+ }
+
+ public static async Task EthEstimateUserOperationGas(
+ ThirdwebClient client,
+ string bundlerUrl,
+ object requestId,
+ UserOperationHexified userOp,
+ string entryPoint
+ )
+ {
+ var response = await BundlerRequest(client, bundlerUrl, requestId, "eth_estimateUserOperationGas", userOp, entryPoint);
+ return JsonConvert.DeserializeObject(response.Result.ToString());
+ }
+
+ public static async Task ThirdwebGetUserOperationGasPrice(ThirdwebClient client, string bundlerUrl, object requestId)
+ {
+ var response = await BundlerRequest(client, bundlerUrl, requestId, "thirdweb_getUserOperationGasPrice");
+ return JsonConvert.DeserializeObject(response.Result.ToString());
+ }
+
+ // Paymaster requests
+
+ public static async Task PMSponsorUserOperation(ThirdwebClient client, string paymasterUrl, object requestId, UserOperationHexified userOp, string entryPoint)
+ {
+ var response = await BundlerRequest(client, paymasterUrl, requestId, "pm_sponsorUserOperation", userOp, new EntryPointWrapper() { entryPoint = entryPoint });
+ try
+ {
+ return JsonConvert.DeserializeObject(response.Result.ToString());
+ }
+ catch
+ {
+ return new PMSponsorOperationResponse() { paymasterAndData = response.Result.ToString() };
+ }
+ }
+
+ // Request
+
+ private static async Task BundlerRequest(ThirdwebClient client, string url, object requestId, string method, params object[] args)
+ {
+ using var httpClient = new 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 httpResponse = await httpClient.SendAsync(httpRequestMessage);
+
+ if (!httpResponse.IsSuccessStatusCode)
+ {
+ throw new Exception($"Bundler Request Failed. Error: {httpResponse.StatusCode} - {httpResponse.ReasonPhrase} - {await httpResponse.Content.ReadAsStringAsync()}");
+ }
+
+ var httpResponseJson = await httpResponse.Content.ReadAsStringAsync();
+
+#if DEBUG
+ Console.WriteLine($"Bundler Response: {httpResponseJson}");
+#endif
+
+ var response = JsonConvert.DeserializeObject(httpResponseJson);
+ return response.Error != null ? throw new Exception($"Bundler Request Failed. Error: {response.Error.Code} - {response.Error.Message} - {response.Error.Data}") : response;
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.Wallets/ThirdwebWallet.cs b/Thirdweb/Thirdweb.Wallets/ThirdwebWallet.cs
new file mode 100644
index 0000000..460d86b
--- /dev/null
+++ b/Thirdweb/Thirdweb.Wallets/ThirdwebWallet.cs
@@ -0,0 +1,96 @@
+using System.Numerics;
+using Nethereum.ABI.EIP712;
+using Nethereum.RPC.Eth.DTOs;
+
+namespace Thirdweb
+{
+ public class ThirdwebWallet
+ {
+ public Dictionary Accounts { get; }
+ public IThirdwebAccount ActiveAccount { get; private set; }
+
+ public ThirdwebWallet()
+ {
+ Accounts = new Dictionary();
+ ActiveAccount = null;
+ }
+
+ public async Task Initialize(List accounts)
+ {
+ if (accounts.Count == 0)
+ {
+ throw new ArgumentException("At least one account must be provided.");
+ }
+
+ for (var i = 0; i < accounts.Count; i++)
+ {
+ if (!await accounts[i].IsConnected())
+ {
+ throw new InvalidOperationException($"Account at index {i} is not connected.");
+ }
+ }
+
+ foreach (var account in accounts)
+ {
+ Accounts.Add(await account.GetAddress(), account);
+ }
+
+ SetActive(Accounts.Keys.First());
+ }
+
+ public void SetActive(string address)
+ {
+ if (!Accounts.ContainsKey(address))
+ {
+ throw new ArgumentException($"Account with address {address} not found.");
+ }
+
+ ActiveAccount = Accounts[address];
+ }
+
+ public async Task GetAddress()
+ {
+ return await ActiveAccount.GetAddress();
+ }
+
+ public async Task EthSign(string message)
+ {
+ return await ActiveAccount.EthSign(message);
+ }
+
+ public async Task PersonalSign(string message)
+ {
+ return await ActiveAccount.PersonalSign(message);
+ }
+
+ public async Task SignTypedDataV4(string json)
+ {
+ return await ActiveAccount.SignTypedDataV4(json);
+ }
+
+ public async Task SignTypedDataV4(T data, TypedData typedData)
+ {
+ return await ActiveAccount.SignTypedDataV4(data, typedData);
+ }
+
+ public async Task SignTransaction(TransactionInput transaction, BigInteger chainId)
+ {
+ return await ActiveAccount.SignTransaction(transaction, chainId);
+ }
+
+ public async Task IsConnected()
+ {
+ return await ActiveAccount.IsConnected();
+ }
+
+ public async Task Disconnect()
+ {
+ foreach (var account in Accounts.Values)
+ {
+ await account.Disconnect();
+ }
+
+ ActiveAccount = null;
+ }
+ }
+}
diff --git a/Thirdweb/Thirdweb.csproj b/Thirdweb/Thirdweb.csproj
index 61acf63..ab2f649 100644
--- a/Thirdweb/Thirdweb.csproj
+++ b/Thirdweb/Thirdweb.csproj
@@ -30,6 +30,12 @@
+
+
+
+
+
+