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 @@ + + + + + +