• 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

97.55
/src/Solnet.Rpc/Models/Transaction.cs
1
using Solnet.Rpc.Builders;
2
using Solnet.Rpc.Utilities;
3
using Solnet.Wallet;
4
using Solnet.Wallet.Utilities;
5
using System;
6
using System.Collections.Generic;
7
using System.IO;
8
using System.Linq;
9
using static Solnet.Rpc.Models.Message;
10

11
namespace Solnet.Rpc.Models
12
{
13
    /// <summary>
14
    /// A pair corresponding of a public key and it's verifiable signature. 
15
    /// </summary>
16
    public class SignaturePubKeyPair
17
    {
18
        /// <summary>
19
        /// The public key to verify the signature against.
20
        /// </summary>
21
        public PublicKey PublicKey { get; set; }
82✔
22

23
        /// <summary>
24
        /// The signature created by the corresponding <see cref="PrivateKey"/> of this pair's <see cref="PublicKey"/>.
25
        /// </summary>
26
        public byte[] Signature { get; set; }
26✔
27
    }
28

29
    /// <summary>
30
    /// Nonce information to be used to build an offline transaction.
31
    /// </summary>
32
    public class NonceInformation
33
    {
34
        /// <summary>
35
        /// The current blockhash stored in the nonce account.
36
        /// </summary>
37
        public string Nonce { get; set; }
17✔
38

39
        /// <summary>
40
        /// An AdvanceNonceAccount instruction.
41
        /// </summary>
42
        public TransactionInstruction Instruction { get; set; }
48✔
43
    }
44

45
    /// <summary>
46
    /// Represents a Transaction in Solana.
47
    /// </summary>
48
    public class Transaction
49
    {
50
        /// <summary>
51
        /// The transaction's fee payer.
52
        /// </summary>
53
        public PublicKey FeePayer { get; set; }
26✔
54

55
        /// <summary>
56
        /// The list of <see cref="TransactionInstruction"/>s present in the transaction.
57
        /// </summary>
58
        public List<TransactionInstruction> Instructions { get; set; }
76✔
59

60
        /// <summary>
61
        /// The recent block hash for the transaction.
62
        /// </summary>
63
        public string RecentBlockHash { get; set; }
47✔
64

65

66
        internal IList<PublicKey> _accountKeys;
67
        /// <summary>
68
        /// The nonce information of the transaction.
69
        /// <remarks>
70
        /// When this is set, the <see cref="NonceInformation"/>'s Nonce is used as the <c>RecentBlockhash</c>.
71
        /// </remarks>
72
        /// </summary>
73
        public NonceInformation NonceInformation { get; set; }
43✔
74

75
        /// <summary>
76
        /// The signatures for the transaction.
77
        /// <remarks>
78
        /// These are typically created by invoking the <c>Build(IList{Account} signers)</c> method of the <see cref="TransactionBuilder"/>,
79
        /// but can be created by deserializing a Transaction and adding signatures manually.
80
        /// </remarks>
81
        /// </summary>
82
        public List<SignaturePubKeyPair> Signatures { get; set; }
102✔
83

84
        /// <summary>
85
        /// Compile the transaction data.
86
        /// </summary>
87
        public virtual byte[] CompileMessage()
88
        {
89
            MessageBuilder messageBuilder = new() { FeePayer = FeePayer };
16✔
90

91
            if (RecentBlockHash != null) messageBuilder.RecentBlockHash = RecentBlockHash;
32✔
92
            if (NonceInformation != null) messageBuilder.NonceInformation = NonceInformation;
25✔
93

94
            foreach (TransactionInstruction instruction in Instructions)
134✔
95
            {
96
                messageBuilder.AddInstruction(instruction);
51✔
97
            }
98

99
            return messageBuilder.Build();
16✔
100
        }
101

102
        /// <summary>
103
        /// Verifies the signatures a given serialized message.
104
        /// </summary>
105
        /// <returns>true if they are valid, false otherwise.</returns>
106
        private bool VerifySignatures(byte[] serializedMessage) =>
107
            Signatures.All(pair => pair.PublicKey.Verify(serializedMessage, pair.Signature));
4✔
108

109
        /// <summary>
110
        /// Verifies the signatures of a complete and signed transaction.
111
        /// </summary>
112
        /// <returns>true if they are valid, false otherwise.</returns>
113
        public bool VerifySignatures() => VerifySignatures(CompileMessage());
2✔
114

115
        /// <summary>
116
        /// Sign the transaction with the specified signers. Multiple signatures may be applied to a transaction.
117
        /// The first signature is considered primary and is used to identify and confirm transaction.
118
        /// <remarks>
119
        /// <para>
120
        /// If the transaction <c>FeePayer</c> is not set, the first signer will be used as the transaction fee payer account.
121
        /// </para>
122
        /// <para>
123
        /// Transaction fields SHOULD NOT be modified after the first call to <c>Sign</c> or an externally created signature
124
        /// has been added to the transaction object, doing so will invalidate the signature and cause the transaction to be
125
        /// rejected by the cluster.
126
        /// </para>
127
        /// <para>
128
        /// The transaction must have been assigned a valid <c>RecentBlockHash</c> or <c>NonceInformation</c> before invoking this method.
129
        /// </para>
130
        /// </remarks>
131
        /// </summary>
132
        /// <param name="signers">The signer accounts.</param>
133
        public bool Sign(IList<Account> signers)
134
        {
135
            Signatures ??= new List<SignaturePubKeyPair>();
2!
136
            IEnumerable<Account> uniqueSigners = DeduplicateSigners(signers);
2✔
137
            byte[] serializedMessage = CompileMessage();
2✔
138

139
            foreach (Account account in uniqueSigners)
8✔
140
            {
141
                byte[] signatureBytes = account.Sign(serializedMessage);
2✔
142
                Signatures.Add(new SignaturePubKeyPair { PublicKey = account.PublicKey, Signature = signatureBytes });
2✔
143
            }
144

145
            return VerifySignatures();
2✔
146
        }
147

148
        /// <summary>
149
        /// Sign the transaction with the specified signer. Multiple signatures may be applied to a transaction.
150
        /// The first signature is considered primary and is used to identify and confirm transaction.
151
        /// <remarks>
152
        /// <para>
153
        /// If the transaction <c>FeePayer</c> is not set, the first signer will be used as the transaction fee payer account.
154
        /// </para>
155
        /// <para>
156
        /// Transaction fields SHOULD NOT be modified after the first call to <c>Sign</c> or an externally created signature
157
        /// has been added to the transaction object, doing so will invalidate the signature and cause the transaction to be
158
        /// rejected by the cluster.
159
        /// </para>
160
        /// <para>
161
        /// The transaction must have been assigned a valid <c>RecentBlockHash</c> or <c>NonceInformation</c> before invoking this method.
162
        /// </para>
163
        /// </remarks>
164
        /// </summary>
165
        /// <param name="signer">The signer account.</param>
166
        public bool Sign(Account signer) => Sign(new List<Account> { signer });
1✔
167

168
        /// <summary>
169
        /// Partially sign a transaction with the specified accounts.
170
        /// All accounts must correspond to either the fee payer or a signer account in the transaction instructions.
171
        /// </summary>
172
        /// <param name="signers">The signer accounts.</param>
173
        public void PartialSign(IList<Account> signers)
174
        {
175
            Signatures ??= new List<SignaturePubKeyPair>();
2✔
176
            IEnumerable<Account> uniqueSigners = DeduplicateSigners(signers);
2✔
177
            byte[] serializedMessage = CompileMessage();
2✔
178

179
            foreach (Account account in uniqueSigners)
8✔
180
            {
181
                byte[] signatureBytes = account.Sign(serializedMessage);
2✔
182
                Signatures.Add(new SignaturePubKeyPair { PublicKey = account.PublicKey, Signature = signatureBytes });
2✔
183
            }
184
        }
2✔
185

186
        /// <summary>
187
        /// Deduplicate the list of given signers.
188
        /// </summary>
189
        /// <param name="signers">The signer accounts.</param>
190
        /// <returns>The signer accounts with removed duplicates</returns>
191
        private static IEnumerable<Account> DeduplicateSigners(IEnumerable<Account> signers)
192
        {
193
            List<Account> uniqueSigners = new();
4✔
194
            HashSet<Account> seen = new();
4✔
195

196
            foreach (Account account in signers)
18✔
197
            {
198
                if (seen.Contains(account)) continue;
5✔
199

200
                seen.Add(account);
4✔
201
                uniqueSigners.Add(account);
4✔
202
            }
203

204
            return uniqueSigners;
4✔
205
        }
206

207
        /// <summary>
208
        /// Partially sign a transaction with the specified account.
209
        /// The account must correspond to either the fee payer or a signer account in the transaction instructions.
210
        /// </summary>
211
        /// <param name="signer">The signer account.</param>
212
        public void PartialSign(Account signer) => PartialSign(new List<Account> { signer });
1✔
213

214
        /// <summary>
215
        /// Signs the transaction's message with the passed signer and add it to the transaction, serializing it.
216
        /// </summary>
217
        /// <param name="signer">The signer.</param>
218
        /// <returns>The serialized transaction.</returns>
219
        public byte[] Build(Account signer)
220
        {
221
            return Build(new List<Account> { signer });
1✔
222
        }
223

224
        /// <summary>
225
        /// Signs the transaction's message with the passed list of signers and adds them to the transaction, serializing it.
226
        /// </summary>
227
        /// <param name="signers">The list of signers.</param>
228
        /// <returns>The serialized transaction.</returns>
229
        public byte[] Build(IList<Account> signers)
230
        {
231
            Sign(signers);
1✔
232

233
            return Serialize();
1✔
234
        }
235

236
        /// <summary>
237
        /// Adds an externally created signature to the transaction.
238
        /// The public key must correspond to either the fee payer or a signer account in the transaction instructions.
239
        /// </summary>
240
        /// <param name="publicKey">The public key of the account that signed the transaction.</param>
241
        /// <param name="signature">The transaction signature.</param>
242
        public void AddSignature(PublicKey publicKey, byte[] signature)
243
        {
244
            Signatures ??= new List<SignaturePubKeyPair>();
2!
245
            Signatures.Add(new SignaturePubKeyPair { PublicKey = publicKey, Signature = signature });
2✔
246
        }
2✔
247

248
        /// <summary>
249
        /// Adds one or more instructions to the transaction.
250
        /// </summary>
251
        /// <param name="instructions">The instructions to add.</param>
252
        /// <returns>The transaction instance.</returns>
253
        public Transaction Add(IEnumerable<TransactionInstruction> instructions)
254
        {
255
            Instructions ??= new List<TransactionInstruction>();
12✔
256
            Instructions.AddRange(instructions);
12✔
257
            return this;
12✔
258
        }
259

260
        /// <summary>
261
        /// Adds an instruction to the transaction.
262
        /// </summary>
263
        /// <param name="instruction">The instruction to add.</param>
264
        /// <returns>The transaction instance.</returns>
265
        public Transaction Add(TransactionInstruction instruction) =>
266
            Add(new List<TransactionInstruction> { instruction });
12✔
267

268
        /// <summary>
269
        /// Serializes the transaction into wire format.
270
        /// </summary>
271
        /// <returns>The transaction encoded in wire format.</returns>
272
        public byte[] Serialize()
273
        {
274
            byte[] signaturesLength = ShortVectorEncoding.EncodeLength(Signatures.Count);
5✔
275
            byte[] serializedMessage = CompileMessage();
5✔
276
            MemoryStream buffer = new(signaturesLength.Length + Signatures.Count * TransactionBuilder.SignatureLength +
5✔
277
                                      serializedMessage.Length);
5✔
278

279
            buffer.Write(signaturesLength);
5✔
280
            foreach (SignaturePubKeyPair signaturePair in Signatures)
28✔
281
            {
282
                buffer.Write(signaturePair.Signature);
9✔
283
            }
284

285
            buffer.Write(serializedMessage);
5✔
286
            return buffer.ToArray();
5✔
287
        }
288

289
        /// <summary>
290
        /// Populate the Transaction from the given message and signatures.
291
        /// </summary>
292
        /// <param name="message">The <see cref="Message"/> object.</param>
293
        /// <param name="signatures">The list of signatures.</param>
294
        /// <returns>The Transaction object.</returns>
295
        public static Transaction Populate(Message message, IList<byte[]> signatures = null)
296
        {
297
            Transaction tx = new()
7✔
298
            {
7✔
299
                RecentBlockHash = message.RecentBlockhash,
7✔
300
                Signatures = new List<SignaturePubKeyPair>(),
7✔
301
                Instructions = new List<TransactionInstruction>()
7✔
302
            };
7✔
303

304
            if (message.Header.RequiredSignatures > 0)
7✔
305
            {
306
                tx.FeePayer = message.AccountKeys[0];
7✔
307
            }
308

309
            if (signatures != null)
7✔
310
            {
311
                for (int i = 0; i < signatures.Count; i++)
20✔
312
                {
313
                    tx.Signatures.Add(new SignaturePubKeyPair
7✔
314
                    {
7✔
315
                        PublicKey = message.AccountKeys[i],
7✔
316
                        Signature = signatures[i]
7✔
317
                    });
7✔
318
                }
319
            }
320

321
            for (int i = 0; i < message.Instructions.Count; i++)
58✔
322
            {
323
                CompiledInstruction compiledInstruction = message.Instructions[i];
22✔
324
                (int accountLength, _) = ShortVectorEncoding.DecodeLength(compiledInstruction.KeyIndicesCount);
22✔
325

326
                List<AccountMeta> accounts = new(accountLength);
22✔
327
                for (int j = 0; j < accountLength; j++)
150✔
328
                {
329
                    int k = compiledInstruction.KeyIndices[j];
53✔
330
                    accounts.Add(new AccountMeta(message.AccountKeys[k], message.IsAccountWritable(k),
53✔
331
                        tx.Signatures.Any(pair => pair.PublicKey.Key == message.AccountKeys[k].Key) || message.IsAccountSigner(k)));
118✔
332
                }
333

334
                TransactionInstruction instruction = new()
22✔
335
                {
22✔
336
                    Keys = accounts,
22✔
337
                    ProgramId = message.AccountKeys[compiledInstruction.ProgramIdIndex],
22✔
338
                    Data = compiledInstruction.Data
22✔
339
                };
22✔
340
                if (i == 0 && accounts.Any(a => a.PublicKey == "SysvarRecentB1ockHashes11111111111111111111"))
36✔
341
                {
342
                    tx.NonceInformation = new NonceInformation { Instruction = instruction, Nonce = tx.RecentBlockHash };
5✔
343
                    continue;
5✔
344
                }
345
                tx.Instructions.Add(instruction);
17✔
346
            }
347

348
            return tx;
7✔
349
        }
350

351
        /// <summary>
352
        /// Populate the Transaction from the given compiled message and signatures.
353
        /// </summary>
354
        /// <param name="message">The compiled message, as base-64 encoded string.</param>
355
        /// <param name="signatures">The list of signatures.</param>
356
        /// <returns>The Transaction object.</returns>
357
        public static Transaction Populate(string message, IList<byte[]> signatures = null)
358
            => Populate(Message.Deserialize(message), signatures);
1✔
359

360
        /// <summary>
361
        /// Deserialize a wire format transaction into a Transaction object.
362
        /// </summary>
363
        /// <param name="data">The data to deserialize into the Transaction object.</param>
364
        /// <returns>The Transaction object.</returns>
365
        public static Transaction Deserialize(ReadOnlySpan<byte> data)
366
        {
367
            // Read number of signatures
368
            (int signaturesLength, int encodedLength) =
2✔
369
                ShortVectorEncoding.DecodeLength(data[..ShortVectorEncoding.SpanLength]);
2✔
370
            List<byte[]> signatures = new(signaturesLength);
2✔
371

372
            for (int i = 0; i < signaturesLength; i++)
12✔
373
            {
374
                ReadOnlySpan<byte> signature =
4✔
375
                    data.Slice(encodedLength + (i * TransactionBuilder.SignatureLength),
4✔
376
                        TransactionBuilder.SignatureLength);
4✔
377
                signatures.Add(signature.ToArray());
4✔
378
            }
379

380

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

384
            // If the transaction is a VersionedTransaction, use VersionedTransaction.Deserialize instead.
385
            if (prefix != maskedPrefix)
2!
386
                return VersionedTransaction.Deserialize(data);
×
387

388

389
            return Populate(
2✔
390
                Message.Deserialize(data[
2✔
391
                    (encodedLength + (signaturesLength * TransactionBuilder.SignatureLength))..]),
2✔
392
                signatures);
2✔
393
        }
394

395
        /// <summary>
396
        /// Deserialize a transaction encoded as base-64 into a Transaction object.
397
        /// </summary>
398
        /// <param name="data">The data to deserialize into the Transaction object.</param>
399
        /// <returns>The Transaction object.</returns>
400
        /// <exception cref="ArgumentNullException">Thrown when the given string is null.</exception>
401
        public static Transaction Deserialize(string data)
402
        {
403
            if (data == null)
3✔
404
                throw new ArgumentNullException(nameof(data));
1✔
405

406
            byte[] decodedBytes;
407

408
            try
409
            {
410
                decodedBytes = Convert.FromBase64String(data);
2✔
411
            }
1✔
412
            catch (Exception ex)
1✔
413
            {
414
                throw new Exception("could not decode transaction data from base64", ex);
1✔
415
            }
416

417
            return Deserialize(decodedBytes);
1✔
418
        }
419
    }
420
}
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