Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
69.70% |
46 / 66 |
|
16.67% |
1 / 6 |
CRAP | |
0.00% |
0 / 1 |
| Blockstream_Info_API | |
69.70% |
46 / 66 |
|
16.67% |
1 / 6 |
23.12 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_address_data | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
| get_address_balance | |
82.35% |
14 / 17 |
|
0.00% |
0 / 1 |
2.02 | |||
| get_received_by_address | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
| get_transactions_received | |
84.38% |
27 / 32 |
|
0.00% |
0 / 1 |
5.10 | |||
| get_blockchain_height | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @see https://github.com/Blockstream/esplora/blob/master/API.md |
| 4 | * |
| 5 | * @package brianhenryie/bh-wp-bitcoin-gateway |
| 6 | */ |
| 7 | |
| 8 | namespace BrianHenryIE\WP_Bitcoin_Gateway\API\Blockchain; |
| 9 | |
| 10 | use BrianHenryIE\WP_Bitcoin_Gateway\API\Blockchain_API_Interface; |
| 11 | use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Address_Balance; |
| 12 | use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Transaction_Interface; |
| 13 | use DateTimeImmutable; |
| 14 | use DateTimeInterface; |
| 15 | use DateTimeZone; |
| 16 | use BrianHenryIE\WP_Bitcoin_Gateway\API_Interface; |
| 17 | use JsonException; |
| 18 | use Psr\Log\LoggerAwareInterface; |
| 19 | use Psr\Log\LoggerAwareTrait; |
| 20 | use Psr\Log\LoggerInterface; |
| 21 | |
| 22 | /** |
| 23 | * @phpstan-type Stats array{funded_txo_count:int, funded_txo_sum:int, spent_txo_count:int, spent_txo_sum:int, tx_count:int} |
| 24 | * @phpstan-import-type TransactionArray from API_Interface as TransactionArray |
| 25 | */ |
| 26 | class Blockstream_Info_API implements Blockchain_API_Interface, LoggerAwareInterface { |
| 27 | use LoggerAwareTrait; |
| 28 | |
| 29 | public function __construct( LoggerInterface $logger ) { |
| 30 | $this->logger = $logger; |
| 31 | } |
| 32 | |
| 33 | /** |
| 34 | * |
| 35 | * @return array{address:string, chain_stats:Stats, mempool_stats:Stats} |
| 36 | */ |
| 37 | protected function get_address_data( string $btc_address ): array { |
| 38 | $address_info_url = 'https://blockstream.info/api/address/' . $btc_address; |
| 39 | |
| 40 | $this->logger->debug( 'URL: ' . $address_info_url ); |
| 41 | |
| 42 | $request_response = wp_remote_get( $address_info_url ); |
| 43 | |
| 44 | if ( is_wp_error( $request_response ) || 200 !== $request_response['response']['code'] ) { |
| 45 | throw new \Exception(); |
| 46 | } |
| 47 | |
| 48 | $address_info = json_decode( $request_response['body'], true ); |
| 49 | |
| 50 | return $address_info; |
| 51 | } |
| 52 | |
| 53 | /** |
| 54 | * @see Blockchain_API_Interface::get_address_balance() |
| 55 | * |
| 56 | * @param string $btc_address |
| 57 | * @param int $number_of_confirmations |
| 58 | * |
| 59 | * @return array{confirmed_balance:string, unconfirmed_balance:string, number_of_confirmations:int} |
| 60 | * @throws \Exception |
| 61 | */ |
| 62 | public function get_address_balance( string $btc_address, int $number_of_confirmations ): Address_Balance { |
| 63 | |
| 64 | if ( 1 !== $number_of_confirmations ) { |
| 65 | error_log( __CLASS__ . ' ' . __FUNCTION__ . ' using 1 for number of confirmations.' ); |
| 66 | |
| 67 | // Maybe `number_of_confirmations` should be block_height and the client can decide is that enough. |
| 68 | } |
| 69 | |
| 70 | $result = array(); |
| 71 | $result['number_of_confirmations'] = $number_of_confirmations; |
| 72 | |
| 73 | $address_info = $this->get_address_data( $btc_address ); |
| 74 | |
| 75 | $confirmed_balance = ( $address_info['chain_stats']['funded_txo_sum'] - $address_info['chain_stats']['spent_txo_sum'] ) / 100000000; |
| 76 | $this->logger->debug( 'Confirmed balance: ' . number_format( $confirmed_balance, 8 ), array( 'address_info' => $address_info ) ); |
| 77 | |
| 78 | $result['confirmed_balance'] = (string) $confirmed_balance; |
| 79 | |
| 80 | $unconfirmed_balance = ( $address_info['mempool_stats']['funded_txo_sum'] - $address_info['mempool_stats']['spent_txo_sum'] ) / 100000000; |
| 81 | $this->logger->debug( 'Unconfirmed balance: ' . number_format( $unconfirmed_balance, 8 ), array( 'address_info' => $address_info ) ); |
| 82 | |
| 83 | $result['unconfirmed_balance'] = (string) $unconfirmed_balance; |
| 84 | |
| 85 | return new class($result) implements Address_Balance { |
| 86 | protected $result; |
| 87 | public function __construct( $result ) { |
| 88 | $this->result = $result; |
| 89 | } |
| 90 | |
| 91 | public function get_confirmed_balance(): string { |
| 92 | return $this->result['confirmed_balance']; |
| 93 | } |
| 94 | |
| 95 | public function get_unconfirmed_balance(): string { |
| 96 | return $this->result['unconfirmed_balance']; |
| 97 | } |
| 98 | |
| 99 | public function get_number_of_confirmations(): int { |
| 100 | return $this->result['number_of_confirmations']; |
| 101 | } |
| 102 | }; |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * The total amount in BTC received at this address. |
| 107 | * |
| 108 | * @param string $btc_address The Bitcoin address. |
| 109 | * |
| 110 | * @return string |
| 111 | * @throws \Exception |
| 112 | */ |
| 113 | public function get_received_by_address( string $btc_address, bool $confirmed ): string { |
| 114 | |
| 115 | $address_info = $this->get_address_data( $btc_address ); |
| 116 | |
| 117 | if ( $confirmed ) { |
| 118 | $calc = $address_info['chain_stats']['funded_txo_sum'] / 100000000; |
| 119 | } else { |
| 120 | $calc = ( $address_info['chain_stats']['funded_txo_sum'] + $address_info['mempool_stats']['funded_txo_sum'] ) / 100000000; |
| 121 | } |
| 122 | return "{$calc}"; |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * @param string $btc_address |
| 127 | * |
| 128 | * @return array<string, Transaction_Interface> |
| 129 | * |
| 130 | * @throws JsonException |
| 131 | */ |
| 132 | public function get_transactions_received( string $btc_address ): array { |
| 133 | |
| 134 | $address_info_url_bs = "https://blockstream.info/api/address/{$btc_address}/txs"; |
| 135 | |
| 136 | $this->logger->debug( 'URL: ' . $address_info_url_bs ); |
| 137 | |
| 138 | $request_response = wp_remote_get( $address_info_url_bs ); |
| 139 | |
| 140 | if ( is_wp_error( $request_response ) ) { |
| 141 | throw new \Exception( $request_response->get_error_message() ); |
| 142 | } |
| 143 | if ( 200 !== $request_response['response']['code'] ) { |
| 144 | throw new \Exception( 'Unexpected response received.' ); |
| 145 | } |
| 146 | |
| 147 | $blockstream_transactions = json_decode( $request_response['body'], true, 512, JSON_THROW_ON_ERROR ); |
| 148 | |
| 149 | /** |
| 150 | * block_time is in unixtime. |
| 151 | * |
| 152 | * @param array{txid:string, version:int, locktime:int, vin:array, vout:array, size:int, weight:int, fee:int, status:array{confirmed:bool, block_height:int, block_hash:string, block_time:int}} $blockstream_transaction |
| 153 | * |
| 154 | * @return Transaction_Interface |
| 155 | */ |
| 156 | $blockstream_mapper = function ( array $blockstream_transaction ): Transaction_Interface { |
| 157 | |
| 158 | return new class( $blockstream_transaction ) implements Transaction_Interface { |
| 159 | |
| 160 | protected $blockstream_transaction; |
| 161 | |
| 162 | public function __construct( $blockstream_transaction ) { |
| 163 | $this->blockstream_transaction = $blockstream_transaction; |
| 164 | } |
| 165 | |
| 166 | public function get_txid(): string { |
| 167 | return (string) $this->blockstream_transaction['txid']; |
| 168 | } |
| 169 | |
| 170 | public function get_time(): \DateTimeInterface { |
| 171 | |
| 172 | $block_time = (int) $this->blockstream_transaction['status']['block_time']; |
| 173 | |
| 174 | return new DateTimeImmutable( '@' . $block_time, new DateTimeZone( 'UTC' ) ); |
| 175 | } |
| 176 | |
| 177 | public function get_value( string $to_address ): float { |
| 178 | $value_including_fee = array_reduce( |
| 179 | $this->blockstream_transaction['vout'], |
| 180 | function ( $carry, $out ) use ( $to_address ) { |
| 181 | if ( $out['scriptpubkey_address'] === $to_address ) { |
| 182 | return $carry + $out['value']; |
| 183 | } |
| 184 | return $carry; |
| 185 | }, |
| 186 | 0 |
| 187 | ); |
| 188 | |
| 189 | return $value_including_fee / 100000000; |
| 190 | } |
| 191 | |
| 192 | public function get_block_height(): int { |
| 193 | |
| 194 | return $this->blockstream_transaction['status']['block_height']; |
| 195 | |
| 196 | // TODO: Confirmations was returning the block height - 1. Presumably that meant mempool/0 confirmations, but I need test data to understand. |
| 197 | // Correct solution is probably to check does $blockstream_transaction['status']['block_height'] exist, else 0. |
| 198 | // Quick fix. |
| 199 | } |
| 200 | }; |
| 201 | }; |
| 202 | |
| 203 | $transactions = array_map( $blockstream_mapper, $blockstream_transactions ); |
| 204 | |
| 205 | $keyed_transactions = array(); |
| 206 | foreach ( $transactions as $transaction ) { |
| 207 | $keyed_transactions[ $transaction->get_txid() ] = $transaction; |
| 208 | } |
| 209 | |
| 210 | return $keyed_transactions; |
| 211 | } |
| 212 | |
| 213 | /** |
| 214 | * @return int |
| 215 | * @throws \Exception |
| 216 | */ |
| 217 | public function get_blockchain_height(): int { |
| 218 | $blocks_url_bs = 'https://blockstream.info/api/blocks/tip/height'; |
| 219 | $request_response = wp_remote_get( $blocks_url_bs ); |
| 220 | if ( is_wp_error( $request_response ) || 200 !== $request_response['response']['code'] ) { |
| 221 | throw new \Exception(); |
| 222 | } |
| 223 | return intval( $request_response['body'] ); |
| 224 | } |
| 225 | } |