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
3namespace Attestto\SolanaPhpSdk;
4
5use GuzzleHttp\Psr7\Message;
6
7use Psr\Http\Client\ClientExceptionInterface;
8use Psr\Http\Client\ClientInterface;
9use Psr\Http\Message\RequestFactoryInterface;
10use GuzzleHttp\Client as GuzzleClient;
11use GuzzleHttp\Psr7\HttpFactory;
12
13
14use Attestto\SolanaPhpSdk\Exceptions\GenericException;
15use Attestto\SolanaPhpSdk\Exceptions\InvalidIdResponseException;
16use Attestto\SolanaPhpSdk\Exceptions\MethodNotFoundException;
17use Psr\Http\Message\StreamFactoryInterface;
18use Psr\Http\Message\UriFactoryInterface;
19use Random\RandomException;
20
21
22/**
23 * @see https://docs.solana.com/developing/clients/jsonrpc-api
24 */
25class 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}