Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.59% covered (success)
92.59%
75 / 81
60.00% covered (warning)
60.00%
6 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Message
92.59% covered (success)
92.59%
75 / 81
60.00% covered (warning)
60.00%
6 / 10
18.13
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 isAccountSigner
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAccountWritable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 isProgramId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 programIds
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 nonProgramIds
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 serialize
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 encodeMessage
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 encodeInstruction
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 from
97.30% covered (success)
97.30%
36 / 37
0.00% covered (danger)
0.00%
0 / 1
4
1<?php
2
3namespace Attestto\SolanaPhpSdk;
4
5use Attestto\SolanaPhpSdk\Exceptions\InputValidationException;
6use Attestto\SolanaPhpSdk\Util\Buffer;
7use Attestto\SolanaPhpSdk\Util\CompiledInstruction;
8use Attestto\SolanaPhpSdk\Util\MessageHeader;
9use Attestto\SolanaPhpSdk\Util\ShortVec;
10
11class Message
12{
13    public MessageHeader $header;
14    /**
15     * @var array<PublicKey>
16     */
17    public array $accountKeys;
18    public string $recentBlockhash;
19    /**
20     * @var array<CompiledInstruction>
21     */
22    public array $instructions;
23
24    /**
25     * int to PublicKey: https://github.com/solana-labs/solana-web3.js/blob/966d7c653198de193f607cdfe19a161420408df2/src/message.ts
26     *
27     * @var array
28     */
29    private array $indexToProgramIds;
30
31    /**
32     * @param MessageHeader $header
33     * @param array<string> $accountKeys
34     * @param string $recentBlockhash
35     * @param array<CompiledInstruction> $instructions
36     */
37    public function __construct(
38        MessageHeader $header,
39        array $accountKeys,
40        string $recentBlockhash,
41        array $instructions
42    )
43    {
44        $this->header = $header;
45        $this->accountKeys = array_map(function (string $accountKey) {
46            return new PublicKey($accountKey);
47        }, $accountKeys);
48        $this->recentBlockhash = $recentBlockhash;
49        $this->instructions = $instructions;
50
51        $this->indexToProgramIds = [];
52
53        foreach ($instructions as $instruction) {
54            $this->indexToProgramIds[$instruction->programIdIndex] = $this->accountKeys[$instruction->programIdIndex];
55        }
56    }
57
58    /**
59     * @param int $index
60     * @return bool
61     */
62    public function isAccountSigner(int $index): bool
63    {
64        return $index < $this->header->numRequiredSignature;
65    }
66
67    /**
68     * @param int $index
69     * @return bool
70     */
71    public function isAccountWritable(int $index): bool
72    {
73        return $index < ($this->header->numRequiredSignature - $this->header->numReadonlySignedAccounts)
74            || ($index >= $this->header->numRequiredSignature && $index < sizeof($this->accountKeys) - $this->header->numReadonlyUnsignedAccounts);
75    }
76
77    /**
78     * @param int $index
79     * @return bool
80     */
81    public function isProgramId(int $index): bool
82    {
83        return array_key_exists($index, $this->indexToProgramIds);
84    }
85
86    /**
87     * @return array<PublicKey>
88     */
89    public function programIds(): array
90    {
91        return array_values($this->indexToProgramIds);
92    }
93
94    /**
95     * @return array
96     */
97    public function nonProgramIds(): array
98    {
99        return array_filter($this->accountKeys, function (PublicKey $account, $index) {
100            return ! $this->isProgramId($index);
101        });
102    }
103
104    /**
105     * @return string
106     */
107    public function serialize(): string
108    {
109        $out = new Buffer();
110
111        $out->push($this->encodeMessage())
112            ->push(ShortVec::encodeLength(sizeof($this->instructions)))
113        ;
114
115        foreach ($this->instructions as $instruction) {
116            $out->push($this->encodeInstruction($instruction));
117        }
118
119        return $out;
120    }
121
122    /**
123     * @return array
124     */
125    protected function encodeMessage(): array
126    {
127        $publicKeys = [];
128
129        foreach ($this->accountKeys as $publicKey) {
130            array_push($publicKeys, ...$publicKey->toBytes());
131        }
132
133        return [
134            // uint8
135            ...unpack("C*", pack("C", $this->header->numRequiredSignature)),
136            // uint8
137            ...unpack("C*", pack("C", $this->header->numReadonlySignedAccounts)),
138            // uint8
139            ...unpack("C*", pack("C", $this->header->numReadonlyUnsignedAccounts)),
140
141            ...ShortVec::encodeLength(sizeof($this->accountKeys)),
142            ...$publicKeys,
143            ...Buffer::fromBase58($this->recentBlockhash)->toArray(),
144        ];
145    }
146
147    protected function encodeInstruction(CompiledInstruction $instruction): array
148    {
149        $data = $instruction->data;
150
151        $accounts = $instruction->accounts;;
152
153        return [
154            // uint8
155            ...unpack("C*", pack("C", $instruction->programIdIndex)),
156
157            ...ShortVec::encodeLength(sizeof($accounts)),
158            ...$accounts,
159
160            ...ShortVec::encodeLength(sizeof($data)),
161            ...$data->toArray(),
162        ];
163    }
164
165    /**
166     * @param array|Buffer $rawMessage
167     * @return Message
168     */
169    public static function from($rawMessage): Message
170    {
171        $rawMessage = Buffer::from($rawMessage);
172
173        $HEADER_OFFSET = 3;
174        if (sizeof($rawMessage) < $HEADER_OFFSET) {
175            throw new InputValidationException('Byte representation of message is missing message header.');
176        }
177
178        $numRequiredSignatures = $rawMessage->shift();
179        $numReadonlySignedAccounts = $rawMessage->shift();
180        $numReadonlyUnsignedAccounts = $rawMessage->shift();
181        $header = new MessageHeader($numRequiredSignatures, $numReadonlySignedAccounts, $numReadonlyUnsignedAccounts);
182
183        $accountKeys = [];
184        list($accountsLength, $accountsOffset) = ShortVec::decodeLength($rawMessage);
185        for ($i = 0; $i < $accountsLength; $i++) {
186            $keyBytes = $rawMessage->slice($accountsOffset, PublicKey::LENGTH);
187            array_push($accountKeys, (new PublicKey($keyBytes))->toBase58());
188            $accountsOffset += PublicKey::LENGTH;
189        }
190        $rawMessage = $rawMessage->slice($accountsOffset);
191
192        $recentBlockhash = $rawMessage->slice(0, PublicKey::LENGTH)->toBase58String();
193        $rawMessage = $rawMessage->slice(PublicKey::LENGTH);
194
195        $instructions = [];
196        list($instructionCount, $offset) = ShortVec::decodeLength($rawMessage);
197        $rawMessage = $rawMessage->slice($offset);
198        for ($i = 0; $i < $instructionCount; $i++) {
199            $programIdIndex = $rawMessage->shift();
200
201            list ($accountsLength, $offset) = ShortVec::decodeLength($rawMessage);
202            $rawMessage = $rawMessage->slice($offset);
203            $accounts = $rawMessage->slice(0, $accountsLength)->toArray();
204            $rawMessage = $rawMessage->slice($accountsLength);
205
206            list ($dataLength, $offset) = ShortVec::decodeLength($rawMessage);
207            $rawMessage = $rawMessage->slice($offset);
208            $data = $rawMessage->slice(0, $dataLength);
209            $rawMessage = $rawMessage->slice($dataLength);
210
211            array_push($instructions, new CompiledInstruction($programIdIndex, $accounts, $data));
212        }
213
214        return new Message(
215            $header,
216            $accountKeys,
217            $recentBlockhash,
218            $instructions
219        );
220    }
221}