| Code Coverage
| Lines | Functions and Methods | Classes and Traits
Total | 87.88% covered (warning) | 87.88% | 29 / 33 | 60.00% covered (warning) | 60.00% | 3 / 5 | CRAP | 0.00% covered (danger) | 0.00% | 0 / 1
SolanaRpcClient | 87.88% covered (warning) | 87.88% | 29 / 33 | 60.00% covered (warning) | 60.00% | 3 / 5 | 14.35 | 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 | 4 |
call | 100.00% covered (success) | 100.00% | 13 / 13 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
buildRpc | 100.00% covered (success) | 100.00% | 6 / 6 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
validateResponse | 66.67% covered (warning) | 66.67% | 6 / 9 | 0.00% covered (danger) | 0.00% | 0 / 1 | 8.81 |
getRandomKey | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |

1 | <?php
2 |
3 | namespace Attestto\SolanaPhpSdk;
4 |
5 | use GuzzleHttp\Psr7\Message;
6 |
7 | use Psr\Http\Client\ClientExceptionInterface;
8 | use Psr\Http\Client\ClientInterface;
9 | use Psr\Http\Message\RequestFactoryInterface;
10 | use GuzzleHttp\Client as GuzzleClient;
11 | use GuzzleHttp\Psr7\HttpFactory;
12 |
13 |
14 | use Attestto\SolanaPhpSdk\Exceptions\GenericException;
15 | use Attestto\SolanaPhpSdk\Exceptions\InvalidIdResponseException;
16 | use Attestto\SolanaPhpSdk\Exceptions\MethodNotFoundException;
17 | use Psr\Http\Message\StreamFactoryInterface;
18 | use Psr\Http\Message\UriFactoryInterface;
19 | use Random\RandomException;
20 |
21 |
22 | /**
23 | * @see https://docs.solana.com/developing/clients/jsonrpc-api
24 | */
25 | class SolanaRpcClient
26 | {
27 | public const LOCAL_ENDPOINT = 'http://localhost:8899';
28 | public const DEVNET_ENDPOINT = 'https://api.devnet.solana.com';
29 | public const TESTNET_ENDPOINT = 'https://api.testnet.solana.com';
30 | public const MAINNET_ENDPOINT = 'https://api.mainnet-beta.solana.com';
31 |
32 | /**
33 | * Per: https://www.jsonrpc.org/specification
34 | */
35 | // Invalid JSON was received by the server.
36 | // An error occurred on the server while parsing the JSON text.
37 | public const ERROR_CODE_PARSE_ERROR = -32700;
38 | // The JSON sent is not a valid Request object.
39 | public const ERROR_CODE_INVALID_REQUEST = -32600;
40 | // The method does not exist / is not available.
41 | public const ERROR_CODE_METHOD_NOT_FOUND = -32601;
42 | // Invalid method parameter(s).
43 | public const ERROR_CODE_INVALID_PARAMETERS = -32602;
44 | // Internal JSON-RPC error.
45 | public const ERROR_CODE_INTERNAL_ERROR = -32603;
46 | // Reserved for implementation-defined server-errors.
47 | // -32000 to -32099 is server error - no const.
48 |
49 | protected string $endpoint;
50 | protected int $randomKey;
51 | // Allows for dependency injection
52 | public ClientInterface $httpClient;
53 | public RequestFactoryInterface $requestFactory;
54 | public StreamFactoryInterface $streamFactory;
55 | public UriFactoryInterface $uriFactory;
56 |
57 | /**
58 | * @param string $endpoint
59 | * @param ClientInterface|null $httpClient
60 | * @param RequestFactoryInterface|null $requestFactory
61 | * @param StreamFactoryInterface|Message|null $streamFactory
62 | * @param UriFactoryInterface|null $uriFactory
63 | * @throws RandomException
64 | */
65 | public function __construct(
66 | string $endpoint,
67 | ClientInterface $httpClient = null,
68 | RequestFactoryInterface $requestFactory = null,
69 | StreamFactoryInterface|Message $streamFactory= null ,
70 | UriFactoryInterface $uriFactory= null
71 | ) {
72 | $this->endpoint = $endpoint ? : self::DEVNET_ENDPOINT;
73 | $this->randomKey = random_int(0, 99999999);
74 | $this->httpClient = $httpClient?: new GuzzleClient();
75 | $this->requestFactory = $requestFactory?: new HttpFactory();
76 |
77 | }
78 |
79 | /**
80 | * @param string $method
81 | * @param array $params
82 | * @param array $headers
83 | * @return mixed
84 | * @throws GenericException
85 | * @throws InvalidIdResponseException
86 | * @throws MethodNotFoundException|ClientExceptionInterface
87 | */
88 | public function call(string $method, array $params = [], array $headers = []): mixed
89 | {
90 |
91 | $body = json_encode($this->buildRpc($method, $params));
92 | $options = [
93 | 'headers' => [
94 | 'Content-Type' => 'application/json',
95 | 'Accept' => 'application/json',
96 | ],
97 | 'body' => $body,
98 |
99 | ];
100 | $response = $this->httpClient->request('POST', $this->endpoint, $options);
101 |
102 | $resp_body = $response->getBody()->getContents();
103 | $resp_object = json_decode($resp_body, true);
104 |
105 | $this->validateResponse($resp_object, $method);
106 |
107 |
108 | return $resp_object['result'] ?? null;
109 | }
110 | /**
111 | * @param string $method
112 | * @param array $params
113 | * @return array
114 | */
115 | public function buildRpc(string $method, array $params): array
116 | {
117 | return [
118 | 'jsonrpc' => '2.0',
119 | 'id' => $this->randomKey,
120 | 'method' => $method,
121 | 'params' => $params,
122 |
123 | ];
124 | }
125 |
126 | /**
127 | * @param mixed $response
128 | * @param string $method
129 | * @throws GenericException
130 | * @throws InvalidIdResponseException
131 | * @throws MethodNotFoundException
132 | */
133 | protected function validateResponse(array $body, string $method): void
134 | {
135 |
136 | // Get response body as string
137 | //$body = $response->getBody()->getContents();
138 |
139 | // Decode JSON response body
140 | //$resp = json_decode($body, true);
141 |
142 |
143 |
144 | if ($body == null) {
145 | throw new GenericException('Invalid JSON response');
146 | }
147 |
148 | // If response contains an 'error' key, handle it
149 | if (isset($body['params']['error']) || isset($body['error'])) {
150 | $error = $body['params']['error']? : $body['error'];
151 | if ($error['code'] === self::ERROR_CODE_METHOD_NOT_FOUND) {
152 | throw new MethodNotFoundException("API Error: Method $method not found.");
153 | } else {
154 | throw new GenericException($error['message']);
155 | }
156 | }
157 |
158 | // If 'id' doesn't match the expected value, throw an exception
159 | if ($body['id'] !== $this->randomKey) {
160 | throw new InvalidIdResponseException($this->randomKey);
161 | }
162 |
163 |
164 | }
165 |
166 | /**
167 | * @return int
168 | */
169 | public function getRandomKey(): int
170 | {
171 | return $this->randomKey;
172 | }
173 | }