|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Blocktrail\SDK; |
|
4
|
|
|
|
|
5
|
|
|
use BitWasp\BitcoinLib\BIP32; |
|
6
|
|
|
use BitWasp\BitcoinLib\BIP39\BIP39; |
|
7
|
|
|
use BitWasp\BitcoinLib\BitcoinLib; |
|
8
|
|
|
use BitWasp\BitcoinLib\RawTransaction; |
|
9
|
|
|
use Blocktrail\SDK\Connection\RestClient; |
|
10
|
|
|
|
|
11
|
|
|
/** |
|
12
|
|
|
* Class BlocktrailSDK |
|
13
|
|
|
*/ |
|
14
|
|
|
class BlocktrailSDK implements BlocktrailSDKInterface { |
|
15
|
|
|
/** |
|
16
|
|
|
* @var Connection\RestClient |
|
17
|
|
|
*/ |
|
18
|
|
|
protected $client; |
|
19
|
|
|
|
|
20
|
|
|
/** |
|
21
|
|
|
* @var string currently only supporting; bitcoin |
|
22
|
|
|
*/ |
|
23
|
|
|
protected $network; |
|
24
|
|
|
|
|
25
|
|
|
/** |
|
26
|
|
|
* @var bool |
|
27
|
|
|
*/ |
|
28
|
|
|
protected $testnet; |
|
29
|
|
|
|
|
30
|
|
|
/** |
|
31
|
|
|
* @param string $apiKey the API_KEY to use for authentication |
|
32
|
|
|
* @param string $apiSecret the API_SECRET to use for authentication |
|
33
|
|
|
* @param string $network the cryptocurrency 'network' to consume, eg BTC, LTC, etc |
|
34
|
|
|
* @param bool $testnet testnet yes/no |
|
35
|
|
|
* @param string $apiVersion the version of the API to consume |
|
36
|
|
|
* @param null $apiEndpoint overwrite the endpoint used |
|
37
|
|
|
* this will cause the $network, $testnet and $apiVersion to be ignored! |
|
38
|
|
|
*/ |
|
39
|
|
|
public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) { |
|
40
|
|
|
if (is_null($apiEndpoint)) { |
|
41
|
|
|
$network = strtoupper($network); |
|
42
|
|
|
|
|
43
|
|
|
if ($testnet) { |
|
44
|
|
|
$network = "t{$network}"; |
|
45
|
|
|
} |
|
46
|
|
|
|
|
47
|
|
|
$apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com"; |
|
48
|
|
|
$apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$network}/"; |
|
49
|
|
|
} |
|
50
|
|
|
|
|
51
|
|
|
// normalize network and set bitcoinlib to the right magic-bytes |
|
52
|
|
|
list($this->network, $this->testnet) = $this->normalizeNetwork($network, $testnet); |
|
53
|
|
|
$this->setBitcoinLibMagicBytes($this->network, $this->testnet); |
|
54
|
|
|
|
|
55
|
|
|
$this->client = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret); |
|
56
|
|
|
} |
|
57
|
|
|
|
|
58
|
|
|
/** |
|
59
|
|
|
* normalize network string |
|
60
|
|
|
* |
|
61
|
|
|
* @param $network |
|
62
|
|
|
* @param $testnet |
|
63
|
|
|
* @return array |
|
64
|
|
|
* @throws \Exception |
|
65
|
|
|
*/ |
|
66
|
|
View Code Duplication |
protected function normalizeNetwork($network, $testnet) { |
|
|
|
|
|
|
67
|
|
|
switch (strtolower($network)) { |
|
68
|
|
|
case 'btc': |
|
69
|
|
|
case 'bitcoin': |
|
70
|
|
|
$network = 'bitcoin'; |
|
71
|
|
|
|
|
72
|
|
|
break; |
|
73
|
|
|
|
|
74
|
|
|
case 'tbtc': |
|
75
|
|
|
case 'bitcoin-testnet': |
|
76
|
|
|
$network = 'bitcoin'; |
|
77
|
|
|
$testnet = true; |
|
78
|
|
|
|
|
79
|
|
|
break; |
|
80
|
|
|
|
|
81
|
|
|
default: |
|
82
|
|
|
throw new \Exception("Unknown network [{$network}]"); |
|
83
|
|
|
} |
|
84
|
|
|
|
|
85
|
|
|
return [$network, $testnet]; |
|
86
|
|
|
} |
|
87
|
|
|
|
|
88
|
|
|
/** |
|
89
|
|
|
* set BitcoinLib to the correct magic-byte defaults for the selected network |
|
90
|
|
|
* |
|
91
|
|
|
* @param $network |
|
92
|
|
|
* @param $testnet |
|
93
|
|
|
*/ |
|
94
|
|
|
protected function setBitcoinLibMagicBytes($network, $testnet) { |
|
95
|
|
|
BitcoinLib::setMagicByteDefaults($network . ($testnet ? '-testnet' : '')); |
|
96
|
|
|
} |
|
97
|
|
|
|
|
98
|
|
|
/** |
|
99
|
|
|
* enable CURL debugging output |
|
100
|
|
|
* |
|
101
|
|
|
* @param bool $debug |
|
102
|
|
|
* |
|
103
|
|
|
* @codeCoverageIgnore |
|
104
|
|
|
*/ |
|
105
|
|
|
public function setCurlDebugging($debug = true) { |
|
106
|
|
|
$this->client->setCurlDebugging($debug); |
|
107
|
|
|
} |
|
108
|
|
|
|
|
109
|
|
|
/** |
|
110
|
|
|
* enable verbose errors |
|
111
|
|
|
* |
|
112
|
|
|
* @param bool $verboseErrors |
|
113
|
|
|
* |
|
114
|
|
|
* @codeCoverageIgnore |
|
115
|
|
|
*/ |
|
116
|
|
|
public function setVerboseErrors($verboseErrors = true) { |
|
117
|
|
|
$this->client->setVerboseErrors($verboseErrors); |
|
118
|
|
|
} |
|
119
|
|
|
|
|
120
|
|
|
/** |
|
121
|
|
|
* set cURL default option on Guzzle client |
|
122
|
|
|
* @param string $key |
|
123
|
|
|
* @param bool $value |
|
124
|
|
|
* |
|
125
|
|
|
* @codeCoverageIgnore |
|
126
|
|
|
*/ |
|
127
|
|
|
public function setCurlDefaultOption($key, $value) { |
|
128
|
|
|
$this->client->setCurlDefaultOption($key, $value); |
|
129
|
|
|
} |
|
130
|
|
|
|
|
131
|
|
|
/** |
|
132
|
|
|
* @return RestClient |
|
133
|
|
|
*/ |
|
134
|
|
|
public function getRestClient() { |
|
135
|
|
|
return $this->client; |
|
136
|
|
|
} |
|
137
|
|
|
|
|
138
|
|
|
/** |
|
139
|
|
|
* get a single address |
|
140
|
|
|
* @param string $address address hash |
|
141
|
|
|
* @return array associative array containing the response |
|
142
|
|
|
*/ |
|
143
|
|
|
public function address($address) { |
|
144
|
|
|
$response = $this->client->get("address/{$address}"); |
|
145
|
|
|
return self::jsonDecode($response->body(), true); |
|
146
|
|
|
} |
|
147
|
|
|
|
|
148
|
|
|
/** |
|
149
|
|
|
* get all transactions for an address (paginated) |
|
150
|
|
|
* @param string $address address hash |
|
151
|
|
|
* @param integer $page pagination: page number |
|
152
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
153
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
154
|
|
|
* @return array associative array containing the response |
|
155
|
|
|
*/ |
|
156
|
|
|
public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') { |
|
157
|
|
|
$queryString = [ |
|
158
|
|
|
'page' => $page, |
|
159
|
|
|
'limit' => $limit, |
|
160
|
|
|
'sort_dir' => $sortDir |
|
161
|
|
|
]; |
|
162
|
|
|
$response = $this->client->get("address/{$address}/transactions", $queryString); |
|
163
|
|
|
return self::jsonDecode($response->body(), true); |
|
164
|
|
|
} |
|
165
|
|
|
|
|
166
|
|
|
/** |
|
167
|
|
|
* get all unconfirmed transactions for an address (paginated) |
|
168
|
|
|
* @param string $address address hash |
|
169
|
|
|
* @param integer $page pagination: page number |
|
170
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
171
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
172
|
|
|
* @return array associative array containing the response |
|
173
|
|
|
*/ |
|
174
|
|
|
public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') { |
|
175
|
|
|
$queryString = [ |
|
176
|
|
|
'page' => $page, |
|
177
|
|
|
'limit' => $limit, |
|
178
|
|
|
'sort_dir' => $sortDir |
|
179
|
|
|
]; |
|
180
|
|
|
$response = $this->client->get("address/{$address}/unconfirmed-transactions", $queryString); |
|
181
|
|
|
return self::jsonDecode($response->body(), true); |
|
182
|
|
|
} |
|
183
|
|
|
|
|
184
|
|
|
/** |
|
185
|
|
|
* get all unspent outputs for an address (paginated) |
|
186
|
|
|
* @param string $address address hash |
|
187
|
|
|
* @param integer $page pagination: page number |
|
188
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
189
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
190
|
|
|
* @return array associative array containing the response |
|
191
|
|
|
*/ |
|
192
|
|
|
public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') { |
|
193
|
|
|
$queryString = [ |
|
194
|
|
|
'page' => $page, |
|
195
|
|
|
'limit' => $limit, |
|
196
|
|
|
'sort_dir' => $sortDir |
|
197
|
|
|
]; |
|
198
|
|
|
$response = $this->client->get("address/{$address}/unspent-outputs", $queryString); |
|
199
|
|
|
return self::jsonDecode($response->body(), true); |
|
200
|
|
|
} |
|
201
|
|
|
|
|
202
|
|
|
/** |
|
203
|
|
|
* verify ownership of an address |
|
204
|
|
|
* @param string $address address hash |
|
205
|
|
|
* @param string $signature a signed message (the address hash) using the private key of the address |
|
206
|
|
|
* @return array associative array containing the response |
|
207
|
|
|
*/ |
|
208
|
|
|
public function verifyAddress($address, $signature) { |
|
209
|
|
|
$postData = ['signature' => $signature]; |
|
210
|
|
|
|
|
211
|
|
|
$response = $this->client->post("address/{$address}/verify", null, $postData, RestClient::AUTH_HTTP_SIG); |
|
212
|
|
|
|
|
213
|
|
|
return self::jsonDecode($response->body(), true); |
|
214
|
|
|
} |
|
215
|
|
|
|
|
216
|
|
|
/** |
|
217
|
|
|
* get all blocks (paginated) |
|
218
|
|
|
* @param integer $page pagination: page number |
|
219
|
|
|
* @param integer $limit pagination: records per page |
|
220
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
221
|
|
|
* @return array associative array containing the response |
|
222
|
|
|
*/ |
|
223
|
|
|
public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') { |
|
224
|
|
|
$queryString = [ |
|
225
|
|
|
'page' => $page, |
|
226
|
|
|
'limit' => $limit, |
|
227
|
|
|
'sort_dir' => $sortDir |
|
228
|
|
|
]; |
|
229
|
|
|
$response = $this->client->get("all-blocks", $queryString); |
|
230
|
|
|
return self::jsonDecode($response->body(), true); |
|
231
|
|
|
} |
|
232
|
|
|
|
|
233
|
|
|
/** |
|
234
|
|
|
* get the latest block |
|
235
|
|
|
* @return array associative array containing the response |
|
236
|
|
|
*/ |
|
237
|
|
|
public function blockLatest() { |
|
238
|
|
|
$response = $this->client->get("block/latest"); |
|
239
|
|
|
return self::jsonDecode($response->body(), true); |
|
240
|
|
|
} |
|
241
|
|
|
|
|
242
|
|
|
/** |
|
243
|
|
|
* get an individual block |
|
244
|
|
|
* @param string|integer $block a block hash or a block height |
|
245
|
|
|
* @return array associative array containing the response |
|
246
|
|
|
*/ |
|
247
|
|
|
public function block($block) { |
|
248
|
|
|
$response = $this->client->get("block/{$block}"); |
|
249
|
|
|
return self::jsonDecode($response->body(), true); |
|
250
|
|
|
} |
|
251
|
|
|
|
|
252
|
|
|
/** |
|
253
|
|
|
* get all transaction in a block (paginated) |
|
254
|
|
|
* @param string|integer $block a block hash or a block height |
|
255
|
|
|
* @param integer $page pagination: page number |
|
256
|
|
|
* @param integer $limit pagination: records per page |
|
257
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
258
|
|
|
* @return array associative array containing the response |
|
259
|
|
|
*/ |
|
260
|
|
|
public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') { |
|
261
|
|
|
$queryString = [ |
|
262
|
|
|
'page' => $page, |
|
263
|
|
|
'limit' => $limit, |
|
264
|
|
|
'sort_dir' => $sortDir |
|
265
|
|
|
]; |
|
266
|
|
|
$response = $this->client->get("block/{$block}/transactions", $queryString); |
|
267
|
|
|
return self::jsonDecode($response->body(), true); |
|
268
|
|
|
} |
|
269
|
|
|
|
|
270
|
|
|
/** |
|
271
|
|
|
* get a single transaction |
|
272
|
|
|
* @param string $txhash transaction hash |
|
273
|
|
|
* @return array associative array containing the response |
|
274
|
|
|
*/ |
|
275
|
|
|
public function transaction($txhash) { |
|
276
|
|
|
$response = $this->client->get("transaction/{$txhash}"); |
|
277
|
|
|
return self::jsonDecode($response->body(), true); |
|
278
|
|
|
} |
|
279
|
|
|
|
|
280
|
|
|
/** |
|
281
|
|
|
* get a paginated list of all webhooks associated with the api user |
|
282
|
|
|
* @param integer $page pagination: page number |
|
283
|
|
|
* @param integer $limit pagination: records per page |
|
284
|
|
|
* @return array associative array containing the response |
|
285
|
|
|
*/ |
|
286
|
|
|
public function allWebhooks($page = 1, $limit = 20) { |
|
287
|
|
|
$queryString = [ |
|
288
|
|
|
'page' => $page, |
|
289
|
|
|
'limit' => $limit |
|
290
|
|
|
]; |
|
291
|
|
|
$response = $this->client->get("webhooks", $queryString); |
|
292
|
|
|
return self::jsonDecode($response->body(), true); |
|
293
|
|
|
} |
|
294
|
|
|
|
|
295
|
|
|
/** |
|
296
|
|
|
* get an existing webhook by it's identifier |
|
297
|
|
|
* @param string $identifier a unique identifier associated with the webhook |
|
298
|
|
|
* @return array associative array containing the response |
|
299
|
|
|
*/ |
|
300
|
|
|
public function getWebhook($identifier) { |
|
301
|
|
|
$response = $this->client->get("webhook/".$identifier); |
|
302
|
|
|
return self::jsonDecode($response->body(), true); |
|
303
|
|
|
} |
|
304
|
|
|
|
|
305
|
|
|
/** |
|
306
|
|
|
* create a new webhook |
|
307
|
|
|
* @param string $url the url to receive the webhook events |
|
308
|
|
|
* @param string $identifier a unique identifier to associate with this webhook |
|
309
|
|
|
* @return array associative array containing the response |
|
310
|
|
|
*/ |
|
311
|
|
|
public function setupWebhook($url, $identifier = null) { |
|
312
|
|
|
$postData = [ |
|
313
|
|
|
'url' => $url, |
|
314
|
|
|
'identifier' => $identifier |
|
315
|
|
|
]; |
|
316
|
|
|
$response = $this->client->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG); |
|
317
|
|
|
return self::jsonDecode($response->body(), true); |
|
318
|
|
|
} |
|
319
|
|
|
|
|
320
|
|
|
/** |
|
321
|
|
|
* update an existing webhook |
|
322
|
|
|
* @param string $identifier the unique identifier of the webhook to update |
|
323
|
|
|
* @param string $newUrl the new url to receive the webhook events |
|
324
|
|
|
* @param string $newIdentifier a new unique identifier to associate with this webhook |
|
325
|
|
|
* @return array associative array containing the response |
|
326
|
|
|
*/ |
|
327
|
|
|
public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) { |
|
328
|
|
|
$putData = [ |
|
329
|
|
|
'url' => $newUrl, |
|
330
|
|
|
'identifier' => $newIdentifier |
|
331
|
|
|
]; |
|
332
|
|
|
$response = $this->client->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG); |
|
333
|
|
|
return self::jsonDecode($response->body(), true); |
|
334
|
|
|
} |
|
335
|
|
|
|
|
336
|
|
|
/** |
|
337
|
|
|
* deletes an existing webhook and any event subscriptions associated with it |
|
338
|
|
|
* @param string $identifier the unique identifier of the webhook to delete |
|
339
|
|
|
* @return boolean true on success |
|
340
|
|
|
*/ |
|
341
|
|
|
public function deleteWebhook($identifier) { |
|
342
|
|
|
$response = $this->client->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG); |
|
343
|
|
|
return self::jsonDecode($response->body(), true); |
|
344
|
|
|
} |
|
345
|
|
|
|
|
346
|
|
|
/** |
|
347
|
|
|
* get a paginated list of all the events a webhook is subscribed to |
|
348
|
|
|
* @param string $identifier the unique identifier of the webhook |
|
349
|
|
|
* @param integer $page pagination: page number |
|
350
|
|
|
* @param integer $limit pagination: records per page |
|
351
|
|
|
* @return array associative array containing the response |
|
352
|
|
|
*/ |
|
353
|
|
|
public function getWebhookEvents($identifier, $page = 1, $limit = 20) { |
|
354
|
|
|
$queryString = [ |
|
355
|
|
|
'page' => $page, |
|
356
|
|
|
'limit' => $limit |
|
357
|
|
|
]; |
|
358
|
|
|
$response = $this->client->get("webhook/{$identifier}/events", $queryString); |
|
359
|
|
|
return self::jsonDecode($response->body(), true); |
|
360
|
|
|
} |
|
361
|
|
|
|
|
362
|
|
|
/** |
|
363
|
|
|
* subscribes a webhook to transaction events of one particular transaction |
|
364
|
|
|
* @param string $identifier the unique identifier of the webhook to be triggered |
|
365
|
|
|
* @param string $transaction the transaction hash |
|
366
|
|
|
* @param integer $confirmations the amount of confirmations to send. |
|
367
|
|
|
* @return array associative array containing the response |
|
368
|
|
|
*/ |
|
369
|
|
View Code Duplication |
public function subscribeTransaction($identifier, $transaction, $confirmations = 6) { |
|
|
|
|
|
|
370
|
|
|
$postData = [ |
|
371
|
|
|
'event_type' => 'transaction', |
|
372
|
|
|
'transaction' => $transaction, |
|
373
|
|
|
'confirmations' => $confirmations, |
|
374
|
|
|
]; |
|
375
|
|
|
$response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG); |
|
376
|
|
|
return self::jsonDecode($response->body(), true); |
|
377
|
|
|
} |
|
378
|
|
|
|
|
379
|
|
|
/** |
|
380
|
|
|
* subscribes a webhook to transaction events on a particular address |
|
381
|
|
|
* @param string $identifier the unique identifier of the webhook to be triggered |
|
382
|
|
|
* @param string $address the address hash |
|
383
|
|
|
* @param integer $confirmations the amount of confirmations to send. |
|
384
|
|
|
* @return array associative array containing the response |
|
385
|
|
|
*/ |
|
386
|
|
View Code Duplication |
public function subscribeAddressTransactions($identifier, $address, $confirmations = 6) { |
|
|
|
|
|
|
387
|
|
|
$postData = [ |
|
388
|
|
|
'event_type' => 'address-transactions', |
|
389
|
|
|
'address' => $address, |
|
390
|
|
|
'confirmations' => $confirmations, |
|
391
|
|
|
]; |
|
392
|
|
|
$response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG); |
|
393
|
|
|
return self::jsonDecode($response->body(), true); |
|
394
|
|
|
} |
|
395
|
|
|
|
|
396
|
|
|
/** |
|
397
|
|
|
* batch subscribes a webhook to multiple transaction events |
|
398
|
|
|
* |
|
399
|
|
|
* @param string $identifier the unique identifier of the webhook |
|
400
|
|
|
* @param array $batchData A 2D array of event data: |
|
401
|
|
|
* [address => $address, confirmations => $confirmations] |
|
402
|
|
|
* where $address is the address to subscibe to |
|
403
|
|
|
* and optionally $confirmations is the amount of confirmations |
|
404
|
|
|
* @return boolean true on success |
|
405
|
|
|
*/ |
|
406
|
|
|
public function batchSubscribeAddressTransactions($identifier, $batchData) { |
|
407
|
|
|
$postData = []; |
|
408
|
|
|
foreach ($batchData as $record) { |
|
409
|
|
|
$postData[] = [ |
|
410
|
|
|
'event_type' => 'address-transactions', |
|
411
|
|
|
'address' => $record['address'], |
|
412
|
|
|
'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6, |
|
413
|
|
|
]; |
|
414
|
|
|
} |
|
415
|
|
|
$response = $this->client->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG); |
|
416
|
|
|
return self::jsonDecode($response->body(), true); |
|
417
|
|
|
} |
|
418
|
|
|
|
|
419
|
|
|
/** |
|
420
|
|
|
* subscribes a webhook to a new block event |
|
421
|
|
|
* @param string $identifier the unique identifier of the webhook to be triggered |
|
422
|
|
|
* @return array associative array containing the response |
|
423
|
|
|
*/ |
|
424
|
|
|
public function subscribeNewBlocks($identifier) { |
|
425
|
|
|
$postData = [ |
|
426
|
|
|
'event_type' => 'block', |
|
427
|
|
|
]; |
|
428
|
|
|
$response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG); |
|
429
|
|
|
return self::jsonDecode($response->body(), true); |
|
430
|
|
|
} |
|
431
|
|
|
|
|
432
|
|
|
/** |
|
433
|
|
|
* removes an transaction event subscription from a webhook |
|
434
|
|
|
* @param string $identifier the unique identifier of the webhook associated with the event subscription |
|
435
|
|
|
* @param string $transaction the transaction hash of the event subscription |
|
436
|
|
|
* @return boolean true on success |
|
437
|
|
|
*/ |
|
438
|
|
|
public function unsubscribeTransaction($identifier, $transaction) { |
|
439
|
|
|
$response = $this->client->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG); |
|
440
|
|
|
return self::jsonDecode($response->body(), true); |
|
441
|
|
|
} |
|
442
|
|
|
|
|
443
|
|
|
/** |
|
444
|
|
|
* removes an address transaction event subscription from a webhook |
|
445
|
|
|
* @param string $identifier the unique identifier of the webhook associated with the event subscription |
|
446
|
|
|
* @param string $address the address hash of the event subscription |
|
447
|
|
|
* @return boolean true on success |
|
448
|
|
|
*/ |
|
449
|
|
|
public function unsubscribeAddressTransactions($identifier, $address) { |
|
450
|
|
|
$response = $this->client->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG); |
|
451
|
|
|
return self::jsonDecode($response->body(), true); |
|
452
|
|
|
} |
|
453
|
|
|
|
|
454
|
|
|
/** |
|
455
|
|
|
* removes a block event subscription from a webhook |
|
456
|
|
|
* @param string $identifier the unique identifier of the webhook associated with the event subscription |
|
457
|
|
|
* @return boolean true on success |
|
458
|
|
|
*/ |
|
459
|
|
|
public function unsubscribeNewBlocks($identifier) { |
|
460
|
|
|
$response = $this->client->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG); |
|
461
|
|
|
return self::jsonDecode($response->body(), true); |
|
462
|
|
|
} |
|
463
|
|
|
|
|
464
|
|
|
/** |
|
465
|
|
|
* create a new wallet |
|
466
|
|
|
* - will generate a new primary seed (with password) and backup seed (without password) |
|
467
|
|
|
* - send the primary seed (BIP39 'encrypted') and backup public key to the server |
|
468
|
|
|
* - receive the blocktrail co-signing public key from the server |
|
469
|
|
|
* |
|
470
|
|
|
* Either takes one argument: |
|
471
|
|
|
* @param array $options |
|
472
|
|
|
* |
|
473
|
|
|
* Or takes three arguments (old, deprecated syntax): |
|
474
|
|
|
* (@nonPHP-doc) @param $identifier |
|
475
|
|
|
* (@nonPHP-doc) @param $password |
|
476
|
|
|
* (@nonPHP-doc) @param int $keyIndex override for the blocktrail cosigning key to use |
|
|
|
|
|
|
477
|
|
|
* |
|
478
|
|
|
* @return array[WalletInterface, (string)primaryMnemonic, (string)backupMnemonic] |
|
|
|
|
|
|
479
|
|
|
* @throws \Exception |
|
480
|
|
|
*/ |
|
481
|
|
|
public function createNewWallet($options) { |
|
482
|
|
|
if (!is_array($options)) { |
|
483
|
|
|
$args = func_get_args(); |
|
484
|
|
|
$options = [ |
|
485
|
|
|
"identifier" => $args[0], |
|
486
|
|
|
"password" => $args[1], |
|
487
|
|
|
"key_index" => isset($args[2]) ? $args[2] : null, |
|
488
|
|
|
]; |
|
489
|
|
|
} |
|
490
|
|
|
|
|
491
|
|
|
$identifier = $options['identifier']; |
|
492
|
|
|
$password = isset($options['passphrase']) ? $options['passphrase'] : (isset($options['password']) ? $options['password'] : null); |
|
493
|
|
|
$keyIndex = isset($options['key_index']) ? $options['key_index'] : 0; |
|
494
|
|
|
|
|
495
|
|
|
$walletPath = WalletPath::create($keyIndex); |
|
496
|
|
|
|
|
497
|
|
|
$storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null; |
|
498
|
|
|
|
|
499
|
|
|
if (isset($options['primary_mnemonic']) && $options['primary_private_key']) { |
|
500
|
|
|
throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey"); |
|
501
|
|
|
} |
|
502
|
|
|
|
|
503
|
|
|
$primaryMnemonic = null; |
|
504
|
|
|
$primaryPrivateKey = null; |
|
505
|
|
|
if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) { |
|
506
|
|
|
if (!$password) { |
|
507
|
|
|
throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase"); |
|
508
|
|
|
} else { |
|
509
|
|
|
// create new primary seed |
|
510
|
|
|
list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->newPrimarySeed($password); |
|
|
|
|
|
|
511
|
|
|
if ($storePrimaryMnemonic !== false) { |
|
512
|
|
|
$storePrimaryMnemonic = true; |
|
513
|
|
|
} |
|
514
|
|
|
} |
|
515
|
|
|
} else if (isset($options['primary_mnemonic'])) { |
|
516
|
|
|
$primaryMnemonic = $options['primary_mnemonic']; |
|
517
|
|
|
} else if (isset($options['primary_private_key'])) { |
|
518
|
|
|
$primaryPrivateKey = $options['primary_private_key']; |
|
519
|
|
|
} |
|
520
|
|
|
|
|
521
|
|
|
if ($storePrimaryMnemonic && $primaryMnemonic && !$password) { |
|
522
|
|
|
throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase"); |
|
523
|
|
|
} |
|
524
|
|
|
|
|
525
|
|
View Code Duplication |
if ($primaryPrivateKey) { |
|
|
|
|
|
|
526
|
|
|
if (is_string($primaryPrivateKey)) { |
|
527
|
|
|
$primaryPrivateKey = [$primaryPrivateKey, "m"]; |
|
528
|
|
|
} |
|
529
|
|
|
} else { |
|
530
|
|
|
$primaryPrivateKey = BIP32::master_key(BIP39::mnemonicToSeedHex($primaryMnemonic, $password), 'bitcoin', $this->testnet); |
|
|
|
|
|
|
531
|
|
|
} |
|
532
|
|
|
|
|
533
|
|
|
if (!$storePrimaryMnemonic) { |
|
534
|
|
|
$primaryMnemonic = false; |
|
535
|
|
|
} |
|
536
|
|
|
|
|
537
|
|
|
// create primary public key from the created private key |
|
538
|
|
|
$primaryPublicKey = BIP32::build_key($primaryPrivateKey, (string)$walletPath->keyIndexPath()->publicPath()); |
|
539
|
|
|
|
|
540
|
|
|
if (isset($options['backup_mnemonic']) && $options['backup_public_key']) { |
|
541
|
|
|
throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey"); |
|
542
|
|
|
} |
|
543
|
|
|
|
|
544
|
|
|
$backupMnemonic = null; |
|
545
|
|
|
$backupPublicKey = null; |
|
546
|
|
|
if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) { |
|
547
|
|
|
list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->newBackupSeed(); |
|
|
|
|
|
|
548
|
|
|
} else if (isset($options['backup_mnemonic'])) { |
|
549
|
|
|
$backupMnemonic = $options['backup_mnemonic']; |
|
550
|
|
|
} else if (isset($options['backup_public_key'])) { |
|
551
|
|
|
$backupPublicKey = $options['backup_public_key']; |
|
552
|
|
|
} |
|
553
|
|
|
|
|
554
|
|
View Code Duplication |
if ($backupPublicKey) { |
|
|
|
|
|
|
555
|
|
|
if (is_string($backupPublicKey)) { |
|
556
|
|
|
$backupPublicKey = [$backupPublicKey, "m"]; |
|
557
|
|
|
} |
|
558
|
|
|
} else { |
|
559
|
|
|
$backupPublicKey = BIP32::extended_private_to_public(BIP32::master_key(BIP39::mnemonicToSeedHex($backupMnemonic, ""), 'bitcoin', $this->testnet)); |
|
|
|
|
|
|
560
|
|
|
} |
|
561
|
|
|
|
|
562
|
|
|
// create a checksum of our private key which we'll later use to verify we used the right password |
|
563
|
|
|
$checksum = BIP32::key_to_address($primaryPrivateKey[0]); |
|
564
|
|
|
|
|
565
|
|
|
// send the public keys to the server to store them |
|
566
|
|
|
// and the mnemonic, which is safe because it's useless without the password |
|
567
|
|
|
$data = $this->_createNewWallet($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex); |
|
|
|
|
|
|
568
|
|
|
// received the blocktrail public keys |
|
569
|
|
|
$blocktrailPublicKeys = $data['blocktrail_public_keys']; |
|
570
|
|
|
|
|
571
|
|
|
$wallet = new Wallet($this, $identifier, $primaryMnemonic, [$keyIndex => $primaryPublicKey], $backupPublicKey, $blocktrailPublicKeys, $keyIndex, $this->network, $this->testnet, $checksum); |
|
572
|
|
|
|
|
573
|
|
|
$wallet->unlock($options); |
|
574
|
|
|
|
|
575
|
|
|
// return wallet and backup mnemonic |
|
576
|
|
|
return [ |
|
577
|
|
|
$wallet, |
|
578
|
|
|
$primaryMnemonic, |
|
579
|
|
|
$backupMnemonic, |
|
580
|
|
|
$blocktrailPublicKeys |
|
581
|
|
|
]; |
|
582
|
|
|
} |
|
583
|
|
|
|
|
584
|
|
|
/** |
|
585
|
|
|
* create wallet using the API |
|
586
|
|
|
* |
|
587
|
|
|
* @param string $identifier the wallet identifier to create |
|
588
|
|
|
* @param array $primaryPublicKey BIP32 extended public key - [key, path] |
|
589
|
|
|
* @param string $backupPublicKey plain public key |
|
590
|
|
|
* @param string $primaryMnemonic mnemonic to store |
|
591
|
|
|
* @param string $checksum checksum to store |
|
592
|
|
|
* @param int $keyIndex account that we expect to use |
|
593
|
|
|
* @return mixed |
|
594
|
|
|
*/ |
|
595
|
|
|
public function _createNewWallet($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex) { |
|
596
|
|
|
$data = [ |
|
597
|
|
|
'identifier' => $identifier, |
|
598
|
|
|
'primary_public_key' => $primaryPublicKey, |
|
599
|
|
|
'backup_public_key' => $backupPublicKey, |
|
600
|
|
|
'primary_mnemonic' => $primaryMnemonic, |
|
601
|
|
|
'checksum' => $checksum, |
|
602
|
|
|
'key_index' => $keyIndex |
|
603
|
|
|
]; |
|
604
|
|
|
|
|
605
|
|
|
$response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG); |
|
606
|
|
|
return self::jsonDecode($response->body(), true); |
|
607
|
|
|
} |
|
608
|
|
|
|
|
609
|
|
|
/** |
|
610
|
|
|
* upgrade wallet to use a new account number |
|
611
|
|
|
* the account number specifies which blocktrail cosigning key is used |
|
612
|
|
|
* |
|
613
|
|
|
* @param string $identifier the wallet identifier to be upgraded |
|
614
|
|
|
* @param int $keyIndex the new account to use |
|
615
|
|
|
* @param array $primaryPublicKey BIP32 extended public key - [key, path] |
|
616
|
|
|
* @return mixed |
|
617
|
|
|
*/ |
|
618
|
|
|
public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) { |
|
619
|
|
|
$data = [ |
|
620
|
|
|
'key_index' => $keyIndex, |
|
621
|
|
|
'primary_public_key' => $primaryPublicKey |
|
622
|
|
|
]; |
|
623
|
|
|
|
|
624
|
|
|
$response = $this->client->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG); |
|
625
|
|
|
return self::jsonDecode($response->body(), true); |
|
626
|
|
|
} |
|
627
|
|
|
|
|
628
|
|
|
/** |
|
629
|
|
|
* initialize a previously created wallet |
|
630
|
|
|
* |
|
631
|
|
|
* Either takes one argument: |
|
632
|
|
|
* @param array $options |
|
633
|
|
|
* |
|
634
|
|
|
* Or takes two arguments (old, deprecated syntax): |
|
635
|
|
|
* (@nonPHP-doc) @param string $identifier the wallet identifier to be initialized |
|
|
|
|
|
|
636
|
|
|
* (@nonPHP-doc) @param string $password the password to decrypt the mnemonic with |
|
|
|
|
|
|
637
|
|
|
* |
|
638
|
|
|
* @return WalletInterface |
|
639
|
|
|
* @throws \Exception |
|
640
|
|
|
*/ |
|
641
|
|
|
public function initWallet($options) { |
|
642
|
|
|
if (!is_array($options)) { |
|
643
|
|
|
$args = func_get_args(); |
|
644
|
|
|
$options = [ |
|
645
|
|
|
"identifier" => $args[0], |
|
646
|
|
|
"password" => $args[1], |
|
647
|
|
|
]; |
|
648
|
|
|
} |
|
649
|
|
|
|
|
650
|
|
|
$identifier = $options['identifier']; |
|
651
|
|
|
$readonly = isset($options['readonly']) ? $options['readonly'] : |
|
652
|
|
|
(isset($options['readOnly']) ? $options['readOnly'] : |
|
653
|
|
|
(isset($options['read-only']) ? $options['read-only'] : |
|
654
|
|
|
false)); |
|
655
|
|
|
|
|
656
|
|
|
// get the wallet data from the server |
|
657
|
|
|
$data = $this->getWallet($identifier); |
|
658
|
|
|
|
|
659
|
|
|
if (!$data) { |
|
660
|
|
|
throw new \Exception("Failed to get wallet"); |
|
661
|
|
|
} |
|
662
|
|
|
|
|
663
|
|
|
// explode the wallet data |
|
664
|
|
|
$primaryMnemonic = isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic']; |
|
665
|
|
|
$checksum = $data['checksum']; |
|
666
|
|
|
$backupPublicKey = $data['backup_public_key']; |
|
667
|
|
|
$primaryPublicKeys = $data['primary_public_keys']; |
|
668
|
|
|
$blocktrailPublicKeys = $data['blocktrail_public_keys']; |
|
669
|
|
|
$keyIndex = isset($options['key_index']) ? $options['key_index'] : $data['key_index']; |
|
670
|
|
|
|
|
671
|
|
|
$wallet = new Wallet($this, $identifier, $primaryMnemonic, $primaryPublicKeys, $backupPublicKey, $blocktrailPublicKeys, $keyIndex, $this->network, $this->testnet, $checksum); |
|
672
|
|
|
|
|
673
|
|
|
if (!$readonly) { |
|
674
|
|
|
$wallet->unlock($options); |
|
675
|
|
|
} |
|
676
|
|
|
|
|
677
|
|
|
return $wallet; |
|
678
|
|
|
} |
|
679
|
|
|
|
|
680
|
|
|
/** |
|
681
|
|
|
* get the wallet data from the server |
|
682
|
|
|
* |
|
683
|
|
|
* @param string $identifier the identifier of the wallet |
|
684
|
|
|
* @return mixed |
|
685
|
|
|
*/ |
|
686
|
|
|
public function getWallet($identifier) { |
|
687
|
|
|
$response = $this->client->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG); |
|
688
|
|
|
return self::jsonDecode($response->body(), true); |
|
689
|
|
|
} |
|
690
|
|
|
|
|
691
|
|
|
/** |
|
692
|
|
|
* delete a wallet from the server |
|
693
|
|
|
* the checksum address and a signature to verify you ownership of the key of that checksum address |
|
694
|
|
|
* is required to be able to delete a wallet |
|
695
|
|
|
* |
|
696
|
|
|
* @param string $identifier the identifier of the wallet |
|
697
|
|
|
* @param string $checksumAddress the address for your master private key (and the checksum used when creating the wallet) |
|
698
|
|
|
* @param string $signature a signature of the checksum address as message signed by the private key matching that address |
|
699
|
|
|
* @param bool $force ignore warnings (such as a non-zero balance) |
|
700
|
|
|
* @return mixed |
|
701
|
|
|
*/ |
|
702
|
|
|
public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) { |
|
703
|
|
|
$response = $this->client->delete("wallet/{$identifier}", ['force' => $force], [ |
|
704
|
|
|
'checksum' => $checksumAddress, |
|
705
|
|
|
'signature' => $signature |
|
706
|
|
|
], RestClient::AUTH_HTTP_SIG, 360); |
|
707
|
|
|
return self::jsonDecode($response->body(), true); |
|
708
|
|
|
} |
|
709
|
|
|
|
|
710
|
|
|
/** |
|
711
|
|
|
* create new backup key; |
|
712
|
|
|
* 1) a BIP39 mnemonic |
|
713
|
|
|
* 2) a seed from that mnemonic with a blank password |
|
714
|
|
|
* 3) a private key from that seed |
|
715
|
|
|
* |
|
716
|
|
|
* @return array [mnemonic, seed, key] |
|
717
|
|
|
*/ |
|
718
|
|
|
protected function newBackupSeed() { |
|
719
|
|
|
list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed(""); |
|
720
|
|
|
|
|
721
|
|
|
return [$backupMnemonic, $backupSeed, $backupPrivateKey]; |
|
722
|
|
|
} |
|
723
|
|
|
|
|
724
|
|
|
/** |
|
725
|
|
|
* create new primary key; |
|
726
|
|
|
* 1) a BIP39 mnemonic |
|
727
|
|
|
* 2) a seed from that mnemonic with the password |
|
728
|
|
|
* 3) a private key from that seed |
|
729
|
|
|
* |
|
730
|
|
|
* @param string $passphrase the password to use in the BIP39 creation of the seed |
|
731
|
|
|
* @return array [mnemonic, seed, key] |
|
732
|
|
|
* @TODO: require a strong password? |
|
733
|
|
|
*/ |
|
734
|
|
|
protected function newPrimarySeed($passphrase) { |
|
735
|
|
|
list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase); |
|
736
|
|
|
|
|
737
|
|
|
return [$primaryMnemonic, $primarySeed, $primaryPrivateKey]; |
|
738
|
|
|
} |
|
739
|
|
|
|
|
740
|
|
|
/** |
|
741
|
|
|
* create a new key; |
|
742
|
|
|
* 1) a BIP39 mnemonic |
|
743
|
|
|
* 2) a seed from that mnemonic with the password |
|
744
|
|
|
* 3) a private key from that seed |
|
745
|
|
|
* |
|
746
|
|
|
* @param string $passphrase the password to use in the BIP39 creation of the seed |
|
747
|
|
|
* @param string $forceEntropy forced entropy instead of random entropy for testing purposes |
|
748
|
|
|
* @return array |
|
749
|
|
|
*/ |
|
750
|
|
|
protected function generateNewSeed($passphrase = "", $forceEntropy = null) { |
|
751
|
|
|
// generate master seed, retry if the generated private key isn't valid (FALSE is returned) |
|
752
|
|
|
do { |
|
753
|
|
|
$mnemonic = $this->generateNewMnemonic($forceEntropy); |
|
754
|
|
|
|
|
755
|
|
|
$seed = BIP39::mnemonicToSeedHex($mnemonic, $passphrase); |
|
756
|
|
|
|
|
757
|
|
|
$key = BIP32::master_key($seed, $this->network, $this->testnet); |
|
|
|
|
|
|
758
|
|
|
} while (!$key); |
|
|
|
|
|
|
759
|
|
|
|
|
760
|
|
|
return [$mnemonic, $seed, $key]; |
|
761
|
|
|
} |
|
762
|
|
|
|
|
763
|
|
|
/** |
|
764
|
|
|
* generate a new mnemonic from some random entropy (512 bit) |
|
765
|
|
|
* |
|
766
|
|
|
* @param string $forceEntropy forced entropy instead of random entropy for testing purposes |
|
767
|
|
|
* @return string |
|
768
|
|
|
* @throws \Exception |
|
769
|
|
|
*/ |
|
770
|
|
|
protected function generateNewMnemonic($forceEntropy = null) { |
|
771
|
|
|
if ($forceEntropy === null) { |
|
772
|
|
|
$entropy = BIP39::generateEntropy(512); |
|
773
|
|
|
} else { |
|
774
|
|
|
$entropy = $forceEntropy; |
|
775
|
|
|
} |
|
776
|
|
|
|
|
777
|
|
|
return BIP39::entropyToMnemonic($entropy); |
|
778
|
|
|
} |
|
779
|
|
|
|
|
780
|
|
|
/** |
|
781
|
|
|
* get the balance for the wallet |
|
782
|
|
|
* |
|
783
|
|
|
* @param string $identifier the identifier of the wallet |
|
784
|
|
|
* @return array |
|
785
|
|
|
*/ |
|
786
|
|
|
public function getWalletBalance($identifier) { |
|
787
|
|
|
$response = $this->client->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG); |
|
788
|
|
|
return self::jsonDecode($response->body(), true); |
|
789
|
|
|
} |
|
790
|
|
|
|
|
791
|
|
|
/** |
|
792
|
|
|
* do HD wallet discovery for the wallet |
|
793
|
|
|
* |
|
794
|
|
|
* this can be REALLY slow, so we've set the timeout to 120s ... |
|
795
|
|
|
* |
|
796
|
|
|
* @param string $identifier the identifier of the wallet |
|
797
|
|
|
* @param int $gap the gap setting to use for discovery |
|
798
|
|
|
* @return mixed |
|
799
|
|
|
*/ |
|
800
|
|
|
public function doWalletDiscovery($identifier, $gap = 200) { |
|
801
|
|
|
$response = $this->client->get("wallet/{$identifier}/discovery", ['gap' => $gap], RestClient::AUTH_HTTP_SIG, 360.0); |
|
802
|
|
|
return self::jsonDecode($response->body(), true); |
|
803
|
|
|
} |
|
804
|
|
|
|
|
805
|
|
|
/** |
|
806
|
|
|
* get a new derivation number for specified parent path |
|
807
|
|
|
* eg; m/44'/1'/0/0 results in m/44'/1'/0/0/0 and next time in m/44'/1'/0/0/1 and next time in m/44'/1'/0/0/2 |
|
808
|
|
|
* |
|
809
|
|
|
* returns the path |
|
810
|
|
|
* |
|
811
|
|
|
* @param string $identifier the identifier of the wallet |
|
812
|
|
|
* @param string $path the parent path for which to get a new derivation |
|
813
|
|
|
* @return string |
|
814
|
|
|
*/ |
|
815
|
|
|
public function getNewDerivation($identifier, $path) { |
|
816
|
|
|
$result = $this->_getNewDerivation($identifier, $path); |
|
817
|
|
|
return $result['path']; |
|
818
|
|
|
} |
|
819
|
|
|
|
|
820
|
|
|
/** |
|
821
|
|
|
* get a new derivation number for specified parent path |
|
822
|
|
|
* eg; m/44'/1'/0/0 results in m/44'/1'/0/0/0 and next time in m/44'/1'/0/0/1 and next time in m/44'/1'/0/0/2 |
|
823
|
|
|
* |
|
824
|
|
|
* @param string $identifier the identifier of the wallet |
|
825
|
|
|
* @param string $path the parent path for which to get a new derivation |
|
826
|
|
|
* @return mixed |
|
827
|
|
|
*/ |
|
828
|
|
|
public function _getNewDerivation($identifier, $path) { |
|
829
|
|
|
$response = $this->client->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG); |
|
830
|
|
|
return self::jsonDecode($response->body(), true); |
|
831
|
|
|
} |
|
832
|
|
|
|
|
833
|
|
|
/** |
|
834
|
|
|
* get the path (and redeemScript) to specified address |
|
835
|
|
|
* |
|
836
|
|
|
* @param string $identifier |
|
837
|
|
|
* @param string $address |
|
838
|
|
|
* @return array |
|
839
|
|
|
* @throws \Exception |
|
840
|
|
|
*/ |
|
841
|
|
|
public function getPathForAddress($identifier, $address) { |
|
842
|
|
|
$response = $this->client->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG); |
|
843
|
|
|
return self::jsonDecode($response->body(), true)['path']; |
|
844
|
|
|
} |
|
845
|
|
|
|
|
846
|
|
|
/** |
|
847
|
|
|
* send the transaction using the API |
|
848
|
|
|
* |
|
849
|
|
|
* @param string $identifier the identifier of the wallet |
|
850
|
|
|
* @param string $rawTransaction raw hex of the transaction (should be partially signed) |
|
851
|
|
|
* @param array $paths list of the paths that were used for the UTXO |
|
852
|
|
|
* @param bool $checkFee let the server verify the fee after signing |
|
853
|
|
|
* @return string the complete raw transaction |
|
854
|
|
|
* @throws \Exception |
|
855
|
|
|
*/ |
|
856
|
|
|
public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false) { |
|
857
|
|
|
$data = [ |
|
858
|
|
|
'raw_transaction' => $rawTransaction, |
|
859
|
|
|
'paths' => $paths |
|
860
|
|
|
]; |
|
861
|
|
|
|
|
862
|
|
|
// dynamic TTL for when we're signing really big transactions |
|
863
|
|
|
$ttl = max(5.0, count($paths) * 0.25) + 4.0; |
|
864
|
|
|
|
|
865
|
|
|
$response = $this->client->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl); |
|
866
|
|
|
$signed = self::jsonDecode($response->body(), true); |
|
867
|
|
|
|
|
868
|
|
|
if (!$signed['complete'] || $signed['complete'] == 'false') { |
|
869
|
|
|
throw new \Exception("Failed to completely sign transaction"); |
|
870
|
|
|
} |
|
871
|
|
|
|
|
872
|
|
|
// create TX hash from the raw signed hex |
|
873
|
|
|
return RawTransaction::txid_from_raw($signed['hex']); |
|
874
|
|
|
} |
|
875
|
|
|
|
|
876
|
|
|
/** |
|
877
|
|
|
* use the API to get the best inputs to use based on the outputs |
|
878
|
|
|
* |
|
879
|
|
|
* the return array has the following format: |
|
880
|
|
|
* [ |
|
881
|
|
|
* "utxos" => [ |
|
882
|
|
|
* [ |
|
883
|
|
|
* "hash" => "<txHash>", |
|
884
|
|
|
* "idx" => "<index of the output of that <txHash>", |
|
885
|
|
|
* "scriptpubkey_hex" => "<scriptPubKey-hex>", |
|
886
|
|
|
* "value" => 32746327, |
|
887
|
|
|
* "address" => "1address", |
|
888
|
|
|
* "path" => "m/44'/1'/0'/0/13", |
|
889
|
|
|
* "redeem_script" => "<redeemScript-hex>", |
|
890
|
|
|
* ], |
|
891
|
|
|
* ], |
|
892
|
|
|
* "fee" => 10000, |
|
893
|
|
|
* "change"=> 1010109201, |
|
894
|
|
|
* ] |
|
895
|
|
|
* |
|
896
|
|
|
* @param string $identifier the identifier of the wallet |
|
897
|
|
|
* @param array $outputs the outputs you want to create - array[address => satoshi-value] |
|
898
|
|
|
* @param bool $lockUTXO when TRUE the UTXOs selected will be locked for a few seconds |
|
899
|
|
|
* so you have some time to spend them without race-conditions |
|
900
|
|
|
* @param bool $allowZeroConf |
|
901
|
|
|
* @param string $feeStrategy |
|
902
|
|
|
* @param null|int $forceFee |
|
903
|
|
|
* @return array |
|
904
|
|
|
* @throws \Exception |
|
905
|
|
|
*/ |
|
906
|
|
View Code Duplication |
public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null) { |
|
|
|
|
|
|
907
|
|
|
$args = [ |
|
908
|
|
|
'lock' => (int)!!$lockUTXO, |
|
909
|
|
|
'zeroconf' => (int)!!$allowZeroConf, |
|
910
|
|
|
'fee_strategy' => $feeStrategy, |
|
911
|
|
|
]; |
|
912
|
|
|
|
|
913
|
|
|
if ($forceFee !== null) { |
|
914
|
|
|
$args['forcefee'] = (int)$forceFee; |
|
915
|
|
|
} |
|
916
|
|
|
|
|
917
|
|
|
$response = $this->client->post( |
|
918
|
|
|
"wallet/{$identifier}/coin-selection", |
|
919
|
|
|
$args, |
|
920
|
|
|
$outputs, |
|
921
|
|
|
RestClient::AUTH_HTTP_SIG |
|
922
|
|
|
); |
|
923
|
|
|
|
|
924
|
|
|
return self::jsonDecode($response->body(), true); |
|
925
|
|
|
} |
|
926
|
|
|
|
|
927
|
|
|
/** |
|
928
|
|
|
* |
|
929
|
|
|
* @param string $identifier the identifier of the wallet |
|
930
|
|
|
* @param bool $allowZeroConf |
|
931
|
|
|
* @param string $feeStrategy |
|
932
|
|
|
* @param null|int $forceFee |
|
933
|
|
|
* @param int $outputCnt |
|
934
|
|
|
* @return array |
|
935
|
|
|
* @throws \Exception |
|
936
|
|
|
*/ |
|
937
|
|
View Code Duplication |
public function walletMaxSpendable($identifier, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) { |
|
|
|
|
|
|
938
|
|
|
$args = [ |
|
939
|
|
|
'zeroconf' => (int)!!$allowZeroConf, |
|
940
|
|
|
'fee_strategy' => $feeStrategy, |
|
941
|
|
|
'outputs' => $outputCnt, |
|
942
|
|
|
]; |
|
943
|
|
|
|
|
944
|
|
|
if ($forceFee !== null) { |
|
945
|
|
|
$args['forcefee'] = (int)$forceFee; |
|
946
|
|
|
} |
|
947
|
|
|
|
|
948
|
|
|
$response = $this->client->get( |
|
949
|
|
|
"wallet/{$identifier}/max-spendable", |
|
950
|
|
|
$args, |
|
951
|
|
|
RestClient::AUTH_HTTP_SIG |
|
952
|
|
|
); |
|
953
|
|
|
|
|
954
|
|
|
return self::jsonDecode($response->body(), true); |
|
955
|
|
|
} |
|
956
|
|
|
|
|
957
|
|
|
/** |
|
958
|
|
|
* @return array ['optimal_fee' => 10000, 'low_priority_fee' => 5000] |
|
959
|
|
|
*/ |
|
960
|
|
|
public function feePerKB() { |
|
961
|
|
|
$response = $this->client->get("fee-per-kb"); |
|
962
|
|
|
return self::jsonDecode($response->body(), true); |
|
963
|
|
|
} |
|
964
|
|
|
|
|
965
|
|
|
/** |
|
966
|
|
|
* get the current price index |
|
967
|
|
|
* |
|
968
|
|
|
* @return array eg; ['USD' => 287.30] |
|
969
|
|
|
*/ |
|
970
|
|
|
public function price() { |
|
971
|
|
|
$response = $this->client->get("price"); |
|
972
|
|
|
return self::jsonDecode($response->body(), true); |
|
973
|
|
|
} |
|
974
|
|
|
|
|
975
|
|
|
/** |
|
976
|
|
|
* setup webhook for wallet |
|
977
|
|
|
* |
|
978
|
|
|
* @param string $identifier the wallet identifier for which to create the webhook |
|
979
|
|
|
* @param string $webhookIdentifier the webhook identifier to use |
|
980
|
|
|
* @param string $url the url to receive the webhook events |
|
981
|
|
|
* @return array |
|
982
|
|
|
*/ |
|
983
|
|
|
public function setupWalletWebhook($identifier, $webhookIdentifier, $url) { |
|
984
|
|
|
$response = $this->client->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG); |
|
985
|
|
|
return self::jsonDecode($response->body(), true); |
|
986
|
|
|
} |
|
987
|
|
|
|
|
988
|
|
|
/** |
|
989
|
|
|
* delete webhook for wallet |
|
990
|
|
|
* |
|
991
|
|
|
* @param string $identifier the wallet identifier for which to delete the webhook |
|
992
|
|
|
* @param string $webhookIdentifier the webhook identifier to delete |
|
993
|
|
|
* @return array |
|
994
|
|
|
*/ |
|
995
|
|
|
public function deleteWalletWebhook($identifier, $webhookIdentifier) { |
|
996
|
|
|
$response = $this->client->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG); |
|
997
|
|
|
return self::jsonDecode($response->body(), true); |
|
998
|
|
|
} |
|
999
|
|
|
|
|
1000
|
|
|
/** |
|
1001
|
|
|
* lock a specific unspent output |
|
1002
|
|
|
* |
|
1003
|
|
|
* @param $identifier |
|
1004
|
|
|
* @param $txHash |
|
1005
|
|
|
* @param $txIdx |
|
1006
|
|
|
* @param int $ttl |
|
1007
|
|
|
* @return bool |
|
1008
|
|
|
*/ |
|
1009
|
|
|
public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) { |
|
1010
|
|
|
$response = $this->client->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG); |
|
1011
|
|
|
return self::jsonDecode($response->body(), true)['locked']; |
|
1012
|
|
|
} |
|
1013
|
|
|
|
|
1014
|
|
|
/** |
|
1015
|
|
|
* unlock a specific unspent output |
|
1016
|
|
|
* |
|
1017
|
|
|
* @param $identifier |
|
1018
|
|
|
* @param $txHash |
|
1019
|
|
|
* @param $txIdx |
|
1020
|
|
|
* @return bool |
|
1021
|
|
|
*/ |
|
1022
|
|
|
public function unlockWalletUTXO($identifier, $txHash, $txIdx) { |
|
1023
|
|
|
$response = $this->client->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG); |
|
1024
|
|
|
return self::jsonDecode($response->body(), true)['unlocked']; |
|
1025
|
|
|
} |
|
1026
|
|
|
|
|
1027
|
|
|
/** |
|
1028
|
|
|
* get all transactions for wallet (paginated) |
|
1029
|
|
|
* |
|
1030
|
|
|
* @param string $identifier the wallet identifier for which to get transactions |
|
1031
|
|
|
* @param integer $page pagination: page number |
|
1032
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
1033
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
1034
|
|
|
* @return array associative array containing the response |
|
1035
|
|
|
*/ |
|
1036
|
|
View Code Duplication |
public function walletTransactions($identifier, $page = 1, $limit = 20, $sortDir = 'asc') { |
|
|
|
|
|
|
1037
|
|
|
$queryString = [ |
|
1038
|
|
|
'page' => $page, |
|
1039
|
|
|
'limit' => $limit, |
|
1040
|
|
|
'sort_dir' => $sortDir |
|
1041
|
|
|
]; |
|
1042
|
|
|
$response = $this->client->get("wallet/{$identifier}/transactions", $queryString, RestClient::AUTH_HTTP_SIG); |
|
1043
|
|
|
return self::jsonDecode($response->body(), true); |
|
1044
|
|
|
} |
|
1045
|
|
|
|
|
1046
|
|
|
/** |
|
1047
|
|
|
* get all addresses for wallet (paginated) |
|
1048
|
|
|
* |
|
1049
|
|
|
* @param string $identifier the wallet identifier for which to get addresses |
|
1050
|
|
|
* @param integer $page pagination: page number |
|
1051
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
1052
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
1053
|
|
|
* @return array associative array containing the response |
|
1054
|
|
|
*/ |
|
1055
|
|
View Code Duplication |
public function walletAddresses($identifier, $page = 1, $limit = 20, $sortDir = 'asc') { |
|
|
|
|
|
|
1056
|
|
|
$queryString = [ |
|
1057
|
|
|
'page' => $page, |
|
1058
|
|
|
'limit' => $limit, |
|
1059
|
|
|
'sort_dir' => $sortDir |
|
1060
|
|
|
]; |
|
1061
|
|
|
$response = $this->client->get("wallet/{$identifier}/addresses", $queryString, RestClient::AUTH_HTTP_SIG); |
|
1062
|
|
|
return self::jsonDecode($response->body(), true); |
|
1063
|
|
|
} |
|
1064
|
|
|
|
|
1065
|
|
|
/** |
|
1066
|
|
|
* get all UTXOs for wallet (paginated) |
|
1067
|
|
|
* |
|
1068
|
|
|
* @param string $identifier the wallet identifier for which to get addresses |
|
1069
|
|
|
* @param integer $page pagination: page number |
|
1070
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
1071
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
1072
|
|
|
* @return array associative array containing the response |
|
1073
|
|
|
*/ |
|
1074
|
|
View Code Duplication |
public function walletUTXOs($identifier, $page = 1, $limit = 20, $sortDir = 'asc') { |
|
|
|
|
|
|
1075
|
|
|
$queryString = [ |
|
1076
|
|
|
'page' => $page, |
|
1077
|
|
|
'limit' => $limit, |
|
1078
|
|
|
'sort_dir' => $sortDir |
|
1079
|
|
|
]; |
|
1080
|
|
|
$response = $this->client->get("wallet/{$identifier}/utxos", $queryString, RestClient::AUTH_HTTP_SIG); |
|
1081
|
|
|
return self::jsonDecode($response->body(), true); |
|
1082
|
|
|
} |
|
1083
|
|
|
|
|
1084
|
|
|
/** |
|
1085
|
|
|
* get a paginated list of all wallets associated with the api user |
|
1086
|
|
|
* |
|
1087
|
|
|
* @param integer $page pagination: page number |
|
1088
|
|
|
* @param integer $limit pagination: records per page |
|
1089
|
|
|
* @return array associative array containing the response |
|
1090
|
|
|
*/ |
|
1091
|
|
|
public function allWallets($page = 1, $limit = 20) { |
|
1092
|
|
|
$queryString = [ |
|
1093
|
|
|
'page' => $page, |
|
1094
|
|
|
'limit' => $limit |
|
1095
|
|
|
]; |
|
1096
|
|
|
$response = $this->client->get("wallets", $queryString, RestClient::AUTH_HTTP_SIG); |
|
1097
|
|
|
return self::jsonDecode($response->body(), true); |
|
1098
|
|
|
} |
|
1099
|
|
|
|
|
1100
|
|
|
/** |
|
1101
|
|
|
* verify a message signed bitcoin-core style |
|
1102
|
|
|
* |
|
1103
|
|
|
* @param string $message |
|
1104
|
|
|
* @param string $address |
|
1105
|
|
|
* @param string $signature |
|
1106
|
|
|
* @return boolean |
|
1107
|
|
|
*/ |
|
1108
|
|
|
public function verifyMessage($message, $address, $signature) { |
|
1109
|
|
|
// we could also use the API instead of the using BitcoinLib to verify |
|
1110
|
|
|
// $this->client->post("verify_message", null, ['message' => $message, 'address' => $address, 'signature' => $signature])['result']; |
|
|
|
|
|
|
1111
|
|
|
|
|
1112
|
|
|
try { |
|
1113
|
|
|
return BitcoinLib::verifyMessage($address, $signature, $message); |
|
1114
|
|
|
} catch (\Exception $e) { |
|
1115
|
|
|
return false; |
|
1116
|
|
|
} |
|
1117
|
|
|
} |
|
1118
|
|
|
|
|
1119
|
|
|
/** |
|
1120
|
|
|
* convert a Satoshi value to a BTC value |
|
1121
|
|
|
* |
|
1122
|
|
|
* @param int $satoshi |
|
1123
|
|
|
* @return float |
|
1124
|
|
|
*/ |
|
1125
|
|
|
public static function toBTC($satoshi) { |
|
1126
|
|
|
return bcdiv((int)(string)$satoshi, 100000000, 8); |
|
1127
|
|
|
} |
|
1128
|
|
|
|
|
1129
|
|
|
/** |
|
1130
|
|
|
* convert a Satoshi value to a BTC value and return it as a string |
|
1131
|
|
|
|
|
1132
|
|
|
* @param int $satoshi |
|
1133
|
|
|
* @return string |
|
1134
|
|
|
*/ |
|
1135
|
|
|
public static function toBTCString($satoshi) { |
|
1136
|
|
|
return sprintf("%.8f", self::toBTC($satoshi)); |
|
1137
|
|
|
} |
|
1138
|
|
|
|
|
1139
|
|
|
/** |
|
1140
|
|
|
* convert a BTC value to a Satoshi value |
|
1141
|
|
|
* |
|
1142
|
|
|
* @param float $btc |
|
1143
|
|
|
* @return string |
|
1144
|
|
|
*/ |
|
1145
|
|
|
public static function toSatoshiString($btc) { |
|
1146
|
|
|
return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0); |
|
1147
|
|
|
} |
|
1148
|
|
|
|
|
1149
|
|
|
/** |
|
1150
|
|
|
* convert a BTC value to a Satoshi value |
|
1151
|
|
|
* |
|
1152
|
|
|
* @param float $btc |
|
1153
|
|
|
* @return string |
|
1154
|
|
|
*/ |
|
1155
|
|
|
public static function toSatoshi($btc) { |
|
1156
|
|
|
return (int)self::toSatoshiString($btc); |
|
1157
|
|
|
} |
|
1158
|
|
|
|
|
1159
|
|
|
/** |
|
1160
|
|
|
* json_decode helper that throws exceptions when it fails to decode |
|
1161
|
|
|
* |
|
1162
|
|
|
* @param $json |
|
1163
|
|
|
* @param bool $assoc |
|
1164
|
|
|
* @return mixed |
|
1165
|
|
|
* @throws \Exception |
|
1166
|
|
|
*/ |
|
1167
|
|
|
protected static function jsonDecode($json, $assoc = false) { |
|
1168
|
|
|
if (!$json) { |
|
1169
|
|
|
throw new \Exception("Can't json_decode empty string [{$json}]"); |
|
1170
|
|
|
} |
|
1171
|
|
|
|
|
1172
|
|
|
$data = json_decode($json, $assoc); |
|
1173
|
|
|
|
|
1174
|
|
|
if ($data === null) { |
|
1175
|
|
|
throw new \Exception("Failed to json_decode [{$json}]"); |
|
1176
|
|
|
} |
|
1177
|
|
|
|
|
1178
|
|
|
return $data; |
|
1179
|
|
|
} |
|
1180
|
|
|
|
|
1181
|
|
|
/** |
|
1182
|
|
|
* sort public keys for multisig script |
|
1183
|
|
|
* |
|
1184
|
|
|
* @param string[] $pubKeys |
|
1185
|
|
|
* @return string[] |
|
1186
|
|
|
*/ |
|
1187
|
|
|
public static function sortMultisigKeys(array $pubKeys) { |
|
1188
|
|
|
$sortedKeys = $pubKeys; |
|
1189
|
|
|
|
|
1190
|
|
|
sort($sortedKeys); |
|
1191
|
|
|
|
|
1192
|
|
|
return $sortedKeys; |
|
1193
|
|
|
} |
|
1194
|
|
|
|
|
1195
|
|
|
/** |
|
1196
|
|
|
* read and decode the json payload from a webhook's POST request. |
|
1197
|
|
|
* |
|
1198
|
|
|
* @param bool $returnObject flag to indicate if an object or associative array should be returned |
|
1199
|
|
|
* @return mixed|null |
|
1200
|
|
|
* @throws \Exception |
|
1201
|
|
|
*/ |
|
1202
|
|
|
public static function getWebhookPayload($returnObject = false) { |
|
1203
|
|
|
$data = file_get_contents("php://input"); |
|
1204
|
|
|
if ($data) { |
|
1205
|
|
|
return self::jsonDecode($data, !$returnObject); |
|
1206
|
|
|
} else { |
|
1207
|
|
|
return null; |
|
1208
|
|
|
} |
|
1209
|
|
|
} |
|
1210
|
|
|
} |
|
1211
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.