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
3namespace Attestto\SolanaPhpSdk;
4
5use Attestto\SolanaPhpSdk\Exceptions\GenericException;
6use Attestto\SolanaPhpSdk\Exceptions\InputValidationException;
7use Attestto\SolanaPhpSdk\Util\AccountMeta;
8use Attestto\SolanaPhpSdk\Util\Buffer;
9use Attestto\SolanaPhpSdk\Util\CompiledInstruction;
10use Attestto\SolanaPhpSdk\Util\HasPublicKey;
11use Attestto\SolanaPhpSdk\Util\HasSecretKey;
12use Attestto\SolanaPhpSdk\Util\MessageHeader;
13use Attestto\SolanaPhpSdk\Util\NonceInformation;
14use Attestto\SolanaPhpSdk\Util\ShortVec;
15use Attestto\SolanaPhpSdk\Util\SignaturePubkeyPair;
16use Attestto\SolanaPhpSdk\Util\Signer;
17
18class 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}