Skip to content

Commit

Permalink
Phone Number Login (#5)
Browse files Browse the repository at this point in the history
* Phone Number Login

* program readme
  • Loading branch information
0xFirekeeper authored Apr 5, 2024
1 parent 6bb26aa commit 7e22813
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 39 deletions.
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

0 comments on commit 7e22813

Please sign in to comment.