| Code Coverage
| Lines | Functions and Methods | Classes and Traits
Total | 89.66% covered (warning) | 89.66% | 52 / 58 | 66.67% covered (warning) | 66.67% | 10 / 15 | CRAP | 0.00% covered (danger) | 0.00% | 0 / 1
PublicKey | 89.66% covered (warning) | 89.66% | 52 / 58 | 66.67% covered (warning) | 66.67% | 10 / 15 | 27.81 | 0.00% covered (danger) | 0.00% | 0 / 1
__construct | 81.82% covered (warning) | 81.82% | 9 / 11 | 0.00% covered (danger) | 0.00% | 0 / 1 | 5.15 |
default | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
equals | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
toBase58 | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
toBytes | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
toBuffer | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
toBinaryString | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
__toString | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
createWithSeed | 100.00% covered (success) | 100.00% | 8 / 8 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
createProgramAddress | 91.67% covered (success) | 91.67% | 11 / 12 | 0.00% covered (danger) | 0.00% | 0 / 1 | 4.01 |
findProgramAddress | 90.00% covered (success) | 90.00% | 9 / 10 | 0.00% covered (danger) | 0.00% | 0 / 1 | 3.01 |
findProgramAddressSync | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
isOnCurve | 100.00% covered (success) | 100.00% | 7 / 7 | 100.00% covered (success) | 100.00% | 1 / 1 | 3 |
base58 | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
getPublicKey | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |

