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
3namespace Attestto\SolanaPhpSdk;
4
5use ParagonIE_Sodium_Compat;
6use RangeException;
7use StephenHill\Base58;
8use Attestto\SolanaPhpSdk\Exceptions\BaseSolanaPhpSdkException;
9use Attestto\SolanaPhpSdk\Exceptions\InputValidationException;
10use Attestto\SolanaPhpSdk\Util\Buffer;
11use Attestto\SolanaPhpSdk\Util\HasPublicKey;
12
13class 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}