From 9fcb5e7b44df278070a0c683969ce13c35a3dd28 Mon Sep 17 00:00:00 2001 From: Vincent Bouzon Date: Sat, 5 Aug 2023 12:49:23 +0200 Subject: [PATCH] Add maxSupportedTransactionVersion parameter --- src/Solnet.Rpc/IRpcClient.cs | 30 +- .../Models/AddressLookupTableState.cs | 41 ++ src/Solnet.Rpc/Models/Block.cs | 136 +++++++ src/Solnet.Rpc/Models/MessageAccountKeys.cs | 55 +++ src/Solnet.Rpc/Models/MessageV0.cs | 324 +++++++++++++++ src/Solnet.Rpc/Models/Transaction.cs | 368 ++++++++++++++++++ src/Solnet.Rpc/SolanaRpcClient.cs | 40 +- .../Http/Blocks/GetBlockConfirmedRequest.json | 2 +- .../Http/Blocks/GetBlockRequest.json | 2 +- .../GetTransactionProcessedRequest.json | 2 +- .../Transaction/GetTransactionRequest.json | 2 +- .../Transaction/GetTransactionRequest2.json | 2 +- 12 files changed, 974 insertions(+), 30 deletions(-) create mode 100644 src/Solnet.Rpc/Models/AddressLookupTableState.cs create mode 100644 src/Solnet.Rpc/Models/MessageAccountKeys.cs create mode 100644 src/Solnet.Rpc/Models/MessageV0.cs diff --git a/src/Solnet.Rpc/IRpcClient.cs b/src/Solnet.Rpc/IRpcClient.cs index e3f7d0ad..007c2d64 100644 --- a/src/Solnet.Rpc/IRpcClient.cs +++ b/src/Solnet.Rpc/IRpcClient.cs @@ -129,10 +129,11 @@ RequestResult> GetAccountInfo(string pubKey, Commitme /// /// The slot. /// The state commitment to consider when querying the ledger state. + /// max supported transaction version /// The level of transaction detail to return, see . /// Whether to populate the rewards array, the default includes rewards. /// Returns a task that holds the asynchronous operation result and state. - Task> GetBlockAsync(ulong slot, Commitment commitment = Commitment.Finalized, + Task> GetBlockAsync(ulong slot, Commitment commitment = Commitment.Finalized, int maxSupportedTransactionVersion = 0, TransactionDetailsFilterType transactionDetails = TransactionDetailsFilterType.Full, bool blockRewards = false); /// @@ -152,12 +153,14 @@ Task> GetBlockAsync(ulong slot, Commitment commitment = /// /// The slot. /// The state commitment to consider when querying the ledger state. + /// max supported transaction version /// The level of transaction detail to return, see . /// Whether to populate the rewards array, the default includes rewards. /// Returns a task that holds the asynchronous operation result and state. [Obsolete("Please use GetBlockAsync whenever possible instead. This method is expected to be removed in solana-core v1.8.")] Task> GetConfirmedBlockAsync(ulong slot, Commitment commitment = Commitment.Finalized, - TransactionDetailsFilterType transactionDetails = TransactionDetailsFilterType.Full, bool blockRewards = false); + int maxSupportedTransactionVersion = 0, TransactionDetailsFilterType transactionDetails = TransactionDetailsFilterType.Full, + bool blockRewards = false); /// /// Returns identity and transaction information about a block in the ledger. @@ -176,11 +179,13 @@ Task> GetConfirmedBlockAsync(ulong slot, Commitment com /// /// The slot. /// The state commitment to consider when querying the ledger state. + /// max supported transaction version /// The level of transaction detail to return, see . /// Whether to populate the rewards array, the default includes rewards. /// Returns an object that wraps the result along with possible errors with the request. RequestResult GetBlock(ulong slot, Commitment commitment = Commitment.Finalized, - TransactionDetailsFilterType transactionDetails = TransactionDetailsFilterType.Full, bool blockRewards = false); + int maxSupportedTransactionVersion = 0, TransactionDetailsFilterType transactionDetails = TransactionDetailsFilterType.Full, + bool blockRewards = false); /// /// Returns identity and transaction information about a confirmed block in the ledger. @@ -199,12 +204,14 @@ RequestResult GetBlock(ulong slot, Commitment commitment = Commitment /// /// The slot. /// The state commitment to consider when querying the ledger state. + /// max supported transaction version /// The level of transaction detail to return, see . /// Whether to populate the rewards array, the default includes rewards. /// Returns an object that wraps the result along with possible errors with the request. [Obsolete("Please use GetBlock whenever possible instead. This method is expected to be removed in solana-core v1.8.")] RequestResult GetConfirmedBlock(ulong slot, Commitment commitment = Commitment.Finalized, - TransactionDetailsFilterType transactionDetails = TransactionDetailsFilterType.Full, bool blockRewards = false); + int maxSupportedTransactionVersion = 0, TransactionDetailsFilterType transactionDetails = TransactionDetailsFilterType.Full, + bool blockRewards = false); /// /// Gets the block commitment of a certain block, identified by slot. @@ -1075,9 +1082,10 @@ Task>> GetTokenSupplyAsync(string toke /// /// Transaction signature as base-58 encoded string. /// The state commitment to consider when querying the ledger state. + /// max supported transaction version /// Returns a task that holds the asynchronous operation result and state. Task> GetTransactionAsync(string signature, - Commitment commitment = Commitment.Finalized); + Commitment commitment = Commitment.Finalized, int maxSupportedTransactionVersion = 0); /// /// Returns transaction details for a confirmed transaction. @@ -1089,10 +1097,12 @@ Task> GetTransactionAsync(string signatur /// /// /// Transaction signature as base-58 encoded string. + /// max supported transaction version /// The state commitment to consider when querying the ledger state. /// Returns an object that wraps the result along with possible errors with the request. [Obsolete("Please use GetTransactionAsync whenever possible instead. This method is expected to be removed in solana-core v1.8.")] - Task> GetConfirmedTransactionAsync(string signature, Commitment commitment = Commitment.Finalized); + Task> GetConfirmedTransactionAsync(string signature, Commitment commitment = Commitment.Finalized, + int maxSupportedTransactionVersion = 0); /// /// Returns transaction details for a confirmed transaction. @@ -1105,8 +1115,10 @@ Task> GetTransactionAsync(string signatur /// /// Transaction signature as base-58 encoded string. /// The state commitment to consider when querying the ledger state. + /// max supported transaction version /// Returns an object that wraps the result along with possible errors with the request. - RequestResult GetTransaction(string signature, Commitment commitment = Commitment.Finalized); + RequestResult GetTransaction(string signature, Commitment commitment = Commitment.Finalized, + int maxSupportedTransactionVersion = 0); /// /// Returns transaction details for a confirmed transaction. @@ -1119,9 +1131,11 @@ Task> GetTransactionAsync(string signatur /// /// Transaction signature as base-58 encoded string. /// The state commitment to consider when querying the ledger state. + /// max supported transaction version /// Returns an object that wraps the result along with possible errors with the request. [Obsolete("Please use GetTransaction whenever possible instead. This method is expected to be removed in solana-core v1.8.")] - RequestResult GetConfirmedTransaction(string signature, Commitment commitment = Commitment.Finalized); + RequestResult GetConfirmedTransaction(string signature, Commitment commitment = Commitment.Finalized, + int maxSupportedTransactionVersion = 0); /// /// Gets the total transaction count of the ledger. diff --git a/src/Solnet.Rpc/Models/AddressLookupTableState.cs b/src/Solnet.Rpc/Models/AddressLookupTableState.cs new file mode 100644 index 00000000..7285e029 --- /dev/null +++ b/src/Solnet.Rpc/Models/AddressLookupTableState.cs @@ -0,0 +1,41 @@ +using Solnet.Wallet; +using System.Collections.Generic; + +namespace Solnet.Rpc.Models +{ + public class AddressLookupTableState + { + public long DeactivationSlot { get; set; } + public int LastExtendedSlot { get; set; } + public int LastExtendedSlowStartIndex { get ; set; } + public PublicKey Authority { get; set; } + public List Addresses { get; set; } + } + + + public class AddressLookupTableAccount + { + private PublicKey _key; + private AddressLookupTableState _state; + + public AddressLookupTableAccount(PublicKey key, AddressLookupTableState state) + { + _key = key; + _state = state; + } + + public bool IsActive + { + get + { + return _state.DeactivationSlot == long.MaxValue; + } + } + + public static AddressLookupTableState Deserialize(byte[] accountData) + { + var meta = DecodeData() + } + + } +} \ No newline at end of file diff --git a/src/Solnet.Rpc/Models/Block.cs b/src/Solnet.Rpc/Models/Block.cs index d7df487e..7fe1787e 100644 --- a/src/Solnet.Rpc/Models/Block.cs +++ b/src/Solnet.Rpc/Models/Block.cs @@ -64,6 +64,12 @@ public class TransactionMetaSlotInfo : TransactionMetaInfo /// Estimated block production time. /// public long? BlockTime { get; set; } + + /// + /// Transaction version + /// + /// + public object Version { get ;set; } } @@ -179,6 +185,12 @@ public class TransactionContentInfo /// List of program instructions that will be executed in sequence and committed in one atomic transaction if all succeed. /// public InstructionInfo[] Instructions { get; set; } + + /// + /// Addresses table lookup + /// + /// + public MessageAddressTableLookup[] AddressTableLookup { get; set; } } /// @@ -247,6 +259,12 @@ public class TransactionMeta /// Array of string log messages or omitted if log message recording was not yet enabled during this transaction. /// public string[] LogMessages { get; set; } + + /// + /// Transaction loaded addresses + /// + /// + public AccountKeysFromLookups LoadedAddresses { get; set; } } /// @@ -382,4 +400,122 @@ public class LatestBlockHash /// public ulong LastValidBlockHeight { get; set; } } + + /// + /// Represents the block info. + /// + public class BlockVersionedInfo + { + /// + /// Estimated block production time. + /// + public long BlockTime { get; set; } + + /// + /// A base-58 encoded public key representing the block hash. + /// + public string Blockhash { get; set; } + + /// + /// A base-58 encoded public key representing the block hash of this block's parent. + /// + /// If the parent block is no longer available due to ledger cleanup, this field will return + /// '11111111111111111111111111111111' + /// + /// + public string PreviousBlockhash { get; set; } + + /// + /// The slot index of this block's parent. + /// + public ulong ParentSlot { get; set; } + + /// + /// The number of blocks beneath this block. + /// + public long? BlockHeight { get; set; } + + /// + /// The rewards for this given block. + /// + public RewardInfo[] Rewards { get; set; } + + /// + /// Collection of transactions and their metadata within this block. + /// + public TransactionMetaInfo[] Transactions { get; set; } + } + + /// + /// Represents the transaction, metadata and its containing slot. + /// + public class TransactionMetaSlotVersionedInfo : TransactionMetaVersionedInfo + { + /// + /// The slot this transaction was processed in. + /// + public ulong Slot { get; set; } + + /// + /// Estimated block production time. + /// + public long? BlockTime { get; set; } + } + + + /// + /// Represents the tuple transaction and metadata. + /// + public class TransactionMetaVersionedInfo + { + /// + /// The transaction information. + /// + public TransactionVersionedInfo Transaction { get; set; } + + /// + /// The metadata information. + /// + public TransactionVersionedMeta Meta { get; set; } + } + + /// + /// Represents a transaction. + /// + public class TransactionVersionedInfo + { + /// + /// The signatures of this transaction. + /// + public string[] Signatures { get; set; } + + /// + /// The message contents of the transaction. + /// + public TransactionContentVersionedInfo Message { get; set; } + } + + /// + /// Represents the contents of the trasaction. + /// + public class TransactionContentVersionedInfo: TransactionContentInfo + { + /// + /// Address table lookup for transaction + /// + /// + public MessageAddressTableLookup[] AddressTableLookups { get; set; } + } + + /// + /// Represents the transaction metadata. + /// + public class TransactionVersionedMeta: TransactionMeta + { + /// + /// Loaded address for the tranasaction + /// + /// + public AccountKeysFromLookups LoadedAddresses { get; set; } + } } \ No newline at end of file diff --git a/src/Solnet.Rpc/Models/MessageAccountKeys.cs b/src/Solnet.Rpc/Models/MessageAccountKeys.cs new file mode 100644 index 00000000..662e27e1 --- /dev/null +++ b/src/Solnet.Rpc/Models/MessageAccountKeys.cs @@ -0,0 +1,55 @@ +using Solnet.Wallet; +using System.Collections.Generic; +using System.Linq; + +namespace Solnet.Rpc.Models +{ + /// + /// A wrapper around a list of s that takes care of deduplication and ordering according to + /// the wire format specification. + /// + internal class MessageAccountKeys + { + /// + /// The static account metas list. + /// + private readonly List _staticAccounts; + + private AccountKeysFromLookups _accountKeysFromLookups; + + + internal List KeySegments + { + get + { + var segments = _staticAccounts.ToList(); + if (_accountKeysFromLookups != null) + { + segments.AddRange(_accountKeysFromLookups.Writables); + segments.AddRange(_accountKeysFromLookups.Readonly); + } + + return segments; + } + } + + /// + /// Initialize the account keys list for use within transaction building. + /// + internal MessageAccountKeys(List staticAccounts, AccountKeysFromLookups accountKeysFromLookups = null) + { + _staticAccounts = staticAccounts; + _accountKeysFromLookups = accountKeysFromLookups; + } + + public PublicKey Get(int index) + { + if (index < KeySegments.Count) + { + return KeySegments[index]; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Solnet.Rpc/Models/MessageV0.cs b/src/Solnet.Rpc/Models/MessageV0.cs new file mode 100644 index 00000000..e84c8d34 --- /dev/null +++ b/src/Solnet.Rpc/Models/MessageV0.cs @@ -0,0 +1,324 @@ +using Solnet.Rpc.Utilities; +using Solnet.Wallet; +using Solnet.Wallet.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Solnet.Rpc.Models +{ + public class VersionedMessage + { + /// + /// Prefix to determine the message version + /// + public static byte VersionPrefixMask = 0x7f; + + private enum MessageVersion + { + Legacy = 0, + Versioned = 1, + } + + private (MessageVersion type, int version) DeserializeVersion(byte prefix) + { + var maskedPrefix = prefix & VersionPrefixMask; + + // if the highest bit of the prefix is not set, the message is not versioned + if (maskedPrefix == prefix) + { + return new (MessageVersion.Legacy, -1); + } + + // the lower 7 bits of the prefix indicate the message version + return new (MessageVersion.Versioned, maskedPrefix); + } + + + } + + /// + /// Represents the versioned Message of a Solana . + /// + public class MessageV0 + { + /// + /// The header of the . + /// + public MessageHeader Header { get; set; } + + /// + /// The list of account s present in the transaction. + /// + public IList AccountKeys { get; set; } + + /// + /// The list of s present in the transaction. + /// + public IList Instructions { get; set; } + + /// + /// Address table lookup for the transaction + /// + /// + public IList AddressTableLookups { get; set; } + + /// + /// The recent block hash for the transaction. + /// + public string RecentBlockhash { get; set; } + + /// + /// Return the message version + /// + /// + public virtual int Version() + { + return 0; + } + + /// + /// Return the number of accounts in the tables lookups + /// + /// + public int GetNumberAccountKeysFromLookups() + { + int count = 0; + foreach(var lookup in AddressTableLookups) + { + count += lookup.ReadonlyIndexes.Length + lookup.WritableIndexes.Length; + } + + return count; + } + + public MessageAccountKeys GetAccountKey(AccountKeysFromLookups accountKeysFromLookups, AddressLookupTableAccount addressLookupTableAccount) + { + if (GetNumberAccountKeysFromLookups() != staticAccount.Count) + } + + /// + /// Check whether an account is writable. + /// + /// The index of the account in the account keys. + /// true if the account is writable, false otherwise. + public bool IsAccountWritable(int index) + { + int numSignedAccounts = Header.RequiredSignatures; + int numStaticAccountKeys = AccountKeys.Count; + + if (index >= numStaticAccountKeys) + { + int lookupAccountKeysIndex = index - numStaticAccountKeys; + int numWritableLookupAccountKeys = this.AddressTableLookups.Select(p => p.WritableIndexes.Length).DefaultIfEmpty(0).Sum(); + return lookupAccountKeysIndex < numWritableLookupAccountKeys; + } + else if (index >= this.Header.RequiredSignatures) + { + int unsignedAccountIndex = index - numSignedAccounts; + int numUnsignedAccounts = numStaticAccountKeys - numSignedAccounts; + int numWritableUnsignedAccounts = numUnsignedAccounts - this.Header.ReadOnlyUnsignedAccounts; + return unsignedAccountIndex < numWritableUnsignedAccounts; + } + else + { + int numWritableSignedAccounts = numSignedAccounts - this.Header.ReadOnlySignedAccounts; + return index < numWritableSignedAccounts; + } + } + + /// + /// Check whether an account is a signer. + /// + /// The index of the account in the account keys. + /// true if the account is an expected signer, false otherwise. + public bool IsAccountSigner(int index) => index < Header.RequiredSignatures; + + /// + /// Serialize the message into the wire format. + /// + /// A byte array corresponding to the serialized message. + public byte[] Serialize() + { + byte[] accountAddressesLength = ShortVectorEncoding.EncodeLength(AccountKeys.Count); + byte[] instructionsLength = ShortVectorEncoding.EncodeLength(Instructions.Count); + byte[] addressTableLookupsLength = ShortVectorEncoding.EncodeLength(AddressTableLookups.Count); + int accountKeysBufferSize = AccountKeys.Count * 32; + + MemoryStream accountKeysBuffer = new(accountKeysBufferSize); + + foreach (PublicKey key in AccountKeys) + { + accountKeysBuffer.Write(key.KeyBytes); + } + + int messageBufferSize = MessageHeader.Layout.HeaderLength + PublicKey.PublicKeyLength + + accountAddressesLength.Length + addressTableLookupsLength.Length + +instructionsLength.Length + Instructions.Count + accountKeysBufferSize; + MemoryStream buffer = new(messageBufferSize); + buffer.Write(Header.ToBytes()); + buffer.Write(accountAddressesLength); + buffer.Write(accountKeysBuffer.ToArray()); + buffer.Write(Encoders.Base58.DecodeData(RecentBlockhash)); + buffer.Write(instructionsLength); + + foreach (CompiledInstruction compiledInstruction in Instructions) + { + buffer.WriteByte(compiledInstruction.ProgramIdIndex); + buffer.Write(compiledInstruction.KeyIndicesCount); + buffer.Write(compiledInstruction.KeyIndices); + buffer.Write(compiledInstruction.DataLength); + buffer.Write(compiledInstruction.Data); + } + + buffer.Write(addressTableLookupsLength); + + foreach(var addressTableLookup in AddressTableLookups) + { + // buffer.Write + } + + + return buffer.ToArray(); + } + + /// + /// Deserialize a compiled message into a Message object. + /// + /// The data to deserialize into the Message object. + /// The Message object instance. + public static MessageV0 Deserialize(ReadOnlySpan data) + { + // Read message header + byte numRequiredSignatures = data[MessageHeader.Layout.RequiredSignaturesOffset]; + byte numReadOnlySignedAccounts = data[MessageHeader.Layout.ReadOnlySignedAccountsOffset]; + byte numReadOnlyUnsignedAccounts = data[MessageHeader.Layout.ReadOnlyUnsignedAccountsOffset]; + + // Read account keys + (int accountAddressLength, int accountAddressLengthEncodedLength) = + ShortVectorEncoding.DecodeLength(data.Slice(MessageHeader.Layout.HeaderLength, + ShortVectorEncoding.SpanLength)); + List accountKeys = new(accountAddressLength); + for (int i = 0; i < accountAddressLength; i++) + { + ReadOnlySpan keyBytes = data.Slice( + MessageHeader.Layout.HeaderLength + accountAddressLengthEncodedLength + + i * PublicKey.PublicKeyLength, + PublicKey.PublicKeyLength); + accountKeys.Add(new PublicKey(keyBytes)); + } + + // Read block hash + string blockHash = + Encoders.Base58.EncodeData(data.Slice( + MessageHeader.Layout.HeaderLength + accountAddressLengthEncodedLength + + accountAddressLength * PublicKey.PublicKeyLength, + PublicKey.PublicKeyLength).ToArray()); + + // Read the number of instructions in the message + (int instructionsLength, int instructionsLengthEncodedLength) = + ShortVectorEncoding.DecodeLength( + data.Slice( + MessageHeader.Layout.HeaderLength + accountAddressLengthEncodedLength + + (accountAddressLength * PublicKey.PublicKeyLength) + PublicKey.PublicKeyLength, + ShortVectorEncoding.SpanLength)); + + List instructions = new(instructionsLength); + int instructionsOffset = + MessageHeader.Layout.HeaderLength + accountAddressLengthEncodedLength + + (accountAddressLength * PublicKey.PublicKeyLength) + PublicKey.PublicKeyLength + + instructionsLengthEncodedLength; + ReadOnlySpan instructionsData = data[instructionsOffset..]; + + // Read the instructions in the message + for (int i = 0; i < instructionsLength; i++) + { + (CompiledInstruction compiledInstruction, int instructionLength) = + CompiledInstruction.Deserialize(instructionsData); + instructions.Add(compiledInstruction); + instructionsData = instructionsData[instructionLength..]; + } + + return new MessageV0 + { + Header = new MessageHeader + { + RequiredSignatures = numRequiredSignatures, + ReadOnlySignedAccounts = numReadOnlySignedAccounts, + ReadOnlyUnsignedAccounts = numReadOnlyUnsignedAccounts + }, + RecentBlockhash = blockHash, + AccountKeys = accountKeys, + Instructions = instructions, + }; + } + + /// + /// Deserialize a compiled message encoded as base-64 into a Message object. + /// + /// The data to deserialize into the Message object. + /// The Transaction object. + /// Thrown when the given string is null. + public static MessageV0 Deserialize(string data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + byte[] decodedBytes; + + try + { + decodedBytes = Convert.FromBase64String(data); + } + catch (Exception ex) + { + throw new Exception("could not decode message data from base64", ex); + } + + return Deserialize(decodedBytes); + } + } + + /// + /// Address table lookup for a versioned transaction + /// + public class MessageAddressTableLookup + { + /// + /// + /// + /// + public PublicKey AccountKey { get; set; } + + /// + /// Indexes for readonly address + /// + /// + public int[] ReadonlyIndexes { get; set; } + + /// + /// Indexes for writable address + /// + /// + public int[] WritableIndexes { get; set; } + } + + /// + /// Represent an account key from a lookup + /// + public class AccountKeysFromLookups + { + /// + /// Writables accounts + /// + /// + public PublicKey[] Writables { get ; set; } + + /// + /// Readonly accounts + /// + /// + public PublicKey[] Readonly { get; set; } + } +} \ No newline at end of file diff --git a/src/Solnet.Rpc/Models/Transaction.cs b/src/Solnet.Rpc/Models/Transaction.cs index a433c53e..1dcc7fb8 100644 --- a/src/Solnet.Rpc/Models/Transaction.cs +++ b/src/Solnet.Rpc/Models/Transaction.cs @@ -405,4 +405,372 @@ public static Transaction Deserialize(string data) return Deserialize(decodedBytes); } } + + + /// + /// Represents a Transaction in Solana. + /// + public class TransactionV0 + { + /// + /// The transaction's fee payer. + /// + public PublicKey FeePayer { get; set; } + + /// + /// The list of s present in the transaction. + /// + public List Instructions { get; set; } + + /// + /// The recent block hash for the transaction. + /// + public string RecentBlockHash { get; set; } + + /// + /// The nonce information of the transaction. + /// + /// When this is set, the 's Nonce is used as the RecentBlockhash. + /// + /// + public NonceInformation NonceInformation { get; set; } + + /// + /// The signatures for the transaction. + /// + /// These are typically created by invoking the Build(IList{Account} signers) method of the , + /// but can be created by deserializing a Transaction and adding signatures manually. + /// + /// + public List Signatures { get; set; } + + /// + /// Compile the transaction data. + /// + public byte[] CompileMessage() + { + MessageBuilder messageBuilder = new() { FeePayer = FeePayer }; + + if (RecentBlockHash != null) messageBuilder.RecentBlockHash = RecentBlockHash; + if (NonceInformation != null) messageBuilder.NonceInformation = NonceInformation; + + foreach (TransactionInstruction instruction in Instructions) + { + messageBuilder.AddInstruction(instruction); + } + + return messageBuilder.Build(); + } + + /// + /// Verifies the signatures a given serialized message. + /// + /// true if they are valid, false otherwise. + private bool VerifySignatures(byte[] serializedMessage) => + Signatures.All(pair => pair.PublicKey.Verify(serializedMessage, pair.Signature)); + + /// + /// Verifies the signatures of a complete and signed transaction. + /// + /// true if they are valid, false otherwise. + public bool VerifySignatures() => VerifySignatures(CompileMessage()); + + /// + /// Sign the transaction with the specified signers. Multiple signatures may be applied to a transaction. + /// The first signature is considered primary and is used to identify and confirm transaction. + /// + /// + /// If the transaction FeePayer is not set, the first signer will be used as the transaction fee payer account. + /// + /// + /// Transaction fields SHOULD NOT be modified after the first call to Sign or an externally created signature + /// has been added to the transaction object, doing so will invalidate the signature and cause the transaction to be + /// rejected by the cluster. + /// + /// + /// The transaction must have been assigned a valid RecentBlockHash or NonceInformation before invoking this method. + /// + /// + /// + /// The signer accounts. + public bool Sign(IList signers) + { + Signatures ??= new List(); + IEnumerable uniqueSigners = DeduplicateSigners(signers); + byte[] serializedMessage = CompileMessage(); + + foreach (Account account in uniqueSigners) + { + byte[] signatureBytes = account.Sign(serializedMessage); + Signatures.Add(new SignaturePubKeyPair { PublicKey = account.PublicKey, Signature = signatureBytes }); + } + + return VerifySignatures(); + } + + /// + /// Sign the transaction with the specified signer. Multiple signatures may be applied to a transaction. + /// The first signature is considered primary and is used to identify and confirm transaction. + /// + /// + /// If the transaction FeePayer is not set, the first signer will be used as the transaction fee payer account. + /// + /// + /// Transaction fields SHOULD NOT be modified after the first call to Sign or an externally created signature + /// has been added to the transaction object, doing so will invalidate the signature and cause the transaction to be + /// rejected by the cluster. + /// + /// + /// The transaction must have been assigned a valid RecentBlockHash or NonceInformation before invoking this method. + /// + /// + /// + /// The signer account. + public bool Sign(Account signer) => Sign(new List { signer }); + + /// + /// Partially sign a transaction with the specified accounts. + /// All accounts must correspond to either the fee payer or a signer account in the transaction instructions. + /// + /// The signer accounts. + public void PartialSign(IList signers) + { + Signatures ??= new List(); + IEnumerable uniqueSigners = DeduplicateSigners(signers); + byte[] serializedMessage = CompileMessage(); + + foreach (Account account in uniqueSigners) + { + byte[] signatureBytes = account.Sign(serializedMessage); + Signatures.Add(new SignaturePubKeyPair { PublicKey = account.PublicKey, Signature = signatureBytes }); + } + } + + /// + /// Deduplicate the list of given signers. + /// + /// The signer accounts. + /// The signer accounts with removed duplicates + private static IEnumerable DeduplicateSigners(IEnumerable signers) + { + List uniqueSigners = new(); + HashSet seen = new(); + + foreach (Account account in signers) + { + if (seen.Contains(account)) continue; + + seen.Add(account); + uniqueSigners.Add(account); + } + + return uniqueSigners; + } + + /// + /// Partially sign a transaction with the specified account. + /// The account must correspond to either the fee payer or a signer account in the transaction instructions. + /// + /// The signer account. + public void PartialSign(Account signer) => PartialSign(new List { signer }); + + /// + /// Signs the transaction's message with the passed signer and add it to the transaction, serializing it. + /// + /// The signer. + /// The serialized transaction. + public byte[] Build(Account signer) + { + return Build(new List { signer }); + } + + /// + /// Signs the transaction's message with the passed list of signers and adds them to the transaction, serializing it. + /// + /// The list of signers. + /// The serialized transaction. + public byte[] Build(IList signers) + { + Sign(signers); + + return Serialize(); + } + + /// + /// Adds an externally created signature to the transaction. + /// The public key must correspond to either the fee payer or a signer account in the transaction instructions. + /// + /// The public key of the account that signed the transaction. + /// The transaction signature. + public void AddSignature(PublicKey publicKey, byte[] signature) + { + Signatures ??= new List(); + Signatures.Add(new SignaturePubKeyPair { PublicKey = publicKey, Signature = signature }); + } + + /// + /// Adds one or more instructions to the transaction. + /// + /// The instructions to add. + /// The transaction instance. + public Transaction Add(IEnumerable instructions) + { + Instructions ??= new List(); + Instructions.AddRange(instructions); + return this; + } + + /// + /// Adds an instruction to the transaction. + /// + /// The instruction to add. + /// The transaction instance. + public Transaction Add(TransactionInstruction instruction) => + Add(new List { instruction }); + + /// + /// Serializes the transaction into wire format. + /// + /// The transaction encoded in wire format. + public byte[] Serialize() + { + byte[] signaturesLength = ShortVectorEncoding.EncodeLength(Signatures.Count); + byte[] serializedMessage = CompileMessage(); + MemoryStream buffer = new(signaturesLength.Length + Signatures.Count * TransactionBuilder.SignatureLength + + serializedMessage.Length); + + buffer.Write(signaturesLength); + foreach (SignaturePubKeyPair signaturePair in Signatures) + { + buffer.Write(signaturePair.Signature); + } + + buffer.Write(serializedMessage); + return buffer.ToArray(); + } + + /// + /// Populate the Transaction from the given message and signatures. + /// + /// The object. + /// The list of signatures. + /// The Transaction object. + public static Transaction Populate(Message message, IList signatures = null) + { + Transaction tx = new() + { + RecentBlockHash = message.RecentBlockhash, + Signatures = new List(), + Instructions = new List() + }; + + if (message.Header.RequiredSignatures > 0) + { + tx.FeePayer = message.AccountKeys[0]; + } + + if (signatures != null) + { + for (int i = 0; i < signatures.Count; i++) + { + tx.Signatures.Add(new SignaturePubKeyPair + { + PublicKey = message.AccountKeys[i], + Signature = signatures[i] + }); + } + } + + for (int i = 0; i < message.Instructions.Count; i++) + { + CompiledInstruction compiledInstruction = message.Instructions[i]; + (int accountLength, _) = ShortVectorEncoding.DecodeLength(compiledInstruction.KeyIndicesCount); + + List accounts = new(accountLength); + for (int j = 0; j < accountLength; j++) + { + int k = compiledInstruction.KeyIndices[j]; + accounts.Add(new AccountMeta(message.AccountKeys[k], message.IsAccountWritable(k), + tx.Signatures.Any(pair => pair.PublicKey.Key == message.AccountKeys[k].Key) || message.IsAccountSigner(k))); + } + + TransactionInstruction instruction = new() + { + Keys = accounts, + ProgramId = message.AccountKeys[compiledInstruction.ProgramIdIndex], + Data = compiledInstruction.Data + }; + if (i == 0 && accounts.Any(a => a.PublicKey == "SysvarRecentB1ockHashes11111111111111111111")) + { + tx.NonceInformation = new NonceInformation { Instruction = instruction, Nonce = tx.RecentBlockHash }; + continue; + } + tx.Instructions.Add(instruction); + } + + return tx; + } + + /// + /// Populate the Transaction from the given compiled message and signatures. + /// + /// The compiled message, as base-64 encoded string. + /// The list of signatures. + /// The Transaction object. + public static Transaction Populate(string message, IList signatures = null) + => Populate(Message.Deserialize(message), signatures); + + /// + /// Deserialize a wire format transaction into a Transaction object. + /// + /// The data to deserialize into the Transaction object. + /// The Transaction object. + public static Transaction Deserialize(ReadOnlySpan data) + { + // Read number of signatures + (int signaturesLength, int encodedLength) = + ShortVectorEncoding.DecodeLength(data[..ShortVectorEncoding.SpanLength]); + List signatures = new(signaturesLength); + + for (int i = 0; i < signaturesLength; i++) + { + ReadOnlySpan signature = + data.Slice(encodedLength + (i * TransactionBuilder.SignatureLength), + TransactionBuilder.SignatureLength); + signatures.Add(signature.ToArray()); + } + + return Populate( + Message.Deserialize(data[ + (encodedLength + (signaturesLength * TransactionBuilder.SignatureLength))..]), + signatures); + } + + /// + /// Deserialize a transaction encoded as base-64 into a Transaction object. + /// + /// The data to deserialize into the Transaction object. + /// The Transaction object. + /// Thrown when the given string is null. + public static Transaction Deserialize(string data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + byte[] decodedBytes; + + try + { + decodedBytes = Convert.FromBase64String(data); + } + catch (Exception ex) + { + throw new Exception("could not decode transaction data from base64", ex); + } + + return Deserialize(decodedBytes); + } + } + + } \ No newline at end of file diff --git a/src/Solnet.Rpc/SolanaRpcClient.cs b/src/Solnet.Rpc/SolanaRpcClient.cs index b24a1235..afcc50ec 100644 --- a/src/Solnet.Rpc/SolanaRpcClient.cs +++ b/src/Solnet.Rpc/SolanaRpcClient.cs @@ -209,7 +209,7 @@ public RequestResult> GetBalance(string pubKey, /// public async Task> GetBlockAsync(ulong slot, - Commitment commitment = Commitment.Finalized, + Commitment commitment = Commitment.Finalized, int maxSupportedTransactionVersion = 0, TransactionDetailsFilterType transactionDetails = TransactionDetailsFilterType.Full, bool blockRewards = false) { @@ -223,14 +223,15 @@ public async Task> GetBlockAsync(ulong slot, KeyValue.Create("encoding", "json"), HandleTransactionDetails(transactionDetails), KeyValue.Create("rewards", blockRewards ? blockRewards : null), + KeyValue.Create("maxSupportedTransactionVersion", maxSupportedTransactionVersion), HandleCommitment(commitment)))); } /// public RequestResult GetBlock(ulong slot, Commitment commitment = Commitment.Finalized, - TransactionDetailsFilterType transactionDetails = TransactionDetailsFilterType.Full, + int maxSupportedTransactionVersion = 0, TransactionDetailsFilterType transactionDetails = TransactionDetailsFilterType.Full, bool blockRewards = false) - => GetBlockAsync(slot, commitment, transactionDetails, blockRewards).Result; + => GetBlockAsync(slot, commitment, maxSupportedTransactionVersion, transactionDetails, blockRewards).Result; /// @@ -249,7 +250,7 @@ public async Task>> GetBlocksAsync(ulong startSlot, ul /// public async Task> GetConfirmedBlockAsync(ulong slot, - Commitment commitment = Commitment.Finalized, + Commitment commitment = Commitment.Finalized, int maxSupportedTransactionVersion = 0, TransactionDetailsFilterType transactionDetails = TransactionDetailsFilterType.Full, bool blockRewards = false) { @@ -263,14 +264,15 @@ public async Task> GetConfirmedBlockAsync(ulong slot, KeyValue.Create("encoding", "json"), HandleTransactionDetails(transactionDetails), KeyValue.Create("rewards", blockRewards ? blockRewards : null), - HandleCommitment(commitment)))); + HandleCommitment(commitment), + KeyValue.Create("maxSupportedTransactionVersion" ,maxSupportedTransactionVersion)))); } /// public RequestResult GetConfirmedBlock(ulong slot, Commitment commitment = Commitment.Finalized, - TransactionDetailsFilterType transactionDetails = TransactionDetailsFilterType.Full, + int maxSupportedTransactionVersion = 0, TransactionDetailsFilterType transactionDetails = TransactionDetailsFilterType.Full, bool blockRewards = false) - => GetConfirmedBlockAsync(slot, commitment, transactionDetails, blockRewards).Result; + => GetConfirmedBlockAsync(slot, commitment, maxSupportedTransactionVersion, transactionDetails, blockRewards).Result; /// @@ -425,31 +427,35 @@ public async Task>>> GetLeaderSched /// public async Task> GetTransactionAsync(string signature, - Commitment commitment = Commitment.Finalized) + Commitment commitment = Commitment.Finalized, int maxSupportedTransactionVersion = 0) { return await SendRequestAsync("getTransaction", Parameters.Create(signature, - ConfigObject.Create(KeyValue.Create("encoding", "json"), HandleCommitment(commitment)))); + ConfigObject.Create(KeyValue.Create("encoding", "json"), + HandleCommitment(commitment), + KeyValue.Create("maxSupportedTransactionVersion", maxSupportedTransactionVersion)))); } - /// + /// public async Task> GetConfirmedTransactionAsync(string signature, - Commitment commitment = Commitment.Finalized) + Commitment commitment = Commitment.Finalized, int maxSupportedTransactionVersion = 0) { return await SendRequestAsync("getConfirmedTransaction", Parameters.Create(signature, - ConfigObject.Create(KeyValue.Create("encoding", "json"), HandleCommitment(commitment)))); + ConfigObject.Create(KeyValue.Create("encoding", "json"), + HandleCommitment(commitment), + KeyValue.Create("maxSupportedTransactionVersion", maxSupportedTransactionVersion)))); } /// public RequestResult GetTransaction(string signature, - Commitment commitment = Commitment.Finalized) - => GetTransactionAsync(signature, commitment).Result; + Commitment commitment = Commitment.Finalized, int maxSupportedTransactionVersion = 0) + => GetTransactionAsync(signature, commitment, maxSupportedTransactionVersion).Result; - /// + /// public RequestResult GetConfirmedTransaction(string signature, - Commitment commitment = Commitment.Finalized) => - GetConfirmedTransactionAsync(signature, commitment).Result; + Commitment commitment = Commitment.Finalized, int maxSupportedTransactionVersion = 0) => + GetConfirmedTransactionAsync(signature, commitment, maxSupportedTransactionVersion).Result; /// public async Task> GetBlockHeightAsync(Commitment commitment = Commitment.Finalized) diff --git a/test/Solnet.Rpc.Test/Resources/Http/Blocks/GetBlockConfirmedRequest.json b/test/Solnet.Rpc.Test/Resources/Http/Blocks/GetBlockConfirmedRequest.json index a7f4a162..9ba5920a 100644 --- a/test/Solnet.Rpc.Test/Resources/Http/Blocks/GetBlockConfirmedRequest.json +++ b/test/Solnet.Rpc.Test/Resources/Http/Blocks/GetBlockConfirmedRequest.json @@ -1 +1 @@ -{"method":"getBlock","params":[79662905,{"encoding":"json","commitment":"confirmed"}],"jsonrpc":"2.0","id":0} \ No newline at end of file +{"method":"getBlock","params":[79662905,{"encoding":"json","maxSupportedTransactionVersion":0,"commitment":"confirmed"}],"jsonrpc":"2.0","id":0} \ No newline at end of file diff --git a/test/Solnet.Rpc.Test/Resources/Http/Blocks/GetBlockRequest.json b/test/Solnet.Rpc.Test/Resources/Http/Blocks/GetBlockRequest.json index 938aad92..a96d097e 100644 --- a/test/Solnet.Rpc.Test/Resources/Http/Blocks/GetBlockRequest.json +++ b/test/Solnet.Rpc.Test/Resources/Http/Blocks/GetBlockRequest.json @@ -1 +1 @@ -{"method":"getBlock","params":[79662905,{"encoding":"json"}],"jsonrpc":"2.0","id":0} \ No newline at end of file +{"method":"getBlock","params":[79662905,{"encoding":"json","maxSupportedTransactionVersion":0}],"jsonrpc":"2.0","id":0} \ No newline at end of file diff --git a/test/Solnet.Rpc.Test/Resources/Http/Transaction/GetTransactionProcessedRequest.json b/test/Solnet.Rpc.Test/Resources/Http/Transaction/GetTransactionProcessedRequest.json index 872a41f3..0a81d834 100644 --- a/test/Solnet.Rpc.Test/Resources/Http/Transaction/GetTransactionProcessedRequest.json +++ b/test/Solnet.Rpc.Test/Resources/Http/Transaction/GetTransactionProcessedRequest.json @@ -1 +1 @@ -{"method":"getTransaction","params":["5as3w4KMpY23MP5T1nkPVksjXjN7hnjHKqiDxRMxUNcw5XsCGtStayZib1kQdyR2D9w8dR11Ha9Xk38KP3kbAwM1",{"encoding":"json","commitment":"processed"}],"jsonrpc":"2.0","id":0} \ No newline at end of file +{"method":"getTransaction","params":["5as3w4KMpY23MP5T1nkPVksjXjN7hnjHKqiDxRMxUNcw5XsCGtStayZib1kQdyR2D9w8dR11Ha9Xk38KP3kbAwM1",{"encoding":"json","commitment":"processed","maxSupportedTransactionVersion":0}],"jsonrpc":"2.0","id":0} \ No newline at end of file diff --git a/test/Solnet.Rpc.Test/Resources/Http/Transaction/GetTransactionRequest.json b/test/Solnet.Rpc.Test/Resources/Http/Transaction/GetTransactionRequest.json index 230a7558..76400a00 100644 --- a/test/Solnet.Rpc.Test/Resources/Http/Transaction/GetTransactionRequest.json +++ b/test/Solnet.Rpc.Test/Resources/Http/Transaction/GetTransactionRequest.json @@ -1 +1 @@ -{"method":"getTransaction","params":["5as3w4KMpY23MP5T1nkPVksjXjN7hnjHKqiDxRMxUNcw5XsCGtStayZib1kQdyR2D9w8dR11Ha9Xk38KP3kbAwM1",{"encoding":"json"}],"jsonrpc":"2.0","id":0} \ No newline at end of file +{"method":"getTransaction","params":["5as3w4KMpY23MP5T1nkPVksjXjN7hnjHKqiDxRMxUNcw5XsCGtStayZib1kQdyR2D9w8dR11Ha9Xk38KP3kbAwM1",{"encoding":"json","maxSupportedTransactionVersion":0}],"jsonrpc":"2.0","id":0} \ No newline at end of file diff --git a/test/Solnet.Rpc.Test/Resources/Http/Transaction/GetTransactionRequest2.json b/test/Solnet.Rpc.Test/Resources/Http/Transaction/GetTransactionRequest2.json index 43b9b7a7..cd04da7b 100644 --- a/test/Solnet.Rpc.Test/Resources/Http/Transaction/GetTransactionRequest2.json +++ b/test/Solnet.Rpc.Test/Resources/Http/Transaction/GetTransactionRequest2.json @@ -1 +1 @@ -{"method":"getTransaction","params":["3Q9mu4ePvtbtQzY1kpGmaViJKyBev6hgUppyXDF9hKgWHHnecwGLE2pSoFvNUF3h7acKyFwWd65bkwr9A1jN2CdT",{"encoding":"json"}],"jsonrpc":"2.0","id":0} \ No newline at end of file +{"method":"getTransaction","params":["3Q9mu4ePvtbtQzY1kpGmaViJKyBev6hgUppyXDF9hKgWHHnecwGLE2pSoFvNUF3h7acKyFwWd65bkwr9A1jN2CdT",{"encoding":"json","maxSupportedTransactionVersion":0}],"jsonrpc":"2.0","id":0} \ No newline at end of file