├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── src ├── BananoBlock.php ├── BananoCLI.php ├── BananoIPC.php ├── BananoRPC.php ├── BananoRPCExt.php ├── BananoTool.php ├── BananoWS.php └── Util │ ├── Base.php │ ├── Bin.php │ └── Uint.php └── test ├── BananoBlock ├── change.php ├── open.php ├── receive.php └── send.php ├── BananoCLI.php ├── BananoIPC.php ├── BananoIPCListen.php ├── BananoRPC.php ├── BananoRPCExt.php ├── BananoTool ├── account2public.php ├── den2den.php ├── den2raw.php ├── diff2mult.php ├── hashHexs.php ├── hex2mnem.php ├── keys.php ├── mnem2hex.php ├── mnem2mseed.php ├── mseed2keys.php ├── mult2diff.php ├── private2public.php ├── public2account.php ├── raw2den.php ├── seed2keys.php ├── sign.php ├── string2burn.php ├── validSign.php ├── validWork.php └── work.php ├── BananoWS.php ├── autoload.php └── speed ├── BananoIPC.php └── BananoRPC.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | .project 3 | /vendor -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 MikeRow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BananoPHP 2 | 3 | PHP libraries and tools for Banano currency 4 | 5 | Examples at [BananoPHP/test](https://github.com/MikeRow/BananoPHP/tree/master/test) 6 | 7 | --- 8 | 9 | ## Install 10 | 11 |
12 | composer require mikerow/bananophp
13 | 
14 | 15 | --- 16 | 17 | ## Features 18 | 19 | - [BananoBlock](https://github.com/MikeRow/BananoPHP/blob/master/src/BananoBlock.php) 20 | 21 | class for building Banano blocks 22 | 23 | - [BananoCLI](https://github.com/MikeRow/BananoPHP/blob/master/src/BananoCLI.php) 24 | 25 | class for interfacing to Banano node CLI 26 | 27 | - [BananoIPC](https://github.com/MikeRow/BananoPHP/blob/master/src/BananoIPC.php) 28 | 29 | class for interfacing to Banano node IPC 30 | 31 | - [BananoRPC](https://github.com/MikeRow/BananoPHP/blob/master/src/BananoRPC.php) 32 | 33 | class for interfacing to Banano node RPC 34 | 35 | - [BananoRPCExt](https://github.com/MikeRow/BananoPHP/blob/master/src/BananoRPCExt.php) 36 | 37 | additional functions for BananoRPC 38 | 39 | - [BananoTool](https://github.com/MikeRow/BananoPHP/blob/master/src/BananoTool.php) 40 | 41 | class for node-independent Banano functions 42 | 43 | - [BananoWS](https://github.com/MikeRow/BananoPHP/blob/master/src/BananoWS.php) 44 | 45 | class for interfacing to Banano node WebSocket 46 | 47 | --- 48 | 49 | ## FAQ 50 | 51 | #### How to perform calculations with Banano denominations or raws? 52 | 53 |
PHP faces troubles when dealing with Banano amounts ... 54 |

55 | 56 | - Data type `float` isn't precise at certain decimal depths 57 | - Data type `integer` size is limited to 64 bit 58 | 59 | A good solution is to perform calculations in raws using [GNU Multiple Precision](https://www.php.net/manual/en/book.gmp.php) 60 | 61 |

62 |
63 | 64 | #### Why not use libsodium instead of Salt or php-blake2? 65 | 66 |
Some limitations prevent the use of libsodium ... 67 |

68 | 69 | - Functions `sodium_crypto_sign_*` use SHA-2 instead Blake2 70 | - Functions `sodium_crypto_generichash_*` don't allow output smaller than 16 bytes 71 | 72 |

73 |
74 | 75 | --- 76 | 77 | ## To do 78 | 79 | - Add Epoch v2 support to BananoBlock 80 | - Add FlatBuffers support to BananoWS 81 | - Increase FlatBuffers performances 82 | - Enable listening on IPC 83 | 84 | --- 85 | 86 | ## Credits 87 | 88 | Thanks a lot for the work and effort of 89 | 90 | - [strawbrary/php-blake2](https://github.com/strawbrary/php-blake2) 91 | - [Textalk/websocket-php](https://github.com/Textalk/websocket-php) 92 | - [google/flatbuffers](https://github.com/google/flatbuffers) 93 | - [Bit-Wasp/bitcoin-lib-php](https://github.com/Bit-Wasp/bitcoin-lib-php) 94 | - [aceat64/EasyBitcoin-PHP](https://github.com/aceat64/EasyBitcoin-PHP) 95 | - [jaimehgb/RaiBlocksPHP](https://github.com/jaimehgb/RaiBlocksPHP) 96 | - [Sergey Kroshnin](https://github.com/SergiySW) 97 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "mikerow/bananophp", 3 | "type" : "library", 4 | "description" : "PHP libraries and tools for Banano currency", 5 | "keywords" : [ 6 | "banano", 7 | "php", 8 | "cli", 9 | "rpc", 10 | "ipc", 11 | "websocket", 12 | "tools", 13 | "blocks" 14 | ], 15 | "homepage" : "https://github.com/MikeRow/BananoPHP", 16 | "license" : "MIT", 17 | "authors" : [{ 18 | "name" : "MikeRow", 19 | "role" : "Developer" 20 | } 21 | ], 22 | "funding" : [{ 23 | "type" : "Banano", 24 | "url" : "https://creeper.banano.cc/explorer/account/ban_1mikerow9bqzyqo4ejra6ugr1srerq1egwmacerquch3dz1wry7mkrz4768m" 25 | } 26 | ], 27 | "minimum-stability" : "stable", 28 | "require" : { 29 | "php-64bit" : "^7.2.0", 30 | "ext-curl" : "*", 31 | "ext-sockets" : "*", 32 | "ext-gmp" : "*", 33 | "ext-mbstring" : "*", 34 | "ext-openssl" : "*", 35 | "mikerow/salt" : "*", 36 | "bitwasp/bitcoin-lib" : "1.0.*", 37 | "textalk/websocket" : "*", 38 | "google/flatbuffers" : "*" 39 | }, 40 | "suggest" : { 41 | "ext-blake2" : "Install blake2 extension in order to speed up some functions: https://github.com/strawbrary/php-blake2" 42 | }, 43 | "autoload" : { 44 | "psr-4" : { 45 | "MikeRow\\BananoPHP\\" : "src/" 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/BananoBlock.php: -------------------------------------------------------------------------------- 1 | privateKey = $private_key; 45 | $this->publicKey = BananoTool::private2public($private_key); 46 | $this->account = BananoTool::public2account($this->publicKey); 47 | } 48 | 49 | 50 | // * 51 | // * Set previous block 52 | // * 53 | 54 | public function setPrev(string $prev_block_id, array $prev_block) 55 | { 56 | if (strlen($prev_block_id) != 64 || !hex2bin($prev_block_id)) { 57 | throw new BananoBlockException("Invalid previous block ID: $prev_block_id"); 58 | } 59 | if (count($prev_block) < 8) { 60 | throw new BananoBlockException("Invalid previous block array count: less than 8"); 61 | } 62 | 63 | $this->prevBlockId = $prev_block_id; 64 | $this->prevBlock = $prev_block; 65 | } 66 | 67 | 68 | // * 69 | // * Automatically set previous block 70 | // * 71 | 72 | public function autoPrev(bool $auto) 73 | { 74 | $this->prevAuto = $auto; 75 | } 76 | 77 | 78 | // * 79 | // * Set work 80 | // * 81 | 82 | public function setWork(string $work) 83 | { 84 | if (strlen($work) != 16 || !hex2bin($work)) { 85 | throw new BananoBlockException("Invalid work: $work"); 86 | } 87 | 88 | $this->work = $work; 89 | } 90 | 91 | 92 | // * 93 | // * Build open block 94 | // * 95 | 96 | public function open(string $pairing_block_id, string $received_amount, string $representative): array 97 | { 98 | // Check inputs 99 | if (strlen($pairing_block_id) != 64 || !hex2bin($pairing_block_id)) { 100 | throw new BananoBlockException("Invalid pairing block ID: $pairing_block_id"); 101 | } 102 | if (!ctype_digit($received_amount)) { 103 | throw new BananoBlockException("Invalid received amount: $received_amount"); 104 | } 105 | if (!BananoTool::account2public($representative, false)) { 106 | throw new BananoBlockException("Invalid representative: $representative"); 107 | } 108 | if ($this->work != null) { 109 | if (strlen($this->work) != 16 || !hex2bin($this->work)) { 110 | throw new BananoBlockException("Invalid work: {$this->work}"); 111 | } 112 | } 113 | 114 | // Build block 115 | $balance = dechex($received_amount); 116 | $balance = str_repeat('0', (32 - strlen($balance))) . $balance; 117 | 118 | $this->rawBlockId = []; 119 | $this->rawBlockId[] = BananoTool::PREAMBLE_HEX; 120 | $this->rawBlockId[] = $this->publicKey; 121 | $this->rawBlockId[] = BananoTool::EMPTY32_HEX; 122 | $this->rawBlockId[] = BananoTool::account2public($representative); 123 | $this->rawBlockId[] = $balance; 124 | $this->rawBlockId[] = $pairing_block_id; 125 | 126 | $this->blockId = BananoTool::hashHexs($this->rawBlockId); 127 | $this->signature = BananoTool::sign($this->blockId, $this->privateKey); 128 | 129 | $this->block = [ 130 | 'type' => 'state', 131 | 'account' => $this->account, 132 | 'previous' => BananoTool::EMPTY32_HEX, 133 | 'representative' => $representative, 134 | 'balance' => hexdec($balance), 135 | 'link' => $pairing_block_id, 136 | 'signature' => $this->signature, 137 | 'work' => $this->work 138 | ]; 139 | 140 | if ($this->prevAuto) { 141 | $this->prevBlock = $this->block; 142 | $this->prevBlockId = $this->blockId; 143 | } else { 144 | $this->prevBlock = []; 145 | $this->prevBlockId = null; 146 | } 147 | 148 | $this->work = null; 149 | 150 | return $this->block; 151 | } 152 | 153 | 154 | // * 155 | // * Build receive block 156 | // * 157 | 158 | public function receive(string $pairing_block_id, string $received_amount, string $representative = null): array 159 | { 160 | // Check previous block info and ID 161 | if (!isset($this->prevBlock['balance']) || 162 | !isset($this->prevBlock['representative']) || 163 | !ctype_digit($this->prevBlock['balance']) || 164 | !BananoTool::account2public($this->prevBlock['representative'], false) 165 | ) { 166 | throw new BananoBlockException("Invalid previous block"); 167 | } 168 | if (strlen($this->prevBlockId) != 64 || !hex2bin($this->prevBlockId)) { 169 | throw new BananoBlockException("Invalid previous block ID: {$this->prevBlockId}"); 170 | } 171 | 172 | // Check inputs 173 | if (strlen($pairing_block_id) != 64 || !hex2bin($pairing_block_id)) { 174 | throw new BananoBlockException("Invalid pairing block ID: $pairing_block_id"); 175 | } 176 | if (!ctype_digit($received_amount)) { 177 | throw new BananoBlockException("Invalid received amount: $received_amount"); 178 | } 179 | if ($representative == null) { 180 | $representative = $this->prevBlock['representative']; 181 | } 182 | if (!BananoTool::account2public($representative, false)) { 183 | throw new BananoBlockException("Invalid representative: $representative"); 184 | } 185 | if ($this->work != null) { 186 | if (strlen($this->work) != 16 || !hex2bin($this->work)) { 187 | throw new BananoBlockException("Invalid work: {$this->work}"); 188 | } 189 | } 190 | 191 | // Build block 192 | $balance = dechex( 193 | gmp_strval( 194 | gmp_add($this->prevBlock['balance'], $received_amount) 195 | ) 196 | ); 197 | $balance = str_repeat('0', (32 - strlen($balance))) . $balance; 198 | 199 | $this->rawBlockId = []; 200 | $this->rawBlockId[] = BananoTool::PREAMBLE_HEX; 201 | $this->rawBlockId[] = $this->publicKey; 202 | $this->rawBlockId[] = $this->prevBlockId; 203 | $this->rawBlockId[] = BananoTool::account2public($representative); 204 | $this->rawBlockId[] = $balance; 205 | $this->rawBlockId[] = $pairing_block_id; 206 | 207 | $this->blockId = BananoTool::hashHexs($this->rawBlockId); 208 | $this->signature = BananoTool::sign($this->blockId, $this->privateKey); 209 | 210 | $this->block = [ 211 | 'type' => 'state', 212 | 'account' => $this->account, 213 | 'previous' => $this->prevBlockId, 214 | 'representative' => $representative, 215 | 'balance' => hexdec($balance), 216 | 'link' => $pairing_block_id, 217 | 'signature' => $this->signature, 218 | 'work' => $this->work 219 | ]; 220 | 221 | if ($this->prevAuto) { 222 | $this->prevBlock = $this->block; 223 | $this->prevBlockId = $this->blockId; 224 | } else { 225 | $this->prevBlock = []; 226 | $this->prevBlockId = null; 227 | } 228 | 229 | $this->work = null; 230 | 231 | return $this->block; 232 | } 233 | 234 | 235 | // * 236 | // * Build send block 237 | // * 238 | 239 | public function send(string $destination, string $sending_amount, string $representative = null): array 240 | { 241 | // Check previous block info and ID 242 | if (!isset($this->prevBlock['balance']) || 243 | !isset($this->prevBlock['representative']) || 244 | !ctype_digit($this->prevBlock['balance']) || 245 | !BananoTool::account2public($this->prevBlock['representative'], false) 246 | ) { 247 | throw new BananoBlockException("Invalid previous block"); 248 | } 249 | if (strlen($this->prevBlockId) != 64 || !hex2bin($this->prevBlockId)) { 250 | throw new BananoBlockException("Invalid previous block ID: {$this->prevBlockId}"); 251 | } 252 | 253 | // Check inputs 254 | if (!BananoTool::account2public($destination, false)) { 255 | throw new BananoBlockException("Invalid destination: $destination"); 256 | } 257 | if (!ctype_digit($sending_amount)) { 258 | throw new BananoBlockException("Invalid sending amount: $sending_amount"); 259 | } 260 | if ($representative == null) { 261 | $representative = $this->prevBlock['representative']; 262 | } 263 | if (!BananoTool::account2public($representative, false)) { 264 | throw new BananoBlockException("Invalid representative: $representative"); 265 | } 266 | if ($this->work != null) { 267 | if (strlen($this->work) != 16 || !hex2bin($this->work)) { 268 | throw new BananoBlockException("Invalid work: {$this->work}"); 269 | } 270 | } 271 | 272 | // Build block 273 | $balance = dechex( 274 | gmp_strval( 275 | gmp_sub($this->prevBlock['balance'], $sending_amount) 276 | ) 277 | ); 278 | if (strpos($balance, '-') !== false) { 279 | throw new BananoBlockException("Insufficient balance: $balance"); 280 | } 281 | $balance = str_repeat('0', (32 - strlen($balance))) . $balance; 282 | 283 | $this->rawBlockId = []; 284 | $this->rawBlockId[] = BananoTool::PREAMBLE_HEX; 285 | $this->rawBlockId[] = $this->publicKey; 286 | $this->rawBlockId[] = $this->prevBlockId; 287 | $this->rawBlockId[] = BananoTool::account2public($representative); 288 | $this->rawBlockId[] = $balance; 289 | $this->rawBlockId[] = BananoTool::account2public($destination); 290 | 291 | $this->blockId = BananoTool::hashHexs($this->rawBlockId); 292 | $this->signature = BananoTool::sign($this->blockId, $this->privateKey); 293 | 294 | $this->block = [ 295 | 'type' => 'state', 296 | 'account' => $this->account, 297 | 'previous' => $this->prevBlockId, 298 | 'representative' => $representative, 299 | 'balance' => hexdec($balance), 300 | 'link' => $destination, 301 | 'signature' => $this->signature, 302 | 'work' => $this->work 303 | ]; 304 | 305 | if ($this->prevAuto) { 306 | $this->prevBlock = $this->block; 307 | $this->prevBlockId = $this->blockId; 308 | } else { 309 | $this->prevBlock = []; 310 | $this->prevBlockId = null; 311 | } 312 | 313 | $this->work = null; 314 | 315 | return $this->block; 316 | } 317 | 318 | 319 | // * 320 | // * Build change block 321 | // * 322 | 323 | public function change(string $representative): array 324 | { 325 | // Check previous block info and ID 326 | if (!isset($this->prevBlock['balance']) || 327 | !isset($this->prevBlock['representative']) || 328 | !ctype_digit($this->prevBlock['balance']) || 329 | !BananoTool::account2public($this->prevBlock['representative'], false) 330 | ) { 331 | throw new BananoBlockException("Invalid previous block"); 332 | } 333 | if (strlen($this->prevBlockId) != 64 || !hex2bin($this->prevBlockId)) { 334 | throw new BananoBlockException("Invalid previous block ID: {$this->prevBlockId}"); 335 | } 336 | 337 | // Check inputs 338 | if (!BananoTool::account2public($representative, false)) { 339 | throw new BananoBlockException("Invalid representative: $representative"); 340 | } 341 | if ($this->work != null) { 342 | if (strlen($this->work) != 16 || !hex2bin($this->work)) { 343 | throw new BananoBlockException("Invalid work: {$this->work}"); 344 | } 345 | } 346 | 347 | // Build block 348 | $balance = dechex($this->prevBlock['balance']); 349 | $balance = str_repeat('0', (32 - strlen($balance))) . $balance; 350 | 351 | $this->rawBlockId = []; 352 | $this->rawBlockId[] = BananoTool::PREAMBLE_HEX; 353 | $this->rawBlockId[] = $this->publicKey; 354 | $this->rawBlockId[] = $this->prevBlockId; 355 | $this->rawBlockId[] = BananoTool::account2public($representative); 356 | $this->rawBlockId[] = $balance; 357 | $this->rawBlockId[] = BananoTool::EMPTY32_HEX; 358 | 359 | $this->blockId = BananoTool::hashHexs($this->rawBlockId); 360 | $this->signature = BananoTool::sign($this->blockId, $this->privateKey); 361 | 362 | $this->block = [ 363 | 'type' => 'state', 364 | 'account' => $this->account, 365 | 'previous' => $this->prevBlockId, 366 | 'representative' => $representative, 367 | 'balance' => hexdec($balance), 368 | 'link' => BananoTool::EMPTY32_HEX, 369 | 'signature' => $this->signature, 370 | 'work' => $this->work 371 | ]; 372 | 373 | if ($this->prevAuto) { 374 | $this->prevBlock = $this->block; 375 | $this->prevBlockId = $this->blockId; 376 | } else { 377 | $this->prevBlock = []; 378 | $this->prevBlockId = null; 379 | } 380 | 381 | $this->work = null; 382 | 383 | return $this->block; 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/BananoCLI.php: -------------------------------------------------------------------------------- 1 | pathToApp = escapeshellarg($path_to_app); 31 | } 32 | 33 | 34 | // * 35 | // * Call 36 | // * 37 | 38 | public function __call($method, array $params) 39 | { 40 | $this->id++; 41 | $this->response = null; 42 | $this->status = null; 43 | $this->error = null; 44 | 45 | if (!isset($params[0])) { 46 | $params[0] = []; 47 | } 48 | 49 | $request = ' --' . $method; 50 | 51 | if (isset($params[0])) { 52 | foreach ($params[0] as $key => $value) { 53 | $request .= ' --' . $key . '=' . $value; 54 | } 55 | } 56 | 57 | $this->error = exec($this->pathToApp . $request . ' 2>&1', $this->response, $this->status); 58 | 59 | if ($this->status == 0) { 60 | $this->error = null; 61 | return $this->response; 62 | } else { 63 | $this->response = null; 64 | return false; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/BananoIPC.php: -------------------------------------------------------------------------------- 1 | pathToSocket = (string) $params[0]; 51 | } else { 52 | $this->pathToSocket = '/tmp/banano'; 53 | } 54 | 55 | // Timeout 56 | if (isset($options['timeout'])) { 57 | $this->options['timeout'] = (float) $options['timeout']; 58 | } else { 59 | $this->options['timeout'] = 15; 60 | } 61 | 62 | // Flags 63 | if (isset($options['flags'])) { 64 | $this->options['flags'] = (int) $options['flags']; 65 | } else { 66 | $this->options['flags'] = STREAM_CLIENT_CONNECT; 67 | } 68 | 69 | // Context 70 | if (isset($options['context']) && is_array($options['context'])) { 71 | $this->options['context'] = stream_context_create($options['context']); 72 | } else { 73 | $this->options['context'] = stream_context_create([]); 74 | } 75 | 76 | 77 | // * TCP 78 | 79 | } elseif ($transport_type == 'tcp') { 80 | // Hostname 81 | if (isset($params[0])) { 82 | $this->hostname = (string) $params[0]; 83 | } else { 84 | $this->hostname = 'localhost'; 85 | } 86 | 87 | // Port 88 | if (isset($params[1])) { 89 | $this->port = (int) $params[1]; 90 | } else { 91 | $this->port = 7077; 92 | } 93 | 94 | // Timeout 95 | if (isset($options['timeout'])) { 96 | $this->options['timeout'] = (float) $options['timeout']; 97 | } else { 98 | $this->options['timeout'] = 15; 99 | } 100 | 101 | // Flags 102 | if (isset($options['flags'])) { 103 | $this->options['flags'] = (int) $options['flags']; 104 | } else { 105 | $this->options['flags'] = STREAM_CLIENT_CONNECT; 106 | } 107 | 108 | // Context 109 | if (isset($options['context']) && is_array($options['context'])) { 110 | $this->options['context'] = stream_context_create($options['context']); 111 | } else { 112 | $this->options['context'] = stream_context_create([]); 113 | } 114 | } else { 115 | throw new BananoIPCException("Invalid transport type: $transport_type"); 116 | } 117 | 118 | $this->transportType = $transport_type; 119 | $this->bananoEncoding = 2; 120 | $this->bananoPreamble = 'N' . chr($this->bananoEncoding) . chr(0) . chr(0); 121 | $this->listen = false; 122 | } 123 | 124 | 125 | // * 126 | // * Set listen 127 | // * 128 | 129 | public function setListen(bool $listen) 130 | { 131 | $this->listen = $listen; 132 | } 133 | 134 | 135 | // * 136 | // * Set Banano encoding 137 | // * 138 | 139 | public function setBananoEncoding(int $banano_encoding) 140 | { 141 | if ($banano_encoding != 1 && 142 | $banano_encoding != 2 && 143 | $banano_encoding != 3 && 144 | $banano_encoding != 4 145 | ) { 146 | throw new BananoIPCException("Invalid Banano encoding: $banano_encoding"); 147 | } 148 | 149 | $this->bananoEncoding = $banano_encoding; 150 | $this->bananoPreamble = 'N' . chr($this->bananoEncoding) . chr(0) . chr(0); 151 | } 152 | 153 | 154 | // * 155 | // * Set Banano API key 156 | // * 157 | 158 | public function setBananoAPIKey(string $banano_api_key) 159 | { 160 | if (empty($banano_api_key)){ 161 | throw new BananoIPCException("Invalid Banano API key: $banano_api_key"); 162 | } 163 | 164 | $this->bananoAPIKey = (string) $banano_api_key; 165 | } 166 | 167 | 168 | // * 169 | // * Open connection 170 | // * 171 | 172 | public function open(): bool 173 | { 174 | // * Unix domain socket 175 | 176 | if ($this->transportType == 'unix') { 177 | $this->transport = stream_socket_client( 178 | "unix://{$this->pathToSocket}", 179 | $this->errorCode, 180 | $this->error, 181 | $this->options['timeout'], 182 | $this->options['flags'], 183 | $this->options['context'] 184 | ); 185 | 186 | 187 | // * TCP 188 | 189 | } elseif ($this->transportType == 'tcp') { 190 | $this->transport = stream_socket_client( 191 | "tcp://{$this->hostname}:{$this->port}", 192 | $this->errorCode, 193 | $this->error, 194 | $this->options['timeout'], 195 | $this->options['flags'], 196 | $this->options['context'] 197 | ); 198 | } else { 199 | throw new BananoIPCException("Invalid transport type"); 200 | } 201 | 202 | if ($this->transport) { 203 | return true; 204 | } else { 205 | return false; 206 | } 207 | } 208 | 209 | 210 | // * 211 | // * Close connection 212 | // * 213 | 214 | public function close() 215 | { 216 | if ($this->transport != null) { 217 | stream_socket_shutdown($this->transport, STREAM_SHUT_RDWR); 218 | $this->transport = null; 219 | } 220 | } 221 | 222 | 223 | // * 224 | // * Call 225 | // * 226 | 227 | public function __call($method, array $params) 228 | { 229 | // Check transport connection 230 | if ($this->transport == null) { 231 | throw new BananoIPCException("Transport connection is not opened"); 232 | } 233 | 234 | $this->id++; 235 | $this->response = null; 236 | $this->responseRaw = null; 237 | $this->responseType = null; 238 | $this->responseTime = null; 239 | $this->error = null; 240 | $this->errorCode = null; 241 | 242 | if (!isset($params[0])) { 243 | $params[0] = []; 244 | } 245 | 246 | 247 | // * 248 | // * Request: Banano encoding switch 249 | // * 250 | 251 | // * 1/2 252 | 253 | if ($this->bananoEncoding == 1 || 254 | $this->bananoEncoding == 2 255 | ) { 256 | $request = $params[0]; 257 | $request['action'] = $method; 258 | 259 | $request = json_encode($request); 260 | 261 | 262 | // * 3 263 | 264 | } elseif ($this->bananoEncoding == 3) { 265 | if (!class_exists('\\MikeRow\\Bandano\\BananoAPI\\' . $method, true)) { 266 | $this->error = 'Invalid call'; 267 | return false; 268 | } 269 | 270 | $builder = new \Google\FlatBuffers\FlatbufferBuilder(0); 271 | 272 | foreach ($params[0] as $key => $value) { 273 | $params[0][$key] = $builder->createString($value); 274 | } 275 | 276 | $correlation_id = $builder->createString((string) $this->id); 277 | $credentials = $builder->createString($this->bananoAPIKey); 278 | $message_type = constant("\\MikeRow\\Bandano\\BananoAPI\\Message::$method"); 279 | 280 | // Build arguments 281 | call_user_func_array( 282 | '\\MikeRow\\Bandano\\BananoAPI\\' . $method . '::start' . $method, 283 | [$builder] 284 | ); 285 | 286 | foreach ($params[0] as $key => $value) { 287 | if (!method_exists('\\MikeRow\\Bandano\\BananoAPI\\' . $method, 'add' . $key)) { 288 | $this->error = 'Invalid call'; 289 | return false; 290 | } 291 | call_user_func_array( 292 | '\\MikeRow\\Bandano\\BananoAPI\\' . $method . '::add' . $key, 293 | [$builder, $value] 294 | ); 295 | } 296 | 297 | $message = call_user_func_array( 298 | '\\MikeRow\\Bandano\\BananoAPI\\' . $method . '::end' . $method, 299 | [$builder] 300 | ); 301 | 302 | // Build envelope 303 | $envelope = \MikeRow\Bandano\BananoAPI\Envelope::createEnvelope( 304 | $builder, 305 | null, 306 | $credentials, 307 | $correlation_id, 308 | $message_type, 309 | $message 310 | ); 311 | 312 | $builder->finish($envelope); 313 | $request = $builder->sizedByteArray(); 314 | 315 | 316 | // * 4 317 | 318 | } elseif ($this->bananoEncoding == 4) { 319 | $request = [ 320 | 'correlation_id' => (string) $this->id, 321 | 'message_type' => $method, 322 | 'message' => $params[0] 323 | ]; 324 | 325 | // Banano API key 326 | if ($this->bananoAPIKey != null) { 327 | $request['credentials'] = $this->bananoAPIKey; 328 | } 329 | 330 | $request = json_encode($request); 331 | } else { 332 | throw new BananoIPCException("Invalid Banano encoding"); 333 | } 334 | 335 | $buffer = $this->bananoPreamble . pack("N", strlen($request)) . $request; 336 | 337 | 338 | // * 339 | // * Request/Response: transport switch 340 | // * 341 | 342 | if ($this->transportType == 'unix' || 343 | $this->transportType == 'tcp' 344 | ) { 345 | // Request 346 | $socket = fwrite($this->transport, $buffer); 347 | if ($socket === false) { 348 | $this->error = 'Unable to send request'; 349 | return false; 350 | } 351 | 352 | // If listening, skip response 353 | if ($this->listen) { 354 | return; 355 | } 356 | 357 | // Response lenght 358 | $size = fread($this->transport, 4); 359 | if ($size === false) { 360 | $this->error = 'Unable to receive response lenght'; 361 | return false; 362 | } 363 | if (strlen($size) == 0) { 364 | $this->error = 'Unable to receive response lenght'; 365 | return false; 366 | } 367 | 368 | $size = unpack("N", $size); 369 | 370 | // Response 371 | $this->responseRaw = fread($this->transport, $size[1]); 372 | if ($this->responseRaw === false) { 373 | $this->error = 'Unable to receive response'; 374 | return false; 375 | } 376 | } else { 377 | throw new BananoIPCException("Invalid transport type"); 378 | } 379 | 380 | 381 | // * 382 | // * Response: Banano encoding switch 383 | // * 384 | 385 | // * 1/2 386 | 387 | if ($this->bananoEncoding == 1 || 388 | $this->bananoEncoding == 2 389 | ) { 390 | $this->response = json_decode($this->responseRaw, true); 391 | 392 | if (isset($this->response['error'])) { 393 | $this->error = $this->response['error']; 394 | $this->response = null; 395 | } 396 | 397 | 398 | // * 3 399 | 400 | } elseif ($this->bananoEncoding == 3) { 401 | $buffer = \Google\FlatBuffers\ByteBuffer::wrap($this->responseRaw); 402 | $envelope = \MikeRow\Bandano\BananoAPI\Envelope::getRootAsEnvelope($buffer); 403 | 404 | $this->responseType = \MikeRow\Bandano\BananoAPI\Message::Name($envelope->getMessageType()); 405 | $this->responseTime = $envelope->getTime(); 406 | 407 | if ($envelope->getCorrelationId() != $this->id) { 408 | $this->error = 'Correlation Id doesn\'t match'; 409 | } 410 | 411 | if ($this->responseType == 'Error') { 412 | $this->error = $envelope->getMessage(new \MikeRow\Bandano\BananoAPI\Error())->getMessage(); 413 | $this->errorCode = $envelope->getMessage(new \MikeRow\Bandano\BananoAPI\Error())->getCode(); 414 | } else { 415 | $model = '\\MikeRow\\Bandano\\BananoAPI\\' . $this->responseType; 416 | 417 | $methods = get_class_methods($model); 418 | foreach ($methods as $method) { 419 | if (substr($method, 0, 3) == 'get' && 420 | $method != 'getRootAs' . $this->responseType 421 | ) { 422 | $this->response[substr($method, 3)] = $envelope->getMessage(new $model())->$method(); 423 | } 424 | } 425 | } 426 | 427 | 428 | // * 4 429 | 430 | } elseif ($this->bananoEncoding == 4) { 431 | $this->response = json_decode($this->responseRaw, true); 432 | 433 | $this->responseType = $this->response['message_type']; 434 | 435 | $this->responseTime = (int) $this->response['time']; 436 | 437 | if ($this->response['correlation_id'] != $this->id) { 438 | $this->error = 'Correlation Id doesn\'t match'; 439 | } 440 | 441 | if ($this->response['message_type'] == 'Error') { 442 | $this->error = $this->response['message']; 443 | $this->errorCode = (int) $this->response['message']['code']; 444 | $this->response = null; 445 | } else { 446 | $this->response = $this->response['message']; 447 | } 448 | } else { 449 | throw new BananoIPCException("Invalid Banano encoding"); 450 | } 451 | 452 | 453 | // * Return 454 | 455 | if ($this->error) { 456 | return false; 457 | } else { 458 | return $this->response; 459 | } 460 | } 461 | 462 | 463 | // * 464 | // * Listen 465 | // * 466 | 467 | public function listen() 468 | { 469 | // Check transport connection 470 | if ($this->transport == null) { 471 | throw new BananoIPCException("Transport connection is not opened"); 472 | } 473 | 474 | // Check if listen is enabled 475 | if (!$this->listen) { 476 | throw new BananoIPCException("Listen is not enabled"); 477 | } 478 | 479 | 480 | // * 481 | // * Response: transport switch 482 | // * 483 | 484 | if ($this->transportType == 'unix' || 485 | $this->transportType == 'tcp' 486 | ) { 487 | // Response lenght 488 | $size = fread($this->transport, 4); 489 | if ($size === false) { 490 | $this->error = 'Unable to receive response lenght'; 491 | return false; 492 | } 493 | if (strlen($size) == 0) { 494 | $this->error = 'Unable to receive response lenght'; 495 | return false; 496 | } 497 | 498 | $size = unpack("N", $size); 499 | 500 | // Response 501 | $this->responseRaw = fread($this->transport, $size[1]); 502 | if ($this->responseRaw === false) { 503 | $this->error = 'Unable to receive response'; 504 | return false; 505 | } 506 | } else { 507 | throw new BananoIPCException("Invalid transport type"); 508 | } 509 | 510 | 511 | // * 512 | // * Response: Banano encoding switch 513 | // * 514 | 515 | // * 1/2 516 | 517 | if ($this->bananoEncoding == 1 || 518 | $this->bananoEncoding == 2 519 | ) { 520 | return json_decode($this->responseRaw, true); 521 | 522 | 523 | // * 3 524 | 525 | } elseif ($this->bananoEncoding == 3) { 526 | return \Google\FlatBuffers\ByteBuffer::wrap($this->responseRaw); 527 | 528 | 529 | // * 4 530 | 531 | } elseif ($this->bananoEncoding == 4) { 532 | return json_decode($this->responseRaw, true); 533 | } else { 534 | throw new BananoIPCException("Invalid Banano encoding"); 535 | } 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /src/BananoRPC.php: -------------------------------------------------------------------------------- 1 | protocol = $protocol; 60 | $this->hostname = $hostname; 61 | $this->port = $port; 62 | $this->url = $url; 63 | $this->bananoApi = 1; 64 | 65 | $this->options = 66 | [ 67 | CURLOPT_RETURNTRANSFER => true, 68 | CURLOPT_FOLLOWLOCATION => true, 69 | CURLOPT_USERAGENT => 'Bandano/BananoRPC', 70 | CURLOPT_MAXREDIRS => 10, 71 | CURLOPT_HTTPHEADER => ['Content-type: application/json'] 72 | ]; 73 | 74 | if (is_array($options)) { 75 | foreach ($options as $key => $value) { 76 | $this->options[$key] = $value; 77 | } 78 | } 79 | } 80 | 81 | 82 | // * 83 | // * Set Banano API 84 | // * 85 | 86 | public function setBananoApi(int $banano_api) 87 | { 88 | if ($banano_api != 1 && 89 | $banano_api != 2 90 | ) { 91 | throw new BananoRPCException("Invalid Banano API: $banano_api"); 92 | } 93 | 94 | $this->bananoApi = $banano_api; 95 | } 96 | 97 | 98 | // * 99 | // * Set Banano API key 100 | // * 101 | 102 | public function setBananoApiKey(string $banano_api_key) 103 | { 104 | if (empty($banano_api_key)){ 105 | throw new BananoRPCException("Invalid Banano API key: $banano_api_key"); 106 | } 107 | 108 | $this->bananoApiKey = (string) $banano_api_key; 109 | } 110 | 111 | 112 | // * 113 | // * Call 114 | // * 115 | 116 | public function __call($method, array $params) 117 | { 118 | $this->id++; 119 | $this->response = null; 120 | $this->responseRaw = null; 121 | $this->responseType = null; 122 | $this->responseTime = null; 123 | $this->status = null; 124 | $this->error = null; 125 | $this->errorCode = null; 126 | 127 | if (!isset($params[0])) { 128 | $params[0] = []; 129 | } 130 | 131 | 132 | // * 133 | // * Request: API switch 134 | // * 135 | 136 | // * v1 137 | 138 | if ($this->bananoApi == 1) { 139 | $request = $params[0]; 140 | $request['action'] = $method; 141 | 142 | 143 | // * v2 144 | 145 | } elseif ($this->bananoApi == 2) { 146 | $request = [ 147 | 'correlation_id' => (string) $this->id, 148 | 'message_type' => $method, 149 | 'message' => $params[0] 150 | ]; 151 | 152 | // Banano API key 153 | if ($this->bananoApiKey != null) { 154 | $request['credentials'] = $this->bananoApiKey; 155 | } 156 | } else { 157 | throw new BananoRPCException("Invalid Banano API key"); 158 | } 159 | 160 | $request = json_encode($request); 161 | 162 | 163 | // * Build the cURL session 164 | 165 | $curl = curl_init("{$this->protocol}://{$this->hostname}:{$this->port}/{$this->url}"); 166 | 167 | $this->options[CURLOPT_POST] = true; 168 | $this->options[CURLOPT_POSTFIELDS] = $request; 169 | 170 | 171 | // * Call 172 | 173 | // This prevents users from getting the following warning when open_basedir is set: 174 | // Warning: curl_setopt() [function.curl-setopt]: CURLOPT_FOLLOWLOCATION cannot be activated when in safe_mode or an open_basedir is set 175 | if (ini_get('open_basedir')) { 176 | unset($options[CURLOPT_FOLLOWLOCATION]); 177 | } 178 | 179 | curl_setopt_array($curl, $this->options); 180 | 181 | // Execute the request and decode to an array 182 | $this->responseRaw = curl_exec($curl); 183 | $this->response = json_decode($this->responseRaw, true); 184 | 185 | 186 | // * 187 | // * Response: API switch 188 | // * 189 | 190 | // * v1 191 | 192 | if ($this->bananoApi == 1) { 193 | if (isset($this->response['error'])) { 194 | $this->error = $this->response['error']; 195 | $this->response = null; 196 | } 197 | 198 | 199 | // * v2 200 | 201 | } elseif ($this->bananoApi == 2) { 202 | $this->responseType = $this->response['message_type']; 203 | 204 | $this->responseTime = (int) $this->response['time']; 205 | 206 | if ($this->response['correlation_id'] != $this->id) { 207 | $this->error = 'Correlation Id doesn\'t match'; 208 | } 209 | 210 | if ($this->response['message_type'] == 'Error') { 211 | $this->error = $this->response['message']; 212 | $this->errorCode = (int) $this->response['message']['code']; 213 | $this->response = null; 214 | } else { 215 | $this->response = $this->response['message']; 216 | } 217 | } else { 218 | throw new BananoRPCException("Invalid Banano API key"); 219 | } 220 | 221 | 222 | // * cURL errors 223 | 224 | // If the status is not 200, something is wrong 225 | $this->status = curl_getinfo($curl, CURLINFO_HTTP_CODE); 226 | 227 | // If there was no error, this will be an empty string 228 | $curl_error = curl_error($curl); 229 | 230 | curl_close($curl); 231 | 232 | if (!empty($curl_error)) { 233 | $this->error = $curl_error; 234 | } 235 | 236 | if ($this->status != 200) { 237 | // If node didn't return a nice error message, we need to make our own 238 | switch ($this->status) { 239 | case 400: 240 | $this->error = 'HTTP_BAD_REQUEST'; 241 | break; 242 | 243 | case 401: 244 | $this->error = 'HTTP_UNAUTHORIZED'; 245 | break; 246 | 247 | case 403: 248 | $this->error = 'HTTP_FORBIDDEN'; 249 | break; 250 | 251 | case 404: 252 | $this->error = 'HTTP_NOT_FOUND'; 253 | break; 254 | } 255 | } 256 | 257 | 258 | // * Return 259 | 260 | if ($this->error) { 261 | return false; 262 | } else { 263 | return $this->response; 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/BananoRPCExt.php: -------------------------------------------------------------------------------- 1 | error = 'Unable to parse Array'; 20 | return false; 21 | } 22 | 23 | $wallet = $args['wallet']; 24 | $destination = $args['destination']; 25 | 26 | // Wallet ok? 27 | $wallet_info = $this->wallet_info(['wallet' => $wallet]); 28 | 29 | if ($this->error != null) { 30 | $this->error = 'Bad wallet number'; 31 | return false; 32 | } 33 | 34 | // Balance ok? 35 | if (gmp_cmp($wallet_info['balance'], 1) < 0) { 36 | $this->error = 'Insufficient balance'; 37 | return false; 38 | } 39 | 40 | // Destination ok? 41 | if (!BananoTool::account2public($destination, false)) { 42 | $this->error = 'Bad destination'; 43 | return false; 44 | } 45 | 46 | // Any sort? 47 | $sort = isset($args['sort']) ? $args['sort'] : 'list'; 48 | 49 | // 50 | 51 | $return = ['balances' => []]; 52 | 53 | // Get wallet balances 54 | $args = [ 55 | 'wallet' => $wallet, 56 | 'threshold' => 1 57 | ]; 58 | 59 | $wallet_balances = $this->wallet_balances($args); 60 | 61 | // Sort balances 62 | if ($sort == 'asc') { 63 | uasort($wallet_balances['balances'], function ($a, $b) { 64 | return gmp_cmp($a['balance'], $b['balance']); 65 | }); 66 | } elseif ($sort == 'desc') { 67 | uasort($wallet_balances['balances'], function ($a, $b) { 68 | return gmp_cmp($b['balance'], $a['balance']); 69 | }); 70 | } else { 71 | // Do nothing 72 | } 73 | 74 | // Sweep wallet 75 | foreach ($wallet_balances['balances'] as $account => $balances) { 76 | if ($account == $destination) { 77 | $return['balances'][$account] = [ 78 | 'notice' => 'Skipped self send', 79 | 'amount' => $balances['balance'] 80 | ]; 81 | continue; 82 | } 83 | 84 | $args = [ 85 | 'wallet' => $wallet, 86 | 'source' => $account, 87 | 'destination' => $destination, 88 | 'amount' => $balances['balance'], 89 | 'id' => uniqid() 90 | ]; 91 | 92 | $send = $this->send($args); 93 | 94 | // Send 95 | $return['balances'][$account] = [ 96 | 'block' => $send['block'], 97 | 'amount' => $balances['balance'] 98 | ]; 99 | 100 | if ($send['block'] == BananoTool::EMPTY32_HEX) { 101 | $return['balances'][$account] = [ 102 | 'error' => 'Bad send', 103 | 'amount' => $balances['balance'] 104 | ]; 105 | } 106 | } 107 | 108 | $this->responseRaw = json_encode($return); 109 | $this->response = $return; 110 | 111 | return $this->response; 112 | } 113 | 114 | public function wallet_wipe(array $args) 115 | { 116 | return $this->wallet_sweep($args); 117 | } 118 | 119 | 120 | // * 121 | // * Wallet send 122 | // * 123 | 124 | public function wallet_send(array $args) 125 | { 126 | // Check args 127 | if (!isset($args['wallet']) || !isset($args['destination']) || !isset($args['amount'])) { 128 | $this->error = 'Unable to parse Array'; 129 | return false; 130 | } 131 | 132 | $wallet = $args['wallet']; 133 | $destination = $args['destination']; 134 | $amount = $args['amount']; 135 | 136 | // Wallet ok? 137 | $wallet_info = $this->wallet_info(['wallet' => $wallet]); 138 | 139 | if ($this->error != null) { 140 | $this->error = 'Bad wallet number'; 141 | return false; 142 | } 143 | 144 | // Destination ok? 145 | if (!BananoTool::account2public($destination, false)) { 146 | $this->error = 'Bad destination'; 147 | return false; 148 | } 149 | 150 | // Amount ok? 151 | if (!ctype_digit($amount)) { 152 | $this->error = 'Bad amount'; 153 | return false; 154 | } 155 | 156 | if (gmp_cmp($amount, 1) < 0) { 157 | $this->error = 'Bad amount'; 158 | return false; 159 | } 160 | 161 | if (gmp_cmp($wallet_info['balance'], $amount) < 0) { 162 | $this->error = 'Insufficient balance'; 163 | return false; 164 | } 165 | 166 | // Any sort? 167 | $sort = isset($args['sort']) ? $args['sort'] : 'list'; 168 | 169 | // 170 | 171 | $return = ['balances' => []]; 172 | $selected_accounts = []; 173 | $amount_left = $amount; 174 | 175 | // Get wallet balances 176 | $args = [ 177 | 'wallet' => $wallet, 178 | 'threshold' => 1 179 | ]; 180 | 181 | $wallet_balances = $this->wallet_balances($args); 182 | 183 | // Sort balances 184 | if ($sort == 'asc') { 185 | uasort($wallet_balances['balances'], function ($a, $b) { 186 | return gmp_cmp($a['balance'], $b['balance']); 187 | }); 188 | } elseif ($sort == 'desc') { 189 | uasort($wallet_balances['balances'], function ($a, $b) { 190 | return gmp_cmp($b['balance'], $a['balance']); 191 | }); 192 | } else { 193 | // Do nothing 194 | } 195 | 196 | // Select accounts 197 | foreach ($wallet_balances['balances'] as $account => $balances) { 198 | if (gmp_cmp($balances['balance'], $amount_left) >= 0) { 199 | $selected_accounts[$account] = $amount_left; 200 | $amount_left = '0'; 201 | } else { 202 | $selected_accounts[$account] = $balances['balance']; 203 | $amount_left = gmp_strval(gmp_sub($amount_left, $balances['balance'])); 204 | } 205 | 206 | if (gmp_cmp($amount_left, '0') <= 0) { 207 | break; // Amount reached 208 | } 209 | } 210 | 211 | // Send from selected accounts 212 | foreach ($selected_accounts as $account => $balance) { 213 | if ($account == $destination) { 214 | $return['balances'][$account] = [ 215 | 'notice' => 'Skipped self send', 216 | 'amount' => $balances['balance'] 217 | ]; 218 | continue; 219 | } 220 | 221 | $args = [ 222 | 'wallet' => $wallet, 223 | 'source' => $account, 224 | 'destination' => $destination, 225 | 'amount' => $balance, 226 | 'id' => uniqid() 227 | ]; 228 | 229 | $send = $this->send($args); 230 | 231 | // Send 232 | $return['balances'][$account] = [ 233 | 'block' => $send['block'], 234 | 'amount' => $balances['balance'] 235 | ]; 236 | 237 | if ($send['block'] == BananoTool::EMPTY32_HEX) { 238 | $return['balances'][$account] = [ 239 | 'error' => 'Bad send', 240 | 'amount' => $balances['balance'] 241 | ]; 242 | } 243 | } 244 | 245 | $this->responseRaw = json_encode($return); 246 | $this->response = $return; 247 | 248 | return $this->response; 249 | } 250 | 251 | 252 | // * 253 | // * Wallet weight 254 | // * 255 | 256 | public function wallet_weight(array $args) 257 | { 258 | // Check args 259 | if (!isset($args['wallet'])) { 260 | $this->error = 'Unable to parse Array'; 261 | return false; 262 | } 263 | 264 | $wallet = $args['wallet']; 265 | 266 | // Wallet ok? 267 | $wallet_info = $this->wallet_info(['wallet' => $wallet]); 268 | 269 | if ($this->error != null) { 270 | $this->error = 'Bad wallet number'; 271 | return false; 272 | } 273 | 274 | // Any sort? 275 | 276 | $sort = isset($args['sort']) ? $args['sort'] : 'list'; 277 | 278 | // 279 | 280 | $return = ['weight' => '', 'weights' => []]; 281 | $wallet_weight = '0'; 282 | 283 | // Get wallet balances 284 | $args = [ 285 | 'wallet' => $wallet 286 | ]; 287 | 288 | $wallet_accounts = $this->account_list($args); 289 | 290 | // Check every weight and sum them 291 | foreach ($wallet_accounts['accounts'] as $account) { 292 | $account_weight = $this->account_weight(['account'=>$account]); 293 | $wallet_weight = gmp_add($wallet_weight, $account_weight['weight']); 294 | $return['weights'][$account] = gmp_strval($account_weight['weight']); 295 | } 296 | 297 | $return['weight'] = gmp_strval($wallet_weight); 298 | 299 | // Sort weights 300 | if ($sort == 'asc') { 301 | uasort($return['weights'], function ($a, $b) { 302 | return gmp_cmp($a, $b); 303 | }); 304 | } elseif ($sort == 'desc') { 305 | uasort($return['weights'], function ($a, $b) { 306 | return gmp_cmp($b, $a); 307 | }); 308 | } else { 309 | // Do nothing 310 | } 311 | 312 | $this->responseRaw = json_encode($return); 313 | $this->response = $return; 314 | 315 | return $this->response; 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/BananoTool.php: -------------------------------------------------------------------------------- 1 | '1000000000000000000000000000', 23 | 'BANANO' => '100000000000000000000000000000' 24 | ]; 25 | 26 | const PREAMBLE_HEX = '0000000000000000000000000000000000000000000000000000000000000006'; 27 | const EMPTY32_HEX = '0000000000000000000000000000000000000000000000000000000000000000'; 28 | const HARDENED = 0x80000000; 29 | 30 | 31 | // * 32 | // * Denomination to raw 33 | // * 34 | 35 | public static function den2raw($amount, string $denomination): string 36 | { 37 | if (!array_key_exists($denomination, self::RAWS)) { 38 | throw new BananoToolException("Invalid denomination: $denomination"); 39 | } 40 | 41 | $raw_to_denomination = self::RAWS[$denomination]; 42 | 43 | if ($amount == 0) { 44 | return '0'; 45 | } 46 | 47 | if (strpos($amount, '.')) { 48 | $dot_pos = strpos($amount, '.'); 49 | $number_len = strlen($amount) - 1; 50 | $raw_to_denomination = substr($raw_to_denomination, 0, -($number_len - $dot_pos)); 51 | } 52 | 53 | $amount = str_replace('.', '', $amount) . str_replace('1', '', $raw_to_denomination); 54 | 55 | // Remove useless zeros from left 56 | while (substr($amount, 0, 1) == '0') { 57 | $amount = substr($amount, 1); 58 | } 59 | 60 | return $amount; 61 | } 62 | 63 | 64 | // * 65 | // * Raw to denomination 66 | // * 67 | 68 | public static function raw2den(string $amount, string $denomination): string 69 | { 70 | if (!array_key_exists($denomination, self::RAWS)) { 71 | throw new BananoToolException("Invalid denomination: $denomination"); 72 | } 73 | 74 | $raw_to_denomination = self::RAWS[$denomination]; 75 | 76 | if ($amount == '0') { 77 | return 0; 78 | } 79 | 80 | $prefix_lenght = 39 - strlen($amount); 81 | 82 | $i = 0; 83 | 84 | while ($i < $prefix_lenght) { 85 | $amount = '0' . $amount; 86 | $i++; 87 | } 88 | 89 | $amount = substr_replace($amount, '.', -(strlen($raw_to_denomination)-1), 0); 90 | 91 | // Remove useless zeroes from left 92 | while (substr($amount, 0, 1) == '0' && substr($amount, 1, 1) != '.') { 93 | $amount = substr($amount, 1); 94 | } 95 | 96 | // Remove useless decimals 97 | while (substr($amount, -1) == '0') { 98 | $amount = substr($amount, 0, -1); 99 | } 100 | 101 | // Remove dot if all decimals are zeros 102 | if (substr($amount, -1) == '.') { 103 | $amount = substr($amount, 0, -1); 104 | } 105 | 106 | return $amount; 107 | } 108 | 109 | 110 | // * 111 | // * Denomination to denomination 112 | // * 113 | 114 | public static function den2den($amount, string $denomination_from, string $denomination_to): string 115 | { 116 | if (!array_key_exists($denomination_from, self::RAWS)) { 117 | throw new BananoToolException("Invalid source denomination: $denomination_from"); 118 | } 119 | if (!array_key_exists($denomination_to, self::RAWS)) { 120 | throw new BananoToolException("Invalid target denomination: $denomination_to"); 121 | } 122 | 123 | $raw = self::den2raw($amount, $denomination_from); 124 | 125 | return self::raw2den($raw, $denomination_to); 126 | } 127 | 128 | 129 | // * 130 | // * Account to public key 131 | // * 132 | 133 | public static function account2public(string $account, bool $get_public_key = true) 134 | { 135 | if ((strpos($account, 'ban_1') === 0 || 136 | strpos($account, 'ban_3') === 0) && 137 | strlen($account) == 64 138 | ) { 139 | $crop = explode('_', $account); 140 | $crop = $crop[1]; 141 | 142 | if (preg_match('/^[13456789abcdefghijkmnopqrstuwxyz]+$/', $crop)) { 143 | $aux = \MikeRow\BananoPHP\Util\Uint::fromString(substr($crop, 0, 52))->toUint4()->toArray(); 144 | array_shift($aux); 145 | $key_uint4 = $aux; 146 | $hash_uint8 = \MikeRow\BananoPHP\Util\Uint::fromString(substr($crop, 52, 60))->toUint8()->toArray(); 147 | $key_uint8 = \MikeRow\BananoPHP\Util\Uint::fromUint4Array($key_uint4)->toUint8(); 148 | 149 | if (!extension_loaded('blake2')) { 150 | $key_hash = new SplFixedArray(64); 151 | $b2b = new Blake2b(); 152 | $ctx = $b2b->init(null, 5); 153 | $b2b->update($ctx, $key_uint8, 32); 154 | $b2b->finish($ctx, $key_hash); 155 | $key_hash = array_reverse(array_slice($key_hash->toArray(), 0, 5)); 156 | } else { 157 | $key_uint8 = \MikeRow\BananoPHP\Util\Bin::arr2bin((array) $key_uint8); 158 | $key_hash = blake2($key_uint8, 5, null, true); 159 | $key_hash = \MikeRow\BananoPHP\Util\Bin::bin2arr(strrev($key_hash)); 160 | } 161 | 162 | if ($hash_uint8 == $key_hash) { 163 | if ($get_public_key) { 164 | return \MikeRow\BananoPHP\Util\Uint::fromUint4Array($key_uint4)->toHexString(); 165 | } else { 166 | return true; 167 | } 168 | } 169 | } 170 | } 171 | 172 | return false; 173 | } 174 | 175 | 176 | // * 177 | // * Public key to account 178 | // * 179 | 180 | public static function public2account(string $public_key): string 181 | { 182 | if (strlen($public_key) != 64 || !hex2bin($public_key)) { 183 | throw new BananoToolException("Invalid public key: $public_key"); 184 | } 185 | 186 | if (!extension_loaded('blake2')) { 187 | $key = \MikeRow\BananoPHP\Util\Uint::fromHex($public_key); 188 | $checksum; 189 | $hash = new SplFixedArray(64); 190 | 191 | $b2b = new Blake2b(); 192 | $ctx = $b2b->init(null, 5); 193 | $b2b->update($ctx, $key->toUint8(), 32); 194 | $b2b->finish($ctx, $hash); 195 | $hash = \MikeRow\BananoPHP\Util\Uint::fromUint8Array(array_slice($hash->toArray(), 0, 5))->reverse(); 196 | $checksum = $hash->toString(); 197 | } else { 198 | $key = \MikeRow\BananoPHP\Util\Uint::fromHex($public_key)->toUint8(); 199 | $key = \MikeRow\BananoPHP\Util\Bin::arr2bin((array) $key); 200 | 201 | $hash = blake2($key, 5, null, true); 202 | $hash = \MikeRow\BananoPHP\Util\Bin::bin2arr(strrev($hash)); 203 | $checksum = \MikeRow\BananoPHP\Util\Uint::fromUint8Array($hash)->toString(); 204 | } 205 | 206 | $c_account = \MikeRow\BananoPHP\Util\Uint::fromHex('0' . $public_key)->toString(); 207 | 208 | return 'ban_' . $c_account . $checksum; 209 | } 210 | 211 | 212 | // * 213 | // * Private key to public key 214 | // * 215 | 216 | public static function private2public(string $private_key): string 217 | { 218 | if (strlen($private_key) != 64 || !hex2bin($private_key)) { 219 | throw new BananoToolException("Invalid private key: $private_key"); 220 | } 221 | 222 | $salt = NanoSalt::instance(); 223 | $private_key = \MikeRow\BananoPHP\Util\Uint::fromHex($private_key)->toUint8(); 224 | $public_key = $salt::crypto_sign_public_from_secret_key($private_key); 225 | 226 | return \MikeRow\BananoPHP\Util\Uint::fromUint8Array($public_key)->toHexString(); 227 | } 228 | 229 | 230 | // * 231 | // * String to burn account 232 | // * 233 | 234 | public static function string2burn(string $string, string $leading_char = '1', string $filling_char = '1'): string 235 | { 236 | if (!preg_match('/^[13456789abcdefghijkmnopqrstuwxyz]+$/', $string) || strlen($string) < 1 || strlen($string) > 51) { 237 | throw new BananoToolException("Invalid string: $string"); 238 | } 239 | if ($leading_char != '1' && $leading_char != '3') { 240 | throw new BananoToolException("Invalid leading character: $leading_char"); 241 | } 242 | if (!preg_match('/^[13456789abcdefghijkmnopqrstuwxyz]+$/', $filling_char) || strlen($filling_char != 1)) { 243 | throw new BananoToolException("Invalid filling character: $filling_char"); 244 | } 245 | 246 | $string = $leading_char . $string . str_repeat($filling_char, (51 - strlen($string))); 247 | 248 | $aux = \MikeRow\BananoPHP\Util\Uint::fromString($string)->toUint4()->toArray(); 249 | array_shift($aux); 250 | $key_uint4 = $aux; 251 | $key_uint8 = \MikeRow\BananoPHP\Util\Uint::fromUint4Array($key_uint4)->toUint8(); 252 | 253 | if (!extension_loaded('blake2')) { 254 | $checksum; 255 | $hash = new SplFixedArray(64); 256 | 257 | $b2b = new Blake2b(); 258 | $ctx = $b2b->init(null, 5); 259 | $b2b->update($ctx, $key_uint8, 32); 260 | $b2b->finish($ctx, $hash); 261 | $hash = \MikeRow\BananoPHP\Util\Uint::fromUint8Array(array_slice($hash->toArray(), 0, 5))->reverse(); 262 | $checksum = $hash->toString(); 263 | } else { 264 | $key = \MikeRow\BananoPHP\Util\Bin::arr2bin((array) $key_uint8); 265 | 266 | $hash = blake2($key, 5, null, true); 267 | $hash = \MikeRow\BananoPHP\Util\Bin::bin2arr(strrev($hash)); 268 | $checksum = \MikeRow\BananoPHP\Util\Uint::fromUint8Array($hash)->toString(); 269 | } 270 | 271 | return 'ban_' . $string . $checksum; 272 | } 273 | 274 | 275 | // * 276 | // * Get random keypair 277 | // * 278 | 279 | public static function keys(bool $get_account = false): array 280 | { 281 | $salt = NanoSalt::instance(); 282 | $keys = $salt->crypto_sign_keypair(); 283 | 284 | $keys[0] = \MikeRow\BananoPHP\Util\Uint::fromUint8Array(array_slice($keys[0]->toArray(), 0, 32))->toHexString(); 285 | $keys[1] = \MikeRow\BananoPHP\Util\Uint::fromUint8Array($keys[1])->toHexString(); 286 | 287 | if ($get_account) { 288 | $keys[] = self::public2account($keys[1]); 289 | } 290 | 291 | return $keys; 292 | } 293 | 294 | 295 | // * 296 | // * Seed to keypair (Blake2b) 297 | // * 298 | 299 | public static function seed2keys(string $seed, int $index = 0, bool $get_account = false): array 300 | { 301 | if (strlen($seed) != 64 || !hex2bin($seed)) { 302 | throw new BananoToolException("Invalid seed: $seed"); 303 | } 304 | if ($index < 0 || $index > 4294967295) { 305 | throw new BananoToolException("Invalid index: $index"); 306 | } 307 | 308 | $seed = \MikeRow\BananoPHP\Util\Uint::fromHex($seed)->toUint8(); 309 | $index = \MikeRow\BananoPHP\Util\Uint::fromDec($index)->toUint8()->toArray(); 310 | 311 | if (count($index) < 4) { 312 | $missing_bytes = []; 313 | for ($i = 0; $i < (4 - count($index)); $i++) { 314 | $missing_bytes[] = 0; 315 | } 316 | $index = array_merge($missing_bytes, $index); 317 | } 318 | 319 | $index = \MikeRow\BananoPHP\Util\Uint::fromUint8Array($index)->toUint8(); 320 | $private_key = new SplFixedArray(64); 321 | 322 | $b2b = new Blake2b(); 323 | $ctx = $b2b->init(null, 32); 324 | $b2b->update($ctx, $seed, 32); 325 | $b2b->update($ctx, $index, 4); 326 | $b2b->finish($ctx, $private_key); 327 | 328 | $private_key = \MikeRow\BananoPHP\Util\Uint::fromUint8Array(array_slice($private_key->toArray(), 0, 32))->toHexString(); 329 | $public_key = self::private2public($private_key); 330 | 331 | $keys = [$private_key,$public_key]; 332 | 333 | if ($get_account) { 334 | $keys[] = self::public2account($public_key); 335 | } 336 | 337 | return $keys; 338 | } 339 | 340 | 341 | // * 342 | // * Mnemonic seed to hexadecimal string (BIP39) 343 | // * 344 | 345 | public static function mnem2hex(array $words): string 346 | { 347 | $mnem_count = count($words); 348 | 349 | if ($mnem_count != 12 && 350 | $mnem_count != 15 && 351 | $mnem_count != 18 && 352 | $mnem_count != 21 && 353 | $mnem_count != 24 354 | ) { 355 | throw new BananoToolException("Invalid words array count: not 12,15,18,21,24"); 356 | } 357 | 358 | $bip39 = new BIP39\BIP39EnglishWordList(); 359 | $bip39_words = $bip39->getWords(); 360 | $bits = []; 361 | $hex = []; 362 | 363 | foreach ($words as $index => $value) { 364 | $word = array_search($value, $bip39_words); 365 | if ($word === false) { 366 | throw new BananoToolException("Invalid menmonic word: $value"); 367 | } 368 | 369 | $words[$index] = decbin($word); 370 | $words[$index] = str_split(str_repeat('0', (11 - strlen($words[$index]))) . $words[$index]); 371 | 372 | foreach ($words[$index] as $bit) { 373 | $bits[] = $bit; 374 | } 375 | } 376 | 377 | for ($i = 0; $i < ceil($mnem_count*2.66666); $i++) { 378 | $hex[] = bindec(implode('', array_slice($bits, $i * 8, 8))); 379 | } 380 | 381 | $hex = \MikeRow\BananoPHP\Util\Uint::fromUint8Array($hex)->toHexString(); 382 | $hex = substr($hex, 0, ceil($mnem_count*2.66666)); 383 | 384 | return $hex; 385 | } 386 | 387 | 388 | // * 389 | // * Hexadecimal string to mnemonic words (BIP39) 390 | // * 391 | 392 | public static function hex2mnem(string $hex): array 393 | { 394 | $hex_lenght = strlen($hex); 395 | 396 | if (($hex_lenght != 32 && 397 | $hex_lenght != 40 && 398 | $hex_lenght != 48 && 399 | $hex_lenght != 56 && 400 | $hex_lenght != 64) || 401 | !hex2bin($hex) 402 | ) { 403 | throw new BananoToolException("Invalid hexadecimal string: $hex"); 404 | } 405 | 406 | $bip39 = new BIP39\BIP39EnglishWordList(); 407 | $bip39_words = $bip39->getWords(); 408 | $bits = []; 409 | $mnemonic = []; 410 | 411 | $hex = \MikeRow\BananoPHP\Util\Uint::fromHex($hex)->toUint8(); 412 | $check = hash('sha256', \MikeRow\BananoPHP\Util\Bin::arr2bin((array) $hex), true); 413 | $hex = array_merge((array) $hex, \MikeRow\BananoPHP\Util\Bin::bin2arr(substr($check, 0, 1))); 414 | 415 | foreach ($hex as $byte) { 416 | $bits_raw = decbin($byte); 417 | $bits = array_merge($bits, str_split(str_repeat('0', (8 - strlen($bits_raw))) . $bits_raw)); 418 | } 419 | 420 | for ($i = 0; $i < floor($hex_lenght/2.66666); $i++) { 421 | $mnemonic[] = $bip39_words[bindec(implode('', array_slice($bits, $i * 11, 11)))]; 422 | } 423 | 424 | return $mnemonic; 425 | } 426 | 427 | 428 | // * 429 | // * Mnemonic words to master seed (BIP39/44) 430 | // * 431 | 432 | public static function mnem2mseed(array $words, string $passphrase = ''): string 433 | { 434 | if (count($words) < 1) { 435 | throw new BananoToolException("Invalid words array count: less than 1"); 436 | } 437 | 438 | $bip39 = new BIP39\BIP39EnglishWordList(); 439 | $bip39_words = $bip39->getWords(); 440 | 441 | foreach ($words as $index => $value) { 442 | $word = array_search($value, $bip39_words); 443 | if ($word === false) { 444 | throw new BananoToolException("Invalid menmonic word: $value"); 445 | } 446 | } 447 | 448 | return strtoupper( 449 | hash_pbkdf2('sha512', implode(' ', $words), 'mnemonic' . $passphrase, 2048, 128) 450 | ); 451 | } 452 | 453 | 454 | // * 455 | // * Master seed to keypair (BIP39/44) 456 | // * 457 | 458 | public static function mseed2keys(string $mseed, int $index = 0, bool $get_account = false): array 459 | { 460 | if (strlen($mseed) != 128 || !hex2bin($mseed)) { 461 | throw new BananoToolException("Invalid master seed: $mseed"); 462 | } 463 | if ($index < 0 || $index > 4294967295) { 464 | throw new BananoToolException("Invalid index: $index"); 465 | } 466 | 467 | $path = ["44","198","$index"]; 468 | 469 | $I = hash_hmac('sha512', hex2bin($mseed), 'ed25519 seed', true); 470 | $HDKey = [substr($I, 0, 32),substr($I, 32, 32)]; 471 | 472 | foreach ($path as $entry) { 473 | $entry = intval($entry); 474 | if ($entry >= self::HARDENED) { 475 | $entry = $entry - self::HARDENED; 476 | } 477 | 478 | $data = chr(0x00) . $HDKey[0] . hex2bin(dechex(self::HARDENED + (int) $entry)); 479 | $I = hash_hmac('sha512', $data, $HDKey[1], true); 480 | $HDKey = [substr($I, 0, 32),substr($I, 32, 32)]; 481 | } 482 | 483 | $private_key = strtoupper(bin2hex($HDKey[0])); 484 | $keys = [$private_key,self::private2public($private_key)]; 485 | 486 | if ($get_account) { 487 | $keys[] = self::public2account($keys[1]); 488 | } 489 | 490 | return $keys; 491 | } 492 | 493 | 494 | // * 495 | // * Hash array of hexadecimals 496 | // * 497 | 498 | public static function hashHexs(array $hexs, int $size = 32): string 499 | { 500 | if (count($hexs) < 1) { 501 | throw new BananoToolException("Invalid hexadecimals array count: less than 1"); 502 | } 503 | if ($size < 1) { 504 | throw new BananoToolException("Invalid size: $size"); 505 | } 506 | 507 | $b2b = new Blake2b(); 508 | 509 | $ctx = $b2b->init(null, $size); 510 | $hash = new SplFixedArray(64); 511 | 512 | foreach ($hexs as $index => $value) { 513 | if (!hex2bin($value)) { 514 | throw new BananoToolException("Invalid hexadecimal string: $value"); 515 | } 516 | 517 | $value = \MikeRow\BananoPHP\Util\Uint::fromHex($value)->toUint8(); 518 | $b2b->update($ctx, $value, count($value)); 519 | } 520 | 521 | $b2b->finish($ctx, $hash); 522 | $hash = $hash->toArray(); 523 | $hash = array_slice($hash, 0, $size); 524 | $hash = \MikeRow\BananoPHP\Util\Uint::fromUint8Array($hash)->toHexString(); 525 | 526 | return $hash; 527 | } 528 | 529 | 530 | // * 531 | // * Sign message 532 | // * 533 | 534 | public static function sign(string $msg, string $private_key): string 535 | { 536 | if (!hex2bin($msg)) { 537 | throw new BananoToolException("Invalid message: $msg"); 538 | } 539 | if (strlen($private_key) != 64 || !hex2bin($private_key)) { 540 | throw new BananoToolException("Invalid private key: $private_key"); 541 | } 542 | 543 | $salt = NanoSalt::instance(); 544 | $private_key = FieldElement::fromArray(\MikeRow\BananoPHP\Util\Uint::fromHex($private_key)->toUint8()); 545 | $public_key = NanoSalt::crypto_sign_public_from_secret_key($private_key); 546 | 547 | $private_key->setSize(64); 548 | $private_key->copy($public_key, 32, 32); 549 | 550 | $msg = \MikeRow\BananoPHP\Util\Uint::fromHex($msg)->toUint8(); 551 | $sm = $salt->crypto_sign($msg, count($msg), $private_key); 552 | 553 | $signature = []; 554 | for ($i = 0; $i < 64; $i++) { 555 | $signature[$i] = $sm[$i]; 556 | } 557 | 558 | return \MikeRow\BananoPHP\Util\Uint::fromUint8Array($signature)->toHexString(); 559 | } 560 | 561 | 562 | // * 563 | // * Validate signature 564 | // * 565 | 566 | public static function validSign(string $msg, string $sig, string $account) 567 | { 568 | if (!hex2bin($msg)) { 569 | throw new BananoToolException("Invalid message: $msg"); 570 | } 571 | if (strlen($sig) != 128 || !hex2bin($sig)) { 572 | throw new BananoToolException("Invalid signature: $sig"); 573 | } 574 | $public_key = self::account2public($account); 575 | if (!$public_key) { 576 | throw new BananoToolException("Invalid account: $account"); 577 | } 578 | 579 | $sig = \MikeRow\BananoPHP\Util\Uint::fromHex($sig)->toUint8(); 580 | $msg = \MikeRow\BananoPHP\Util\Uint::fromHex($msg)->toUint8(); 581 | $public_key = \MikeRow\BananoPHP\Util\Uint::fromHex($public_key)->toUint8(); 582 | 583 | $sm = new SplFixedArray(64 + count($msg)); 584 | $m = new SplFixedArray(64 + count($msg)); 585 | 586 | for ($i = 0; $i < 64; $i++) { 587 | $sm[$i] = $sig[$i]; 588 | } 589 | for ($i = 0; $i < count($msg); $i++) { 590 | $sm[$i+64] = $msg[$i]; 591 | } 592 | 593 | $open2 = NanoSalt::crypto_sign_open2($m, $sm, count($sm), $public_key); 594 | 595 | if ($open2 == null) { 596 | return false; 597 | } 598 | 599 | $open2 = \MikeRow\BananoPHP\Util\Uint::fromUint8Array($open2)->toHexString(); 600 | 601 | return $open2; 602 | } 603 | 604 | 605 | // * 606 | // * Multiplier to difficulty 607 | // * 608 | 609 | public static function mult2diff(string $difficulty, float $multiplier): string 610 | { 611 | if (strlen($difficulty) != 16 || !hex2bin($difficulty)) { 612 | throw new BananoToolException("Invalid difficulty: $difficulty"); 613 | } 614 | if ($multiplier <= 0) { 615 | throw new BananoToolException("Invalid multiplier: $multiplier"); 616 | } 617 | 618 | $ref = (float) 18446744073709551616; 619 | $difficulty = hexdec($difficulty); 620 | 621 | return dechex(($difficulty - $ref) / $multiplier + $ref); 622 | } 623 | 624 | 625 | // * 626 | // * Difficulty to muliplier 627 | // * 628 | 629 | public static function diff2mult(string $base_difficulty, string $difficulty): float 630 | { 631 | if (strlen($base_difficulty) != 16 || !hex2bin($base_difficulty)) { 632 | throw new BananoToolException("Invalid base difficulty: $base_difficulty"); 633 | } 634 | if (strlen($difficulty) != 16 || !hex2bin($difficulty)) { 635 | throw new BananoToolException("Invalid difficulty: $difficulty"); 636 | } 637 | 638 | $ref = (float) 18446744073709551616; 639 | $base_difficulty = hexdec($base_difficulty); 640 | $difficulty = hexdec($difficulty); 641 | 642 | return (float) ($ref - $base_difficulty) / (float) ($ref - $difficulty); 643 | } 644 | 645 | 646 | // * 647 | // * Generate work 648 | // * 649 | 650 | public static function work(string $hash, string $difficulty): string 651 | { 652 | if (strlen($hash) != 64 || !hex2bin($hash)) { 653 | throw new BananoToolException("Invalid hash: $hash"); 654 | } 655 | if (strlen($difficulty) != 16 || !hex2bin($difficulty)) { 656 | throw new BananoToolException("Invalid difficulty: $difficulty"); 657 | } 658 | 659 | $hash = \MikeRow\BananoPHP\Util\Uint::fromHex($hash)->toUint8(); 660 | $difficulty = hex2bin($difficulty); 661 | 662 | if (!extension_loaded('blake2')) { 663 | $b2b = new Blake2b(); 664 | $rng = random_bytes(8); 665 | $rng = \MikeRow\BananoPHP\Util\bin2arr($rng); 666 | 667 | while (true) { 668 | $output = new SplFixedArray(64); 669 | 670 | $ctx = $b2b->init(null, 8); 671 | $b2b->update($ctx, $rng, 8); 672 | $b2b->update($ctx, $hash, 32); 673 | $b2b->finish($ctx, $output); 674 | 675 | $output = $output->toArray(); 676 | $output = array_slice($output, 0, 8); 677 | $output = array_reverse($output); 678 | //$output = \MikeRow\BananoPHP\Util\Uint::fromUint8Array($output)->toHexString(); 679 | 680 | if (strcasecmp(\MikeRow\BananoPHP\Util\Bin::arr2bin($output), $difficulty) >= 0) { 681 | return \MikeRow\BananoPHP\Util\Uint::fromUint8Array(array_reverse($rng))->toHexString(); 682 | } 683 | 684 | $rng = $output; 685 | } 686 | } else { 687 | $hash = \MikeRow\BananoPHP\Util\Bin::arr2bin((array) $hash); 688 | $rng = random_bytes(8); 689 | 690 | while (true) { 691 | $output = strrev(blake2($rng . $hash, 8, null, true)); 692 | 693 | if (strcasecmp($output, $difficulty) >= 0) { 694 | return \MikeRow\BananoPHP\Util\Uint::fromUint8Array(array_reverse(\MikeRow\BananoPHP\Util\bin2arr($rng)))->toHexString(); 695 | } 696 | 697 | $rng = $output; 698 | } 699 | } 700 | } 701 | 702 | 703 | // * 704 | // * Validate work 705 | // * 706 | 707 | public static function validWork(string $hash, string $difficulty, string $work): bool 708 | { 709 | if (strlen($hash) != 64 || !hex2bin($hash)) { 710 | throw new BananoToolException("Invalid hash: $hash"); 711 | } 712 | if (strlen($difficulty) != 16 || !hex2bin($difficulty)) { 713 | throw new BananoToolException("Invalid difficulty: $difficulty"); 714 | } 715 | if (strlen($work) != 16 || !hex2bin($work)) { 716 | throw new BananoToolException("Invalid work: $work"); 717 | } 718 | 719 | $hash = \MikeRow\BananoPHP\Util\Uint::fromHex($hash)->toUint8(); 720 | $work = \MikeRow\BananoPHP\Util\Uint::fromHex($work)->toUint8(); 721 | $work = array_reverse($work->toArray()); 722 | $work = SplFixedArray::fromArray($work); 723 | 724 | $res = new SplFixedArray(64); 725 | 726 | $blake2b = new Blake2b(); 727 | $ctx = $blake2b->init(null, 8); 728 | $blake2b->update($ctx, $work, 8); 729 | $blake2b->update($ctx, $hash, 32); 730 | $blake2b->finish($ctx, $res); 731 | 732 | $res = $res->toArray(); 733 | $res = array_slice($res, 0, 8); 734 | $res = array_reverse($res); 735 | $res = \MikeRow\BananoPHP\Util\Uint::fromUint8Array($res)->toHexString(); 736 | 737 | if (hexdec($res) >= hexdec($difficulty)) { 738 | return true; 739 | } 740 | 741 | return false; 742 | } 743 | } 744 | -------------------------------------------------------------------------------- /src/BananoWS.php: -------------------------------------------------------------------------------- 1 | options = []; 48 | 49 | // Timeout 50 | if (isset($options['timeout'])) { 51 | $this->options['timeout'] = (float) $options['timeout']; 52 | } 53 | 54 | // Fragment size 55 | if (isset($options['fragment_size'])) { 56 | $this->options['fragment_size'] = (int) $options['fragment_size']; 57 | } 58 | 59 | // Context 60 | if (isset($options['context']) && is_array($options['context'])) { 61 | $this->options['context'] = stream_context_create($options['context']); 62 | } 63 | 64 | // Headers 65 | if (isset($options['headers']) && is_array($options['headers'])) { 66 | $this->options['headers'] = $options['headers']; 67 | } 68 | 69 | $this->protocol = $protocol; 70 | $this->hostname = $hostname; 71 | $this->port = $port; 72 | $this->url = $url; 73 | } 74 | 75 | 76 | // * 77 | // * Open connection 78 | // * 79 | 80 | public function open(): bool 81 | { 82 | try { 83 | $this->websocket = new \WebSocket\Client("{$this->protocol}://{$this->hostname}:{$this->port}/{$this->url}", $this->options); 84 | return true; 85 | } catch (\WebSocket\ConnectionException $e) { 86 | return false; 87 | } 88 | } 89 | 90 | 91 | // * 92 | // * Close connection 93 | // * 94 | 95 | public function close() 96 | { 97 | if ($this->websocket != null) { 98 | $this->websocket->close(); 99 | $this->weboscket = null; 100 | } 101 | } 102 | 103 | 104 | // * 105 | // * Subscribe to topic 106 | // * 107 | 108 | public function subscribe(string $topic, array $options = null, bool $ack = false): int 109 | { 110 | // Check WebSocket connection 111 | if ($this->websocket == null) { 112 | throw new BananoWSException("WebSocket connection is not opened"); 113 | } 114 | 115 | // Check inputs 116 | if (empty($topic)){ 117 | throw new BananoWSException("Invalid topic: $topic"); 118 | } 119 | 120 | $this->id++; 121 | 122 | $subscribe = [ 123 | 'action' => 'subscribe', 124 | 'topic' => $topic, 125 | 'id' => $this->id 126 | ]; 127 | 128 | if (!empty($options)) { 129 | $subscribe['options'] = $options; 130 | } 131 | 132 | if ($ack) { 133 | $subscribe['ack'] = true; 134 | } 135 | 136 | $subscribe = json_encode($subscribe); 137 | $this->websocket->send($subscribe); 138 | 139 | return $this->id; 140 | } 141 | 142 | 143 | // * 144 | // * Update subscription 145 | // * 146 | 147 | public function update(string $topic, array $options, bool $ack = false): int 148 | { 149 | // Check WebSocket connection 150 | if ($this->websocket == null) { 151 | throw new BananoWSException("WebSocket connection is not opened"); 152 | } 153 | 154 | // Check inputs 155 | if (empty($topic)){ 156 | throw new BananoWSException("Invalid topic: $topic"); 157 | } 158 | 159 | $this->id++; 160 | 161 | $update = [ 162 | 'action' => 'update', 163 | 'topic' => $topic, 164 | 'id' => $this->id, 165 | 'options' => $options 166 | ]; 167 | 168 | if ($ack) { 169 | $update['ack'] = true; 170 | } 171 | 172 | $update = json_encode($update); 173 | $this->websocket->send($update); 174 | 175 | return $this->id; 176 | } 177 | 178 | 179 | // * 180 | // * Unsubscribe to topic 181 | // * 182 | 183 | public function unsubscribe(string $topic, bool $ack = false): int 184 | { 185 | // Check WebSocket connection 186 | if ($this->websocket == null) { 187 | throw new BananoWSException("WebSocket connection is not opened"); 188 | } 189 | 190 | // Check inputs 191 | if (empty($topic)){ 192 | throw new BananoWSException("Invalid topic: $topic"); 193 | } 194 | 195 | $this->id++; 196 | 197 | $unsubscribe = [ 198 | 'action' => 'unsubscribe', 199 | 'topic' => $topic, 200 | 'id' => $this->id 201 | ]; 202 | 203 | if ($ack) { 204 | $unsubscribe['ack'] = true; 205 | } 206 | 207 | $unsubscribe = json_encode($unsubscribe); 208 | $this->websocket->send($unsubscribe); 209 | 210 | return $this->id; 211 | } 212 | 213 | 214 | // * 215 | // * Keepalive 216 | // * 217 | 218 | public function keepalive(): int 219 | { 220 | // Check WebSocket connection 221 | if ($this->websocket == null) { 222 | throw new BananoWSException("WebSocket connection is not opened"); 223 | } 224 | 225 | $this->id++; 226 | 227 | $keepalive = [ 228 | 'action' => 'ping', 229 | 'id' => $this->id 230 | ]; 231 | 232 | $keepalive = json_encode($keepalive); 233 | $this->websocket->send($keepalive); 234 | 235 | return $this->id; 236 | } 237 | 238 | 239 | // * 240 | // * Listen 241 | // * 242 | 243 | public function listen() 244 | { 245 | // Check WebSocket connection 246 | if ($this->websocket == null) { 247 | throw new BananoWSException("WebSocket connection is not opened"); 248 | } 249 | 250 | while (true) { 251 | try { 252 | return json_decode($this->websocket->receive(), true); 253 | } catch (\WebSocket\ConnectionException $e) { 254 | // 255 | } 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/Util/Base.php: -------------------------------------------------------------------------------- 1 | http://www.danvk.org/hex2dec.html 4 | 5 | /** 6 | * A function for converting hex <-> dec w/o loss of precision. 7 | * 8 | * The problem is that parseInt("0x12345...") isn't precise enough to convert 9 | * 64-bit integers correctly. 10 | * 11 | * Internally, this uses arrays to encode decimal digits starting with the least 12 | * significant: 13 | * 8 = [8] 14 | * 16 = [6, 1] 15 | * 1024 = [4, 2, 0, 1] 16 | */ 17 | 18 | namespace MikeRow\BananoPHP\Util; 19 | 20 | class Base 21 | { 22 | public static function add($x, $y, $base) 23 | { 24 | $z = []; 25 | $n = max(count($x), count($y)); 26 | $carry = 0; 27 | $i = 0; 28 | while ($i < $n || $carry) 29 | { 30 | $xi = $i < count($x) ? $x[$i] : 0; 31 | $yi = $i < count($y) ? $y[$i] : 0; 32 | $zi = $carry + $xi + $yi; 33 | $z[] = $zi % $base; 34 | $carry = floor($zi / $base); 35 | $i++; 36 | } 37 | return $z; 38 | } 39 | 40 | public static function multiplyByNumber($num, $x, $base) { 41 | if ($num < 0) return null; 42 | if ($num == 0) return []; 43 | 44 | $result = []; 45 | $power = $x; 46 | while (true) 47 | { 48 | if ($num & 1) 49 | { 50 | $result = self::add($result, $power, $base); 51 | } 52 | $num = $num >> 1; 53 | if ($num === 0) 54 | break; 55 | $power = self::add($power, $power, $base); 56 | } 57 | 58 | return $result; 59 | } 60 | 61 | public static function parseToDigitsArray($str, $base) { 62 | $digits = str_split($str); 63 | $ary = []; 64 | for ($i = count($digits) - 1; $i >= 0; $i--) 65 | { 66 | $n = intval($digits[$i], $base); 67 | if (!is_numeric($n)) return null; 68 | $ary[] = $n; 69 | } 70 | return $ary; 71 | } 72 | 73 | public static function convertBase($str, $fromBase, $toBase) { 74 | $digits = self::parseToDigitsArray($str, $fromBase); 75 | if ($digits === null) return null; 76 | 77 | $outArray = []; 78 | $power = [1]; 79 | for ($i = 0; $i < count($digits); $i++) { 80 | // invariant: at this point, fromBase^i = power 81 | if ($digits[$i]) 82 | { 83 | $outArray = self::add($outArray, self::multiplyByNumber($digits[$i], $power, $toBase), $toBase); 84 | } 85 | $power = self::multiplyByNumber($fromBase, $power, $toBase); 86 | } 87 | 88 | $out = ''; 89 | for ($i = count($outArray) - 1; $i >= 0; $i--) { 90 | $out .= base_convert($outArray[$i], $fromBase, $toBase); 91 | } 92 | return $out; 93 | } 94 | 95 | public static function decToHex($decStr) { 96 | $hex = self::convertBase($decStr, 10, 16); 97 | if(strlen($hex) % 2 != 0) 98 | $hex = '0' . $hex; 99 | return $hex ? $hex : null; 100 | } 101 | 102 | public static function hexToDec($hexStr) { 103 | if (substr($hexStr, 0, 2) === '0x') $hexStr = substr($hexStr, 2); 104 | $hexStr = strtolower($hexStr); 105 | return self::convertBase($hexStr, 16, 10); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Util/Bin.php: -------------------------------------------------------------------------------- 1 | 255) { 14 | return false; 15 | } 16 | } 17 | 18 | return implode(array_map('chr', $array)); 19 | } 20 | 21 | public static function bin2arr(string $string): array 22 | { 23 | return array_map('ord', str_split($string)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Util/Uint.php: -------------------------------------------------------------------------------- 1 | u8 = $this->u4 = $this->u5 = $this->hex = $this->string = false; 23 | } 24 | 25 | public static function fromHex($hex) 26 | { 27 | $ret = new Uint(); 28 | $ret->hex = strtoupper($hex); 29 | $ret->u8 = $ret->hexU8($hex); 30 | $ret->u4 = $ret->hexU4($hex); 31 | return $ret; 32 | } 33 | 34 | public static function fromUint8Array($u8) 35 | { 36 | $ret = new Uint(); 37 | $ret->u8 = $u8; 38 | $ret->hex = $ret->u8Hex($u8); 39 | return $ret; 40 | } 41 | 42 | public static function fromUint4Array($u4) 43 | { 44 | $ret = new Uint(); 45 | $ret->u4 = $u4; 46 | $ret->hex = $ret->u4Hex($u4); 47 | $ret->u8 = $ret->u4U8($u4); 48 | return $ret; 49 | } 50 | 51 | public static function fromString($str) 52 | { 53 | $ret = new Uint(); 54 | $ret->u5 = $ret->stringU5($str); 55 | $ret->string = $str; 56 | $ret->u4 = $ret->u5U4($ret->u5); 57 | $ret->u8 = $ret->u4U8($ret->u4); 58 | return $ret; 59 | } 60 | 61 | public static function fromDec($dec) 62 | { 63 | $ret = new Uint(); 64 | $ret->dec = $dec; 65 | $ret->hex = Base::decToHex($dec); 66 | $ret->u8 = $ret->hexU8($ret->hex); 67 | return $ret; 68 | } 69 | 70 | public function toHexString() 71 | { 72 | if($this->hex) 73 | return $this->hex; 74 | else 75 | return $this->u8Hex($this->u8); 76 | } 77 | 78 | public function toString() 79 | { 80 | if($this->string) 81 | return $this->string; 82 | if($this->u5) 83 | return $this->u5String($this->u5); 84 | if($this->u4) 85 | return $this->u5String($this->u4U5($this->u4)); 86 | if($this->u8) 87 | return $this->u5String($this->u4U5($this->u8U4($this->u8))); 88 | return false; 89 | } 90 | 91 | public function toUint8() 92 | { 93 | return $this->u8; 94 | } 95 | 96 | public function toUint4() 97 | { 98 | if($this->u4) 99 | return $this->u4; 100 | if($this->u8) 101 | return $this->u8U4($this->u8); 102 | return false; 103 | } 104 | 105 | public function hexU8($hex) 106 | { 107 | if(strlen($hex) % 2 != 0) 108 | $hex = '0' . $hex; 109 | $arr = new SplFixedArray(strlen($hex) / 2); 110 | for($i = 0; $i < strlen($hex); $i+=2) 111 | { 112 | $arr[$i/2] = base_convert($hex[$i] . $hex[$i+1], 16, 10); 113 | } 114 | return $arr; 115 | } 116 | 117 | public function hexU4($hex) 118 | { 119 | $arr = new SplFixedArray(strlen($hex)); 120 | for($i = 0; $i < strlen($hex); $i++) 121 | { 122 | $arr[$i] = base_convert($hex[$i], 16, 10); 123 | } 124 | return $arr; 125 | } 126 | 127 | public function u8Hex($bytes) 128 | { 129 | $hex = ''; 130 | foreach($bytes as $byte) 131 | { 132 | $aux = base_convert($byte, 10, 16); 133 | if(strlen($aux) == 1) 134 | $aux = '0' . $aux; 135 | $hex .= $aux; 136 | } 137 | return strtoupper($hex); 138 | } 139 | 140 | public function u4Hex($bytes) 141 | { 142 | $hex = ''; 143 | foreach($bytes as $byte) 144 | $hex .= base_convert($byte, 10, 16); 145 | return strtoupper($hex); 146 | } 147 | 148 | public function u8U4($u8) 149 | { 150 | $u4 = new SplFixedArray(count($u8) * 2); 151 | for($i = 0; $i < count($u8); $i++) 152 | { 153 | $u4[$i*2] = $u8[$i] / 16 | 0; 154 | $u4[$i*2 + 1] = $u8[$i] % 16; 155 | } 156 | return $u4; 157 | } 158 | 159 | public function u4U8($u4) 160 | { 161 | $u8 = new SplFixedArray(count($u4) / 2); 162 | for($i = 0; $i < count($u8); $i++) 163 | $u8[$i] = $u4[$i*2] * 16 + $u4[$i*2 + 1]; 164 | return $u8; 165 | } 166 | 167 | public function u4U5($u4) 168 | { 169 | $u5 = new SplFixedArray(count($u4) / 5 * 4); 170 | for($i = 1; $i <= count($u5); $i++) 171 | { 172 | $n = $i - 1; 173 | $m = $i % 4; 174 | $z = $n + (($i - $m) / 4); 175 | $right = $u4[$z] << $m; 176 | if((count($u5) - $i) % 4 == 0) 177 | $left = $u4[$z - 1] << 4; 178 | else 179 | $left = $u4[$z + 1] >> (4 - $m); 180 | $u5[$n] = ($left + $right) % 32; 181 | } 182 | return $u5; 183 | } 184 | 185 | public function u5U4($u5) 186 | { 187 | $u4 = new SplFixedArray(count($u5) / 4 * 5); 188 | for($i = 1; $i <= count($u4); $i++) 189 | { 190 | $n = $i - 1; 191 | $m = $i % 5; 192 | $z = $n - (($i - $m) / 5); 193 | if($i > 1) 194 | $right = $u5[$z - 1] << (5 - $m); 195 | else $right = 0; 196 | $left = $u5[$z] >> $m; 197 | $u4[$n] = ($left + $right) % 16; 198 | } 199 | return $u4; 200 | } 201 | 202 | public function stringU5($str) 203 | { 204 | $letters = '13456789abcdefghijkmnopqrstuwxyz'; 205 | $len = strlen($str); 206 | $arr = str_split($str); 207 | $u5 = new SplFixedArray($len); 208 | for($i = 0; $i < $len; $i++) 209 | $u5[$i] = strpos($letters, $arr[$i]); 210 | return $u5; 211 | } 212 | 213 | public function u5String($u5) 214 | { 215 | $letters = str_split('13456789abcdefghijkmnopqrstuwxyz'); 216 | $str = ""; 217 | for($i = 0; $i < count($u5); $i++) 218 | $str .= $letters[$u5[$i]]; 219 | return $str; 220 | } 221 | 222 | public function dec2hex($str, $bytes = null) 223 | { 224 | $dec = str_split($hex); 225 | $sum = []; 226 | $hex = []; 227 | $i = $s = 0; 228 | while(count($dec)) 229 | { 230 | $s = 1 * array_shift($dec); 231 | for($i = 0; $s || $i < count($sum); $i++) 232 | { 233 | $s += ($sum[$i] || 0) * 10; 234 | $sum[$i] = $s % 16; 235 | $s = ($s - $sum[$i]) / 16; 236 | } 237 | } 238 | while(count($sum)) 239 | { 240 | $hex[] = base_convert(array_shift($sum), 10, 16); 241 | } 242 | 243 | $hex = implode('', $hex); 244 | 245 | if(strlen($hex) % 2 != 0) 246 | $hex = "0" . $hex; 247 | 248 | if($bytes > strlen($hex) / 2) 249 | { 250 | $diff = $bytes - strlen($hex) / 2; 251 | for($i = 0; $i < $diff; $i++) 252 | $hex = "00" . $hex; 253 | } 254 | return $hex; 255 | } 256 | 257 | public function reverse() 258 | { 259 | if($this->u8) 260 | { 261 | $len = count($this->u8); 262 | for($i = 0; $i != $len - 1 - $i; $i++) 263 | { 264 | $aux = $this->u8[$i]; 265 | $this->u8[$i] = $this->u8[$len - 1 - $i]; 266 | $this->u8[$len - 1 - $i] = $aux; 267 | } 268 | } 269 | return self::fromUint8Array($this->toUint8()); 270 | } 271 | 272 | public static function expandSize($size, $uint) 273 | { 274 | if(get_class($uint) != 'Uint') 275 | return false; 276 | if(count($uint->toUint8()) < $size) 277 | { 278 | $u8 = $uint->toUint8(); 279 | $new = new SplFixedArray($size); 280 | $diff = $size - count($u8); 281 | for($i = 0; $i < $size; $i++) 282 | { 283 | if($i < $diff) 284 | $new[$i] = 0; 285 | else 286 | $new[$i] = $u8[$i - $diff]; 287 | } 288 | return self::fromUint8Array($new); 289 | } 290 | return $uint; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /test/BananoBlock/change.php: -------------------------------------------------------------------------------- 1 | account_info(['account' => $account]); 22 | $block_info = $bananorpc->block_info([ 23 | 'json_block' => true, 24 | 'hash' => $account_info['frontier'] 25 | ]); 26 | 27 | // Generate work 28 | $work = BananoTool::work($account_info['frontier'], $change_difficulty); 29 | 30 | // Build block 31 | $bananoblock->setPrev($account_info['frontier'], $block_info['contents']); 32 | $bananoblock->setWork($work); 33 | $bananoblock->change($representative); 34 | 35 | // Publish block 36 | $process = $bananorpc->process([ 37 | 'json_block' => 'true', 38 | 'block' => $bananoblock->block 39 | ]); 40 | 41 | // Results and debug 42 | if ($bananorpc->error) { 43 | echo $bananorpc->error . PHP_EOL; 44 | } 45 | 46 | var_dump($process); 47 | -------------------------------------------------------------------------------- /test/BananoBlock/open.php: -------------------------------------------------------------------------------- 1 | setWork($work); 27 | $bananoblock->open($pairing_block_id, $received_amount, $representative); 28 | 29 | // Publish block 30 | $process = $bananorpc->process([ 31 | 'json_block' => 'true', 32 | 'block' => $bananoblock->block 33 | ]); 34 | 35 | // Results and debug 36 | if ($bananorpc->error) { 37 | echo $bananorpc->error . PHP_EOL; 38 | } 39 | 40 | var_dump($process); 41 | -------------------------------------------------------------------------------- /test/BananoBlock/receive.php: -------------------------------------------------------------------------------- 1 | account_info(['account' => $account]); 24 | $block_info = $bananorpc->block_info([ 25 | 'json_block' => true, 26 | 'hash' => $account_info['frontier'] 27 | ]); 28 | 29 | // Generate work 30 | $work = BananoTool::work($account_info['frontier'], $receive_difficulty); 31 | 32 | // Build block 33 | $bananoblock->setPrev($account_info['frontier'], $block_info['contents']); 34 | $bananoblock->setWork($work); 35 | $bananoblock->receive($pairing_block_id, $received_amount, $representative); 36 | 37 | // Publish block 38 | $process = $bananorpc->process([ 39 | 'json_block' => 'true', 40 | 'block' => $bananoblock->block 41 | ]); 42 | 43 | // Results and debug 44 | if ($bananorpc->error) { 45 | echo $bananorpc->error . PHP_EOL; 46 | } 47 | 48 | var_dump($process); 49 | -------------------------------------------------------------------------------- /test/BananoBlock/send.php: -------------------------------------------------------------------------------- 1 | account_info(['account' => $account]); 24 | $block_info = $bananorpc->block_info([ 25 | 'json_block' => true, 26 | 'hash' => $account_info['frontier'] 27 | ]); 28 | 29 | // Generate work 30 | $work = BananoTool::work($account_info['frontier'], $send_difficulty); 31 | 32 | // Build block 33 | $bananoblock->setPrev($account_info['frontier'], $block_info['contents']); 34 | $bananoblock->setWork($work); 35 | $bananoblock->send($destination, $sending_amount, $representative); 36 | 37 | // Publish block 38 | $process = $bananorpc->process([ 39 | 'json_block' => 'true', 40 | 'block' => $bananoblock->block 41 | ]); 42 | 43 | // Results and debug 44 | if ($bananorpc->error) { 45 | echo $bananorpc->error . PHP_EOL; 46 | } 47 | 48 | var_dump($process); 49 | -------------------------------------------------------------------------------- /test/BananoCLI.php: -------------------------------------------------------------------------------- 1 | account_key(['account' => $account]); 10 | 11 | var_dump($bananocli); 12 | -------------------------------------------------------------------------------- /test/BananoIPC.php: -------------------------------------------------------------------------------- 1 | setBananoEncoding(1); 13 | 14 | $bananoipc_unix->open(); 15 | 16 | $bananoipc_unix->account_weight(['account' => $account]); 17 | 18 | var_dump($bananoipc_unix); 19 | 20 | $bananoipc_unix->close(); 21 | 22 | 23 | // * Unix domain socket encoding 2 24 | 25 | $bananoipc_unix = new MikeRow\BananoPHP\BananoIPC('unix', ['/tmp/nano']); 26 | 27 | $bananoipc_unix->setBananoEncoding(2); 28 | 29 | $bananoipc_unix->open(); 30 | 31 | $bananoipc_unix->account_weight(['account' => $account]); 32 | 33 | var_dump($bananoipc_unix); 34 | 35 | $bananoipc_unix->close(); 36 | 37 | 38 | // * Unix domain socket encoding 3 39 | 40 | $bananoipc_unix = new MikeRow\BananoPHP\BananoIPC('unix', ['/tmp/nano']); 41 | 42 | $bananoipc_unix->setBananoEncoding(3); 43 | 44 | $bananoipc_unix->open(); 45 | 46 | $bananoipc_unix->AccountWeight(['Account' => $account]); 47 | 48 | var_dump($bananoipc_unix); 49 | 50 | $bananoipc_unix->close(); 51 | 52 | 53 | // * Unix domain socket encoding 4 54 | 55 | $bananoipc_unix = new MikeRow\BananoPHP\BananoIPC('unix', ['/tmp/nano']); 56 | 57 | $bananoipc_unix->setBananoEncoding(4); 58 | 59 | $bananoipc_unix->open(); 60 | 61 | $bananoipc_unix->AccountWeight(['account' => $account]); 62 | 63 | var_dump($bananoipc_unix); 64 | 65 | $bananoipc_unix->close(); 66 | 67 | 68 | // * TCP encoding 1 69 | 70 | $bananoipc_tcp = new MikeRow\BananoPHP\BananoIPC('tcp', ['localhost', 7077]); 71 | 72 | $bananoipc_tcp->setBananoEncoding(1); 73 | 74 | $bananoipc_tcp->open(); 75 | 76 | $bananoipc_tcp->account_weight(['account' => $account]); 77 | 78 | var_dump($bananoipc_tcp); 79 | 80 | $bananoipc_tcp->close(); 81 | 82 | 83 | // * TCP encoding 2 84 | 85 | $bananoipc_tcp = new MikeRow\BananoPHP\BananoIPC('tcp', ['localhost', 7077]); 86 | 87 | $bananoipc_tcp->setBananoEncoding(2); 88 | 89 | $bananoipc_tcp->open(); 90 | 91 | $bananoipc_tcp->account_weight(['account' => $account]); 92 | 93 | var_dump($bananoipc_tcp); 94 | 95 | $bananoipc_tcp->close(); 96 | 97 | 98 | // * TCP encoding 3 99 | 100 | $bananoipc_tcp = new MikeRow\BananoPHP\BananoIPC('tcp', ['localhost', 7077]); 101 | 102 | $bananoipc_tcp->setBananoEncoding(3); 103 | 104 | $bananoipc_tcp->open(); 105 | 106 | $bananoipc_tcp->AccountWeight(['Account' => $account]); 107 | 108 | var_dump($bananoipc_tcp); 109 | 110 | $bananoipc_tcp->close(); 111 | 112 | 113 | // * TCP encoding 4 114 | 115 | $bananoipc_tcp = new MikeRow\BananoPHP\BananoIPC('tcp', ['localhost', 7077]); 116 | 117 | $bananoipc_tcp->setBananoEncoding(4); 118 | 119 | $bananoipc_tcp->open(); 120 | 121 | $bananoipc_tcp->AccountWeight(['account' => $account]); 122 | 123 | var_dump($bananoipc_tcp); 124 | 125 | $bananoipc_tcp->close(); 126 | -------------------------------------------------------------------------------- /test/BananoIPCListen.php: -------------------------------------------------------------------------------- 1 | setBananoEncoding(3); 13 | 14 | $bananoipc_unix->setListen(true); 15 | 16 | $bananoipc_unix->open(); 17 | 18 | $args = [ 19 | 'ServiceName' => 'TopicConfirmation' 20 | ]; 21 | 22 | $bananoipc_unix->ServiceRegister($args); 23 | 24 | $i = 0; 25 | while ($i<5) { 26 | print_r($bananoipc_unix->listen()); 27 | $i++; 28 | } 29 | 30 | $bananoipc_unix->close(); 31 | -------------------------------------------------------------------------------- /test/BananoRPC.php: -------------------------------------------------------------------------------- 1 | setBananoApi(1); 13 | 14 | $bananorpc->account_weight(['account' => $account]); 15 | 16 | var_dump($bananorpc); 17 | 18 | 19 | // * API v2 20 | 21 | $bananorpc = new MikeRow\BananoPHP\BananoRPC('http', 'localhost', 7076, 'api/v2'); 22 | 23 | $bananorpc->setBananoApi(2); 24 | 25 | $bananorpc->AccountWeight(['account' => $account]); 26 | 27 | var_dump($bananorpc); 28 | -------------------------------------------------------------------------------- /test/BananoRPCExt.php: -------------------------------------------------------------------------------- 1 | account_weight(['account' => $account]); 10 | 11 | var_dump($bananorpcext); 12 | -------------------------------------------------------------------------------- /test/BananoTool/account2public.php: -------------------------------------------------------------------------------- 1 | open(); 8 | 9 | $bananows->subscribe('confirmation', null, true); 10 | 11 | $i = 0; 12 | while ($i<5) { 13 | print_r($bananows->listen()); 14 | $i++; 15 | } 16 | 17 | $bananows->unsubscribe('confirmation'); 18 | 19 | $bananows->close(); 20 | -------------------------------------------------------------------------------- /test/autoload.php: -------------------------------------------------------------------------------- 1 | setBananoEncoding(1); 15 | 16 | $bananoipc_unix->open(); 17 | 18 | $t0 = microtime(true); 19 | 20 | for ($i = 0; $i < $cycles; $i++) { 21 | $bananoipc_unix->account_weight(['account' => $account]); 22 | } 23 | 24 | echo 'Time unix enc 1: ' . (microtime(true) - $t0) . PHP_EOL; 25 | 26 | $bananoipc_unix->close(); 27 | 28 | 29 | // * Unix domain socket encoding 2 30 | 31 | $bananoipc_unix = new MikeRow\BananoPHP\BananoIPC('unix', ['/tmp/nano']); 32 | 33 | $bananoipc_unix->setBananoEncoding(2); 34 | 35 | $bananoipc_unix->open(); 36 | 37 | $t0 = microtime(true); 38 | 39 | for ($i = 0; $i < $cycles; $i++) { 40 | $bananoipc_unix->account_weight(['account' => $account]); 41 | } 42 | 43 | echo 'Time unix enc 2: ' . (microtime(true) - $t0) . PHP_EOL; 44 | 45 | $bananoipc_unix->close(); 46 | 47 | 48 | // * Unix domain socket encoding 3 49 | 50 | $bananoipc_unix = new MikeRow\BananoPHP\BananoIPC('unix', ['/tmp/nano']); 51 | 52 | $bananoipc_unix->setBananoEncoding(3); 53 | 54 | $bananoipc_unix->open(); 55 | 56 | $t0 = microtime(true); 57 | 58 | for ($i = 0; $i < $cycles; $i++) { 59 | $bananoipc_unix->AccountWeight(['Account' => $account]); 60 | } 61 | 62 | echo 'Time unix enc 3: ' . (microtime(true) - $t0) . PHP_EOL; 63 | 64 | $bananoipc_unix->close(); 65 | 66 | 67 | // * Unix domain socket encoding 4 68 | 69 | $bananoipc_unix = new MikeRow\BananoPHP\BananoIPC('unix', ['/tmp/nano']); 70 | 71 | $bananoipc_unix->setBananoEncoding(4); 72 | 73 | $bananoipc_unix->open(); 74 | 75 | $t0 = microtime(true); 76 | 77 | for ($i = 0; $i < $cycles; $i++) { 78 | $bananoipc_unix->AccountWeight(['account' => $account]); 79 | } 80 | 81 | echo 'Time unix enc 4: ' . (microtime(true) - $t0) . PHP_EOL; 82 | 83 | $bananoipc_unix->close(); 84 | 85 | 86 | // * TCP encoding 1 87 | 88 | $bananoipc_tcp = new MikeRow\BananoPHP\BananoIPC('tcp', ['localhost', 7077]); 89 | 90 | $bananoipc_tcp->setBananoEncoding(1); 91 | 92 | $bananoipc_tcp->open(); 93 | 94 | $t0 = microtime(true); 95 | 96 | for ($i = 0; $i < $cycles; $i++) { 97 | $bananoipc_tcp->account_weight(['account' => $account]); 98 | } 99 | 100 | echo 'Time TCP enc 1: ' . (microtime(true) - $t0) . PHP_EOL; 101 | 102 | $bananoipc_tcp->close(); 103 | 104 | 105 | // * TCP encoding 2 106 | 107 | $bananoipc_tcp = new MikeRow\BananoPHP\BananoIPC('tcp', ['localhost', 7077]); 108 | 109 | $bananoipc_tcp->setBananoEncoding(2); 110 | 111 | $bananoipc_tcp->open(); 112 | 113 | $t0 = microtime(true); 114 | 115 | for ($i = 0; $i < $cycles; $i++) { 116 | $bananoipc_tcp->account_weight(['account' => $account]); 117 | } 118 | 119 | echo 'Time TCP enc 2: ' . (microtime(true) - $t0) . PHP_EOL; 120 | 121 | $bananoipc_tcp->close(); 122 | 123 | 124 | // * TCP encoding 3 125 | 126 | $bananoipc_tcp = new MikeRow\BananoPHP\BananoIPC('tcp', ['localhost', 7077]); 127 | 128 | $bananoipc_tcp->setBananoEncoding(3); 129 | 130 | $bananoipc_tcp->open(); 131 | 132 | $t0 = microtime(true); 133 | 134 | for ($i = 0; $i < $cycles; $i++) { 135 | $bananoipc_tcp->AccountWeight(['Account' => $account]); 136 | } 137 | 138 | echo 'Time TCP enc 3: ' . (microtime(true) - $t0) . PHP_EOL; 139 | 140 | $bananoipc_tcp->close(); 141 | 142 | 143 | // * TCP encoding 4 144 | 145 | $bananoipc_tcp = new MikeRow\BananoPHP\BananoIPC('tcp', ['localhost', 7077]); 146 | 147 | $bananoipc_tcp->setBananoEncoding(4); 148 | 149 | $bananoipc_tcp->open(); 150 | 151 | $t0 = microtime(true); 152 | 153 | for ($i = 0; $i < $cycles; $i++) { 154 | $bananoipc_tcp->AccountWeight(['account' => $account]); 155 | } 156 | 157 | echo 'Time TCP enc 4: ' . (microtime(true) - $t0) . PHP_EOL; 158 | 159 | $bananoipc_tcp->close(); 160 | -------------------------------------------------------------------------------- /test/speed/BananoRPC.php: -------------------------------------------------------------------------------- 1 | setBananoApi(1); 15 | 16 | $t0 = microtime(true); 17 | 18 | for ($i = 0; $i < $cycles; $i++) { 19 | $bananorpc->account_weight(['account' => $account]); 20 | } 21 | 22 | echo 'Time v1: ' . (microtime(true) - $t0) . PHP_EOL; 23 | 24 | 25 | // * API v2 26 | 27 | $bananorpc = new MikeRow\BananoPHP\BananoRPC('http', 'localhost', 7076, 'api/v2'); 28 | 29 | $bananorpc->setBananoApi(2); 30 | 31 | $t0 = microtime(true); 32 | 33 | for ($i = 0; $i < $cycles; $i++) { 34 | $bananorpc->AccountWeight(['account' => $account]); 35 | } 36 | 37 | echo 'Time v2: ' . (microtime(true) - $t0) . PHP_EOL; 38 | --------------------------------------------------------------------------------