Skip to content

Commit

Permalink
✨ Implement Versioned Transactions (#44)
Browse files Browse the repository at this point in the history
* ✨ Implement Versioned Transaction

* 🔖 Bump version to 2.6.1.1
  • Loading branch information
GabrielePicco committed Sep 12, 2023
1 parent 6a13eb7 commit 1fdfb7c
Show file tree
Hide file tree
Showing 14 changed files with 708 additions and 26 deletions.
2 changes: 1 addition & 1 deletion SharedBuildProperties.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Product>Solana.Unity</Product>
<Version>2.6.1.0</Version>
<Version>2.6.1.1</Version>
<Copyright>Copyright 2022 &#169; Magicblock Labs</Copyright>
<Authors>Magicblock Labs</Authors>
<PublisherName>Magicblock Labs</PublisherName>
Expand Down
4 changes: 2 additions & 2 deletions src/Solana.Unity.Dex/Jupiter/JupiterDex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public JupiterDexAg(PublicKey account, string endpoint = "https://quote-api.jup.
new("inputMint", inputMint.ToString()),
new("outputMint", outputMint.ToString()),
new("amount", amount.ToString()),
new("asLegacyTransaction", "true")
new("asLegacyTransaction", "false")
};

if (slippageBps.HasValue) queryParams.Add(new KeyValuePair<string, string>("slippageBps", slippageBps.Value.ToString()));
Expand Down Expand Up @@ -152,7 +152,7 @@ public JupiterDexAg(PublicKey account, string endpoint = "https://quote-api.jup.
FeeAccount = feeAccount,
ComputeUnitPriceMicroLamports = computeUnitPriceMicroLamports,
UseTokenLedger = useTokenLedger,
AsLegacyTransaction = true
AsLegacyTransaction = false
};

var requestJson = JsonConvert.SerializeObject(req, _serializerOptions);
Expand Down
18 changes: 9 additions & 9 deletions src/Solana.Unity.Rpc/Builders/MessageBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ public class MessageBuilder
/// <summary>
/// The length of the block hash.
/// </summary>
private const int BlockHashLength = 32;
protected const int BlockHashLength = 32;

/// <summary>
/// The message header.
/// </summary>
private MessageHeader _messageHeader;
protected MessageHeader _messageHeader;

/// <summary>
/// The account keys list.
/// </summary>
private readonly AccountKeysList _accountKeysList;
protected readonly AccountKeysList _accountKeysList;


/// <summary>
Expand All @@ -40,7 +40,7 @@ public class MessageBuilder
/// <summary>
/// The list of instructions contained within this transaction.
/// </summary>
internal List<TransactionInstruction> Instructions { get; private set; }
internal List<TransactionInstruction> Instructions { get; private protected set; }

/// <summary>
/// The hash of a recent block.
Expand Down Expand Up @@ -83,7 +83,7 @@ internal MessageBuilder AddInstruction(TransactionInstruction instruction)
/// Builds the message into the wire format.
/// </summary>
/// <returns>The encoded message.</returns>
internal byte[] Build()
internal virtual byte[] Build()
{
if (RecentBlockHash == null && NonceInformation == null)
throw new Exception("recent block hash or nonce information is required");
Expand Down Expand Up @@ -119,7 +119,7 @@ internal byte[] Build()
keyIndices[i] = FindAccountIndex(keysList, instruction.Keys[i].PublicKey);
}

CompiledInstruction compiledInstruction = new CompiledInstruction
CompiledInstruction compiledInstruction = new()
{
ProgramIdIndex = FindAccountIndex(keysList, instruction.ProgramId),
KeyIndicesCount = ShortVectorEncoding.EncodeLength(keyCount),
Expand Down Expand Up @@ -184,7 +184,7 @@ internal byte[] Build()
/// Gets the keys for the accounts present in the message.
/// </summary>
/// <returns>The list of <see cref="AccountMeta"/>.</returns>
private List<AccountMeta> GetAccountKeys()
protected List<AccountMeta> GetAccountKeys()
{
List<AccountMeta> newList = new();
var keysList = _accountKeysList.AccountList;
Expand Down Expand Up @@ -219,7 +219,7 @@ private List<AccountMeta> GetAccountKeys()
/// <param name="accountMetas">The <see cref="AccountMeta"/>.</param>
/// <param name="publicKey">The public key.</param>
/// <returns>The index of the</returns>
private static byte FindAccountIndex(IList<AccountMeta> accountMetas, byte[] publicKey)
protected static byte FindAccountIndex(IList<AccountMeta> accountMetas, byte[] publicKey)
{
string encodedKey = Encoders.Base58.EncodeData(publicKey);
return FindAccountIndex(accountMetas, encodedKey);
Expand All @@ -231,7 +231,7 @@ private static byte FindAccountIndex(IList<AccountMeta> accountMetas, byte[] pub
/// <param name="accountMetas">The <see cref="AccountMeta"/>.</param>
/// <param name="publicKey">The public key.</param>
/// <returns>The index of the</returns>
private static byte FindAccountIndex(IList<AccountMeta> accountMetas, string publicKey)
protected static byte FindAccountIndex(IList<AccountMeta> accountMetas, string publicKey)
{
for (byte index = 0; index < accountMetas.Count; index++)
{
Expand Down
134 changes: 134 additions & 0 deletions src/Solana.Unity.Rpc/Builders/VersionedMessageBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using Solana.Unity.Rpc.Models;
using Solana.Unity.Rpc.Utilities;
using Solana.Unity.Wallet;
using Solana.Unity.Wallet.Utilities;
using System;
using System.Collections.Generic;
using System.IO;

namespace Solana.Unity.Rpc.Builders
{
/// <summary>
/// A compiled instruction within the message.
/// </summary>
public class VersionedMessageBuilder: MessageBuilder
{

/// <summary>
/// Address Table Lookups
/// </summary>
public List<MessageAddressTableLookup> AddressTableLookups { get; set; }

/// <summary>
/// Builds the message into the wire format.
/// </summary>
/// <returns>The encoded message.</returns>
internal override byte[] Build()
{
if (RecentBlockHash == null && NonceInformation == null)
throw new Exception("recent block hash or nonce information is required");
if (Instructions == null)
throw new Exception("no instructions provided in the transaction");

// In case the user specified nonce information, we'll use it.
if (NonceInformation != null)
{
RecentBlockHash = NonceInformation.Nonce;
_accountKeysList.Add(NonceInformation.Instruction.Keys);
_accountKeysList.Add(AccountMeta.ReadOnly(new PublicKey(NonceInformation.Instruction.ProgramId),
false));
List<TransactionInstruction> newInstructions = new() { NonceInformation.Instruction };
newInstructions.AddRange(Instructions);
Instructions = newInstructions;
}

_messageHeader = new MessageHeader();

List<AccountMeta> keysList = GetAccountKeys();
byte[] accountAddressesLength = ShortVectorEncoding.EncodeLength(keysList.Count);
int compiledInstructionsLength = 0;
List<CompiledInstruction> compiledInstructions = new();

foreach (TransactionInstruction instruction in Instructions)
{
int keyCount = instruction.Keys.Count;
byte[] keyIndices = new byte[keyCount];

if (instruction.GetType() == typeof(VersionedTransactionInstruction))
{
keyIndices = ((VersionedTransactionInstruction)instruction).KeyIndices;
}
else
{
for (int i = 0; i < keyCount; i++)
{
keyIndices[i] = FindAccountIndex(keysList, instruction.Keys[i].PublicKey);
}
}

CompiledInstruction compiledInstruction = new()
{
ProgramIdIndex = FindAccountIndex(keysList, instruction.ProgramId),
KeyIndicesCount = ShortVectorEncoding.EncodeLength(keyIndices.Length),
KeyIndices = keyIndices,
DataLength = ShortVectorEncoding.EncodeLength(instruction.Data.Length),
Data = instruction.Data
};
compiledInstructions.Add(compiledInstruction);
compiledInstructionsLength += compiledInstruction.Length();
}

int accountKeysBufferSize = _accountKeysList.AccountList.Count * 32;
MemoryStream accountKeysBuffer = new MemoryStream(accountKeysBufferSize);
byte[] instructionsLength = ShortVectorEncoding.EncodeLength(compiledInstructions.Count);

foreach (AccountMeta accountMeta in keysList)
{
accountKeysBuffer.Write(accountMeta.PublicKeyBytes, 0, accountMeta.PublicKeyBytes.Length);
if (accountMeta.IsSigner)
{
_messageHeader.RequiredSignatures += 1;
if (!accountMeta.IsWritable)
_messageHeader.ReadOnlySignedAccounts += 1;
}
else
{
if (!accountMeta.IsWritable)
_messageHeader.ReadOnlyUnsignedAccounts += 1;
}
}

#region Build Message Body

int messageBufferSize = MessageHeader.Layout.HeaderLength + BlockHashLength +
accountAddressesLength.Length +
+instructionsLength.Length + compiledInstructionsLength + accountKeysBufferSize;
MemoryStream buffer = new MemoryStream(messageBufferSize);
byte[] messageHeaderBytes = _messageHeader.ToBytes();

buffer.Write(new byte[] { 128 }, 0, 1);
buffer.Write(messageHeaderBytes, 0, messageHeaderBytes.Length);
buffer.Write(accountAddressesLength, 0, accountAddressesLength.Length);
buffer.Write(accountKeysBuffer.ToArray(), 0, accountKeysBuffer.ToArray().Length);
var encodedRecentBlockHash = Encoders.Base58.DecodeData(RecentBlockHash);
buffer.Write(encodedRecentBlockHash, 0, encodedRecentBlockHash.Length);
buffer.Write(instructionsLength, 0, instructionsLength.Length);

foreach (CompiledInstruction compiledInstruction in compiledInstructions)
{
buffer.WriteByte(compiledInstruction.ProgramIdIndex);
buffer.Write(compiledInstruction.KeyIndicesCount, 0, compiledInstruction.KeyIndicesCount.Length);
buffer.Write(compiledInstruction.KeyIndices, 0, compiledInstruction.KeyIndices.Length);
buffer.Write(compiledInstruction.DataLength, 0, compiledInstruction.DataLength.Length);
buffer.Write(compiledInstruction.Data, 0, compiledInstruction.Data.Length);
}

#endregion

var serializeAddressTableLookups = AddressTableLookupUtils.SerializeAddressTableLookups(AddressTableLookups);
buffer.Write(serializeAddressTableLookups, 0, serializeAddressTableLookups.Length);

return buffer.ToArray();
}
}
}
20 changes: 10 additions & 10 deletions src/Solana.Unity.Rpc/Core/Http/CrossHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Solana.Unity.Rpc.Core.Http;
/// </summary>
public static class CrossHttpClient
{
private static TaskCompletionSource<UnityWebRequest.Result> _currentRequestTask;
//private static TaskCompletionSource<UnityWebRequest.Result> _currentRequestTask;

/// <summary>
/// Send an async request using HttpClient or UnityWebRequest if running on Unity
Expand Down Expand Up @@ -50,10 +50,10 @@ private static async Task<HttpResponseMessage> SendUnityWebRequest(Uri uri, Http
request.SetRequestHeader("Content-Type", "application/json");
}
request.downloadHandler = new DownloadHandlerBuffer();
if (_currentRequestTask != null)
{
await _currentRequestTask.Task;
}
// if (_currentRequestTask != null)
// {
// await _currentRequestTask.Task;
// }
UnityWebRequest.Result result = await SendRequest(request);

if (result == UnityWebRequest.Result.Success)
Expand All @@ -70,8 +70,8 @@ private static async Task<HttpResponseMessage> SendUnityWebRequest(Uri uri, Http
{
response.Content = new StringContent("Error: " + e.Message);
response.StatusCode = HttpStatusCode.ExpectationFailed;
_currentRequestTask?.TrySetException(e);
_currentRequestTask = null;
// _currentRequestTask?.TrySetException(e);
// _currentRequestTask = null;
}
return response;
}
Expand All @@ -81,7 +81,7 @@ private static Task<UnityWebRequest.Result> SendRequest(UnityWebRequest request)
TaskCompletionSource<UnityWebRequest.Result> sendRequestTask = new();
try
{
_currentRequestTask = sendRequestTask;
//_currentRequestTask = sendRequestTask;
UnityWebRequestAsyncOperation op = request.SendWebRequest();

if (request.isDone)
Expand All @@ -107,8 +107,8 @@ private static Task<UnityWebRequest.Result> SendRequest(UnityWebRequest request)
catch (Exception ex)
{
sendRequestTask.TrySetException(ex);
_currentRequestTask.SetException(ex);
_currentRequestTask = null;
// _currentRequestTask.SetException(ex);
// _currentRequestTask = null;
}
return sendRequestTask.Task;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Solana.Unity.Rpc/Models/AccountKeysList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Solana.Unity.Rpc.Models
/// A wrapper around a list of <see cref="AccountMeta"/>s that takes care of deduplication and ordering according to
/// the wire format specification.
/// </summary>
internal class AccountKeysList
public class AccountKeysList
{
/// <summary>
/// The account metas list.
Expand Down
7 changes: 7 additions & 0 deletions src/Solana.Unity.Rpc/Models/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ public byte[] Serialize()
/// <returns>The Message object instance.</returns>
public static Message Deserialize(ReadOnlySpan<byte> data)
{
// Check that the message is not a VersionedMessage
byte prefix = data[0];
byte maskedPrefix = (byte)(prefix & VersionedMessage.VersionPrefixMask);
if(prefix != maskedPrefix)
throw new NotSupportedException("The message is a VersionedMessage, use VersionedMessage." +
"Deserialize instead.");

// Read message header
byte numRequiredSignatures = data[MessageHeader.Layout.RequiredSignaturesOffset];
byte numReadOnlySignedAccounts = data[MessageHeader.Layout.ReadOnlySignedAccountsOffset];
Expand Down
14 changes: 11 additions & 3 deletions src/Solana.Unity.Rpc/Models/Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public class Transaction
/// The list of <see cref="PublicKey"/>s present in the transaction.
/// Memorized when deserializing a transaction. Avoid to change the keys order when deserializing.
/// </summary>
private IList<PublicKey> _accountKeys;
internal IList<PublicKey> _accountKeys;

/// <summary>
/// The recent block hash for the transaction.
Expand All @@ -88,7 +88,7 @@ public class Transaction
/// <summary>
/// Compile the transaction data.
/// </summary>
public byte[] CompileMessage()
public virtual byte[] CompileMessage()
{
MessageBuilder messageBuilder = new() { FeePayer = FeePayer, AccountKeys = _accountKeys };

Expand Down Expand Up @@ -273,7 +273,7 @@ public Transaction Add(IEnumerable<TransactionInstruction> instructions)
/// Serializes the transaction into wire format.
/// </summary>
/// <returns>The transaction encoded in wire format.</returns>
public byte[] Serialize()
public virtual byte[] Serialize()
{
byte[] signaturesLength = ShortVectorEncoding.EncodeLength(Signatures.Count);
byte[] serializedMessage = CompileMessage();
Expand Down Expand Up @@ -381,6 +381,14 @@ public static Transaction Deserialize(ReadOnlySpan<byte> data)
TransactionBuilder.SignatureLength);
signatures.Add(signature.ToArray());
}

byte prefix = data[encodedLength + (signaturesLength * TransactionBuilder.SignatureLength)];
byte maskedPrefix = (byte)(prefix & VersionedMessage.VersionPrefixMask);

// If the transaction is a VersionedTransaction, use VersionedTransaction.Deserialize instead.
if (prefix != maskedPrefix)
return VersionedTransaction.Deserialize(data);

return Populate(
Message.Deserialize(data[
(encodedLength + (signaturesLength * TransactionBuilder.SignatureLength))..]),
Expand Down
Loading

0 comments on commit 1fdfb7c

Please sign in to comment.