Skip to content

Commit

Permalink
embedded wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
0xFirekeeper committed Mar 30, 2024
1 parent 798f4a8 commit 1afe66d
Show file tree
Hide file tree
Showing 28 changed files with 2,592 additions and 35 deletions.
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@
<PackageVersion Include="Nethereum.Signer" Version="4.19.0" />
<PackageVersion Include="Nethereum.Signer.EIP712" Version="4.19.0" />
<PackageVersion Include="Portable.BouncyCastle" Version="1.9.0" />
<PackageVersion Include="Amazon.Extensions.CognitoAuthentication" Version="2.5.0" />
<PackageVersion Include="AWSSDK.Lambda" Version="3.7.113.2" />
<PackageVersion Include="Nethereum.HdWallet" Version="4.14.0" />
</ItemGroup>
</Project>
31 changes: 29 additions & 2 deletions Thirdweb.Console/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Numerics;
using dotenv.net;
using Nethereum.Hex.HexTypes;
using Newtonsoft.Json;
using Thirdweb;

Expand All @@ -27,8 +26,36 @@
var readResult = await ThirdwebContract.ReadContract<string>(contract, "name");
Console.WriteLine($"Contract read result: {readResult}");

var accountOptions = new ThirdwebAccountOptions(client: client, type: WalletType.PrivateKey, privateKey: privateKey);
var accountOptions = new ThirdwebAccountOptions(client: client, type: WalletType.Embedded, email: "[email protected]");
var account = new ThirdwebAccount(accountOptions);
await account.Initialize();
var isConnected = account.IsConnected();
await account.Disconnect(); // Force email

if (!account.IsConnected())
{
if (account.Wallet is not Embedded embedded)
{
throw new Exception("Account is not connected and is not an embedded wallet.");
}
await embedded.SendOTP();
var otp = Console.ReadLine();
(var address, var canRetry) = await embedded.SubmitOTP(otp);
if (canRetry)
{
Console.WriteLine("OTP failed. Please try again.");
var otp2 = Console.ReadLine();
(address, _) = await embedded.SubmitOTP(otp2);
}
if (string.IsNullOrEmpty(address))
{
throw new Exception("Unknown Error, Null Address");
}
else
{
Console.WriteLine($"Successfully Logged In! Address: {address}");
}
}
Console.WriteLine($"Wallet address: {account.GetAddress()}");

var message = "Hello, Thirdweb!";
Expand Down
2 changes: 1 addition & 1 deletion Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public static async Task<string> WriteContract(ThirdwebAccount account, Thirdweb
transaction.Nonce = new HexBigInteger(await rpc.SendRequestAsync<string>("eth_getTransactionCount", account.GetAddress(), "latest"));

string hash;
if (account.Options.Type == WalletType.PrivateKey)
if (account.Options.Type == WalletType.PrivateKey || account.Options.Type == WalletType.Embedded)
{
var signedTx = account.SignTransaction(transaction, contract.Chain);
Console.WriteLine($"Signed transaction: {signedTx}");
Expand Down
17 changes: 10 additions & 7 deletions Thirdweb/Thirdweb.Wallets/IWallet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

namespace Thirdweb
{
internal interface IWallet
public interface IWallet
{
string GetAddress();
string EthSign(string message);
string PersonalSign(string message);
string SignTypedDataV4(string json);
string SignTypedDataV4<T, TDomain>(T data, TypedData<TDomain> typedData);
string SignTransaction(TransactionInput transaction, BigInteger chainId);
internal Task Initialize();
internal string GetAddress();
internal string EthSign(string message);
internal string PersonalSign(string message);
internal string SignTypedDataV4(string json);
internal string SignTypedDataV4<T, TDomain>(T data, TypedData<TDomain> typedData);
internal string SignTransaction(TransactionInput transaction, BigInteger chainId);
internal bool IsConnected();
internal Task Disconnect();
}
}
209 changes: 209 additions & 0 deletions Thirdweb/Thirdweb.Wallets/Thirdweb.Wallets.Embedded/Embedded.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
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;
using Thirdweb.EWS;

public class Embedded : IWallet
{
EmbeddedWallet _embeddedWallet;
User _user;
EthECKey _ecKey;
string _email;

internal Embedded(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;
}

public async Task Initialize()
{
try
{
_user = await _embeddedWallet.GetUserAsync(_email, "EmailOTP");
_ecKey = new EthECKey(_user.Account.PrivateKey);
}
catch
{
Console.WriteLine("User not found. Please call Embedded.LoginWithOTP() to create a new user.");
_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 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)
{
bool 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 (GetAddress(), false);
}
}

#endregion

public string GetAddress()
{
return _ecKey.GetPublicAddress();
}

public string 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 signature;
}

public string 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 signature;
}

public string 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 signature;
}

public string SignTypedDataV4<T, TDomain>(T data, TypedData<TDomain> 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 signature;
}

public string SignTransaction(TransactionInput transaction, BigInteger chainId)
{
if (transaction == null)
{
throw new ArgumentNullException(nameof(transaction));
}

if (string.IsNullOrWhiteSpace(transaction.From))
{
transaction.From = GetAddress();
}
else if (transaction.From != 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 bool IsConnected()
{
return _ecKey != null;
}

public async Task Disconnect()
{
await _embeddedWallet.SignOutAsync();
_user = null;
_ecKey = null;
}
}
Loading

0 comments on commit 1afe66d

Please sign in to comment.