| Code Coverage
| Lines | Functions and Methods | Classes and Traits
Total | 85.97% covered (warning) | 85.97% | 190 / 221 | 38.10% covered (danger) | 38.10% | 8 / 21 | CRAP | 0.00% covered (danger) | 0.00% | 0 / 1
Transaction | 85.97% covered (warning) | 85.97% | 190 / 221 | 38.10% covered (danger) | 38.10% | 8 / 21 | 100.56 | 0.00% covered (danger) | 0.00% | 0 / 1
__construct | 100.00% covered (success) | 100.00% | 4 / 4 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
signature | 66.67% covered (warning) | 66.67% | 2 / 3 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.15 |
add | 57.14% covered (warning) | 57.14% | 4 / 7 | 0.00% covered (danger) | 0.00% | 0 / 1 | 5.26 |
compileMessage | 91.67% covered (success) | 91.67% | 88 / 96 | 0.00% covered (danger) | 0.00% | 0 / 1 | 28.45 |
serializeMessage | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
setSigners | 100.00% covered (success) | 100.00% | 4 / 4 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
addSigner | 100.00% covered (success) | 100.00% | 4 / 4 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
sign | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
partialSign | 91.67% covered (success) | 91.67% | 11 / 12 | 0.00% covered (danger) | 0.00% | 0 / 1 | 4.01 |
addSignature | 0.00% covered (danger) | 0.00% | 0 / 3 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
_addSignature | 75.00% covered (warning) | 75.00% | 3 / 4 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.06 |
verifySignatures | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
_verifySignature | 71.43% covered (warning) | 71.43% | 5 / 7 | 0.00% covered (danger) | 0.00% | 0 / 1 | 5.58 |
serialize | 75.00% covered (warning) | 75.00% | 3 / 4 | 0.00% covered (danger) | 0.00% | 0 / 1 | 3.14 |
_serialize | 70.59% covered (warning) | 70.59% | 12 / 17 | 0.00% covered (danger) | 0.00% | 0 / 1 | 8.25 |
from | 100.00% covered (success) | 100.00% | 9 / 9 | 100.00% covered (success) | 100.00% | 1 / 1 | 2 |
populate | 96.00% covered (success) | 96.00% | 24 / 25 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
arraySearchAccountMetaForPublicKey | 100.00% covered (success) | 100.00% | 5 / 5 | 100.00% covered (success) | 100.00% | 1 / 1 | 3 |
arrayUnique | 100.00% covered (success) | 100.00% | 6 / 6 | 100.00% covered (success) | 100.00% | 1 / 1 | 3 |
toPublicKey | 40.00% covered (danger) | 40.00% | 2 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 4.94 |
toSecretKey | 66.67% covered (warning) | 66.67% | 2 / 3 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.15 |

