diff --git a/Aptos.Tests/Aptos.Clients/Aptos.AccountClient.Tests.cs b/Aptos.Tests/Aptos.Clients/Aptos.AccountClient.Tests.cs index e2f5947..a2f5699 100644 --- a/Aptos.Tests/Aptos.Clients/Aptos.AccountClient.Tests.cs +++ b/Aptos.Tests/Aptos.Clients/Aptos.AccountClient.Tests.cs @@ -41,7 +41,7 @@ await client.Account.GetCoinBalance( Assert.Null( await client.Account.GetCoinBalance( "0x66cb05df2d855fbae92cdb2dfac9a0b29c969a03998fa817735d27391b52b189", - "0x123a" + "0xc" ) ); } diff --git a/Aptos.Tests/Aptos.Core/AccountAddress.Tests.cs b/Aptos.Tests/Aptos.Core/AccountAddress.Tests.cs index 781cf43..e4d3c0f 100644 --- a/Aptos.Tests/Aptos.Core/AccountAddress.Tests.cs +++ b/Aptos.Tests/Aptos.Core/AccountAddress.Tests.cs @@ -30,8 +30,8 @@ public void WhenIConvertTheTypeToAType(string _, string outputType) { _output = outputType switch { - "string" => AccountAddress.From(_inputValue), - "string long" => AccountAddress.From(_inputValue).ToStringLong(), + "string" => AccountAddress.From(_inputValue, 63), + "string long" => AccountAddress.From(_inputValue, 63).ToStringLong(), _ => throw new ArgumentException("Invalid type"), }; } @@ -50,7 +50,7 @@ public void ThenTheResultShouldBeTypeValue(string type, string value) switch (type) { case "address": - Assert.Equal(AccountAddress.From(value), _output); + Assert.Equal(AccountAddress.From(value, 63), _output); break; case "string": Assert.Equal(value.Trim('\"'), _output.ToString()); diff --git a/Aptos.Tests/Aptos.Crypto/MultiKey.Tests.cs b/Aptos.Tests/Aptos.Crypto/MultiKey.Tests.cs index dd240c1..2ec6439 100644 --- a/Aptos.Tests/Aptos.Crypto/MultiKey.Tests.cs +++ b/Aptos.Tests/Aptos.Crypto/MultiKey.Tests.cs @@ -1,7 +1,5 @@ namespace Aptos.Tests.Crypto; -using Aptos.Indexer.GraphQL; -using Newtonsoft.Json; using Xunit.Gherkin.Quick; [FeatureFile("../../../../features/multi_key.feature")] @@ -45,9 +43,10 @@ public void GivenValue(string type, string values) var signerTypes = splitValues[1].Split(","); var signers = splitValues[2].Split(","); List deserializedSigners = signerTypes - .Select( + .Select( (type, i) => - type switch + { + Account account = type switch { "ed25519_ed25519_pk" => new Ed25519Account( Ed25519PrivateKey.Deserialize(new(signers[i])) @@ -57,7 +56,11 @@ public void GivenValue(string type, string values) ), "account_keyless" => KeylessAccount.Deserialize(new(signers[i])), _ => throw new ArgumentException("Invalid signer type"), - } + }; + if (account is KeylessAccount keylessAccount) + BaseTests.MockKeylessAccount(keylessAccount); + return account; + } ) .ToList(); _inputValue = new MultiKeyAccount(MultiKey.Deserialize(new(key)), deserializedSigners); diff --git a/Aptos.Tests/Aptos.Tests.csproj b/Aptos.Tests/Aptos.Tests.csproj index 433af4c..a584acd 100644 --- a/Aptos.Tests/Aptos.Tests.csproj +++ b/Aptos.Tests/Aptos.Tests.csproj @@ -6,9 +6,11 @@ true + + diff --git a/Aptos.Tests/BaseTests.cs b/Aptos.Tests/BaseTests.cs index 25c2c62..3c0d51f 100644 --- a/Aptos.Tests/BaseTests.cs +++ b/Aptos.Tests/BaseTests.cs @@ -1,4 +1,6 @@ +using System.Reflection; using dotenv.net; +using Moq; namespace Aptos.Tests; @@ -30,4 +32,19 @@ public static string[] ParseArray(string input) // Split the input string by commas and convert to integers return [.. input.Split(',')]; } + + /// + /// Mock a KeylessAccount to enable signing even if the EphemeralKeyPair is expired. + /// + /// The keyless account with an expired values. + public static void MockKeylessAccount(KeylessAccount keylessAccount) + { + var field = typeof(KeylessAccount).GetField( + "EphemeralKeyPair", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public + ); + var mock = new Mock(keylessAccount.EphemeralKeyPair); + mock.Setup(m => m.IsExpired()).Returns(false); + field?.SetValue(keylessAccount, mock.Object); + } } diff --git a/Aptos/Aptos.Accounts/EphemeralKeyPair.cs b/Aptos/Aptos.Accounts/EphemeralKeyPair.cs index 6c6b3a3..253d15f 100644 --- a/Aptos/Aptos.Accounts/EphemeralKeyPair.cs +++ b/Aptos/Aptos.Accounts/EphemeralKeyPair.cs @@ -18,7 +18,7 @@ public class EphemeralKeyPair : Serializable /// The private key is used to sign transactions. This private key is not tied to any account on the chain as it is /// ephemeral (not permanent) in nature. /// - private PrivateKey _privateKey; + private readonly PrivateKey _privateKey; /// /// A public key used to verify transactions. This public key is not tied to any account on the chain as it is @@ -39,6 +39,9 @@ public class EphemeralKeyPair : Serializable /// public readonly string Nonce; + public EphemeralKeyPair(EphemeralKeyPair other) + : this(other._privateKey, other.ExpiryTimestamp, other.Blinder) { } + public EphemeralKeyPair( PrivateKey privateKey, ulong? expiryTimestamp = null, @@ -64,7 +67,7 @@ public EphemeralKeyPair( Nonce = Hash.PoseidonHash(fields).ToString(); } - public bool IsExpired() => (ulong)DateTime.Now.ToUnixTimestamp() > ExpiryTimestamp; + public virtual bool IsExpired() => (ulong)DateTime.Now.ToUnixTimestamp() > ExpiryTimestamp; public EphemeralSignature Sign(byte[] data) { diff --git a/Aptos/Aptos.Accounts/KeylessAccount.cs b/Aptos/Aptos.Accounts/KeylessAccount.cs index d0f5ee3..bef044f 100644 --- a/Aptos/Aptos.Accounts/KeylessAccount.cs +++ b/Aptos/Aptos.Accounts/KeylessAccount.cs @@ -84,8 +84,6 @@ public override Signature Sign(AnyRawTransaction transaction) public override Signature Sign(byte[] message) { - if (EphemeralKeyPair.IsExpired()) - throw new Exception("Ephemeral keypair has expired"); var token = new JsonWebToken(Jwt); return new KeylessSignature( ephemeralCertificate: new EphemeralCertificate( diff --git a/Aptos/Aptos.Core/AccountAddress.cs b/Aptos/Aptos.Core/AccountAddress.cs index 4225868..367fb98 100644 --- a/Aptos/Aptos.Core/AccountAddress.cs +++ b/Aptos/Aptos.Core/AccountAddress.cs @@ -95,7 +95,7 @@ public override void SerializeForScriptFunction(Serializer s) public static AccountAddress Deserialize(Deserializer d) => new(d.FixedBytes(LENGTH)); - public static AccountAddress FromString(string str) + public static AccountAddress FromString(string str, int maxMissingChars = 4) { string parsedInput = str; @@ -114,10 +114,16 @@ public static AccountAddress FromString(string str) AccountAddressInvalidReason.TooLong ); - byte[] addressBytes; + if (maxMissingChars > 63 || maxMissingChars < 0) + throw new AccountAddressParsingException( + $"maxMissingChars must be between or equal to 0 and 63. Received {maxMissingChars}", + AccountAddressInvalidReason.InvalidPaddingStrictness + ); + + AccountAddress address; try { - addressBytes = Utilities.HexStringToBytes(parsedInput.PadLeft(64, '0')); + address = new AccountAddress(Utilities.HexStringToBytes(parsedInput.PadLeft(64, '0'))); } catch { @@ -127,7 +133,18 @@ public static AccountAddress FromString(string str) ); } - return new AccountAddress(addressBytes); + if (parsedInput.Length < 64 - maxMissingChars) + { + if (!address.IsSpecial()) + { + throw new AccountAddressParsingException( + $"Hex string is too short, must be between {64 - maxMissingChars} and 64 chars, excluding the leading 0x. You may need to fix the address by pading it with 0s before passing it to `fromString` (e.g. .PadLeft(64, '0')). Received {str}", + AccountAddressInvalidReason.TooShort + ); + } + } + + return address; } public static AccountAddress FromStringStrict(string str) @@ -161,7 +178,8 @@ public static AccountAddress FromStringStrict(string str) return address; } - public static AccountAddress From(string str) => FromString(str); + public static AccountAddress From(string str, int maxMissingChars = 4) => + FromString(str, maxMissingChars); public static AccountAddress From(byte[] bytes) => new(bytes); diff --git a/Aptos/Aptos.Crypto/MultiKey.cs b/Aptos/Aptos.Crypto/MultiKey.cs index de67f3e..9bf2916 100644 --- a/Aptos/Aptos.Crypto/MultiKey.cs +++ b/Aptos/Aptos.Crypto/MultiKey.cs @@ -40,6 +40,20 @@ public static int BitCount(byte b) n = (n + (n >> 4)) & 0x0F0F0F0Fu; return (int)((n * 0x01010101u) >> 24); } + + public static MultiKeySignature GetSimulationSignature(MultiKey multiKey) => + GetSimulationSignature(multiKey.PublicKeys.Count); + + public static MultiKeySignature GetSimulationSignature(int keysCount) + { + return new MultiKeySignature( + signatures: Enumerable + .Range(0, keysCount) + .Select(i => (PublicKeySignature)new Ed25519Signature(new byte[64])) + .ToList(), + bitmap: CreateBitmap(Enumerable.Range(0, keysCount).Select(i => i).ToArray()) + ); + } } public partial class MultiKey : Serializable, IVerifyingKey diff --git a/Aptos/Aptos.Exceptions/Common.cs b/Aptos/Aptos.Exceptions/Common.cs index b1e2e72..53b0bb5 100644 --- a/Aptos/Aptos.Exceptions/Common.cs +++ b/Aptos/Aptos.Exceptions/Common.cs @@ -21,6 +21,7 @@ public enum AccountAddressInvalidReason LeadingZeroXRequired, LongFormRequiredUnlessSpecial, InvalidPaddingZeroes, + InvalidPaddingStrictness, } public class AccountAddressParsingException(string message, AccountAddressInvalidReason reason) diff --git a/Aptos/Aptos.Transactions/SimulateTransactionData.cs b/Aptos/Aptos.Transactions/SimulateTransactionData.cs index 4637d7a..cd29e29 100644 --- a/Aptos/Aptos.Transactions/SimulateTransactionData.cs +++ b/Aptos/Aptos.Transactions/SimulateTransactionData.cs @@ -1,3 +1,5 @@ +using OneOf; + namespace Aptos; public class SimulateTransactionOptions( @@ -13,15 +15,16 @@ public class SimulateTransactionOptions( public class SimulateTransactionData( AnyRawTransaction transaction, - PublicKey signerPublicKey, - PublicKey[]? secondarySignersPublicKeys = null, - PublicKey? feePayerPublicKey = null, + OneOf signerPublicKey, + OneOf[]? secondarySignersPublicKeys = null, + OneOf? feePayerPublicKey = null, SimulateTransactionOptions? options = null ) { public AnyRawTransaction Transaction = transaction; - public PublicKey SignerPublicKey = signerPublicKey; - public PublicKey[]? SecondarySignersPublicKeys = secondarySignersPublicKeys; - public PublicKey? FeePayerPublicKey = feePayerPublicKey; + public OneOf SignerPublicKey = signerPublicKey; + public OneOf[]? SecondarySignersPublicKeys = + secondarySignersPublicKeys; + public OneOf? FeePayerPublicKey = feePayerPublicKey; public SimulateTransactionOptions? Options = options; } diff --git a/Aptos/Aptos.Transactions/TransactionBuilder.cs b/Aptos/Aptos.Transactions/TransactionBuilder.cs index 2bcbc64..953ffe2 100644 --- a/Aptos/Aptos.Transactions/TransactionBuilder.cs +++ b/Aptos/Aptos.Transactions/TransactionBuilder.cs @@ -1,38 +1,54 @@ namespace Aptos; using Aptos.Core; -using Aptos.Exceptions; +using OneOf; public static class TransactionBuilder { #region GetAuthenticator - public static AccountAuthenticator GetAuthenticatorForSimulation(PublicKey publicKey) + public static AccountAuthenticator GetAuthenticatorForSimulation( + OneOf publicOrVerifyingKey + ) { Ed25519Signature invalidSignature = new(new byte[64]); - if (publicKey is Ed25519PublicKey ed25519PublicKey) - { - return new AccountAuthenticatorEd25519(ed25519PublicKey, invalidSignature); - } - - if (publicKey is KeylessPublicKey keylessPublicKey) - { - return new AccountAuthenticatorSingleKey( - keylessPublicKey, - Keyless.GetSimulationSignature() - ); - } + return publicOrVerifyingKey.Match( + publicKey => + { + if (publicKey is Ed25519PublicKey ed25519PublicKey) + return new AccountAuthenticatorEd25519(ed25519PublicKey, invalidSignature); + + if (publicKey is KeylessPublicKey keylessPublicKey) + return new AccountAuthenticatorSingleKey( + keylessPublicKey, + Keyless.GetSimulationSignature() + ); + if (publicKey is FederatedKeylessPublicKey federatedKeylessPublicKey) + return new AccountAuthenticatorSingleKey( + federatedKeylessPublicKey, + Keyless.GetSimulationSignature() + ); + + return new AccountAuthenticatorSingleKey(publicKey, invalidSignature); + }, + verifyingKey => + { + if (verifyingKey is SingleKey singleKey) + return new AccountAuthenticatorSingleKey(singleKey.PublicKey, invalidSignature); - if (publicKey is FederatedKeylessPublicKey federatedKeylessPublicKey) - { - return new AccountAuthenticatorSingleKey( - federatedKeylessPublicKey, - Keyless.GetSimulationSignature() - ); - } + if (verifyingKey is MultiKey multiKey) + return new AccountAuthenticatorMultiKey( + multiKey, + MultiKey.GetSimulationSignature(multiKey) + ); return new AccountAuthenticatorSingleKey(publicKey, invalidSignature); + throw new ArgumentException( + $"{verifyingKey.GetType().Name} is not supported for simulation." + ); + } + ); } #endregion @@ -124,7 +140,7 @@ SimulateTransactionData data ?? []; AccountAuthenticator feePayerAuthenticator = GetAuthenticatorForSimulation( - data.FeePayerPublicKey + (OneOf)data.FeePayerPublicKey ); TransactionAuthenticatorFeePayer feePayerTransactionAuthenticator = diff --git a/Aptos/Aptos.csproj b/Aptos/Aptos.csproj index ae9083a..448375d 100644 --- a/Aptos/Aptos.csproj +++ b/Aptos/Aptos.csproj @@ -18,6 +18,7 @@ + diff --git a/Directory.Build.props b/Directory.Build.props index 535b5c9..9bcaab2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ - 0.0.9 + 0.0.11 net8.0;net7.0;net6.0;netstandard2.1 net8.0 diff --git a/index.md b/index.md index c7f849e..18685c3 100644 --- a/index.md +++ b/index.md @@ -16,7 +16,7 @@ dotnet add package Aptos ## Explore -- [Aptos](/docs/Aptos.html): The main namespace for interacting with the Aptos blockchain. -- [Aptos.Core](/docs/Aptos.Core.html): Core utilities for the SDK. -- [Aptos.Schemes](/docs/Aptos.Schemes.html): All authentication schemes supported in the SDK. -- [Aptos.Exceptions](/docs/Aptos.Exceptions.html): All data types for exceptions thrown by the SDK. \ No newline at end of file +- [Aptos](/aptos-dotnet-sdk/docs/Aptos.html): The main namespace for interacting with the Aptos blockchain. +- [Aptos.Core](/aptos-dotnet-sdk/docs/Aptos.Core.html): Core utilities for the SDK. +- [Aptos.Schemes](/aptos-dotnet-sdk/docs/Aptos.Schemes.html): All authentication schemes supported in the SDK. +- [Aptos.Exceptions](/aptos-dotnet-sdk/docs/Aptos.Exceptions.html): All data types for exceptions thrown by the SDK. \ No newline at end of file