diff --git a/Aptos.Examples/Main.cs b/Aptos.Examples/Main.cs index 0eaa1d8..1f4aa43 100644 --- a/Aptos.Examples/Main.cs +++ b/Aptos.Examples/Main.cs @@ -9,6 +9,7 @@ public class RunExample ViewFunctionExample.Run, ComplexViewFunctionExample.Run, SimpleTransferKeylessExample.Run, + SimpleTransferFederatedKeylessExample.Run, SimpleTransferEd25519Example.Run, SimpleTransferSingleKeyExample.Run, SimpleTransferMultiKeyExample.Run, diff --git a/Aptos.Examples/SimpleTransferFederatedKeylessExample.cs b/Aptos.Examples/SimpleTransferFederatedKeylessExample.cs new file mode 100644 index 0000000..cb74447 --- /dev/null +++ b/Aptos.Examples/SimpleTransferFederatedKeylessExample.cs @@ -0,0 +1,91 @@ +namespace Aptos.Examples; + +public class SimpleTransferFederatedKeylessExample +{ + public static async Task Run() + { + var aptos = new AptosClient(new AptosConfig(Networks.Devnet)); + KeylessAccount keylessAccount; + var bob = Account.Generate(); + var ekp = EphemeralKeyPair.Generate(); + + Console.WriteLine("=== Keyless Account Example ===\n"); + { + // Begin the login flow + + var loginFlow = + $"https://dev-qtdgjv22jh0v1k7g.us.auth0.com/authorize?client_id=dzqI77x0M5YwdOSUx6j25xkdOt8SIxeE&redirect_uri=http%3A%2F%2Flocalhost%3A5173%2Fcallback&response_type=id_token&scope=openid&nonce={ekp.Nonce}"; + Console.WriteLine($"Login URL: {loginFlow} \n"); + + Console.WriteLine("1. Open the link above in your browser"); + Console.WriteLine("2. Login with your Auth0 account"); + Console.WriteLine("3. Copy the 'id_token' from the url bar\n"); + + // Ask for the JWT token + + Console.WriteLine("Paste the JWT (id_token) token here and press enter: "); + var jwt = Console.ReadLine(); + + Console.WriteLine("\nPaste the address where the JWKs are installed: "); + var address = Console.ReadLine(); + + // Derive the keyless account + + Console.WriteLine("\nDeriving federated keyless account..."); + if (jwt == null) + throw new ArgumentException("No JWT token provided"); + keylessAccount = await aptos.Keyless.DeriveAccount( + jwt, + ekp, + jwkAddress: AccountAddress.FromString(address) + ); + + Console.WriteLine("=== Addresses ===\n"); + + Console.WriteLine($"Federated keyless account address is: {keylessAccount.Address}"); + Console.WriteLine($"Bob account address is: {bob.Address}"); + } + + Console.WriteLine("\n=== Funding Accounts ===\n"); + { + await aptos.Faucet.FundAccount(keylessAccount.Address, 100_000_000); + await aptos.Faucet.FundAccount(bob.Address, 100_000_000); + + Console.WriteLine("Successfully funded keyless account!"); + } + + Console.WriteLine("\n=== Sending APT from Keyless Account to Bob ===\n"); + { + Console.WriteLine("Building transaction..."); + var txn = await aptos.Transaction.Build( + sender: keylessAccount, + data: new GenerateEntryFunctionPayloadData( + function: "0x1::aptos_account::transfer_coins", + typeArguments: ["0x1::aptos_coin::AptosCoin"], + functionArguments: [bob.Address, "100000"] + ) + ); + + Console.WriteLine("Signing and submitting transaction..."); + var pendingTxn = await aptos.Transaction.SignAndSubmitTransaction(keylessAccount, txn); + Console.WriteLine($"Submitted transaction with hash: {pendingTxn.Hash}"); + var committedTxn = await aptos.Transaction.WaitForTransaction(pendingTxn.Hash); + if (committedTxn.Success) + { + Console.WriteLine("Transaction success!"); + } + else + { + Console.WriteLine("Transaction failed!"); + } + } + + Console.WriteLine("\n=== Balances ===\n"); + { + var keylessAccountBalance = await aptos.Account.GetCoinBalance(keylessAccount.Address); + var bobAccountBalance = await aptos.Account.GetCoinBalance(bob.Address); + Console.WriteLine($"Keyless account balance: {keylessAccountBalance?.Amount ?? 0} APT"); + Console.WriteLine($"Bob account balance: {bobAccountBalance?.Amount ?? 0} APT"); + } + } +} diff --git a/Aptos/Aptos.Accounts/KeylessAccount.cs b/Aptos/Aptos.Accounts/KeylessAccount.cs index bef044f..555fe5e 100644 --- a/Aptos/Aptos.Accounts/KeylessAccount.cs +++ b/Aptos/Aptos.Accounts/KeylessAccount.cs @@ -40,19 +40,20 @@ public class KeylessAccount : Account public readonly string Jwt; - public KeylessAccount( + private KeylessAccount( + SingleKey verifyingKey, string jwt, EphemeralKeyPair ekp, ZeroKnowledgeSignature proof, byte[] pepper, - string uidKey = "sub", - AccountAddress? address = null + string uidKey, + AccountAddress? address ) { if (pepper.Length != PEPPER_LENGTH) throw new ArgumentException($"Pepper length in bytes should be {PEPPER_LENGTH}"); - _verifyingKey = new SingleKey(KeylessPublicKey.FromJwt(jwt, pepper, uidKey)); + _verifyingKey = verifyingKey; _address = address ?? _verifyingKey.AuthKey().DerivedAddress(); EphemeralKeyPair = ekp; Proof = proof; @@ -66,6 +67,43 @@ public KeylessAccount( UidVal = token.GetClaim(uidKey).Value; } + public KeylessAccount( + string jwt, + EphemeralKeyPair ekp, + ZeroKnowledgeSignature proof, + byte[] pepper, + string uidKey = "sub", + AccountAddress? address = null + ) + : this( + new SingleKey(KeylessPublicKey.FromJwt(jwt, pepper, uidKey)), + jwt, + ekp, + proof, + pepper, + uidKey, + address + ) { } + + public KeylessAccount( + AccountAddress jwkAddress, + string jwt, + EphemeralKeyPair ekp, + ZeroKnowledgeSignature proof, + byte[] pepper, + string uidKey = "sub", + AccountAddress? address = null + ) + : this( + new SingleKey(FederatedKeylessPublicKey.FromJwt(jwt, pepper, jwkAddress, uidKey)), + jwt, + ekp, + proof, + pepper, + uidKey, + address + ) { } + public bool VerifySignature(byte[] message, KeylessSignature signature) { if (EphemeralKeyPair.IsExpired()) diff --git a/Aptos/Aptos.Clients/KeylessClient.cs b/Aptos/Aptos.Clients/KeylessClient.cs index 32f57bf..ce3666f 100644 --- a/Aptos/Aptos.Clients/KeylessClient.cs +++ b/Aptos/Aptos.Clients/KeylessClient.cs @@ -11,22 +11,29 @@ public async Task DeriveAccount( string jwt, EphemeralKeyPair ekp, string uidKey = "sub", - byte[]? pepper = null + byte[]? pepper = null, + AccountAddress? jwkAddress = null ) { if (pepper == null) pepper = await GetPepper(jwt, ekp, uidKey); var proof = await GetProof(jwt, ekp, pepper, uidKey); - // Derive the keyless account from the JWT and EphemeralKeyPair - var publicKey = KeylessPublicKey.FromJwt(jwt, pepper, uidKey); - var address = await _client.Account.LookupOriginalAccountAddress( - new SingleKey(publicKey).AuthKey().DerivedAddress().ToString() + new SingleKey( + jwkAddress != null + ? FederatedKeylessPublicKey.FromJwt(jwt, pepper, jwkAddress, uidKey) + : KeylessPublicKey.FromJwt(jwt, pepper, uidKey) + ) + .AuthKey() + .DerivedAddress() + .ToString() ); - // Create and return the keyless account - return new KeylessAccount(jwt, ekp, proof, pepper, uidKey, address); + // Create and return the keyless account using the appropriate constructor + return jwkAddress != null + ? new KeylessAccount(jwkAddress, jwt, ekp, proof, pepper, uidKey, address) + : new KeylessAccount(jwt, ekp, proof, pepper, uidKey, address); } public async Task GetPepper( diff --git a/Aptos/Aptos.Crypto/PublicKey.cs b/Aptos/Aptos.Crypto/PublicKey.cs index 13bde53..0b51395 100644 --- a/Aptos/Aptos.Crypto/PublicKey.cs +++ b/Aptos/Aptos.Crypto/PublicKey.cs @@ -81,6 +81,9 @@ JsonSerializer serializer "ed25519" => new Ed25519PublicKey(anyValue.Value), "secp256k1_ecdsa" => new Secp256k1PublicKey(anyValue.Value), "keyless" => KeylessPublicKey.Deserialize(new Deserializer(anyValue.Value)), + "federated_keyless" => FederatedKeylessPublicKey.Deserialize( + new Deserializer(anyValue.Value) + ), _ => throw new Exception($"Unknown public key type: {type}"), }; } diff --git a/Directory.Build.props b/Directory.Build.props index 4c4eada..5feb75c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ - 0.0.12 + 0.0.13 net8.0;net7.0;net6.0;netstandard2.1 net8.0