• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bmresearch / Solnet / 8950601615

04 May 2024 12:47PM UTC coverage: 77.239% (-3.7%) from 80.902%
8950601615

push

github

web-flow
Update dotnet.yml

1123 of 1710 branches covered (65.67%)

Branch coverage included in aggregate %.

5199 of 6475 relevant lines covered (80.29%)

1304486.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

42.98
/src/Solnet.Rpc/Models/Message.cs
1
using Solnet.Rpc.Utilities;
2
using Solnet.Wallet;
3
using Solnet.Wallet.Utilities;
4
using System;
5
using System.Collections.Generic;
6
using System.IO;
7
using System.Linq;
8

9
namespace Solnet.Rpc.Models
10
{
11
    /// <summary>
12
    /// The message header.
13
    /// </summary>
14
    public class MessageHeader
15
    {
16
        #region Layout
17

18
        /// <summary>
19
        /// Represents the layout of the <see cref="MessageHeader"/> encoded values.
20
        /// </summary>
21
        internal static class Layout
22
        {
23
            /// <summary>
24
            /// The offset at which the byte that defines the number of required signatures begins.
25
            /// </summary>
26
            internal const int RequiredSignaturesOffset = 0;
27

28
            /// <summary>
29
            /// The offset at which the byte that defines the number of read-only signer accounts begins.
30
            /// </summary>
31
            internal const int ReadOnlySignedAccountsOffset = 1;
32

33
            /// <summary>
34
            /// The offset at which the byte that defines the number of read-only non-signer accounts begins.
35
            /// </summary>
36
            internal const int ReadOnlyUnsignedAccountsOffset = 2;
37

38
            /// <summary>
39
            /// The message header length.
40
            /// </summary>
41
            internal const int HeaderLength = 3;
42
        }
43

44
        #endregion Layout
45

46
        /// <summary>
47
        /// The number of required signatures.
48
        /// </summary>
49
        public byte RequiredSignatures { get; set; }
256✔
50

51
        /// <summary>
52
        /// The number of read-only signed accounts.
53
        /// </summary>
54
        public byte ReadOnlySignedAccounts { get; set; }
123✔
55

56
        /// <summary>
57
        /// The number of read-only non-signed accounts.
58
        /// </summary>
59
        public byte ReadOnlyUnsignedAccounts { get; set; }
227✔
60

61
        /// <summary>
62
        /// Convert the message header to byte array format.
63
        /// </summary>
64
        /// <returns>The byte array.</returns>
65
        internal byte[] ToBytes()
66
        {
67
            return new[] { RequiredSignatures, ReadOnlySignedAccounts, ReadOnlyUnsignedAccounts };
25✔
68
        }
69
    }
70

71
    /// <summary>
72
    /// Represents the Message of a Solana <see cref="Transaction"/>.
73
    /// </summary>
74
    public class Message
75
    {
76
        /// <summary>
77
        /// The header of the <see cref="Message"/>.
78
        /// </summary>
79
        public MessageHeader Header { get; set; }
226✔
80

81
        /// <summary>
82
        /// The list of account <see cref="PublicKey"/>s present in the transaction.
83
        /// </summary>
84
        public IList<PublicKey> AccountKeys { get; set; }
408✔
85

86
        /// <summary>
87
        /// The list of <see cref="TransactionInstruction"/>s present in the transaction.
88
        /// </summary>
89
        public IList<CompiledInstruction> Instructions { get; set; }
144✔
90

91
        /// <summary>
92
        /// The recent block hash for the transaction.
93
        /// </summary>
94
        public string RecentBlockhash { get; set; }
53✔
95

96
        /// <summary>
97
        /// Check whether an account is writable.
98
        /// </summary>
99
        /// <param name="index">The index of the account in the account keys.</param>
100
        /// <returns>true if the account is writable, false otherwise.</returns>
101
        public bool IsAccountWritable(int index) => index < Header.RequiredSignatures - Header.ReadOnlySignedAccounts ||
53!
102
                                                    (index >= Header.RequiredSignatures &&
53✔
103
                                                     index < AccountKeys.Count - Header.ReadOnlyUnsignedAccounts);
53✔
104

105
        /// <summary>
106
        /// Check whether an account is a signer.
107
        /// </summary>
108
        /// <param name="index">The index of the account in the account keys.</param>
109
        /// <returns>true if the account is an expected signer, false otherwise.</returns>
110
        public bool IsAccountSigner(int index) => index < Header.RequiredSignatures;
27✔
111

112
        /// <summary>
113
        /// Serialize the message into the wire format.
114
        /// </summary>
115
        /// <returns>A byte array corresponding to the serialized message.</returns>
116
        public byte[] Serialize()
117
        {
118
            byte[] accountAddressesLength = ShortVectorEncoding.EncodeLength(AccountKeys.Count);
1✔
119
            byte[] instructionsLength = ShortVectorEncoding.EncodeLength(Instructions.Count);
1✔
120
            int accountKeysBufferSize = AccountKeys.Count * 32;
1✔
121

122
            MemoryStream accountKeysBuffer = new(accountKeysBufferSize);
1✔
123

124
            foreach (PublicKey key in AccountKeys)
14✔
125
            {
126
                accountKeysBuffer.Write(key.KeyBytes);
6✔
127
            }
128

129
            int messageBufferSize = MessageHeader.Layout.HeaderLength + PublicKey.PublicKeyLength +
1✔
130
                                    accountAddressesLength.Length +
1✔
131
                                    +instructionsLength.Length + Instructions.Count + accountKeysBufferSize;
1✔
132
            MemoryStream buffer = new(messageBufferSize);
1✔
133
            buffer.Write(Header.ToBytes());
1✔
134
            buffer.Write(accountAddressesLength);
1✔
135
            buffer.Write(accountKeysBuffer.ToArray());
1✔
136
            buffer.Write(Encoders.Base58.DecodeData(RecentBlockhash));
1✔
137
            buffer.Write(instructionsLength);
1✔
138

139
            foreach (CompiledInstruction compiledInstruction in Instructions)
6✔
140
            {
141
                buffer.WriteByte(compiledInstruction.ProgramIdIndex);
2✔
142
                buffer.Write(compiledInstruction.KeyIndicesCount);
2✔
143
                buffer.Write(compiledInstruction.KeyIndices);
2✔
144
                buffer.Write(compiledInstruction.DataLength);
2✔
145
                buffer.Write(compiledInstruction.Data);
2✔
146
            }
147
            return buffer.ToArray();
1✔
148
        }
149

150
        /// <summary>
151
        /// Deserialize a compiled message into a Message object.
152
        /// </summary>
153
        /// <param name="data">The data to deserialize into the Message object.</param>
154
        /// <returns>The Message object instance.</returns>
155
        public static Message Deserialize(ReadOnlySpan<byte> data)
156
        {
157

158
            // Check that the message is not a VersionedMessage
159
            byte prefix = data[0];
43✔
160
            byte maskedPrefix = (byte)(prefix & VersionedMessage.VersionPrefixMask);
43✔
161
            if (prefix != maskedPrefix)
43!
162
                throw new NotSupportedException("The message is a VersionedMessage, use VersionedMessage." +
×
163
                                                    "Deserialize instead.");
×
164

165
            // Read message header
166
            byte numRequiredSignatures = data[MessageHeader.Layout.RequiredSignaturesOffset];
43✔
167
            byte numReadOnlySignedAccounts = data[MessageHeader.Layout.ReadOnlySignedAccountsOffset];
43✔
168
            byte numReadOnlyUnsignedAccounts = data[MessageHeader.Layout.ReadOnlyUnsignedAccountsOffset];
43✔
169

170
            // Read account keys
171
            (int accountAddressLength, int accountAddressLengthEncodedLength) =
43✔
172
                ShortVectorEncoding.DecodeLength(data.Slice(MessageHeader.Layout.HeaderLength,
43✔
173
                    ShortVectorEncoding.SpanLength));
43✔
174
            List<PublicKey> accountKeys = new(accountAddressLength);
43✔
175
            for (int i = 0; i < accountAddressLength; i++)
804✔
176
            {
177
                ReadOnlySpan<byte> keyBytes = data.Slice(
359✔
178
                    MessageHeader.Layout.HeaderLength + accountAddressLengthEncodedLength +
359✔
179
                    i * PublicKey.PublicKeyLength,
359✔
180
                    PublicKey.PublicKeyLength);
359✔
181
                accountKeys.Add(new PublicKey(keyBytes));
359✔
182
            }
183

184
            // Read block hash
185
            string blockHash =
43✔
186
                Encoders.Base58.EncodeData(data.Slice(
43✔
187
                    MessageHeader.Layout.HeaderLength + accountAddressLengthEncodedLength +
43✔
188
                    accountAddressLength * PublicKey.PublicKeyLength,
43✔
189
                    PublicKey.PublicKeyLength).ToArray());
43✔
190

191
            // Read the number of instructions in the message
192
            (int instructionsLength, int instructionsLengthEncodedLength) =
43✔
193
                ShortVectorEncoding.DecodeLength(
43✔
194
                    data.Slice(
43✔
195
                        MessageHeader.Layout.HeaderLength + accountAddressLengthEncodedLength +
43✔
196
                        (accountAddressLength * PublicKey.PublicKeyLength) + PublicKey.PublicKeyLength,
43✔
197
                        ShortVectorEncoding.SpanLength));
43✔
198

199
            List<CompiledInstruction> instructions = new(instructionsLength);
43✔
200
            int instructionsOffset =
43✔
201
                MessageHeader.Layout.HeaderLength + accountAddressLengthEncodedLength +
43✔
202
                (accountAddressLength * PublicKey.PublicKeyLength) + PublicKey.PublicKeyLength +
43✔
203
                instructionsLengthEncodedLength;
43✔
204
            ReadOnlySpan<byte> instructionsData = data[instructionsOffset..];
43✔
205

206
            // Read the instructions in the message
207
            for (int i = 0; i < instructionsLength; i++)
314✔
208
            {
209
                (CompiledInstruction compiledInstruction, int instructionLength) =
114✔
210
                    CompiledInstruction.Deserialize(instructionsData);
114✔
211
                instructions.Add(compiledInstruction);
114✔
212
                instructionsData = instructionsData[instructionLength..];
114✔
213
            }
214

215
            return new Message
43✔
216
            {
43✔
217
                Header = new MessageHeader
43✔
218
                {
43✔
219
                    RequiredSignatures = numRequiredSignatures,
43✔
220
                    ReadOnlySignedAccounts = numReadOnlySignedAccounts,
43✔
221
                    ReadOnlyUnsignedAccounts = numReadOnlyUnsignedAccounts
43✔
222
                },
43✔
223
                RecentBlockhash = blockHash,
43✔
224
                AccountKeys = accountKeys,
43✔
225
                Instructions = instructions,
43✔
226
            };
43✔
227
        }
228

229
        /// <summary>
230
        /// Deserialize a compiled message encoded as base-64 into a Message object.
231
        /// </summary>
232
        /// <param name="data">The data to deserialize into the Message object.</param>
233
        /// <returns>The Transaction object.</returns>
234
        /// <exception cref="ArgumentNullException">Thrown when the given string is null.</exception>
235
        public static Message Deserialize(string data)
236
        {
237
            if (data == null)
37!
238
                throw new ArgumentNullException(nameof(data));
×
239

240
            byte[] decodedBytes;
241

242
            try
243
            {
244
                decodedBytes = Convert.FromBase64String(data);
37✔
245
            }
37✔
246
            catch (Exception ex)
×
247
            {
248
                throw new Exception("could not decode message data from base64", ex);
×
249
            }
250

251
            return Deserialize(decodedBytes);
37✔
252
        }
253

254
        public class VersionedMessage : Message
255
        {
256
            /// <summary>
257
            /// Version prefix Mask.
258
            /// </summary>
259
            public const byte VersionPrefixMask = 0x7F;
260

261
            /// <summary>
262
            /// Address table lookup
263
            /// </summary>
264
            public List<MessageAddressTableLookup> AddressTableLookups { get; set; }
×
265

266

267
            /// <summary>
268
            /// Deserialize a compiled message into a Message object.
269
            /// </summary>
270
            /// <param name="data">The data to deserialize into the Message object.</param>
271
            /// <returns>The Message object instance.</returns>
272
            public static new VersionedMessage Deserialize(ReadOnlySpan<byte> data)
273
            {
274
                byte prefix = data[0];
×
275
                byte maskedPrefix = (byte)(prefix & VersionPrefixMask);
×
276

277
                if (prefix == maskedPrefix)
×
278
                    throw new NotSupportedException("Expected versioned message but received legacy message");
×
279

280
                byte version = maskedPrefix;
×
281

282
                if (version != 0)
×
283
                    throw new NotSupportedException($"Expected versioned message with version 0 but found version {version}");
×
284

285
                data = data.Slice(1, data.Length - 1); // Remove the processed prefix byte
×
286

287
                // Read message header
288
                byte numRequiredSignatures = data[MessageHeader.Layout.RequiredSignaturesOffset];
×
289
                byte numReadOnlySignedAccounts = data[MessageHeader.Layout.ReadOnlySignedAccountsOffset];
×
290
                byte numReadOnlyUnsignedAccounts = data[MessageHeader.Layout.ReadOnlyUnsignedAccountsOffset];
×
291

292
                // Read account keys
293
                (int accountAddressLength, int accountAddressLengthEncodedLength) =
×
294
                    ShortVectorEncoding.DecodeLength(data.Slice(MessageHeader.Layout.HeaderLength,
×
295
                        ShortVectorEncoding.SpanLength));
×
296
                List<PublicKey> accountKeys = new(accountAddressLength);
×
297
                for (int i = 0; i < accountAddressLength; i++)
×
298
                {
299
                    ReadOnlySpan<byte> keyBytes = data.Slice(
×
300
                        MessageHeader.Layout.HeaderLength + accountAddressLengthEncodedLength +
×
301
                        i * PublicKey.PublicKeyLength,
×
302
                        PublicKey.PublicKeyLength);
×
303
                    accountKeys.Add(new PublicKey(keyBytes));
×
304
                }
305

306
                // Read block hash
307
                string blockHash =
×
308
                    Encoders.Base58.EncodeData(data.Slice(
×
309
                        MessageHeader.Layout.HeaderLength + accountAddressLengthEncodedLength +
×
310
                        accountAddressLength * PublicKey.PublicKeyLength,
×
311
                        PublicKey.PublicKeyLength).ToArray());
×
312

313
                // Read the number of instructions in the message
314
                (int instructionsLength, int instructionsLengthEncodedLength) =
×
315
                    ShortVectorEncoding.DecodeLength(
×
316
                        data.Slice(
×
317
                            MessageHeader.Layout.HeaderLength + accountAddressLengthEncodedLength +
×
318
                            (accountAddressLength * PublicKey.PublicKeyLength) + PublicKey.PublicKeyLength,
×
319
                            ShortVectorEncoding.SpanLength));
×
320

321
                List<CompiledInstruction> instructions = new(instructionsLength);
×
322
                int instructionsOffset =
×
323
                    MessageHeader.Layout.HeaderLength + accountAddressLengthEncodedLength +
×
324
                    (accountAddressLength * PublicKey.PublicKeyLength) + PublicKey.PublicKeyLength +
×
325
                    instructionsLengthEncodedLength;
×
326
                ReadOnlySpan<byte> instructionsData = data[instructionsOffset..];
×
327

328
                int instructionsDataLength = 0;
×
329

330
                // Read the instructions in the message
331
                for (int i = 0; i < instructionsLength; i++)
×
332
                {
333
                    (CompiledInstruction compiledInstruction, int instructionLength) =
×
334
                        CompiledInstruction.Deserialize(instructionsData);
×
335
                    instructions.Add(compiledInstruction);
×
336
                    instructionsData = instructionsData[instructionLength..];
×
337
                    instructionsDataLength += instructionLength;
×
338
                }
339

340
                // Read the address table lookups
341
                int tableLookupOffset =
×
342
                    MessageHeader.Layout.HeaderLength + accountAddressLengthEncodedLength +
×
343
                    (accountAddressLength * PublicKey.PublicKeyLength) + PublicKey.PublicKeyLength +
×
344
                    instructionsLengthEncodedLength + instructionsDataLength;
×
345

346
                ReadOnlySpan<byte> tableLookupData = data[tableLookupOffset..];
×
347

348
                (int addressTableLookupsCount, int addressTableLookupsEncodedCount) = ShortVectorEncoding.DecodeLength(tableLookupData);
×
349
                List<MessageAddressTableLookup> addressTableLookups = new();
×
350
                tableLookupData = tableLookupData[addressTableLookupsEncodedCount..];
×
351

352
                for (int i = 0; i < addressTableLookupsCount; i++)
×
353
                {
354
                    byte[] accountKeyBytes = tableLookupData.Slice(0, PublicKey.PublicKeyLength).ToArray();
×
355
                    PublicKey accountKey = new(accountKeyBytes);
×
356
                    tableLookupData = tableLookupData.Slice(PublicKey.PublicKeyLength);
×
357

358
                    (int writableIndexesLength, int writableIndexesEncodedLength) = ShortVectorEncoding.DecodeLength(tableLookupData);
×
359
                    List<byte> writableIndexes = tableLookupData.Slice(writableIndexesEncodedLength, writableIndexesLength).ToArray().ToList();
×
360
                    tableLookupData = tableLookupData.Slice(writableIndexesEncodedLength + writableIndexesLength);
×
361

362
                    (int readonlyIndexesLength, int readonlyIndexesEncodedLength) = ShortVectorEncoding.DecodeLength(tableLookupData);
×
363
                    List<byte> readonlyIndexes = tableLookupData.Slice(readonlyIndexesEncodedLength, readonlyIndexesLength).ToArray().ToList();
×
364
                    tableLookupData = tableLookupData.Slice(readonlyIndexesEncodedLength + readonlyIndexesLength);
×
365

366
                    addressTableLookups.Add(new MessageAddressTableLookup
×
367
                    {
×
368
                        AccountKey = accountKey,
×
369
                        WritableIndexes = writableIndexes.ToArray(),
×
370
                        ReadonlyIndexes = readonlyIndexes.ToArray()
×
371
                    });
×
372
                }
373

374
                return new VersionedMessage()
×
375
                {
×
376
                    Header = new MessageHeader()
×
377
                    {
×
378
                        RequiredSignatures = numRequiredSignatures,
×
379
                        ReadOnlySignedAccounts = numReadOnlySignedAccounts,
×
380
                        ReadOnlyUnsignedAccounts = numReadOnlyUnsignedAccounts
×
381
                    },
×
382
                    RecentBlockhash = blockHash,
×
383
                    AccountKeys = accountKeys,
×
384
                    Instructions = instructions,
×
385
                    AddressTableLookups = addressTableLookups
×
386
                };
×
387
            }
388

389
            /// <summary>
390
            /// Deserialize a compiled message encoded as base-64 into a Message object.
391
            /// </summary>
392
            /// <param name="data">The data to deserialize into the Message object.</param>
393
            /// <returns>The Transaction object.</returns>
394
            /// <exception cref="ArgumentNullException">Thrown when the given string is null.</exception>
395
            public static new VersionedMessage Deserialize(string data)
396
            {
397
                if (data == null)
×
398
                    throw new ArgumentNullException(nameof(data));
×
399

400
                byte[] decodedBytes;
401

402
                try
403
                {
404
                    decodedBytes = Convert.FromBase64String(data);
×
405
                }
×
406
                catch (Exception ex)
×
407
                {
408
                    throw new Exception("could not decode message data from base64", ex);
×
409
                }
410

411
                return Deserialize(decodedBytes);
×
412
            }
413

414
            /// <summary>
415
            /// Deserialize the message version
416
            /// </summary>
417
            /// <param name="serializedMessage"></param>
418
            /// <returns></returns>
419
            public static string DeserializeMessageVersion(byte[] serializedMessage)
420
            {
421
                byte prefix = serializedMessage[0];
×
422
                byte maskedPrefix = (byte)(prefix & VersionPrefixMask);
×
423

424
                // If the highest bit of the prefix is not set, the message is not versioned
425
                if (maskedPrefix == prefix)
×
426
                {
427
                    return "legacy";
×
428
                }
429

430
                // The lower 7 bits of the prefix indicate the message version
431
                return maskedPrefix.ToString();
×
432
            }
433
        }
434

435
        /// <summary>
436
        /// Message Address Lookup table
437
        /// </summary>
438
        public class MessageAddressTableLookup
439
        {
440
            /// <summary>
441
            /// Account Key
442
            /// </summary>
443
            public PublicKey AccountKey { get; set; }
×
444

445
            /// <summary>
446
            /// Writable indexes
447
            /// </summary>
448
            public byte[] WritableIndexes { get; set; }
×
449

450
            /// <summary>
451
            /// Read only indexes
452
            /// </summary>
453
            public byte[] ReadonlyIndexes { get; set; }
×
454
        }
455

456
        /// <summary>
457
        /// Message Address Lookup table
458
        /// </summary>
459
        public static class AddressTableLookupUtils
460
        {
461
            /// <summary>
462
            /// Serialize the address table lookups
463
            /// </summary>
464
            /// <param name="addressTableLookups"></param>
465
            /// <returns></returns>
466
            public static byte[] SerializeAddressTableLookups(List<MessageAddressTableLookup> addressTableLookups)
467
            {
468
                MemoryStream buffer = new();
×
469

470
                var encodedAddressTableLookupsLength = ShortVectorEncoding.EncodeLength(addressTableLookups.Count);
×
471
                buffer.Write(encodedAddressTableLookupsLength, 0, encodedAddressTableLookupsLength.Length);
×
472

473
                foreach (var lookup in addressTableLookups)
×
474
                {
475
                    // Write the Account Key
476
                    buffer.Write(lookup.AccountKey, 0, PublicKey.PublicKeyLength);
×
477

478
                    // Write the Writable Indexes
479
                    var encodedWritableIndexesLength = ShortVectorEncoding.EncodeLength(lookup.WritableIndexes.Length);
×
480
                    buffer.Write(encodedWritableIndexesLength, 0, encodedWritableIndexesLength.Length);
×
481
                    buffer.Write(lookup.WritableIndexes, 0, lookup.WritableIndexes.Length);
×
482

483
                    // Write the Readonly Indexes
484
                    var encodedReadonlyIndexesLength = ShortVectorEncoding.EncodeLength(lookup.ReadonlyIndexes.Length);
×
485
                    buffer.Write(encodedReadonlyIndexesLength, 0, encodedReadonlyIndexesLength.Length);
×
486
                    buffer.Write(lookup.ReadonlyIndexes, 0, lookup.ReadonlyIndexes.Length);
×
487
                }
488

489
                return buffer.ToArray();
×
490
            }
491
        }
492
    }
493
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc