├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE ├── README.md ├── build.xml ├── composer.json ├── phpunit.xml ├── src ├── Api │ ├── ElectrumClient.php │ └── MiningClient.php ├── Client.php ├── Connection.php ├── Exception │ └── ApiError.php ├── Notification │ ├── AddressNotification.php │ ├── HeadersNotification.php │ ├── MiningNotification.php │ ├── NotificationInterface.php │ ├── NumBlocksNotification.php │ └── SetDifficultyNotification.php └── Request │ ├── Request.php │ ├── RequestFactory.php │ └── Response.php └── tests ├── AbstractStratumTest.php ├── Api ├── ElectrumClientTest.php └── MiningClientTest.php ├── ClientTest.php ├── ConnectionTest.php ├── Notification ├── AddressNotificationTest.php ├── HeadersNotificationTest.php ├── MiningNotificationTest.php ├── NumBlocksNotificationTest.php └── SetDifficultyNotificationTest.php └── Request ├── ErrorTest.php ├── RequestFactoryTest.php ├── RequestTest.php └── ResponseTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | vendor/ 3 | composer.lock 4 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: [vendor/*, build/*, tests/*] 3 | 4 | before_commands: 5 | - "composer install --prefer-source --no-dev" 6 | 7 | tools: 8 | php_cpd: true 9 | php_pdepend: true 10 | php_analyzer: true 11 | php_sim: true 12 | php_changetracking: true 13 | php_mess_detector: true 14 | php_code_sniffer: true 15 | sensiolabs_security_checker: true 16 | php_code_coverage: true 17 | php_pdepend: 18 | excluded_dirs: [vendor, build, tests] 19 | external_code_coverage: 20 | timeout: 1200 21 | runs: 1 22 | 23 | changetracking: 24 | bug_patterns: ["\bfix(?:es|ed)?\b"] 25 | feature_patterns: ["\badd(?:s|ed)?\b", "\bimplement(?:s|ed)?\b"] 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - hhvm 8 | - nightly 9 | 10 | before_install: 11 | - composer selfupdate 12 | 13 | install: 14 | - composer update 15 | 16 | script: 17 | - php vendor/bin/phpunit --coverage-clover build/logs/clover.xml 18 | - php vendor/bin/phpcs -n --standard=PSR1,PSR2 --report=full src/ 19 | 20 | after_success: 21 | - wget https://scrutinizer-ci.com/ocular.phar 22 | - php ocular.phar code-coverage:upload --format=php-clover build/logs/clover.xml 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## stratum-php 2 | [![Build Status](https://travis-ci.org/Bit-Wasp/stratum-php.svg?branch=master)](http://travis-ci.org/Bit-Wasp/stratum-php) 3 | [![Code Coverage](https://scrutinizer-ci.com/g/bit-wasp/stratum-php/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/bit-wasp/stratum-php/?branch=master) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Bit-Wasp/stratum-php/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Bit-Wasp/stratum-php/?branch=master) 5 | 6 | Implementation of the Stratum protocol (for electrum and mining) using ReactPHP 7 | 8 | ### Client 9 | 10 | The Client class is used to make a connection to a host. It takes a `ConnectorInterface` 11 | and `RequestFactory`. 12 | 13 | `react/socket-client` provides a number of connectors, which can be combined 14 | to produce the desired functionality. 15 | 16 | ```php 17 | use \BitWasp\Stratum\Client; 18 | use \BitWasp\Stratum\Connection; 19 | use \BitWasp\Stratum\Request\RequestFactory; 20 | 21 | $loop = \React\EventLoop\Factory::create(); 22 | 23 | $resolver = new \React\Dns\Resolver\Factory(); 24 | 25 | // Raw TCP, cannot perform DNS resolution 26 | $tcp = new \React\SocketClient\TcpConnector($loop); 27 | 28 | // TCP Connector with a DNS resolver 29 | $dns = new \React\SocketClient\DnsConnector($tcp, $resolver->create('8.8.8.8', $loop)); 30 | 31 | // Encrypted connection 32 | $context_options = []; 33 | 34 | $tls = new \React\SocketClient\SecureConnector($dns, $loop, $context_options); 35 | 36 | $requests = new RequestFactory; 37 | $client = new Client($tls, $requests); 38 | 39 | $host = ''; 40 | $port = ''; 41 | 42 | $client->connect($host, $port)->then(function (Connection $conn) { 43 | /* success */ 44 | }, function (\Exception $e) { 45 | /* error */ 46 | print_r($e->getMessage()); 47 | }); 48 | 49 | $loop->run(); 50 | ``` 51 | 52 | The SecureConnector initiates a TLS session to encrypt your connection. $context_options is an optional 53 | value, but many Electrum servers have misconfigured SSL certificates! (incorrect CN field, or are self-signed) 54 | These will not be accepted with the default verification settings, and can be disabled by changing the $context_options 55 | ``` 56 | $context_options = ["verify_name" => false, "allow_self_signed" => true]; 57 | ``` 58 | 59 | ### Connection 60 | 61 | A `Connection` represents a connection to a peer. 62 | 63 | Requests can be sent to the peer using `Connection::request($method, $params = [])`, 64 | which returns a Promise for the pending result. When a response with the same ID is 65 | received, the promise will resolve this as the result. 66 | 67 | ```php 68 | $conn->request('server.banner')->then(function (Response $response) { 69 | print_r($response->getResult()); 70 | }, function (\Exception $e) { 71 | echo $e->getMessage(); 72 | }); 73 | ``` 74 | 75 | `Request` instances can be sent using `Connection::sendRequest(Request $request)` 76 | which also returns a promise. 77 | 78 | For a list of methods for the electrum and mining clients, see the respective Api classes. 79 | The constants are method's for these APIs. 80 | 81 | ```php 82 | $conn->sendRequest(new Request(null, 'server.banner'))->then(function (Response $response) { 83 | print_r($response->getResult()); 84 | }, function (\Exception $e) { 85 | echo $e->getMessage(); 86 | }); 87 | ``` 88 | 89 | `NotificationInterface`'s can be sent using `Connection::sendNotify(NotificationInterface $note)` 90 | Notifications are not requests, and don't receive a response. This method is only relevant if 91 | using `Connection` from a servers perspective. 92 | 93 | ```php 94 | $conn->sendNotification(new NumBlocksNotification(123123)); 95 | ``` 96 | 97 | #### Api's 98 | 99 | The Stratum protocol is implemented by electrum servers and stratum mining pools. 100 | Their methods are exposed by `ElectrumClient` and `MiningClient` respectively. 101 | 102 | The api methods cause a Request to be sent, returning a promise to capture the result. 103 | 104 | ```php 105 | use \BitWasp\Stratum\Api\ElectrumClient; 106 | use \BitWasp\Stratum\Client; 107 | use \BitWasp\Stratum\Connection; 108 | use \BitWasp\Stratum\Request\Response; 109 | use \BitWasp\Stratum\Request\RequestFactory; 110 | 111 | $loop = \React\EventLoop\Factory::create(); 112 | $tcp = new \React\SocketClient\TcpConnector($loop) ; 113 | 114 | $resolver = new \React\Dns\Resolver\Factory(); 115 | $dns = new \React\SocketClient\DnsConnector($tcp,$resolver->create('8.8.8.8', $loop)); 116 | $tls = new \React\SocketClient\SecureConnector($dns, $loop); 117 | $requests = new RequestFactory; 118 | $client = new Client($tls, $requests); 119 | 120 | $host = 'anduck.net'; 121 | $port = 50002; 122 | $client->connect($host, $port)->then(function (Connection $conn) { 123 | $electrum = new ElectrumClient($conn); 124 | $electrum->addressListUnspent('1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L')->then(function (Response $response) { 125 | print_r($response->getResult()); 126 | }); 127 | }, function (\Exception $e) { 128 | echo 'error'; 129 | echo $e->getMessage().PHP_EOL; 130 | /* error */ 131 | }); 132 | 133 | $loop->run(); 134 | ``` 135 | 136 | 137 | #### Events 138 | 139 | `Connection` emits a `message` event when a message is received which 140 | was not initiated by a Request. These messages are typically due to subscriptions. 141 | 142 | The following events are emitted automatically by the library when encountered. 143 | The event name is the method used to enable the subscription. 144 | - 'blockchain.headers.subscribe' emits a `HeadersNotification` 145 | - 'blockchain.address.subscribe' emits a `AddressNotification` 146 | - 'blockchain.numblocks.subscribe' emits a `NumBlocksNotification` 147 | - 'mining.subscribe' emits a `MiningNotification` 148 | - 'mining.set_difficulty' emits a `SetDifficultyNotification` 149 | 150 | ```php 151 | use \BitWasp\Stratum\Api\ElectrumClient; 152 | use \BitWasp\Stratum\Client; 153 | use \BitWasp\Stratum\Connection; 154 | use \BitWasp\Stratum\Notification\AddressNotification; 155 | use \BitWasp\Stratum\Request\RequestFactory; 156 | 157 | $loop = React\EventLoop\Factory::create(); 158 | $tcp = new \React\SocketClient\TcpConnector($loop); 159 | $resolver = new \React\Dns\Resolver\Factory(); 160 | $dns = new \React\SocketClient\DnsConnector($tcp, $resolver->create('8.8.8.8', $loop)); 161 | $tls = new \React\SocketClient\SecureConnector($dns, $loop); 162 | 163 | $requests = new RequestFactory; 164 | $client = new Client($tls, $requests); 165 | 166 | $host = 'anduck.net'; 167 | $port = 50002; 168 | $client->connect($host, $port)->then(function (Connection $conn) { 169 | $conn->on('message', function ($message) { 170 | echo "Message received: ".PHP_EOL; 171 | print_r($message); 172 | }); 173 | 174 | $conn->on(ElectrumClient::ADDRESS_SUBSCRIBE, function (AddressNotification $address) { 175 | echo "Received address update\n"; 176 | }); 177 | 178 | $electrum = new ElectrumClient($conn); 179 | $electrum->subscribeAddress('1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L')->then(function () { 180 | echo "subscribed\n"; 181 | }); 182 | }, function (\Exception $e) { 183 | echo "ERROR: " . $e->getMessage().PHP_EOL; 184 | }); 185 | 186 | $loop->run(); 187 | ``` 188 | 189 | ### Further Information 190 | 191 | - http://docs.electrum.org/en/latest/protocol.html 192 | - https://electrum.orain.org/wiki/Stratum_protocol_specification 193 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitwasp/stratum", 3 | "description": "library for interfacing with stratum servers, used to serve both bitcoin mining and electrum clients", 4 | "type": "library", 5 | "homepage": "https://github.com/Bit-Wasp/stratum-php", 6 | "license": "Unlicense", 7 | "authors": [ 8 | { 9 | "name": "Thomas Kerin", 10 | "homepage": "https://thomaskerin.io", 11 | "role": "Author" 12 | } 13 | ], 14 | "require": { 15 | "react/socket-client": "~0.5" 16 | }, 17 | "require-dev": { 18 | "bitwasp/testing-php": "~0.1" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "BitWasp\\Stratum\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "BitWasp\\Stratum\\Tests\\": "tests/" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | tests/ 8 | 9 | 10 | 11 | 12 | 13 | src 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Api/ElectrumClient.php: -------------------------------------------------------------------------------- 1 | conn = $connection; 42 | } 43 | 44 | /** 45 | * @param string $clientVersion 46 | * @param string $protocolVersion 47 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 48 | */ 49 | public function getServerVersion($clientVersion, $protocolVersion) 50 | { 51 | return $this->conn->request(self::SERVER_VERSION, [$clientVersion, $protocolVersion]); 52 | } 53 | 54 | /** 55 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 56 | */ 57 | public function getServerBanner() 58 | { 59 | return $this->conn->request(self::SERVER_BANNER); 60 | } 61 | 62 | /** 63 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 64 | */ 65 | public function getDonationAddress() 66 | { 67 | return $this->conn->request(self::SERVER_DONATION_ADDR); 68 | } 69 | 70 | /** 71 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 72 | */ 73 | public function getServerPeers() 74 | { 75 | return $this->conn->request(self::SERVER_PEERS_SUBSCRIBE); 76 | } 77 | 78 | /** 79 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 80 | */ 81 | public function subscribeNumBlocks() 82 | { 83 | return $this->conn->request(self::NUMBLOCKS_SUBSCRIBE); 84 | } 85 | 86 | /** 87 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 88 | */ 89 | public function subscribeHeaders() 90 | { 91 | return $this->conn->request(self::HEADERS_SUBSCRIBE); 92 | } 93 | 94 | /** 95 | * @param string $address 96 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 97 | */ 98 | public function subscribeAddress($address) 99 | { 100 | return $this->conn->request(self::ADDRESS_SUBSCRIBE, [$address]); 101 | } 102 | 103 | /** 104 | * @param string $hex 105 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 106 | */ 107 | public function transactionBroadcast($hex) 108 | { 109 | return $this->conn->request(self::TRANSACTION_BROADCAST, [$hex]); 110 | } 111 | 112 | /** 113 | * @param string $txid 114 | * @param int $height 115 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 116 | */ 117 | public function transactionGetMerkle($txid, $height) 118 | { 119 | return $this->conn->request(self::TRANSACTION_GET_MERKLE, [$txid, $height]); 120 | } 121 | 122 | /** 123 | * @param string $txid 124 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 125 | */ 126 | public function transactionGet($txid) 127 | { 128 | return $this->conn->request(self::TRANSACTION_GET, [$txid]); 129 | } 130 | 131 | /** 132 | * @param string $address 133 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 134 | */ 135 | public function addressGetHistory($address) 136 | { 137 | return $this->conn->request(self::ADDRESS_GET_HISTORY, [$address]); 138 | } 139 | 140 | /** 141 | * @param string $address 142 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 143 | */ 144 | public function addressGetBalance($address) 145 | { 146 | return $this->conn->request(self::ADDRESS_GET_BALANCE, [$address]); 147 | } 148 | 149 | /** 150 | * @param string $address 151 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 152 | */ 153 | public function addressGetProof($address) 154 | { 155 | return $this->conn->request(self::ADDRESS_GET_PROOF, [$address]); 156 | } 157 | 158 | /** 159 | * @param string $address 160 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 161 | */ 162 | public function addressListUnspent($address) 163 | { 164 | return $this->conn->request(self::ADDRESS_LIST_UNSPENT, [$address]); 165 | } 166 | 167 | /** 168 | * @param string $address 169 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 170 | */ 171 | public function addressGetMempool($address) 172 | { 173 | return $this->conn->request(self::ADDRESS_GET_MEMPOOL, [$address]); 174 | } 175 | 176 | /** 177 | * @param string $txid 178 | * @param int $nOutput 179 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 180 | */ 181 | public function utxoGetAddress($txid, $nOutput) 182 | { 183 | return $this->conn->request(self::UTXO_GET_ADDRESS, [$txid, $nOutput]); 184 | } 185 | 186 | /** 187 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 188 | */ 189 | public function estimateFee() 190 | { 191 | return $this->conn->request(self::ESTIMATE_FEE); 192 | } 193 | 194 | /** 195 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 196 | */ 197 | public function relayFee() 198 | { 199 | return $this->conn->request(self::RELAY_FEE); 200 | } 201 | 202 | /** 203 | * @param int $blockHeight 204 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 205 | */ 206 | public function blockGetHeader($blockHeight) 207 | { 208 | return $this->conn->request(self::BLOCK_GET_HEADER, [$blockHeight]); 209 | } 210 | 211 | /** 212 | * @param int $blockHeight 213 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 214 | */ 215 | public function blockGetChunk($blockHeight) 216 | { 217 | return $this->conn->request(self::BLOCK_GET_CHUNK, [$blockHeight]); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/Api/MiningClient.php: -------------------------------------------------------------------------------- 1 | conn = $connection; 27 | } 28 | 29 | /** 30 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 31 | */ 32 | public function subscribeMining() 33 | { 34 | return $this->conn->request(self::MINING_SUBSCRIBE); 35 | } 36 | 37 | /** 38 | * @param string $user 39 | * @param string $password 40 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 41 | */ 42 | public function authorize($user, $password) 43 | { 44 | return $this->conn->request(self::AUTHORIZE, [$user, $password]); 45 | } 46 | 47 | /** 48 | * @param string $worker_name 49 | * @param string $job_id 50 | * @param int $extranonce2 51 | * @param int $ntime 52 | * @param int $nonce 53 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 54 | */ 55 | public function submit($worker_name, $job_id, $extranonce2, $ntime, $nonce) 56 | { 57 | return $this->conn->request(self::SUBMIT, [$worker_name, $job_id, $extranonce2, $ntime, $nonce]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | connector = $connector; 29 | $this->requestFactory = $requestFactory; 30 | } 31 | 32 | /** 33 | * @param string $host 34 | * @param int $port 35 | * @return string 36 | */ 37 | public function connect($host, $port) 38 | { 39 | return $this->connector->create($host, $port)->then(function (Stream $stream) { 40 | return new Connection($stream, $this->requestFactory); 41 | }, function (\Exception $e) { 42 | throw $e; 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | factory = $requestFactory; 49 | $this->stream = $stream; 50 | $this->stream->on('data', [$this, 'onData']); 51 | } 52 | 53 | public function close() 54 | { 55 | return $this->stream->close(); 56 | } 57 | 58 | /** 59 | * @param string $data 60 | * @return bool|void 61 | */ 62 | public function sendData($data) 63 | { 64 | return $this->stream->write($data); 65 | } 66 | 67 | /** 68 | * @param NotificationInterface $notification 69 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 70 | */ 71 | public function sendNotify(NotificationInterface $notification) 72 | { 73 | return $this->sendData($notification->toRequest()->write()); 74 | } 75 | 76 | /** 77 | * @param Request $request 78 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 79 | */ 80 | public function sendRequest(Request $request) 81 | { 82 | $result = new Deferred(); 83 | $this->deferred[$request->getId()] = $result; 84 | $this->sendData($request->write()); 85 | return $result->promise(); 86 | } 87 | 88 | /** 89 | * @param string $method 90 | * @param array $params 91 | * @return \React\Promise\Promise|\React\Promise\PromiseInterface 92 | */ 93 | public function request($method, array $params = []) 94 | { 95 | $request = $this->factory->create($method, $params); 96 | return $this->sendRequest($request); 97 | } 98 | 99 | /** 100 | * @param string $data 101 | */ 102 | public function onData($data) 103 | { 104 | $buffer = $this->streamBuffer . $data; 105 | 106 | while (($nextPos = strpos($buffer, "\n"))) { 107 | $msg = substr($buffer, 0, $nextPos); 108 | $buffer = substr($buffer, $nextPos); 109 | if (substr($buffer, -1) == "\n") { 110 | $buffer = substr($buffer, 1); 111 | } 112 | $this->onMessage($msg); 113 | } 114 | 115 | if (!$buffer) { 116 | $this->streamBuffer = ''; 117 | } else { 118 | $this->streamBuffer = $buffer; 119 | } 120 | } 121 | 122 | /** 123 | * @param string $data 124 | * @throws \BitWasp\Stratum\Exception\ApiError 125 | * @throws \Exception 126 | */ 127 | public function onMessage($data) 128 | { 129 | $response = $this->factory->response($data); 130 | if (isset($this->deferred[$response->getId()])) { 131 | $this->deferred[$response->getId()]->resolve($response); 132 | } else { 133 | $this->emit('message', [$response]); 134 | 135 | if ($response instanceof Request) { 136 | $params = $response->getParams(); 137 | 138 | switch ($response->getMethod()) { 139 | case ElectrumClient::HEADERS_SUBSCRIBE: 140 | if (!isset($params[0])) { 141 | throw new \RuntimeException('Headers notification missing body'); 142 | } 143 | 144 | $header = $params[0]; 145 | if (count($header) !== 8) { 146 | throw new \RuntimeException('Headers notification missing parameter'); 147 | } 148 | 149 | $this->emit(ElectrumClient::HEADERS_SUBSCRIBE, [new HeadersNotification($header[0], $header[1], $header[2], $header[3], $header[4], $header[5], $header[6], $header[7])]); 150 | break; 151 | case ElectrumClient::ADDRESS_SUBSCRIBE: 152 | if (!isset($params[0]) || !isset($params[1])) { 153 | throw new \RuntimeException('Address notification missing address/txid'); 154 | } 155 | 156 | $this->emit(ElectrumClient::ADDRESS_SUBSCRIBE, [new AddressNotification($params[0], $params[1])]); 157 | break; 158 | case ElectrumClient::NUMBLOCKS_SUBSCRIBE: 159 | if (!isset($params[0])) { 160 | throw new \RuntimeException('Missing notification parameter: height'); 161 | } 162 | 163 | $this->emit(ElectrumClient::NUMBLOCKS_SUBSCRIBE, [new NumBlocksNotification($params[0])]); 164 | break; 165 | case MiningClient::SET_DIFFICULTY: 166 | if (!isset($params[0])) { 167 | throw new \RuntimeException('Missing mining difficulty notification parameter'); 168 | } 169 | 170 | $this->emit(MiningClient::SET_DIFFICULTY, [new SetDifficultyNotification($params[0])]); 171 | break; 172 | case MiningClient::NOTIFY: 173 | if (count($params) !== 9) { 174 | throw new \RuntimeException('Missing mining notification parameter'); 175 | } 176 | 177 | $this->emit(MiningClient::NOTIFY, [new MiningNotification($params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $params[6], $params[7], $params[8])]); 178 | break; 179 | } 180 | } 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Exception/ApiError.php: -------------------------------------------------------------------------------- 1 | id = $id; 19 | parent::__construct($error); 20 | } 21 | 22 | /** 23 | * @return string 24 | */ 25 | public function getId() 26 | { 27 | return $this->id; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function write() 34 | { 35 | return json_encode([ 36 | 'id' => $this->id, 37 | 'error' => $this->getMessage() 38 | ]) . "\n"; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Notification/AddressNotification.php: -------------------------------------------------------------------------------- 1 | address = $address; 28 | $this->txid = $txid; 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getAddress() 35 | { 36 | return $this->address; 37 | } 38 | 39 | /** 40 | * @return string 41 | */ 42 | public function getTxid() 43 | { 44 | return $this->txid; 45 | } 46 | 47 | /** 48 | * @return Request 49 | */ 50 | public function toRequest() 51 | { 52 | return new Request(null, ElectrumClient::ADDRESS_SUBSCRIBE, [$this->address, $this->txid]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Notification/HeadersNotification.php: -------------------------------------------------------------------------------- 1 | nonce = $nonce; 64 | $this->prevBlock = $prevBlock; 65 | $this->timestamp = $timestamp; 66 | $this->merkleRoot = $merkleroot; 67 | $this->height = $height; 68 | $this->utxoRoot = $utxoRoot; 69 | $this->version = $version; 70 | $this->bits = $bits; 71 | } 72 | 73 | /** 74 | * @return int 75 | */ 76 | public function getVersion() 77 | { 78 | return $this->version; 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getPrevBlock() 85 | { 86 | return $this->prevBlock; 87 | } 88 | 89 | /** 90 | * @return string 91 | */ 92 | public function getMerkleRoot() 93 | { 94 | return $this->merkleRoot; 95 | } 96 | 97 | /** 98 | * @return int 99 | */ 100 | public function getTimestamp() 101 | { 102 | return $this->timestamp; 103 | } 104 | 105 | /** 106 | * @return int 107 | */ 108 | public function getBits() 109 | { 110 | return $this->bits; 111 | } 112 | 113 | /** 114 | * @return int 115 | */ 116 | public function getNonce() 117 | { 118 | return $this->nonce; 119 | } 120 | 121 | /** 122 | * @return int 123 | */ 124 | public function getHeight() 125 | { 126 | return $this->height; 127 | } 128 | 129 | /** 130 | * @return string 131 | */ 132 | public function getUtxoRoot() 133 | { 134 | return $this->utxoRoot; 135 | } 136 | 137 | /** 138 | * @return Request 139 | */ 140 | public function toRequest() 141 | { 142 | return new Request(null, ElectrumClient::HEADERS_SUBSCRIBE, [[ 143 | $this->nonce, $this->prevBlock, $this->timestamp, 144 | $this->merkleRoot, $this->height, $this->utxoRoot, 145 | $this->version, $this->bits 146 | ]]); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Notification/MiningNotification.php: -------------------------------------------------------------------------------- 1 | job_id = $job_id; 70 | $this->prevhash = $prevhash; 71 | $this->coinb1 = $coinb1; 72 | $this->coinb2 = $coinb2; 73 | $this->merkle_branch = $merkle_branch; 74 | $this->version = $version; 75 | $this->nbits = $bits; 76 | $this->ntime = $time; 77 | $this->clean_jobs = $cleanjobs; 78 | } 79 | 80 | /** 81 | * @return string 82 | */ 83 | public function getJobId() 84 | { 85 | return $this->job_id; 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | public function getPrevhash() 92 | { 93 | return $this->prevhash; 94 | } 95 | 96 | /** 97 | * @return string 98 | */ 99 | public function getCoinb1() 100 | { 101 | return $this->coinb1; 102 | } 103 | 104 | /** 105 | * @return string 106 | */ 107 | public function getCoinb2() 108 | { 109 | return $this->coinb2; 110 | } 111 | 112 | /** 113 | * @return array 114 | */ 115 | public function getMerkleBranch() 116 | { 117 | return $this->merkle_branch; 118 | } 119 | 120 | /** 121 | * @return int 122 | */ 123 | public function getVersion() 124 | { 125 | return $this->version; 126 | } 127 | 128 | /** 129 | * @return int 130 | */ 131 | public function getBits() 132 | { 133 | return $this->nbits; 134 | } 135 | 136 | /** 137 | * @return int 138 | */ 139 | public function getTime() 140 | { 141 | return $this->ntime; 142 | } 143 | 144 | /** 145 | * @return boolean 146 | */ 147 | public function isCleanJobs() 148 | { 149 | return $this->clean_jobs; 150 | } 151 | 152 | /** 153 | * @return Request 154 | */ 155 | public function toRequest() 156 | { 157 | return new Request(null, MiningClient::MINING_SUBSCRIBE, [$this->job_id, $this->prevhash, $this->coinb1, $this->coinb2, $this->merkle_branch, $this->version, $this->nbits, $this->ntime, $this->clean_jobs]); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Notification/NotificationInterface.php: -------------------------------------------------------------------------------- 1 | height = $height; 22 | } 23 | 24 | /** 25 | * @return int 26 | */ 27 | public function getHeight() 28 | { 29 | return $this->height; 30 | } 31 | 32 | /** 33 | * @return Request 34 | */ 35 | public function toRequest() 36 | { 37 | return new Request(null, ElectrumClient::NUMBLOCKS_SUBSCRIBE, [$this->height]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Notification/SetDifficultyNotification.php: -------------------------------------------------------------------------------- 1 | difficulty = $difficulty; 22 | } 23 | 24 | /** 25 | * @return int|string 26 | */ 27 | public function getDifficulty() 28 | { 29 | return $this->difficulty; 30 | } 31 | 32 | /** 33 | * @return Request 34 | */ 35 | public function toRequest() 36 | { 37 | return new Request(null, MiningClient::SET_DIFFICULTY, [$this->difficulty]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Request/Request.php: -------------------------------------------------------------------------------- 1 | id = $id; 29 | $this->method = $method; 30 | $this->params = $params; 31 | } 32 | 33 | /** 34 | * @return int 35 | */ 36 | public function getId() 37 | { 38 | return $this->id; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getMethod() 45 | { 46 | return $this->method; 47 | } 48 | 49 | /** 50 | * @return array 51 | */ 52 | public function getParams() 53 | { 54 | return $this->params; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function write() 61 | { 62 | return json_encode( 63 | [ 64 | "json-rpc" => "2.0", 65 | "id" => $this->id, 66 | "method" => $this->method, 67 | "params" => $this->params 68 | ] 69 | ) . "\n"; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Request/RequestFactory.php: -------------------------------------------------------------------------------- 1 | nonces)); 24 | 25 | return new Request($id, $method, $params); 26 | } 27 | 28 | /** 29 | * @param string $string 30 | * @return Response|Request|ApiError 31 | * @throws \Exception 32 | */ 33 | public function response($string) 34 | { 35 | $decoded = json_decode(trim($string), true); 36 | 37 | if (json_last_error() === JSON_ERROR_NONE) { 38 | $id = isset($decoded['id']) ? $decoded['id'] : null; 39 | 40 | if (isset($decoded['error'])) { 41 | return new ApiError($id, $decoded['error']); 42 | } elseif (isset($decoded['method']) && isset($decoded['params'])) { 43 | return new Request($id, $decoded['method'], $decoded['params']); 44 | } elseif (isset($decoded['result'])) { 45 | return new Response($id, $decoded['result']); 46 | } 47 | 48 | throw new \Exception('Response missing error/params/result'); 49 | } 50 | 51 | throw new \Exception('Invalid JSON'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Request/Response.php: -------------------------------------------------------------------------------- 1 | id = $id; 24 | $this->result = $result; 25 | } 26 | 27 | /** 28 | * @return int|string 29 | */ 30 | public function getId() 31 | { 32 | return $this->id; 33 | } 34 | 35 | /** 36 | * @return mixed 37 | */ 38 | public function getResult() 39 | { 40 | return $this->result; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function write() 47 | { 48 | return json_encode([ 49 | 'id' => $this->id, 50 | 'result' => $this->result 51 | ]) . "\n"; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/AbstractStratumTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('\BitWasp\Stratum\Connection') 44 | ->disableOriginalConstructor() 45 | ->setMethods(['request']) 46 | ->getMock(); 47 | 48 | $conn->expects($this->once())->method('request') 49 | ->with($stratumMethod, $params) 50 | ->willReturn(new FulfilledPromise(new Request(1, $stratumMethod, $params))); 51 | 52 | $server = new ElectrumClient($conn); 53 | 54 | $result = call_user_func_array([$server, $method], $params); 55 | $result->then(function (Request $request) use ($stratumMethod) { 56 | 57 | }, function () { 58 | $this->fail(); 59 | }); 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Api/MiningClientTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('\BitWasp\Stratum\Connection') 27 | ->disableOriginalConstructor() 28 | ->setMethods(['request']) 29 | ->getMock(); 30 | 31 | $conn->expects($this->once())->method('request') 32 | ->with($stratumMethod, $params) 33 | ->willReturn(new FulfilledPromise(new Request(1, $stratumMethod, $params))); 34 | 35 | $server = new MiningClient($conn); 36 | 37 | $result = call_user_func_array([$server, $method], $params); 38 | $result->then(function (Request $request) use ($stratumMethod) { 39 | 40 | }, function () { 41 | $this->fail(); 42 | }); 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/ClientTest.php: -------------------------------------------------------------------------------- 1 | promise()->then(function ($value) use ($loop) { 20 | $this->assertEquals(1, $value); 21 | $loop->stop(); 22 | }, function () { 23 | $this->fail('Promise was rejected'); 24 | }); 25 | 26 | $server = new Server($loop); 27 | $server->listen(54321, '0.0.0.0'); 28 | $server->on('connection', function (Stream $stream) use ($deferred, $server) { 29 | $deferred->resolve(1); 30 | $server->shutdown(); 31 | }); 32 | 33 | $request = new RequestFactory(); 34 | $connector = new TcpConnector($loop); 35 | $client = new Client($connector, $request); 36 | $client->connect('127.0.0.1', 54321); 37 | 38 | $loop->run(); 39 | } 40 | 41 | public function testConnectFails() 42 | { 43 | 44 | $loop = new StreamSelectLoop(); 45 | $deferred = new Deferred(); 46 | $deferred->promise()->then(function () { 47 | $this->fail('should not have succeeded'); 48 | }, function ($value) use ($loop) { 49 | $this->assertEquals(1, $value); 50 | $loop->stop(); 51 | }); 52 | 53 | $request = new RequestFactory(); 54 | $connector = new TcpConnector($loop); 55 | $client = new Client($connector, $request); 56 | $client->connect('127.0.0.1', 54320); 57 | 58 | $loop->run(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/ConnectionTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('\React\Stream\Stream') 26 | ->disableOriginalConstructor() 27 | ->setMethods(['write', 'create', 'close']) 28 | ->getMock(); 29 | } 30 | 31 | public function testSend() 32 | { 33 | $sentData = new Deferred(); 34 | $sentData->promise()->then(function () { 35 | $this->assertEquals('test', func_get_args()[0]); 36 | }, function () { 37 | $this->fail(); 38 | }); 39 | 40 | $stream = $this->getMockStream(); 41 | $stream->expects($this->once()) 42 | ->method('write') 43 | ->willReturnCallback(function () use (&$sentData) { 44 | $args = func_get_args(); 45 | if (isset($args[0])) { 46 | $sentData->resolve($args[0]); 47 | } else { 48 | $sentData->reject(); 49 | } 50 | }); 51 | 52 | $factory = new RequestFactory(); 53 | $connection = new Connection($stream, $factory); 54 | $connection->sendData('test'); 55 | } 56 | 57 | public function testNotify() 58 | { 59 | $note = new NumBlocksNotification(392312); 60 | 61 | $serialized = new Deferred(); 62 | $serialized->promise()->then(function () use ($note) { 63 | $this->assertEquals($note->toRequest()->write(), func_get_args()[0]); 64 | }, function () { 65 | $this->fail(); 66 | }); 67 | 68 | $stream = $this->getMockStream(); 69 | $stream->expects($this->once()) 70 | ->method('write') 71 | ->willReturnCallback(function () use ($note, &$serialized) { 72 | $args = func_get_args(); 73 | if (isset($args[0])) { 74 | $serialized->resolve($args[0]); 75 | } else { 76 | $serialized->reject(); 77 | } 78 | }); 79 | 80 | $factory = new RequestFactory(); 81 | $connection = new Connection($stream, $factory); 82 | $connection->sendNotify($note); 83 | } 84 | 85 | public function testRequest() 86 | { 87 | $expected = new Request(null, 'test.method', [123]); 88 | 89 | $serialized = new Deferred(); 90 | $serialized->promise()->then(function (Request $request) use ($expected) { 91 | $this->assertEquals($expected->getMethod(), $request->getMethod()); 92 | $this->assertEquals($expected->getParams(), $request->getParams()); 93 | $this->assertEquals($expected->write(), func_get_args()[0]); 94 | }, function ($e) { 95 | $this->fail($e); 96 | }); 97 | 98 | $stream = $this->getMockStream(); 99 | $stream->expects($this->once()) 100 | ->method('write') 101 | ->willReturnCallback(function () use (&$serialized) { 102 | $args = func_get_args(); 103 | if (isset($args[0])) { 104 | $serialized->resolve($args[0]); 105 | } else { 106 | $serialized->reject(); 107 | } 108 | }); 109 | 110 | $factory = new RequestFactory(); 111 | $connection = new Connection($stream, $factory); 112 | $connection->request('test.method', [123]); 113 | } 114 | 115 | public function testClose() 116 | { 117 | $stream = $this->getMockStream(); 118 | $stream->expects($this->once())->method('close'); 119 | 120 | $factory = new RequestFactory(); 121 | $connection = new Connection($stream, $factory); 122 | $connection->close(); 123 | } 124 | 125 | public function getNotificationVectors() 126 | { 127 | return [ 128 | ["id"=> null, "method" => ElectrumClient::ADDRESS_SUBSCRIBE, "params" => ['1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L', '690ce08a148447f482eb3a74d714f30a6d4fe06a918a0893d823fd4aca4df580']], 129 | ["id"=> null, "method" => ElectrumClient::NUMBLOCKS_SUBSCRIBE, "params" => [1]], 130 | ["id"=> null, "method" => MiningClient::SET_DIFFICULTY, "params" => [1]], 131 | ["id"=> null, "method" => ElectrumClient::HEADERS_SUBSCRIBE, "params" => [array_values(json_decode('{"nonce": 132 | 3355909169, "prev_block_hash": 133 | "00000000000000002b3ef284c2c754ab6e6abc40a0e31a974f966d8a2b4d5206", 134 | "timestamp": 1408252887, "merkle_root": 135 | "6d979a3d8d0f8757ed96adcd4781b9707cc192824e398679833abcb2afdf8d73", 136 | "block_height": 316023, "utxo_root": 137 | "4220a1a3ed99d2621c397c742e81c95be054c81078d7eeb34736e2cdd7506a03", 138 | "version": 2, "bits": 406305378}', true))]], 139 | ["id"=> null, "method" => MiningClient::NOTIFY, "params" => json_decode('["bf", "4d16b6f85af6e2198f44ae2a6de67f78487ae5611b77c6c0440b921e00000000", 140 | "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff20020862062f503253482f04b8864e5008", 141 | "072f736c7573682f000000000100f2052a010000001976a914d23fcdf86f7e756a64a7a9688ef9903327048ed988ac00000000", [], 142 | "00000002", "1c2ac4af", "504e86b9", false]', true)] 143 | ]; 144 | } 145 | 146 | /** 147 | * @param string $id 148 | * @param string $method 149 | * @param array $params 150 | * @throws \React\Socket\ConnectionException 151 | * @dataProvider getNotificationVectors 152 | */ 153 | public function testDataTriggersNotification($id, $method, $params) 154 | { 155 | $event = $method; 156 | 157 | $array = ["id"=> $id, "method" => $method, "params" => $params]; 158 | $data = json_encode($array)."\n"; 159 | 160 | $loop = new StreamSelectLoop(); 161 | $request = new RequestFactory(); 162 | 163 | $server = new Server($loop); 164 | $server->on('connection', function (SocketConnection $connection) use ($data, $server) { 165 | $connection->write($data); 166 | $connection->on('close', function () use ($server) { 167 | $server->shutdown(); 168 | }); 169 | }); 170 | $server->listen(54323, '127.0.0.1'); 171 | 172 | $deferred = new Deferred(); 173 | $deferred->promise()->then(function (NotificationInterface $note) use ($data, $server) { 174 | $value = $note->toRequest(); 175 | $this->assertEquals($data['method'], $value->getMethod()); 176 | $this->assertEquals($data['params'], $value->getParams()); 177 | }); 178 | 179 | $tcp = new TcpConnector($loop); 180 | $client = new Client($tcp, $request); 181 | $client->connect('127.0.0.1', 54323)->then(function (Connection $connection) use ($event, $deferred, $loop) { 182 | $connection->on($event, function (NotificationInterface $request) use ($deferred, $connection) { 183 | $deferred->resolve($request); 184 | $connection->close(); 185 | }); 186 | }); 187 | 188 | $loop->run(); 189 | } 190 | 191 | public function testReturnsResponse() 192 | { 193 | $loop = new StreamSelectLoop(); 194 | $request = new RequestFactory(); 195 | 196 | $server = new Server($loop); 197 | $server->on('connection', function (SocketConnection $connection) use ($server, $request) { 198 | $connection->on('data', function ($data) use ($connection, $request) { 199 | $req = $request->response($data); 200 | $response = new Response($req->getId(), ['1.0']); 201 | $connection->write($response->write()); 202 | }); 203 | 204 | $connection->on('close', function () use ($server) { 205 | $server->shutdown(); 206 | }); 207 | }); 208 | 209 | $server->listen(54323, '127.0.0.1'); 210 | 211 | $tcp = new TcpConnector($loop); 212 | $client = new Client($tcp, $request); 213 | $client->connect('127.0.0.1', 54323)->then(function (Connection $connection) use ($loop) { 214 | $deferred = new Deferred(); 215 | $deferred->promise()->then(function ($value) { 216 | $this->assertEquals(1, $value); 217 | }); 218 | 219 | $electrum = new ElectrumClient($connection); 220 | $electrum->getServerVersion('1.9.6', ' 0.6')->then(function () use ($deferred, $connection) { 221 | $deferred->resolve(1); 222 | $connection->close(); 223 | }, function () use ($loop) { 224 | $loop->stop(); 225 | $this->fail(); 226 | }); 227 | }); 228 | 229 | $loop->run(); 230 | } 231 | 232 | public function getNotificationFailureVectors() 233 | { 234 | return [ 235 | [null, ElectrumClient::ADDRESS_SUBSCRIBE, []], 236 | [null, ElectrumClient::ADDRESS_SUBSCRIBE, ['1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L']], 237 | [null, ElectrumClient::NUMBLOCKS_SUBSCRIBE, []], 238 | [null, ElectrumClient::HEADERS_SUBSCRIBE, []], 239 | [null, ElectrumClient::HEADERS_SUBSCRIBE, [array_fill(0, 7, 0)]], 240 | [null, MiningClient::NOTIFY, [array_fill(0, 8, 0)]], 241 | [null, MiningClient::SET_DIFFICULTY, []], 242 | ]; 243 | } 244 | 245 | /** 246 | * @param $id 247 | * @param $method 248 | * @param $params 249 | * @expectedException \RuntimeException 250 | * @dataProvider getNotificationFailureVectors 251 | */ 252 | public function testDataNotificationFailures($id, $method, $params) 253 | { 254 | $array = ["id"=> $id, "method" => $method, "params" => $params]; 255 | $data = json_encode($array)."\n"; 256 | 257 | $request = new RequestFactory(); 258 | 259 | $loop = new StreamSelectLoop(); 260 | $connection = new Connection(new Stream(fopen('php://stdin', 'r+'), $loop), $request); 261 | $connection->onMessage($data); 262 | } 263 | 264 | public function testOnDataWorksWithMultilineMessages() 265 | { 266 | $data = json_encode([ 267 | "id"=> 1, "result" => [1] 268 | ])."\n". 269 | json_encode([ 270 | "id"=> 2, "result" => [2] 271 | ])."\n"; 272 | 273 | $loop = new StreamSelectLoop(); 274 | $request = new RequestFactory(); 275 | $connection = new Connection(new Stream(fopen('php://stdin', 'r'), $loop), $request); 276 | $counter = 0; 277 | $connection->on('message', function () use (&$counter) { 278 | $counter++; 279 | }); 280 | $connection->onData($data); 281 | $this->assertEquals(2, $counter); 282 | } 283 | 284 | public function testOnDataWorksWithPartialMessages() 285 | { 286 | $data = json_encode([ 287 | "id"=> 1, "result" => [1] 288 | ])."\n"; 289 | $half1 = substr($data, 0, 4); 290 | $half2 = substr($data, 4); 291 | 292 | $loop = new StreamSelectLoop(); 293 | $request = new RequestFactory(); 294 | $connection = new Connection(new Stream(fopen('php://stdin', 'r'), $loop), $request); 295 | $counter = 0; 296 | $connection->on('message', function () use (&$counter) { 297 | $counter++; 298 | }); 299 | 300 | $connection->onData($half1); 301 | $connection->onData($half2); 302 | $this->assertEquals(1, $counter); 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /tests/Notification/AddressNotificationTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($address, $note->getAddress()); 19 | $this->assertEquals($txid, $note->getTxid()); 20 | } 21 | 22 | public function testToRequest() 23 | { 24 | $address = '1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L'; 25 | $txid = '690ce08a148447f482eb3a74d714f30a6d4fe06a918a0893d823fd4aca4df580'; 26 | 27 | $note = new AddressNotification($address, $txid); 28 | 29 | $request = $note->toRequest(); 30 | $this->assertEquals(null, $request->getId()); 31 | $this->assertEquals(ElectrumClient::ADDRESS_SUBSCRIBE, $request->getMethod()); 32 | $this->assertEquals([$address, $txid], $request->getParams()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Notification/HeadersNotificationTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($sample['nonce'], $headers->getNonce()); 33 | $this->assertEquals($sample['prev_block_hash'], $headers->getPrevBlock()); 34 | $this->assertEquals($sample['timestamp'], $headers->getTimestamp()); 35 | $this->assertEquals($sample['merkle_root'], $headers->getMerkleRoot()); 36 | $this->assertEquals($sample['block_height'], $headers->getHeight()); 37 | $this->assertEquals($sample['utxo_root'], $headers->getUtxoRoot()); 38 | $this->assertEquals($sample['version'], $headers->getVersion()); 39 | $this->assertEquals($sample['bits'], $headers->getBits()); 40 | } 41 | 42 | 43 | public function testToRequest() 44 | { 45 | 46 | $sample = json_decode('{"nonce": 47 | 3355909169, "prev_block_hash": 48 | "00000000000000002b3ef284c2c754ab6e6abc40a0e31a974f966d8a2b4d5206", 49 | "timestamp": 1408252887, "merkle_root": 50 | "6d979a3d8d0f8757ed96adcd4781b9707cc192824e398679833abcb2afdf8d73", 51 | "block_height": 316023, "utxo_root": 52 | "4220a1a3ed99d2621c397c742e81c95be054c81078d7eeb34736e2cdd7506a03", 53 | "version": 2, "bits": 406305378}', true); 54 | $values = array_values($sample); 55 | 56 | $headers = new HeadersNotification( 57 | $sample['nonce'], 58 | $sample['prev_block_hash'], 59 | $sample['timestamp'], 60 | $sample['merkle_root'], 61 | $sample['block_height'], 62 | $sample['utxo_root'], 63 | $sample['version'], 64 | $sample['bits'] 65 | ); 66 | $request = $headers->toRequest(); 67 | $this->assertEquals(null, $request->getId()); 68 | $this->assertEquals(ElectrumClient::HEADERS_SUBSCRIBE, $request->getMethod()); 69 | $this->assertEquals([$values], $request->getParams()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Notification/MiningNotificationTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($sample[0], $note->getJobId()); 20 | $this->assertEquals($sample[1], $note->getPrevhash()); 21 | $this->assertEquals($sample[2], $note->getCoinb1()); 22 | $this->assertEquals($sample[3], $note->getCoinb2()); 23 | $this->assertEquals($sample[4], $note->getMerkleBranch()); 24 | $this->assertEquals($sample[5], $note->getVersion()); 25 | $this->assertEquals($sample[6], $note->getBits()); 26 | $this->assertEquals($sample[7], $note->getTime()); 27 | $this->assertEquals($sample[8], $note->isCleanJobs()); 28 | } 29 | 30 | public function testToRequest() 31 | { 32 | $sample = json_decode('["bf", "4d16b6f85af6e2198f44ae2a6de67f78487ae5611b77c6c0440b921e00000000", 33 | "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff20020862062f503253482f04b8864e5008", 34 | "072f736c7573682f000000000100f2052a010000001976a914d23fcdf86f7e756a64a7a9688ef9903327048ed988ac00000000", [], 35 | "00000002", "1c2ac4af", "504e86b9", false]', true); 36 | 37 | $note = new MiningNotification($sample[0], $sample[1], $sample[2], $sample[3], $sample[4], $sample[5], $sample[6], $sample[7], $sample[8]); 38 | $request = $note->toRequest(); 39 | $this->assertEquals(null, $request->getId()); 40 | $this->assertEquals(MiningClient::MINING_SUBSCRIBE, $request->getMethod()); 41 | $this->assertEquals($sample, $request->getParams()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Notification/NumBlocksNotificationTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($height, $note->getHeight()); 16 | } 17 | 18 | public function testToRequest() 19 | { 20 | $height = 123123; 21 | $note = new NumBlocksNotification($height); 22 | $request = $note->toRequest(); 23 | $this->assertEquals(null, $request->getId()); 24 | $this->assertEquals(ElectrumClient::NUMBLOCKS_SUBSCRIBE, $request->getMethod()); 25 | $this->assertEquals([$height], $request->getParams()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Notification/SetDifficultyNotificationTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($diff, $note->getDifficulty()); 16 | } 17 | 18 | public function testToRequest() 19 | { 20 | $diff = 1; 21 | $note = new SetDifficultyNotification($diff); 22 | $request = $note->toRequest(); 23 | $this->assertEquals(null, $request->getId()); 24 | $this->assertEquals(MiningClient::SET_DIFFICULTY, $request->getMethod()); 25 | $this->assertEquals([$diff], $request->getParams()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Request/ErrorTest.php: -------------------------------------------------------------------------------- 1 | response(json_encode(['id' => 909, 'error' => $error])); 19 | $this->assertEquals($id, $e->getId()); 20 | $this->assertEquals($error, $e->getMessage()); 21 | 22 | $written = json_encode(['id' => $id, 'error' =>$error])."\n"; 23 | $this->assertEquals($written, $e->write()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Request/RequestFactoryTest.php: -------------------------------------------------------------------------------- 1 | response('{'); 18 | } 19 | 20 | /** 21 | * @expectedException \Exception 22 | * @expectedExceptionMessage Response missing error/params/result 23 | */ 24 | public function testMalformedResponse() 25 | { 26 | $factory = new RequestFactory(); 27 | $factory->response('{}'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Request/RequestTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($id, $request->getId()); 19 | $this->assertEquals($method, $request->getMethod()); 20 | $this->assertEquals($params, $request->getParams()); 21 | 22 | $written = json_encode(["json-rpc" => "2.0", "id" => $id, "method" => $method, "params" => $params]) . "\n"; 23 | $this->assertEquals($written, $request->write()); 24 | } 25 | 26 | public function testRequestFactoryCreate() 27 | { 28 | $factory = new RequestFactory(); 29 | 30 | $method = 'service.help'; 31 | $params = ['a','b','c']; 32 | $request = $factory->create($method, $params); 33 | 34 | $this->assertEquals($method, $request->getMethod()); 35 | $this->assertEquals($params, $request->getParams()); 36 | } 37 | 38 | public function testRequestFactoryParse() 39 | { 40 | $factory = new RequestFactory(); 41 | 42 | $id = 909; 43 | $method = 'this.method'; 44 | $params = ['a','b','c']; 45 | 46 | /** @var Request $request */ 47 | $request = $factory->response(json_encode(['id'=>909, 'method' => $method, 'params' => $params])); 48 | 49 | $this->assertInstanceOf('BitWasp\Stratum\Request\Request', $request); 50 | $this->assertEquals($id, $request->getId()); 51 | $this->assertEquals($method, $request->getMethod()); 52 | $this->assertEquals($params, $request->getParams()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Request/ResponseTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($id, $response->getId()); 18 | $this->assertEquals($result, $response->getResult()); 19 | 20 | $written = json_encode(["id"=>$id, "result" => $result]) . "\n"; 21 | $this->assertEquals($written, $response->write()); 22 | } 23 | 24 | public function testRequestFactory() 25 | { 26 | $factory = new RequestFactory(); 27 | 28 | $id = 909; 29 | $result = ['a','b','c']; 30 | $response = $factory->response(json_encode(['id'=>909, 'result' => $result])); 31 | 32 | $this->assertInstanceOf('BitWasp\Stratum\Request\Response', $response); 33 | $this->assertEquals($id, $response->getId()); 34 | $this->assertEquals($result, $response->getResult()); 35 | } 36 | } 37 | --------------------------------------------------------------------------------