1 | <?php
2 |
3 | namespace Attestto\SolanaPhpSdk;
4 |
5 | use Attestto\SolanaPhpSdk\Exceptions\GenericException;
6 | use Attestto\SolanaPhpSdk\Exceptions\InputValidationException;
7 | use Attestto\SolanaPhpSdk\Util\AccountMeta;
8 | use Attestto\SolanaPhpSdk\Util\Buffer;
9 | use Attestto\SolanaPhpSdk\Util\CompiledInstruction;
10 | use Attestto\SolanaPhpSdk\Util\HasPublicKey;
11 | use Attestto\SolanaPhpSdk\Util\HasSecretKey;
12 | use Attestto\SolanaPhpSdk\Util\MessageHeader;
13 | use Attestto\SolanaPhpSdk\Util\NonceInformation;
14 | use Attestto\SolanaPhpSdk\Util\ShortVec;
15 | use Attestto\SolanaPhpSdk\Util\SignaturePubkeyPair;
16 | use Attestto\SolanaPhpSdk\Util\Signer;
17 |
18 | class Transaction
19 | {
20 | /**
21 | * Default (empty) signature
22 | *
23 | * Signatures are 64 bytes in length
24 | *
25 | * Buffer.alloc(64).fill(0);
26 | */
27 | const DEFAULT_SIGNATURE = [
28 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
29 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
30 | ];
31 |
32 | /**
33 | *
34 | */
35 | const SIGNATURE_LENGTH = 64;
36 |
37 | /**
38 | *
39 | */
40 | const PACKET_DATA_SIZE = 1280 - 40 - 8;
41 |
42 | /**
43 | * @var array<SignaturePubkeyPair>
44 | */
45 | public array $signatures;
46 | public ?string $recentBlockhash;
47 | public ?NonceInformation $nonceInformation;
48 | public ?PublicKey $feePayer;
49 | /**
50 | * @var array<TransactionInstruction>
51 | */
52 | public array $instructions = [];
53 |
54 | public function __construct(
55 | ?string $recentBlockhash = null,
56 | ?NonceInformation $nonceInformation = null,
57 | ?PublicKey $feePayer = null,
58 | ?array $signatures = []
59 | )
60 | {
61 | $this->recentBlockhash = $recentBlockhash;
62 | $this->nonceInformation = $nonceInformation;
63 | $this->feePayer = $feePayer;
64 | $this->signatures = $signatures;
65 | }
66 |
67 | /**
68 | * The first (payer) Transaction signature
69 | *
70 | * @return string|null
71 | */
72 | public function signature(): ?string
73 | {
74 | if (sizeof($this->signatures)) {
75 | return $this->signatures[0]->signature;
76 | }
77 |
78 | return null;
79 | }
80 |
81 | /**
82 | * @param ...$items
83 | * @return $this
84 | * @throws GenericException
85 | */
86 | public function add(...$items): Transaction
87 | {
88 | foreach ($items as $item) {
89 | if ($item instanceof TransactionInstruction) {
90 | $this->instructions[] = $item;
91 | } elseif ($item instanceof Transaction) {
92 | array_push($this->instructions, ...$item->instructions);
93 | } else {
94 | throw new InputValidationException("Invalid parameter to add(). Only Transaction and TransactionInstruction are allows.");
95 | }
96 | }
97 |
98 | return $this;
99 | }
100 |
101 | /**
102 | * Compile transaction data
103 | *
104 | * @return Message
105 | * @throws GenericException
106 | */
107 | public function compileMessage(): Message
108 | {
109 | $nonceInfo = $this->nonceInformation;
110 |
111 | if ($nonceInfo && sizeof($this->instructions) && $this->instructions[0] !== $nonceInfo->nonceInstruction) {
112 | $this->recentBlockhash = $nonceInfo->nonce;
113 | array_unshift($this->instructions, $nonceInfo->nonceInstruction);
114 | }
115 |
116 | $recentBlockhash = $this->recentBlockhash;
117 | if (! $recentBlockhash) {
118 | throw new InputValidationException('Transaction recentBlockhash required.');
119 | } elseif (! sizeof($this->instructions)) {
120 | throw new InputValidationException('No instructions provided.');
121 | }
122 |
123 | if ($this->feePayer) {
124 | $feePayer = $this->feePayer;
125 | } elseif (sizeof($this->signatures) && $this->signatures[0]->getPublicKey()) {
126 | $feePayer = $this->signatures[0]->getPublicKey();
127 | } else {
128 | throw new InputValidationException('Transaction fee payer required.');
129 | }
130 |
131 |
132 | /**
133 | * @var array<string> $programIds
134 | */
135 | $programIds = [];
136 | /**
137 | * @var array<AccountMeta> $accountMetas
138 | */
139 | $accountMetas = [];
140 |
141 | foreach ($this->instructions as $i => $instruction) {
142 | if (! $instruction->programId) {
143 | throw new InputValidationException("Transaction instruction index {$i} has undefined program id.");
144 | }
145 |
146 | array_push($accountMetas, ...$instruction->keys);
147 |
148 | $programId = $instruction->programId->toBase58();
149 | if (! in_array($programId, $programIds)) {
150 | array_push($programIds, $programId);
151 | }
152 | }
153 |
154 | // Append programID account metas
155 | foreach ($programIds as $programId) {
156 | array_push($accountMetas, new AccountMeta(
157 | new PublicKey($programId),
158 | false,
159 | false
160 | ));
161 | }
162 |
163 | // Sort. Prioritizing first by signer, then by writable
164 | usort($accountMetas, function (AccountMeta $x, AccountMeta $y) {
165 | if ($x->isSigner !== $y->isSigner) {
166 | return $x->isSigner ? -1 : 1;
167 | }
168 |
169 | if ($x->isWritable !== $y->isWritable) {
170 | return $x->isWritable ? -1 : 1;
171 | }
172 |
173 | return 0;
174 | });
175 |
176 | // Cull duplicate account metas
177 | /**
178 | * @var array<AccountMeta> $uniqueMetas
179 | */
180 | $uniqueMetas = [];
181 | foreach ($accountMetas as $accountMeta) {
182 | $eachPublicKey = $accountMeta->getPublicKey();
183 | $uniqueIndex = $this->arraySearchAccountMetaForPublicKey($uniqueMetas, $eachPublicKey);
184 |
185 | if ($uniqueIndex > -1) {
186 | $uniqueMetas[$uniqueIndex]->isWritable = $uniqueMetas[$uniqueIndex]->isWritable || $accountMeta->isWritable;
187 | } else {
188 | array_push($uniqueMetas, $accountMeta);
189 | }
190 | }
191 |
192 | // Move fee payer to the front
193 | $feePayerIndex = $this->arraySearchAccountMetaForPublicKey($uniqueMetas, $feePayer);
194 | if ($feePayerIndex > -1) {
195 | list($payerMeta) = array_splice($uniqueMetas, $feePayerIndex, 1);
196 | $payerMeta->isSigner = true;
197 | $payerMeta->isWritable = true;
198 | array_unshift($uniqueMetas, $payerMeta);
199 | } else {
200 | array_unshift($uniqueMetas, new AccountMeta($feePayer, true, true));
201 | }
202 |
203 | // Disallow unknown signers
204 | foreach ($this->signatures as $signature) {
205 | $uniqueIndex = $this->arraySearchAccountMetaForPublicKey($uniqueMetas, $signature);
206 | if ($uniqueIndex > -1) {
207 | $uniqueMetas[$uniqueIndex]->isSigner = true;
208 | } else {
209 | throw new InputValidationException("Unknown signer: {$signature->getPublicKey()->toBase58()}");
210 | }
211 | }
212 |
213 | $numRequiredSignatures = 0;
214 | $numReadonlySignedAccounts = 0;
215 | $numReadonlyUnsignedAccounts = 0;
216 |
217 | // Split out signing from non-signing keys and count header values
218 | /**
219 | * @var array<string> $signedKeys
220 | */
221 | $signedKeys = [];
222 | /**
223 | * @var array<string> $unsignedKeys
224 | */
225 | $unsignedKeys = [];
226 |
227 | foreach ($uniqueMetas as $accountMeta) {
228 | if ($accountMeta->isSigner) {
229 | array_push($signedKeys, $accountMeta->getPublicKey()->toBase58());
230 | $numRequiredSignatures++;
231 | if (! $accountMeta->isWritable) {
232 | $numReadonlySignedAccounts++;
233 | }
234 | } else {
235 | array_push($unsignedKeys, $accountMeta->getPublicKey()->toBase58());
236 | if (! $accountMeta->isWritable) {
237 | $numReadonlyUnsignedAccounts++;
238 | }
239 | }
240 | }
241 |
242 | // Initialize signature array, if needed
243 | if (! $this->signatures) {
244 | $this->signatures = array_map(function($signedKey) {
245 | return new SignaturePubkeyPair(new PublicKey($signedKey), null);
246 | }, $signedKeys);
247 | }
248 |
249 | $accountKeys = array_merge($signedKeys, $unsignedKeys);
250 | /**
251 | * @var array<CompiledInstruction> $instructions
252 | */
253 | $instructions = array_map(function (TransactionInstruction $instruction) use ($accountKeys) {
254 | $programIdIndex = array_search($instruction->programId->toBase58(), $accountKeys);
255 | $encodedData = $instruction->data;
256 | $accounts = array_map(function (AccountMeta $meta) use ($accountKeys) {
257 | return array_search($meta->getPublicKey()->toBase58(), $accountKeys);
258 | }, $instruction->keys);
259 | return new CompiledInstruction(
260 | $programIdIndex,
261 | $accounts,
262 | $encodedData
263 | );
264 | }, $this->instructions);
265 |
266 | return new Message(
267 | new MessageHeader(
268 | $numRequiredSignatures,
269 | $numReadonlySignedAccounts,
270 | $numReadonlyUnsignedAccounts
271 | ),
272 | $accountKeys,
273 | $recentBlockhash,
274 | $instructions
275 | );
276 | }
277 |
278 | /**
279 | * The Python library takes a little different approach to their implementation of Transaction. It seems simpler to me
280 | * and does not involve the compile method from the JS library. An early implementation of this class used this in a
281 | * 1 to 1 port of the Javascript library, however as I iterated I went away from that.
282 | *
283 | * TODO: Keep this around for a few weeks and delete once we are sure all the kinks with the current implementation
284 | * have been worked out.
285 | *
286 | * @return Message
287 | */
288 | // protected function compile(): Message
289 | // {
290 | // $message = $this->compileMessage();
291 | // $signedKeys = array_slice($message->accountKeys, 0, $message->header->numRequiredSignature);
292 | //
293 | // if (sizeof($this->signatures) === sizeof($signedKeys)
294 | // && $this->signatures == $signedKeys) {
295 | // return $message;
296 | // }
297 | //
298 | // $this->signatures = array_map(function (PublicKey $publicKey) {
299 | // return new SignaturePubkeyPair($publicKey, null);
300 | // }, $signedKeys);
301 | //
302 | // return $message;
303 | // }
304 |
305 | /**
306 | * Get a buffer of the Transaction data that need to be covered by signatures
307 | */
308 | public function serializeMessage(): string
309 | {
310 | return $this->compileMessage()->serialize();
311 | }
312 |
313 | /**
314 | * Specify the public keys which will be used to sign the Transaction.
315 | * The first signer will be used as the transaction fee payer account.
316 | *
317 | * Signatures can be added with either `partialSign` or `addSignature`
318 | *
319 | * @deprecated Deprecated since v0.84.0. Only the fee payer needs to be
320 | * specified and it can be set in the Transaction constructor or with the
321 | * `feePayer` property.
322 | *
323 | * @param array<PublicKey> $signers
324 | */
325 | public function setSigners(...$signers)
326 | {
327 | $uniqueSigners = $this->arrayUnique($signers);
328 |
329 | $this->signatures = array_map(function(PublicKey $signer) {
330 | return new SignaturePubkeyPair($signer, null);
331 | }, $uniqueSigners);
332 | }
333 |
334 | /**
335 | * Fill in a signature for a partially signed Transaction.
336 | * The `signer` must be the corresponding `Keypair` for a `PublicKey` that was
337 | * previously provided to `signPartial`
338 | *
339 | * @param Keypair $signer
340 | */
341 | public function addSigner(Keypair $signer)
342 | {
343 | $message = $this->compileMessage();
344 | $signData = $message->serialize();
345 | $signature = sodium_crypto_sign_detached($signData, $this->toSecretKey($signer));
346 | $this->_addSignature($signer->getPublicKey(), $signature);
347 | }
348 |
349 | /**
350 | * Sign the Transaction with the specified signers. Multiple signatures may
351 | * be applied to a Transaction. The first signature is considered "primary"
352 | * and is used identify and confirm transactions.
353 | *
354 | * If the Transaction `feePayer` is not set, the first signer will be used
355 | * as the transaction fee payer account.
356 | *
357 | * Transaction fields should not be modified after the first call to `sign`,
358 | * as doing so may invalidate the signature and cause the Transaction to be
359 | * rejected.
360 | *
361 | * The Transaction must be assigned a valid `recentBlockhash` before invoking this method
362 | *
363 | * @param array<Signer|Keypair> $signers
364 | * @throws InputValidationException
365 | */
366 | public function sign(...$signers): void
367 | {
368 | $this->partialSign(...$signers);
369 | }
370 |
371 | /**
372 | * Partially sign a transaction with the specified accounts. All accounts must
373 | * correspond to either the fee payer or a signer account in the transaction
374 | * instructions.
375 | *
376 | * All the caveats from the `sign` method apply to `partialSign`
377 | *
378 | * @param array<Signer|Keypair> $signers $sgners PUTOs Keypairs!!
379 | * @throws GenericException
380 | * @throws \SodiumException
381 | * @throws InputValidationException
382 | */
383 | public function partialSign(...$signers): void
384 | {
385 | // Dedupe signers
386 | $uniqueSigners = $this->arrayUnique($signers);
387 |
388 | $this->signatures = array_map(function ($signer) {
389 | return new SignaturePubkeyPair($this->toPublicKey($signer), null);
390 | }, $uniqueSigners);
391 |
392 | $message = $this->compileMessage();
393 | $signData = $message->serialize();
394 |
395 | foreach ($uniqueSigners as $signer) {
396 | if ($signer instanceof Keypair) {
397 | $signature = sodium_crypto_sign_detached($signData, $this->toSecretKey($signer));
398 | if (strlen($signature) != self::SIGNATURE_LENGTH) {
399 | throw new InputValidationException('Signature has invalid length.');
400 | }
401 | $this->_addSignature($this->toPublicKey($signer), $signature);
402 | }
403 | }
404 | }
405 |
406 | /**
407 | * Add an externally created signature to a transaction. The public key
408 | * must correspond to either the fee payer or a signer account in the transaction
409 | * instructions.
410 | *
411 | * @param PublicKey $publicKey
412 | * @param string $signature
413 | * @throws GenericException
414 | * @throws InputValidationException
415 | */
416 | public function addSignature(PublicKey $publicKey, string $signature): void
417 | {
418 | if (strlen($signature) !== self::SIGNATURE_LENGTH) {
419 | throw new InputValidationException('Signature has invalid length.');
420 | }
421 |
422 | // $this->compile(); // Ensure signatures array is populated
423 | $this->_addSignature($publicKey, $signature);
424 | }
425 |
426 | /**
427 | * @param PublicKey $publicKey
428 | * @param string $signature
429 | * @throws InputValidationException
430 | */
431 | protected function _addSignature(PublicKey $publicKey, string $signature): void
432 | {
433 | $indexOfPublicKey = $this->arraySearchAccountMetaForPublicKey($this->signatures, $publicKey);
434 |
435 | if ($indexOfPublicKey === -1) {
436 | throw new InputValidationException("Unknown signer: {$publicKey->toBase58()}");
437 | }
438 |
439 | $this->signatures[$indexOfPublicKey]->signature = $signature;
440 | }
441 |
442 | /**
443 | * @return bool
444 | */
445 | public function verifySignatures(): bool
446 | {
447 | return $this->_verifySignature($this->serializeMessage(), true);
448 | }
449 |
450 | /**
451 | * @param string $signData
452 | * @param bool $requireAllSignatures
453 | * @return bool
454 | */
455 | protected function _verifySignature(string $signData, bool $requireAllSignatures): bool
456 | {
457 | foreach ($this->signatures as $signature) {
458 | if (! $signature->signature) {
459 | if ($requireAllSignatures) {
460 | return false;
461 | }
462 | } else {
463 | if (! sodium_crypto_sign_verify_detached($signature->signature, $signData, $signature->getPublicKey()->toBinaryString())) {
464 | return false;
465 | }
466 | }
467 | }
468 |
469 | return true;
470 | }
471 |
472 | /**
473 | * Serialize the Transaction in the wire format.
474 | *
475 | * @param bool|null $requireAllSignature
476 | * @param bool|null $verifySignatures
477 | */
478 | public function serialize(bool $requireAllSignature = true, bool $verifySignatures = true)
479 | {
480 | $signData = $this->serializeMessage();
481 |
482 | if ($verifySignatures && ! $this->_verifySignature($signData, $requireAllSignature)) {
483 | throw new GenericException('Signature verification failed');
484 | }
485 |
486 | return $this->_serialize($signData);
487 | }
488 |
489 | /**
490 | * @param string $signData
491 | * @return string
492 | */
493 | protected function _serialize(string $signData): string
494 | {
495 | if (sizeof($this->signatures) >= self::SIGNATURE_LENGTH * 4) {
496 | throw new InputValidationException('Too many signatures to encode.');
497 | }
498 |
499 | $wireTransaction = new Buffer();
500 |
501 | $signatureCount = ShortVec::encodeLength(sizeof($this->signatures));
502 |
503 | // Encode signature count
504 | $wireTransaction->push($signatureCount);
505 |
506 | // Encode signatures
507 | foreach ($this->signatures as $signature) {
508 | if ($signature->signature && strlen($signature->signature) != self::SIGNATURE_LENGTH) {
509 | throw new GenericException("signature has invalid length: {$signature->signature}");
510 | }
511 |
512 | if ($sig = $signature->signature) {
513 | $wireTransaction->push($sig);
514 | } else {
515 | $wireTransaction->push(array_pad([], self::SIGNATURE_LENGTH, 0));
516 | }
517 | }
518 |
519 | // Encode signed data
520 | $wireTransaction->push($signData);
521 |
522 | if (sizeof($wireTransaction) > self::PACKET_DATA_SIZE) {
523 | $actualSize = sizeof($wireTransaction);
524 | $maxSize = self::PACKET_DATA_SIZE;
525 | throw new GenericException("transaction too large: {$actualSize} > {$maxSize}");
526 | }
527 |
528 | return $wireTransaction;
529 | }
530 |
531 | /**
532 | * Parse a wire transaction into a Transaction object.
533 | *
534 | * @param $buffer
535 | * @return Transaction
536 | */
537 | public static function from($buffer): Transaction
538 | {
539 | $buffer = Buffer::from($buffer);
540 |
541 | list($signatureCount, $offset) = ShortVec::decodeLength($buffer);
542 | $signatures = [];
543 | for ($i = 0; $i < $signatureCount; $i++) {
544 | $signature = $buffer->slice($offset, self::SIGNATURE_LENGTH);
545 | array_push($signatures, $signature->toBase58String());
546 | $offset += self::SIGNATURE_LENGTH;
547 | }
548 |
549 | $buffer = $buffer->slice($offset);
550 |
551 | return Transaction::populate(Message::from($buffer), $signatures);
552 | }
553 |
554 | /**
555 | * Populate Transaction object from message and signatures
556 | *
557 | * @param Message $message
558 | * @param array<string> $signatures
559 | * @return Transaction
560 | */
561 | public static function populate(Message $message, array $signatures): Transaction
562 | {
563 | $transaction = new Transaction();
564 | $transaction->recentBlockhash = $message->recentBlockhash;
565 |
566 | if ($message->header->numRequiredSignature > 0) {
567 | $transaction->feePayer = $message->accountKeys[0];
568 | }
569 |
570 | foreach ($signatures as $i => $signature) {
571 | array_push($transaction->signatures, new SignaturePubkeyPair(
572 | $message->accountKeys[$i],
573 | $signature === Buffer::from(self::DEFAULT_SIGNATURE)->toBase58String()
574 | ? null
575 | : Buffer::fromBase58($signature)->toString()
576 | ));
577 | }
578 |
579 | foreach ($message->instructions as $instruction) {
580 | $keys = array_map(function (int $accountIndex) use ($transaction, $message) {
581 | $publicKey = $message->accountKeys[$accountIndex];
582 | $isSigner = static::arraySearchAccountMetaForPublicKey($transaction->signatures, $publicKey) !== -1
583 | || $message->isAccountSigner($accountIndex);
584 | $isWritable = $message->isAccountWritable($accountIndex);
585 | return new AccountMeta($publicKey, $isSigner, $isWritable);
586 | }, $instruction->accounts);
587 |
588 | array_push($transaction->instructions, new TransactionInstruction(
589 | $message->accountKeys[$instruction->programIdIndex],
590 | $keys,
591 | $instruction->data
592 | ));
593 | }
594 |
595 | return $transaction;
596 | }
597 |
598 | /**
599 | * @param array<AccountMeta> $haystack
600 | * @param PublicKey|SignaturePubkeyPair|AccountMeta|string $needle
601 | * @return int|string
602 | */
603 | static protected function arraySearchAccountMetaForPublicKey(array $haystack, $needle)
604 | {
605 | $publicKeyToSearchFor = static::toPublicKey($needle);
606 |
607 | foreach ($haystack as $i => $item) {
608 | if (static::toPublicKey($item) == $publicKeyToSearchFor) {
609 | return $i;
610 | }
611 | }
612 |
613 | return -1;
614 | }
615 |
616 | /**
617 | * @param array $haystack
618 | * @return array
619 | * @throws GenericException
620 | */
621 | static protected function arrayUnique(array $haystack)
622 | {
623 | $unique = [];
624 | foreach ($haystack as $item) {
625 | $indexOfSigner = static::arraySearchAccountMetaForPublicKey($unique, $item);
626 |
627 | if ($indexOfSigner === -1) {
628 | array_push($unique, $item);
629 | }
630 | }
631 |
632 | return $unique;
633 | }
634 |
635 | /**
636 | * @param $base58String
637 | * @return PublicKey
638 | * @throws GenericException
639 | */
640 | static protected function toPublicKey($fromKeypair): PublicKey
641 | {
642 | //dd($base58String);
643 | if ($fromKeypair instanceof HasPublicKey) {
644 |
645 | return $fromKeypair->getPublicKey();
646 | } elseif (is_string($fromKeypair)) {
647 |
648 | return new PublicKey($fromKeypair);
649 | } else {
650 | throw new InputValidationException('Unsupported input: ' . get_class($fromKeypair));
651 | }
652 | }
653 |
654 | /**
655 | * Pulls out the secret key and casts it to a string.
656 | *
657 | * @param $source
658 | * @return string
659 | * @throws InputValidationException
660 | */
661 | protected function toSecretKey($source): string
662 | {
663 | if ($source instanceof HasSecretKey) {
664 | return $source->getSecretKey();
665 | } else {
666 | throw new InputValidationException('Unsupported input: ' . get_class($source));
667 | }
668 | }
669 | }