| 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 |
3 | namespace Attestto\SolanaPhpSdk;
4 |
5 | use Attestto\SolanaPhpSdk\Exceptions\InputValidationException;
6 | use Attestto\SolanaPhpSdk\Util\Buffer;
7 | use Attestto\SolanaPhpSdk\Util\CompiledInstruction;
8 | use Attestto\SolanaPhpSdk\Util\MessageHeader;
9 | use Attestto\SolanaPhpSdk\Util\ShortVec;
10 |
11 | class 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 | }