1 | <?php
2 |
3 | namespace Attestto\SolanaPhpSdk;
4 |
5 | use ParagonIE_Sodium_Compat;
6 | use RangeException;
7 | use StephenHill\Base58;
8 | use Attestto\SolanaPhpSdk\Exceptions\BaseSolanaPhpSdkException;
9 | use Attestto\SolanaPhpSdk\Exceptions\InputValidationException;
10 | use Attestto\SolanaPhpSdk\Util\Buffer;
11 | use Attestto\SolanaPhpSdk\Util\HasPublicKey;
12 |
13 | class PublicKey implements HasPublicKey
14 | {
15 | const LENGTH = 32;
16 | const MAX_SEED_LENGTH = 32;
17 |
18 | /**
19 | * @var Buffer
20 | */
21 | protected Buffer $buffer;
22 |
23 | /**
24 | * @param array|string $bn
25 | */
26 | public function __construct($bnORBase58String)
27 | {
28 | if (is_integer($bnORBase58String)) {
29 | $this->buffer = Buffer::from()->pad(self::LENGTH, $bnORBase58String);
30 | } elseif (is_string($bnORBase58String)) {
31 | // https://stackoverflow.com/questions/25343508/detect-if-string-is-binary
32 | $isBinaryString = preg_match('~[^\x20-\x7E\t\r\n]~', $bnORBase58String) > 0;
33 |
34 | // if not binary string already, assumed to be a base58 string.
35 | if ($isBinaryString) {
36 | $this->buffer = Buffer::from($bnORBase58String);
37 | } else {
38 | $this->buffer = Buffer::fromBase58($bnORBase58String);
39 | }
40 |
41 | } else {
42 | $this->buffer = Buffer::from($bnORBase58String);
43 | }
44 |
45 | if (sizeof($this->buffer) !== self::LENGTH) {
46 | $len = sizeof($this->buffer);
47 | throw new InputValidationException("Invalid public key input. Expected length 32. Found: {$len}");
48 | }
49 | }
50 |
51 | /**
52 | * @return PublicKey
53 | */
54 | public static function default(): PublicKey
55 | {
56 | return new static('11111111111111111111111111111111');
57 | }
58 |
59 | /**
60 | * Check if two publicKeys are equal
61 | */
62 | public function equals($publicKey): bool
63 | {
64 | return $publicKey instanceof PublicKey && $publicKey->buffer === $this->buffer;
65 | }
66 |
67 | /**
68 | * Return the base-58 representation of the public key
69 | */
70 | public function toBase58(): string
71 | {
72 | return $this->base58()->encode($this->buffer->toString());
73 | }
74 |
75 | /**
76 | * Return the byte array representation of the public key
77 | */
78 | public function toBytes(): array
79 | {
80 | return $this->buffer->toArray();
81 | }
82 |
83 | /**
84 | * Return the Buffer representation of the public key
85 | */
86 | public function toBuffer(): Buffer
87 | {
88 | return $this->buffer;
89 | }
90 |
91 | /**
92 | * @return string
93 | */
94 | public function toBinaryString(): string
95 | {
96 | return $this->buffer;
97 | }
98 |
99 | /**
100 | * Return the base-58 representation of the public key
101 | */
102 | public function __toString()
103 | {
104 | return $this->toBase58();
105 | }
106 |
107 | /**
108 | * Derive a public key from another key, a seed, and a program ID.
109 | * The program ID will also serve as the owner of the public key, giving
110 | * it permission to write data to the account.
111 | *
112 | * @param PublicKey $fromPublicKey
113 | * @param string $seed
114 | * @param PublicKey $programId
115 | * @return PublicKey
116 | */
117 | public static function createWithSeed(PublicKey $fromPublicKey, string $seed, PublicKey $programId): PublicKey
118 | {
119 | $buffer = new Buffer();
120 |
121 | $buffer->push($fromPublicKey)
122 | ->push($seed)
123 | ->push($programId)
124 | ;
125 |
126 | $hash = hash('sha256', $buffer);
127 | $binaryString = sodium_hex2bin($hash);
128 | return new PublicKey($binaryString);
129 | }
130 |
131 | /**
132 | * Derive a program address from seeds and a program ID.
133 | *
134 | * @param array $seeds
135 | * @param PublicKey $programId
136 | * @return PublicKey
137 | */
138 | public static function createProgramAddress(array $seeds, PublicKey $programId): PublicKey
139 | {
140 | $buffer = new Buffer();
141 | foreach ($seeds as $seed) {
142 | $seed = Buffer::from($seed);
143 | if (sizeof($seed) > self::MAX_SEED_LENGTH) {
144 | throw new InputValidationException("Max seed length exceeded.");
145 | }
146 | $buffer->push($seed);
147 | }
148 |
149 | $buffer->push($programId)->push('ProgramDerivedAddress');
150 |
151 | $hash = hash('sha256', $buffer);
152 | $binaryString = sodium_hex2bin($hash);
153 |
154 | if (static::isOnCurve($binaryString)) {
155 | throw new InputValidationException('Invalid seeds, address must fall off the curve.');
156 | }
157 |
158 | return new PublicKey($binaryString);
159 | }
160 |
161 | /**
162 | * @param array $seeds
163 | * @param PublicKey $programId
164 | * @return array 2 elements, [0] = PublicKey, [1] = nonce
165 | * @throws BaseSolanaPhpSdkException
166 | */
167 | static function findProgramAddress(array $seeds, PublicKey $programId): array
168 | {
169 | $nonce = 255;
170 |
171 | while ($nonce != 0) {
172 | try {
173 | $copyOfSeedsWithNonce = $seeds;
174 | $copyOfSeedsWithNonce[] = [$nonce];
175 | $address = static::createProgramAddress($copyOfSeedsWithNonce, $programId);
176 | } catch (\Exception $exception) {
177 | $nonce--;
178 | continue;
179 | }
180 | return [$address, $nonce];
181 | }
182 |
183 | throw new BaseSolanaPhpSdkException('Unable to find a viable program address nonce.');
184 | }
185 |
186 | /**
187 | *
188 | * @param array $seeds
189 | * @param PublicKey $programId
190 | * @return array 2 elements, [0] = PublicKey, [1] = integer
191 | */
192 | static function findProgramAddressSync(array $seeds, PublicKey $programId): array
193 | {
194 | return static::findProgramAddress($seeds, $programId);
195 | }
196 |
197 | /**
198 | * Check that a pubkey is on the ed25519 curve.
199 | */
200 | static function isOnCurve(mixed $publicKey): bool
201 | {
202 | try {
203 | $binaryString = $publicKey instanceof PublicKey
204 | ? $publicKey->toBinaryString()
205 | : $publicKey;
206 |
207 | /**
208 | * Sodium extension method sometimes returns "conversion failed" exception.
209 | * $_ = sodium_crypto_sign_ed25519_pk_to_curve25519($binaryString);
210 | */
211 | $_ = ParagonIE_Sodium_Compat::crypto_sign_ed25519_pk_to_curve25519($binaryString);
212 |
213 | return true;
214 | } catch (RangeException|\SodiumException $exception) {
215 | return false;
216 | }
217 | }
218 |
219 | /**
220 | * Convenience.
221 | *
222 | * @return Base58
223 | */
224 | public static function base58(): Base58
225 | {
226 | return new Base58();
227 | }
228 |
229 | public function getPublicKey(): PublicKey
230 | {
231 | return $this;
232 | }
233 | }