diff --git a/README.md b/README.md index 1acf3ea..2566870 100644 --- a/README.md +++ b/README.md @@ -24,28 +24,39 @@ dotnet add package Thirdweb ```csharp using Thirdweb; -// Create a client -var client = new ThirdwebClient(secretKey: secretKey); +// Do not use secret keys client side, use client id/bundle id instead +var secretKey = Environment.GetEnvironmentVariable("THIRDWEB_SECRET_KEY"); +// Do not use private keys client side, use embedded/smart accounts instead +var privateKey = Environment.GetEnvironmentVariable("PRIVATE_KEY"); + +// Fetch timeout options are optional, default is 60000ms +var client = new ThirdwebClient(secretKey: secretKey, fetchTimeoutOptions: new TimeoutOptions(storage: 30000, rpc: 60000)); + +// Access RPC directly if needed, generally not recommended +// var rpc = ThirdwebRPC.GetRpcInstance(client, 421614); +// var blockNumber = await rpc.SendRequestAsync("eth_blockNumber"); +// Console.WriteLine($"Block number: {blockNumber}"); // Interact with a contract -var contract = new ThirdwebContract(client: client, address: "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D", chain: 1, abi: "function name() view returns (string)"); +var contract = new ThirdwebContract(client: client, address: "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D", chain: 1, abi: "MyC#EscapedContractABI"); var readResult = await ThirdwebContract.ReadContract(contract, "name"); Console.WriteLine($"Contract read result: {readResult}"); -// Or directly interact with the RPC -var rpc = ThirdwebRPC.GetRpcInstance(client, 1); -var blockNumber = await rpc.SendRequestAsync("eth_blockNumber"); -Console.WriteLine($"Block number: {blockNumber}"); - -// Create accounts -var privateKeyAccount = new PrivateKeyAccount(client, privateKey); -var embeddedAccount = new EmbeddedAccount(client, "myawesomeemail@gmail.com"); -var smartAccount = new SmartAccount(client, embeddedAccount, "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", true, 421614); +// Create accounts (this is an advanced use case, typically one account is plenty) +var privateKeyAccount = new PrivateKeyAccount(client: client, privateKeyHex: privateKey); +var embeddedAccount = new EmbeddedAccount(client: client, email: "firekeeper+7121271d@thirdweb.com"); // or email: null, phoneNumber: "+1234567890" +var smartAccount = new SmartAccount(client: client, personalAccount: embeddedAccount, factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", gasless: true, chainId: 421614); // Attempt to connect pk accounts await privateKeyAccount.Connect(); await embeddedAccount.Connect(); +// Reset embedded account (optional step for testing login flow) +if (await embeddedAccount.IsConnected()) +{ + await embeddedAccount.Disconnect(); +} + // Relog if embedded account not logged in if (!await embeddedAccount.IsConnected()) { @@ -66,7 +77,7 @@ if (!await embeddedAccount.IsConnected()) } } -// Connect the smart account with embedded signer and grant a session key to pk account +// Connect the smart account with embedded signer and grant a session key to pk account (advanced use case) await smartAccount.Connect(); _ = await smartAccount.CreateSessionKey( signerAddress: await privateKeyAccount.GetAddress(), @@ -78,8 +89,15 @@ _ = await smartAccount.CreateSessionKey( reqValidityEndTimestamp: Utils.GetUnixTimeStampIn10Years().ToString() ); -// Reconnect to same smart account with pk account as signer -smartAccount = new SmartAccount(client, privateKeyAccount, "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", true, 421614, await smartAccount.GetAddress()); +// Reconnect to same smart account with pk account as signer (specifying account address override) +smartAccount = new SmartAccount( + client: client, + personalAccount: privateKeyAccount, + factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", + gasless: true, + chainId: 421614, + accountAddressOverride: await smartAccount.GetAddress() +); await smartAccount.Connect(); // Log addresses @@ -87,14 +105,33 @@ Console.WriteLine($"PrivateKey Account: {await privateKeyAccount.GetAddress()}") Console.WriteLine($"Embedded Account: {await embeddedAccount.GetAddress()}"); Console.WriteLine($"Smart Account: {await smartAccount.GetAddress()}"); -// Initialize wallet +// Initialize wallet (a wallet can hold multiple accounts, but only one can be active at a time) var thirdwebWallet = new ThirdwebWallet(); await thirdwebWallet.Initialize(new List { privateKeyAccount, embeddedAccount, smartAccount }); thirdwebWallet.SetActive(await smartAccount.GetAddress()); Console.WriteLine($"Active account: {await thirdwebWallet.GetAddress()}"); -// Sign, triggering deploy as needed and 1271 verification +// Sign, triggering deploy as needed and 1271 verification if it's a smart wallet 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}"); + +// Storage actions + +// // Will download from IPFS or normal urls +// var downloadResult = await ThirdwebStorage.Download(client: client, uri: "AnyUrlIncludingIpfs"); +// Console.WriteLine($"Download result: {downloadResult}"); + +// // Will upload to IPFS +// var uploadResult = await ThirdwebStorage.Upload(client: client, path: "AnyPath"); +// Console.WriteLine($"Upload result preview: {uploadResult.PreviewUrl}"); ``` diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs index aa66567..2430ac6 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -9,11 +9,15 @@ private static async Task Main(string[] args) { DotEnv.Load(); + // Do not use secret keys client side, use client id/bundle id instead var secretKey = Environment.GetEnvironmentVariable("THIRDWEB_SECRET_KEY"); + // Do not use private keys client side, use embedded/smart accounts instead var privateKey = Environment.GetEnvironmentVariable("PRIVATE_KEY"); + // Fetch timeout options are optional, default is 60000ms var client = new ThirdwebClient(secretKey: secretKey, fetchTimeoutOptions: new TimeoutOptions(storage: 30000, rpc: 60000)); + // Access RPC directly if needed, generally not recommended // var rpc = ThirdwebRPC.GetRpcInstance(client, 421614); // var blockNumber = await rpc.SendRequestAsync("eth_blockNumber"); // Console.WriteLine($"Block number: {blockNumber}"); @@ -27,18 +31,20 @@ private static async Task Main(string[] args) var readResult = await ThirdwebContract.ReadContract(contract, "name"); Console.WriteLine($"Contract read result: {readResult}"); - // Create accounts - var privateKeyAccount = new PrivateKeyAccount(client, privateKey); - var embeddedAccount = new EmbeddedAccount(client, "firekeeper+7121271d@thirdweb.com"); - var smartAccount = new SmartAccount(client, embeddedAccount, "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", true, 421614); + // Create accounts (this is an advanced use case, typically one account is plenty) + var privateKeyAccount = new PrivateKeyAccount(client: client, privateKeyHex: privateKey); + var embeddedAccount = new EmbeddedAccount(client: client, email: "firekeeper+7121271d@thirdweb.com"); // or email: null, phoneNumber: "+1234567890" + var smartAccount = new SmartAccount(client: client, personalAccount: embeddedAccount, factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", gasless: true, chainId: 421614); // Attempt to connect pk accounts await privateKeyAccount.Connect(); await embeddedAccount.Connect(); - // Reset embedded account + // Reset embedded account (optional step for testing login flow) if (await embeddedAccount.IsConnected()) + { await embeddedAccount.Disconnect(); + } // Relog if embedded account not logged in if (!await embeddedAccount.IsConnected()) @@ -60,7 +66,7 @@ private static async Task Main(string[] args) } } - // Connect the smart account with embedded signer and grant a session key to pk account + // Connect the smart account with embedded signer and grant a session key to pk account (advanced use case) await smartAccount.Connect(); _ = await smartAccount.CreateSessionKey( signerAddress: await privateKeyAccount.GetAddress(), @@ -72,8 +78,15 @@ private static async Task Main(string[] args) reqValidityEndTimestamp: Utils.GetUnixTimeStampIn10Years().ToString() ); - // Reconnect to same smart account with pk account as signer - smartAccount = new SmartAccount(client, privateKeyAccount, "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", true, 421614, await smartAccount.GetAddress()); + // Reconnect to same smart account with pk account as signer (specifying account address override) + smartAccount = new SmartAccount( + client: client, + personalAccount: privateKeyAccount, + factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", + gasless: true, + chainId: 421614, + accountAddressOverride: await smartAccount.GetAddress() + ); await smartAccount.Connect(); // Log addresses @@ -81,13 +94,13 @@ private static async Task Main(string[] args) Console.WriteLine($"Embedded Account: {await embeddedAccount.GetAddress()}"); Console.WriteLine($"Smart Account: {await smartAccount.GetAddress()}"); - // Initialize wallet + // Initialize wallet (a wallet can hold multiple accounts, but only one can be active at a time) var thirdwebWallet = new ThirdwebWallet(); await thirdwebWallet.Initialize(new List { privateKeyAccount, embeddedAccount, smartAccount }); thirdwebWallet.SetActive(await smartAccount.GetAddress()); Console.WriteLine($"Active account: {await thirdwebWallet.GetAddress()}"); - // Sign, triggering deploy as needed and 1271 verification + // Sign, triggering deploy as needed and 1271 verification if it's a smart wallet var message = "Hello, Thirdweb!"; var signature = await thirdwebWallet.PersonalSign(message); Console.WriteLine($"Signed message: {signature}"); @@ -100,5 +113,15 @@ private static async Task Main(string[] args) var balanceAfter = await ThirdwebContract.ReadContract(contract, "balanceOf", await thirdwebWallet.GetAddress()); Console.WriteLine($"Balance after mint: {balanceAfter}"); + + // Storage actions + + // // Will download from IPFS or normal urls + // var downloadResult = await ThirdwebStorage.Download(client: client, uri: "AnyUrlIncludingIpfs"); + // Console.WriteLine($"Download result: {downloadResult}"); + + // // Will upload to IPFS + // var uploadResult = await ThirdwebStorage.Upload(client: client, path: "AnyPath"); + // Console.WriteLine($"Upload result preview: {uploadResult.PreviewUrl}"); } } diff --git a/Thirdweb.Console/Thirdweb.Console.csproj b/Thirdweb.Console/Thirdweb.Console.csproj index 7d6573c..2acaa4b 100644 --- a/Thirdweb.Console/Thirdweb.Console.csproj +++ b/Thirdweb.Console/Thirdweb.Console.csproj @@ -3,7 +3,7 @@ Exe - net7.0 + net8.0 enable enable false diff --git a/Thirdweb.Tests/Thirdweb.Tests.csproj b/Thirdweb.Tests/Thirdweb.Tests.csproj index 2f9b75f..439baa6 100644 --- a/Thirdweb.Tests/Thirdweb.Tests.csproj +++ b/Thirdweb.Tests/Thirdweb.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable false diff --git a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedAccount.cs b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedAccount.cs index bc797ec..4deb193 100644 --- a/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedAccount.cs +++ b/Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedAccount.cs @@ -21,16 +21,18 @@ public class EmbeddedAccount : IThirdwebAccount private User _user; private EthECKey _ecKey; private string _email; + private string _phoneNumber; - public EmbeddedAccount(ThirdwebClient client, string email) + public EmbeddedAccount(ThirdwebClient client, string email = null, string phoneNumber = null) { - if (string.IsNullOrEmpty(email)) + if (string.IsNullOrEmpty(email) && string.IsNullOrEmpty(phoneNumber)) { - throw new ArgumentException("Email must be provided to use Embedded Wallets."); + throw new ArgumentException("Email or Phone Number must be provided to login."); } _embeddedWallet = new EmbeddedWallet(client); _email = email; + _phoneNumber = phoneNumber; _client = client; } @@ -38,7 +40,7 @@ public async Task Connect() { try { - _user = await _embeddedWallet.GetUserAsync(_email, "EmailOTP"); + _user = await _embeddedWallet.GetUserAsync(_email, _email == null ? "PhoneOTP" : "EmailOTP"); _ecKey = new EthECKey(_user.Account.PrivateKey); } catch @@ -49,19 +51,31 @@ public async Task Connect() } } - #region Email OTP Flow + #region OTP Flow public async Task SendOTP() { - if (string.IsNullOrEmpty(_email)) + if (string.IsNullOrEmpty(_email) && string.IsNullOrEmpty(_phoneNumber)) { - throw new Exception("Email is required for OTP login"); + throw new Exception("Email or Phone Number 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."); + if (_email != null) + { + (var isNewUser, var isNewDevice, var needsRecoveryCode) = await _embeddedWallet.SendOtpEmailAsync(_email); + } + else if (_phoneNumber != null) + { + (var isNewUser, var isNewDevice, var needsRecoveryCode) = await _embeddedWallet.SendOtpPhoneAsync(_phoneNumber); + } + else + { + throw new Exception("Email or Phone Number must be provided to login."); + } + + Console.WriteLine("OTP sent to user. Please call EmbeddedAccount.SubmitOTP to login."); } catch (Exception e) { @@ -71,7 +85,17 @@ public async Task SendOTP() public async Task<(string, bool)> SubmitOTP(string otp) { - var res = await _embeddedWallet.VerifyOtpAsync(_email, otp, null); + if (string.IsNullOrEmpty(otp)) + { + throw new ArgumentNullException(nameof(otp), "OTP cannot be null or empty."); + } + + if (string.IsNullOrEmpty(_email) && string.IsNullOrEmpty(_phoneNumber)) + { + throw new Exception("Email or Phone Number is required for OTP login"); + } + + var res = _email == null ? await _embeddedWallet.VerifyPhoneOtpAsync(_phoneNumber, otp, null) : await _embeddedWallet.VerifyOtpAsync(_email, otp, null); if (res.User == null) { var canRetry = res.CanRetry; @@ -93,6 +117,16 @@ public async Task SendOTP() } } + public Task GetEmail() + { + return Task.FromResult(_email); + } + + public Task GetPhoneNumber() + { + return Task.FromResult(_phoneNumber); + } + #endregion public Task GetAddress()