Skip to content

Commit 1fdfb7c

Browse files
✨ Implement Versioned Transactions (#44)
* ✨ Implement Versioned Transaction * 🔖 Bump version to 2.6.1.1
1 parent 6a13eb7 commit 1fdfb7c

File tree

14 files changed

+708
-26
lines changed

14 files changed

+708
-26
lines changed

SharedBuildProperties.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
33
<PropertyGroup>
44
<Product>Solana.Unity</Product>
5-
<Version>2.6.1.0</Version>
5+
<Version>2.6.1.1</Version>
66
<Copyright>Copyright 2022 &#169; Magicblock Labs</Copyright>
77
<Authors>Magicblock Labs</Authors>
88
<PublisherName>Magicblock Labs</PublisherName>

src/Solana.Unity.Dex/Jupiter/JupiterDex.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public async Task<SwapQuoteAg> GetSwapQuote(
9191
new("inputMint", inputMint.ToString()),
9292
new("outputMint", outputMint.ToString()),
9393
new("amount", amount.ToString()),
94-
new("asLegacyTransaction", "true")
94+
new("asLegacyTransaction", "false")
9595
};
9696

9797
if (slippageBps.HasValue) queryParams.Add(new KeyValuePair<string, string>("slippageBps", slippageBps.Value.ToString()));
@@ -152,7 +152,7 @@ public async Task<Transaction> Swap(
152152
FeeAccount = feeAccount,
153153
ComputeUnitPriceMicroLamports = computeUnitPriceMicroLamports,
154154
UseTokenLedger = useTokenLedger,
155-
AsLegacyTransaction = true
155+
AsLegacyTransaction = false
156156
};
157157

158158
var requestJson = JsonConvert.SerializeObject(req, _serializerOptions);

src/Solana.Unity.Rpc/Builders/MessageBuilder.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,17 @@ public class MessageBuilder
1818
/// <summary>
1919
/// The length of the block hash.
2020
/// </summary>
21-
private const int BlockHashLength = 32;
21+
protected const int BlockHashLength = 32;
2222

2323
/// <summary>
2424
/// The message header.
2525
/// </summary>
26-
private MessageHeader _messageHeader;
26+
protected MessageHeader _messageHeader;
2727

2828
/// <summary>
2929
/// The account keys list.
3030
/// </summary>
31-
private readonly AccountKeysList _accountKeysList;
31+
protected readonly AccountKeysList _accountKeysList;
3232

3333

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

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

122-
CompiledInstruction compiledInstruction = new CompiledInstruction
122+
CompiledInstruction compiledInstruction = new()
123123
{
124124
ProgramIdIndex = FindAccountIndex(keysList, instruction.ProgramId),
125125
KeyIndicesCount = ShortVectorEncoding.EncodeLength(keyCount),
@@ -184,7 +184,7 @@ internal byte[] Build()
184184
/// Gets the keys for the accounts present in the message.
185185
/// </summary>
186186
/// <returns>The list of <see cref="AccountMeta"/>.</returns>
187-
private List<AccountMeta> GetAccountKeys()
187+
protected List<AccountMeta> GetAccountKeys()
188188
{
189189
List<AccountMeta> newList = new();
190190
var keysList = _accountKeysList.AccountList;
@@ -219,7 +219,7 @@ private List<AccountMeta> GetAccountKeys()
219219
/// <param name="accountMetas">The <see cref="AccountMeta"/>.</param>
220220
/// <param name="publicKey">The public key.</param>
221221
/// <returns>The index of the</returns>
222-
private static byte FindAccountIndex(IList<AccountMeta> accountMetas, byte[] publicKey)
222+
protected static byte FindAccountIndex(IList<AccountMeta> accountMetas, byte[] publicKey)
223223
{
224224
string encodedKey = Encoders.Base58.EncodeData(publicKey);
225225
return FindAccountIndex(accountMetas, encodedKey);
@@ -231,7 +231,7 @@ private static byte FindAccountIndex(IList<AccountMeta> accountMetas, byte[] pub
231231
/// <param name="accountMetas">The <see cref="AccountMeta"/>.</param>
232232
/// <param name="publicKey">The public key.</param>
233233
/// <returns>The index of the</returns>
234-
private static byte FindAccountIndex(IList<AccountMeta> accountMetas, string publicKey)
234+
protected static byte FindAccountIndex(IList<AccountMeta> accountMetas, string publicKey)
235235
{
236236
for (byte index = 0; index < accountMetas.Count; index++)
237237
{
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using Solana.Unity.Rpc.Models;
2+
using Solana.Unity.Rpc.Utilities;
3+
using Solana.Unity.Wallet;
4+
using Solana.Unity.Wallet.Utilities;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.IO;
8+
9+
namespace Solana.Unity.Rpc.Builders
10+
{
11+
/// <summary>
12+
/// A compiled instruction within the message.
13+
/// </summary>
14+
public class VersionedMessageBuilder: MessageBuilder
15+
{
16+
17+
/// <summary>
18+
/// Address Table Lookups
19+
/// </summary>
20+
public List<MessageAddressTableLookup> AddressTableLookups { get; set; }
21+
22+
/// <summary>
23+
/// Builds the message into the wire format.
24+
/// </summary>
25+
/// <returns>The encoded message.</returns>
26+
internal override byte[] Build()
27+
{
28+
if (RecentBlockHash == null && NonceInformation == null)
29+
throw new Exception("recent block hash or nonce information is required");
30+
if (Instructions == null)
31+
throw new Exception("no instructions provided in the transaction");
32+
33+
// In case the user specified nonce information, we'll use it.
34+
if (NonceInformation != null)
35+
{
36+
RecentBlockHash = NonceInformation.Nonce;
37+
_accountKeysList.Add(NonceInformation.Instruction.Keys);
38+
_accountKeysList.Add(AccountMeta.ReadOnly(new PublicKey(NonceInformation.Instruction.ProgramId),
39+
false));
40+
List<TransactionInstruction> newInstructions = new() { NonceInformation.Instruction };
41+
newInstructions.AddRange(Instructions);
42+
Instructions = newInstructions;
43+
}
44+
45+
_messageHeader = new MessageHeader();
46+
47+
List<AccountMeta> keysList = GetAccountKeys();
48+
byte[] accountAddressesLength = ShortVectorEncoding.EncodeLength(keysList.Count);
49+
int compiledInstructionsLength = 0;
50+
List<CompiledInstruction> compiledInstructions = new();
51+
52+
foreach (TransactionInstruction instruction in Instructions)
53+
{
54+
int keyCount = instruction.Keys.Count;
55+
byte[] keyIndices = new byte[keyCount];
56+
57+
if (instruction.GetType() == typeof(VersionedTransactionInstruction))
58+
{
59+
keyIndices = ((VersionedTransactionInstruction)instruction).KeyIndices;
60+
}
61+
else
62+
{
63+
for (int i = 0; i < keyCount; i++)
64+
{
65+
keyIndices[i] = FindAccountIndex(keysList, instruction.Keys[i].PublicKey);
66+
}
67+
}
68+
69+
CompiledInstruction compiledInstruction = new()
70+
{
71+
ProgramIdIndex = FindAccountIndex(keysList, instruction.ProgramId),
72+
KeyIndicesCount = ShortVectorEncoding.EncodeLength(keyIndices.Length),
73+
KeyIndices = keyIndices,
74+
DataLength = ShortVectorEncoding.EncodeLength(instruction.Data.Length),
75+
Data = instruction.Data
76+
};
77+
compiledInstructions.Add(compiledInstruction);
78+
compiledInstructionsLength += compiledInstruction.Length();
79+
}
80+
81+
int accountKeysBufferSize = _accountKeysList.AccountList.Count * 32;
82+
MemoryStream accountKeysBuffer = new MemoryStream(accountKeysBufferSize);
83+
byte[] instructionsLength = ShortVectorEncoding.EncodeLength(compiledInstructions.Count);
84+
85+
foreach (AccountMeta accountMeta in keysList)
86+
{
87+
accountKeysBuffer.Write(accountMeta.PublicKeyBytes, 0, accountMeta.PublicKeyBytes.Length);
88+
if (accountMeta.IsSigner)
89+
{
90+
_messageHeader.RequiredSignatures += 1;
91+
if (!accountMeta.IsWritable)
92+
_messageHeader.ReadOnlySignedAccounts += 1;
93+
}
94+
else
95+
{
96+
if (!accountMeta.IsWritable)
97+
_messageHeader.ReadOnlyUnsignedAccounts += 1;
98+
}
99+
}
100+
101+
#region Build Message Body
102+
103+
int messageBufferSize = MessageHeader.Layout.HeaderLength + BlockHashLength +
104+
accountAddressesLength.Length +
105+
+instructionsLength.Length + compiledInstructionsLength + accountKeysBufferSize;
106+
MemoryStream buffer = new MemoryStream(messageBufferSize);
107+
byte[] messageHeaderBytes = _messageHeader.ToBytes();
108+
109+
buffer.Write(new byte[] { 128 }, 0, 1);
110+
buffer.Write(messageHeaderBytes, 0, messageHeaderBytes.Length);
111+
buffer.Write(accountAddressesLength, 0, accountAddressesLength.Length);
112+
buffer.Write(accountKeysBuffer.ToArray(), 0, accountKeysBuffer.ToArray().Length);
113+
var encodedRecentBlockHash = Encoders.Base58.DecodeData(RecentBlockHash);
114+
buffer.Write(encodedRecentBlockHash, 0, encodedRecentBlockHash.Length);
115+
buffer.Write(instructionsLength, 0, instructionsLength.Length);
116+
117+
foreach (CompiledInstruction compiledInstruction in compiledInstructions)
118+
{
119+
buffer.WriteByte(compiledInstruction.ProgramIdIndex);
120+
buffer.Write(compiledInstruction.KeyIndicesCount, 0, compiledInstruction.KeyIndicesCount.Length);
121+
buffer.Write(compiledInstruction.KeyIndices, 0, compiledInstruction.KeyIndices.Length);
122+
buffer.Write(compiledInstruction.DataLength, 0, compiledInstruction.DataLength.Length);
123+
buffer.Write(compiledInstruction.Data, 0, compiledInstruction.Data.Length);
124+
}
125+
126+
#endregion
127+
128+
var serializeAddressTableLookups = AddressTableLookupUtils.SerializeAddressTableLookups(AddressTableLookups);
129+
buffer.Write(serializeAddressTableLookups, 0, serializeAddressTableLookups.Length);
130+
131+
return buffer.ToArray();
132+
}
133+
}
134+
}

src/Solana.Unity.Rpc/Core/Http/CrossHttpClient.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace Solana.Unity.Rpc.Core.Http;
1313
/// </summary>
1414
public static class CrossHttpClient
1515
{
16-
private static TaskCompletionSource<UnityWebRequest.Result> _currentRequestTask;
16+
//private static TaskCompletionSource<UnityWebRequest.Result> _currentRequestTask;
1717

1818
/// <summary>
1919
/// Send an async request using HttpClient or UnityWebRequest if running on Unity
@@ -50,10 +50,10 @@ private static async Task<HttpResponseMessage> SendUnityWebRequest(Uri uri, Http
5050
request.SetRequestHeader("Content-Type", "application/json");
5151
}
5252
request.downloadHandler = new DownloadHandlerBuffer();
53-
if (_currentRequestTask != null)
54-
{
55-
await _currentRequestTask.Task;
56-
}
53+
// if (_currentRequestTask != null)
54+
// {
55+
// await _currentRequestTask.Task;
56+
// }
5757
UnityWebRequest.Result result = await SendRequest(request);
5858

5959
if (result == UnityWebRequest.Result.Success)
@@ -70,8 +70,8 @@ private static async Task<HttpResponseMessage> SendUnityWebRequest(Uri uri, Http
7070
{
7171
response.Content = new StringContent("Error: " + e.Message);
7272
response.StatusCode = HttpStatusCode.ExpectationFailed;
73-
_currentRequestTask?.TrySetException(e);
74-
_currentRequestTask = null;
73+
// _currentRequestTask?.TrySetException(e);
74+
// _currentRequestTask = null;
7575
}
7676
return response;
7777
}
@@ -81,7 +81,7 @@ private static async Task<HttpResponseMessage> SendUnityWebRequest(Uri uri, Http
8181
TaskCompletionSource<UnityWebRequest.Result> sendRequestTask = new();
8282
try
8383
{
84-
_currentRequestTask = sendRequestTask;
84+
//_currentRequestTask = sendRequestTask;
8585
UnityWebRequestAsyncOperation op = request.SendWebRequest();
8686

8787
if (request.isDone)
@@ -107,8 +107,8 @@ private static async Task<HttpResponseMessage> SendUnityWebRequest(Uri uri, Http
107107
catch (Exception ex)
108108
{
109109
sendRequestTask.TrySetException(ex);
110-
_currentRequestTask.SetException(ex);
111-
_currentRequestTask = null;
110+
// _currentRequestTask.SetException(ex);
111+
// _currentRequestTask = null;
112112
}
113113
return sendRequestTask.Task;
114114
}

src/Solana.Unity.Rpc/Models/AccountKeysList.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Solana.Unity.Rpc.Models
77
/// A wrapper around a list of <see cref="AccountMeta"/>s that takes care of deduplication and ordering according to
88
/// the wire format specification.
99
/// </summary>
10-
internal class AccountKeysList
10+
public class AccountKeysList
1111
{
1212
/// <summary>
1313
/// The account metas list.

src/Solana.Unity.Rpc/Models/Message.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ public byte[] Serialize()
154154
/// <returns>The Message object instance.</returns>
155155
public static Message Deserialize(ReadOnlySpan<byte> data)
156156
{
157+
// Check that the message is not a VersionedMessage
158+
byte prefix = data[0];
159+
byte maskedPrefix = (byte)(prefix & VersionedMessage.VersionPrefixMask);
160+
if(prefix != maskedPrefix)
161+
throw new NotSupportedException("The message is a VersionedMessage, use VersionedMessage." +
162+
"Deserialize instead.");
163+
157164
// Read message header
158165
byte numRequiredSignatures = data[MessageHeader.Layout.RequiredSignaturesOffset];
159166
byte numReadOnlySignedAccounts = data[MessageHeader.Layout.ReadOnlySignedAccountsOffset];

src/Solana.Unity.Rpc/Models/Transaction.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public class Transaction
6161
/// The list of <see cref="PublicKey"/>s present in the transaction.
6262
/// Memorized when deserializing a transaction. Avoid to change the keys order when deserializing.
6363
/// </summary>
64-
private IList<PublicKey> _accountKeys;
64+
internal IList<PublicKey> _accountKeys;
6565

6666
/// <summary>
6767
/// The recent block hash for the transaction.
@@ -88,7 +88,7 @@ public class Transaction
8888
/// <summary>
8989
/// Compile the transaction data.
9090
/// </summary>
91-
public byte[] CompileMessage()
91+
public virtual byte[] CompileMessage()
9292
{
9393
MessageBuilder messageBuilder = new() { FeePayer = FeePayer, AccountKeys = _accountKeys };
9494

@@ -273,7 +273,7 @@ public Transaction Add(TransactionInstruction instruction) =>
273273
/// Serializes the transaction into wire format.
274274
/// </summary>
275275
/// <returns>The transaction encoded in wire format.</returns>
276-
public byte[] Serialize()
276+
public virtual byte[] Serialize()
277277
{
278278
byte[] signaturesLength = ShortVectorEncoding.EncodeLength(Signatures.Count);
279279
byte[] serializedMessage = CompileMessage();
@@ -381,6 +381,14 @@ public static Transaction Deserialize(ReadOnlySpan<byte> data)
381381
TransactionBuilder.SignatureLength);
382382
signatures.Add(signature.ToArray());
383383
}
384+
385+
byte prefix = data[encodedLength + (signaturesLength * TransactionBuilder.SignatureLength)];
386+
byte maskedPrefix = (byte)(prefix & VersionedMessage.VersionPrefixMask);
387+
388+
// If the transaction is a VersionedTransaction, use VersionedTransaction.Deserialize instead.
389+
if (prefix != maskedPrefix)
390+
return VersionedTransaction.Deserialize(data);
391+
384392
return Populate(
385393
Message.Deserialize(data[
386394
(encodedLength + (signaturesLength * TransactionBuilder.SignatureLength))..]),

0 commit comments

Comments
 (0)