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 | } |