Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Phone Number Login #5

Merged
merged 2 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 54 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>("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<string>(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<string>("eth_blockNumber");
Console.WriteLine($"Block number: {blockNumber}");

// Create accounts
var privateKeyAccount = new PrivateKeyAccount(client, privateKey);
var embeddedAccount = new EmbeddedAccount(client, "[email protected]");
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: "[email protected]"); // 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())
{
Expand All @@ -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(),
Expand All @@ -78,23 +89,49 @@ _ = 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
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<IThirdwebAccount> { 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<BigInteger>(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<BigInteger>(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<string>(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}");
```
43 changes: 33 additions & 10 deletions Thirdweb.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>("eth_blockNumber");
// Console.WriteLine($"Block number: {blockNumber}");
Expand All @@ -27,18 +31,20 @@ private static async Task Main(string[] args)
var readResult = await ThirdwebContract.ReadContract<string>(contract, "name");
Console.WriteLine($"Contract read result: {readResult}");

// Create accounts
var privateKeyAccount = new PrivateKeyAccount(client, privateKey);
var embeddedAccount = new EmbeddedAccount(client, "[email protected]");
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: "[email protected]"); // 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())
Expand All @@ -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(),
Expand All @@ -72,22 +78,29 @@ 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
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<IThirdwebAccount> { 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}");
Expand All @@ -100,5 +113,15 @@ private static async Task Main(string[] args)

var balanceAfter = await ThirdwebContract.ReadContract<BigInteger>(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<string>(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}");
}
}
2 changes: 1 addition & 1 deletion Thirdweb.Console/Thirdweb.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
Expand Down
2 changes: 1 addition & 1 deletion Thirdweb.Tests/Thirdweb.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
Expand Down
54 changes: 44 additions & 10 deletions Thirdweb/Thirdweb.Wallets/EmbeddedAccount/EmbeddedAccount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,26 @@ 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;
}

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
Expand All @@ -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)
{
Expand All @@ -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;
Expand All @@ -93,6 +117,16 @@ public async Task SendOTP()
}
}

public Task<string> GetEmail()
{
return Task.FromResult(_email);
}

public Task<string> GetPhoneNumber()
{
return Task.FromResult(_phoneNumber);
}

#endregion

public Task<string> GetAddress()
Expand Down
Loading