|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Blocktrail\SDK; |
|
4
|
|
|
|
|
5
|
|
|
use BitWasp\Bitcoin\Address\AddressFactory; |
|
6
|
|
|
use BitWasp\Bitcoin\Address\PayToPubKeyHashAddress; |
|
7
|
|
|
use BitWasp\Bitcoin\Bitcoin; |
|
8
|
|
|
use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer; |
|
9
|
|
|
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface; |
|
10
|
|
|
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\CompactSignatureSerializerInterface; |
|
11
|
|
|
use BitWasp\Bitcoin\Crypto\Random\Random; |
|
12
|
|
|
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKey; |
|
13
|
|
|
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory; |
|
14
|
|
|
use BitWasp\Bitcoin\MessageSigner\MessageSigner; |
|
15
|
|
|
use BitWasp\Bitcoin\MessageSigner\SignedMessage; |
|
16
|
|
|
use BitWasp\Bitcoin\Mnemonic\Bip39\Bip39SeedGenerator; |
|
17
|
|
|
use BitWasp\Bitcoin\Mnemonic\MnemonicFactory; |
|
18
|
|
|
use BitWasp\Bitcoin\Network\NetworkFactory; |
|
19
|
|
|
use BitWasp\Bitcoin\Transaction\TransactionFactory; |
|
20
|
|
|
use BitWasp\Buffertools\Buffer; |
|
21
|
|
|
use BitWasp\Buffertools\BufferInterface; |
|
22
|
|
|
use Blocktrail\CryptoJSAES\CryptoJSAES; |
|
23
|
|
|
use Blocktrail\SDK\Bitcoin\BIP32Key; |
|
24
|
|
|
use Blocktrail\SDK\Connection\RestClient; |
|
25
|
|
|
use Blocktrail\SDK\V3Crypt\Encryption; |
|
26
|
|
|
use Blocktrail\SDK\V3Crypt\Mnemonic; |
|
27
|
|
|
|
|
28
|
|
|
/** |
|
29
|
|
|
* Class BlocktrailSDK |
|
30
|
|
|
*/ |
|
31
|
|
|
class BlocktrailSDK implements BlocktrailSDKInterface { |
|
32
|
|
|
/** |
|
33
|
|
|
* @var Connection\RestClient |
|
34
|
|
|
*/ |
|
35
|
|
|
protected $client; |
|
36
|
|
|
|
|
37
|
|
|
/** |
|
38
|
|
|
* @var string currently only supporting; bitcoin |
|
39
|
|
|
*/ |
|
40
|
|
|
protected $network; |
|
41
|
|
|
|
|
42
|
|
|
/** |
|
43
|
|
|
* @var bool |
|
44
|
|
|
*/ |
|
45
|
|
|
protected $testnet; |
|
46
|
|
|
|
|
47
|
|
|
/** |
|
48
|
|
|
* @param string $apiKey the API_KEY to use for authentication |
|
49
|
|
|
* @param string $apiSecret the API_SECRET to use for authentication |
|
50
|
|
|
* @param string $network the cryptocurrency 'network' to consume, eg BTC, LTC, etc |
|
51
|
|
|
* @param bool $testnet testnet yes/no |
|
52
|
|
|
* @param string $apiVersion the version of the API to consume |
|
53
|
|
|
* @param null $apiEndpoint overwrite the endpoint used |
|
54
|
|
|
* this will cause the $network, $testnet and $apiVersion to be ignored! |
|
55
|
|
|
*/ |
|
56
|
|
|
public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) { |
|
57
|
|
|
if (is_null($apiEndpoint)) { |
|
58
|
|
|
$network = strtoupper($network); |
|
59
|
|
|
|
|
60
|
|
|
if ($testnet) { |
|
61
|
|
|
$network = "t{$network}"; |
|
62
|
|
|
} |
|
63
|
|
|
|
|
64
|
|
|
$apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com"; |
|
65
|
|
|
$apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$network}/"; |
|
66
|
|
|
} |
|
67
|
|
|
|
|
68
|
|
|
// normalize network and set bitcoinlib to the right magic-bytes |
|
69
|
|
|
list($this->network, $this->testnet) = $this->normalizeNetwork($network, $testnet); |
|
70
|
|
|
$this->setBitcoinLibMagicBytes($this->network, $this->testnet); |
|
71
|
|
|
|
|
72
|
|
|
$this->client = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret); |
|
73
|
|
|
} |
|
74
|
|
|
|
|
75
|
|
|
/** |
|
76
|
|
|
* normalize network string |
|
77
|
|
|
* |
|
78
|
|
|
* @param $network |
|
79
|
|
|
* @param $testnet |
|
80
|
|
|
* @return array |
|
81
|
|
|
* @throws \Exception |
|
82
|
|
|
*/ |
|
83
|
|
View Code Duplication |
protected function normalizeNetwork($network, $testnet) { |
|
|
|
|
|
|
84
|
|
|
switch (strtolower($network)) { |
|
85
|
|
|
case 'btc': |
|
86
|
|
|
case 'bitcoin': |
|
87
|
|
|
$network = 'bitcoin'; |
|
88
|
|
|
|
|
89
|
|
|
break; |
|
90
|
|
|
|
|
91
|
|
|
case 'tbtc': |
|
92
|
|
|
case 'bitcoin-testnet': |
|
93
|
|
|
$network = 'bitcoin'; |
|
94
|
|
|
$testnet = true; |
|
95
|
|
|
|
|
96
|
|
|
break; |
|
97
|
|
|
|
|
98
|
|
|
default: |
|
99
|
|
|
throw new \Exception("Unknown network [{$network}]"); |
|
100
|
|
|
} |
|
101
|
|
|
|
|
102
|
|
|
return [$network, $testnet]; |
|
103
|
|
|
} |
|
104
|
|
|
|
|
105
|
|
|
/** |
|
106
|
|
|
* set BitcoinLib to the correct magic-byte defaults for the selected network |
|
107
|
|
|
* |
|
108
|
|
|
* @param $network |
|
109
|
|
|
* @param $testnet |
|
110
|
|
|
*/ |
|
111
|
|
|
protected function setBitcoinLibMagicBytes($network, $testnet) { |
|
112
|
|
|
assert($network == "bitcoin"); |
|
113
|
|
|
Bitcoin::setNetwork($testnet ? NetworkFactory::bitcoinTestnet() : NetworkFactory::bitcoin()); |
|
114
|
|
|
} |
|
115
|
|
|
|
|
116
|
|
|
/** |
|
117
|
|
|
* enable CURL debugging output |
|
118
|
|
|
* |
|
119
|
|
|
* @param bool $debug |
|
120
|
|
|
* |
|
121
|
|
|
* @codeCoverageIgnore |
|
122
|
|
|
*/ |
|
123
|
|
|
public function setCurlDebugging($debug = true) { |
|
124
|
|
|
$this->client->setCurlDebugging($debug); |
|
125
|
|
|
} |
|
126
|
|
|
|
|
127
|
|
|
/** |
|
128
|
|
|
* enable verbose errors |
|
129
|
|
|
* |
|
130
|
|
|
* @param bool $verboseErrors |
|
131
|
|
|
* |
|
132
|
|
|
* @codeCoverageIgnore |
|
133
|
|
|
*/ |
|
134
|
|
|
public function setVerboseErrors($verboseErrors = true) { |
|
135
|
|
|
$this->client->setVerboseErrors($verboseErrors); |
|
136
|
|
|
} |
|
137
|
|
|
|
|
138
|
|
|
/** |
|
139
|
|
|
* set cURL default option on Guzzle client |
|
140
|
|
|
* @param string $key |
|
141
|
|
|
* @param bool $value |
|
142
|
|
|
* |
|
143
|
|
|
* @codeCoverageIgnore |
|
144
|
|
|
*/ |
|
145
|
|
|
public function setCurlDefaultOption($key, $value) { |
|
146
|
|
|
$this->client->setCurlDefaultOption($key, $value); |
|
147
|
|
|
} |
|
148
|
|
|
|
|
149
|
|
|
/** |
|
150
|
|
|
* @return RestClient |
|
151
|
|
|
*/ |
|
152
|
|
|
public function getRestClient() { |
|
153
|
|
|
return $this->client; |
|
154
|
|
|
} |
|
155
|
|
|
|
|
156
|
|
|
/** |
|
157
|
|
|
* get a single address |
|
158
|
|
|
* @param string $address address hash |
|
159
|
|
|
* @return array associative array containing the response |
|
160
|
|
|
*/ |
|
161
|
|
|
public function address($address) { |
|
162
|
|
|
$response = $this->client->get("address/{$address}"); |
|
163
|
|
|
return self::jsonDecode($response->body(), true); |
|
164
|
|
|
} |
|
165
|
|
|
|
|
166
|
|
|
/** |
|
167
|
|
|
* get all 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 addressTransactions($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}/transactions", $queryString); |
|
181
|
|
|
return self::jsonDecode($response->body(), true); |
|
182
|
|
|
} |
|
183
|
|
|
|
|
184
|
|
|
/** |
|
185
|
|
|
* get all unconfirmed transactions 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 addressUnconfirmedTransactions($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}/unconfirmed-transactions", $queryString); |
|
199
|
|
|
return self::jsonDecode($response->body(), true); |
|
200
|
|
|
} |
|
201
|
|
|
|
|
202
|
|
|
/** |
|
203
|
|
|
* get all unspent outputs for an address (paginated) |
|
204
|
|
|
* @param string $address address hash |
|
205
|
|
|
* @param integer $page pagination: page number |
|
206
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
207
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
208
|
|
|
* @return array associative array containing the response |
|
209
|
|
|
*/ |
|
210
|
|
|
public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') { |
|
211
|
|
|
$queryString = [ |
|
212
|
|
|
'page' => $page, |
|
213
|
|
|
'limit' => $limit, |
|
214
|
|
|
'sort_dir' => $sortDir |
|
215
|
|
|
]; |
|
216
|
|
|
$response = $this->client->get("address/{$address}/unspent-outputs", $queryString); |
|
217
|
|
|
return self::jsonDecode($response->body(), true); |
|
218
|
|
|
} |
|
219
|
|
|
|
|
220
|
|
|
/** |
|
221
|
|
|
* get all unspent outputs for a batch of addresses (paginated) |
|
222
|
|
|
* |
|
223
|
|
|
* @param string[] $addresses |
|
224
|
|
|
* @param integer $page pagination: page number |
|
225
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
226
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
227
|
|
|
* @return array associative array containing the response |
|
228
|
|
|
* @throws \Exception |
|
229
|
|
|
*/ |
|
230
|
|
|
public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') { |
|
231
|
|
|
$queryString = [ |
|
232
|
|
|
'page' => $page, |
|
233
|
|
|
'limit' => $limit, |
|
234
|
|
|
'sort_dir' => $sortDir |
|
235
|
|
|
]; |
|
236
|
|
|
$response = $this->client->post("address/unspent-outputs", $queryString, ['addresses' => $addresses]); |
|
237
|
|
|
return self::jsonDecode($response->body(), true); |
|
238
|
|
|
} |
|
239
|
|
|
|
|
240
|
|
|
/** |
|
241
|
|
|
* verify ownership of an address |
|
242
|
|
|
* @param string $address address hash |
|
243
|
|
|
* @param string $signature a signed message (the address hash) using the private key of the address |
|
244
|
|
|
* @return array associative array containing the response |
|
245
|
|
|
*/ |
|
246
|
|
|
public function verifyAddress($address, $signature) { |
|
247
|
|
|
$postData = ['signature' => $signature]; |
|
248
|
|
|
|
|
249
|
|
|
$response = $this->client->post("address/{$address}/verify", null, $postData, RestClient::AUTH_HTTP_SIG); |
|
250
|
|
|
|
|
251
|
|
|
return self::jsonDecode($response->body(), true); |
|
252
|
|
|
} |
|
253
|
|
|
|
|
254
|
|
|
/** |
|
255
|
|
|
* get all blocks (paginated) |
|
256
|
|
|
* @param integer $page pagination: page number |
|
257
|
|
|
* @param integer $limit pagination: records per page |
|
258
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
259
|
|
|
* @return array associative array containing the response |
|
260
|
|
|
*/ |
|
261
|
|
|
public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') { |
|
262
|
|
|
$queryString = [ |
|
263
|
|
|
'page' => $page, |
|
264
|
|
|
'limit' => $limit, |
|
265
|
|
|
'sort_dir' => $sortDir |
|
266
|
|
|
]; |
|
267
|
|
|
$response = $this->client->get("all-blocks", $queryString); |
|
268
|
|
|
return self::jsonDecode($response->body(), true); |
|
269
|
|
|
} |
|
270
|
|
|
|
|
271
|
|
|
/** |
|
272
|
|
|
* get the latest block |
|
273
|
|
|
* @return array associative array containing the response |
|
274
|
|
|
*/ |
|
275
|
|
|
public function blockLatest() { |
|
276
|
|
|
$response = $this->client->get("block/latest"); |
|
277
|
|
|
return self::jsonDecode($response->body(), true); |
|
278
|
|
|
} |
|
279
|
|
|
|
|
280
|
|
|
/** |
|
281
|
|
|
* get an individual block |
|
282
|
|
|
* @param string|integer $block a block hash or a block height |
|
283
|
|
|
* @return array associative array containing the response |
|
284
|
|
|
*/ |
|
285
|
|
|
public function block($block) { |
|
286
|
|
|
$response = $this->client->get("block/{$block}"); |
|
287
|
|
|
return self::jsonDecode($response->body(), true); |
|
288
|
|
|
} |
|
289
|
|
|
|
|
290
|
|
|
/** |
|
291
|
|
|
* get all transaction in a block (paginated) |
|
292
|
|
|
* @param string|integer $block a block hash or a block height |
|
293
|
|
|
* @param integer $page pagination: page number |
|
294
|
|
|
* @param integer $limit pagination: records per page |
|
295
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
296
|
|
|
* @return array associative array containing the response |
|
297
|
|
|
*/ |
|
298
|
|
|
public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') { |
|
299
|
|
|
$queryString = [ |
|
300
|
|
|
'page' => $page, |
|
301
|
|
|
'limit' => $limit, |
|
302
|
|
|
'sort_dir' => $sortDir |
|
303
|
|
|
]; |
|
304
|
|
|
$response = $this->client->get("block/{$block}/transactions", $queryString); |
|
305
|
|
|
return self::jsonDecode($response->body(), true); |
|
306
|
|
|
} |
|
307
|
|
|
|
|
308
|
|
|
/** |
|
309
|
|
|
* get a single transaction |
|
310
|
|
|
* @param string $txhash transaction hash |
|
311
|
|
|
* @return array associative array containing the response |
|
312
|
|
|
*/ |
|
313
|
|
|
public function transaction($txhash) { |
|
314
|
|
|
$response = $this->client->get("transaction/{$txhash}"); |
|
315
|
|
|
return self::jsonDecode($response->body(), true); |
|
316
|
|
|
} |
|
317
|
|
|
|
|
318
|
|
|
/** |
|
319
|
|
|
* get a paginated list of all webhooks associated with the api user |
|
320
|
|
|
* @param integer $page pagination: page number |
|
321
|
|
|
* @param integer $limit pagination: records per page |
|
322
|
|
|
* @return array associative array containing the response |
|
323
|
|
|
*/ |
|
324
|
|
|
public function allWebhooks($page = 1, $limit = 20) { |
|
325
|
|
|
$queryString = [ |
|
326
|
|
|
'page' => $page, |
|
327
|
|
|
'limit' => $limit |
|
328
|
|
|
]; |
|
329
|
|
|
$response = $this->client->get("webhooks", $queryString); |
|
330
|
|
|
return self::jsonDecode($response->body(), true); |
|
331
|
|
|
} |
|
332
|
|
|
|
|
333
|
|
|
/** |
|
334
|
|
|
* get an existing webhook by it's identifier |
|
335
|
|
|
* @param string $identifier a unique identifier associated with the webhook |
|
336
|
|
|
* @return array associative array containing the response |
|
337
|
|
|
*/ |
|
338
|
|
|
public function getWebhook($identifier) { |
|
339
|
|
|
$response = $this->client->get("webhook/".$identifier); |
|
340
|
|
|
return self::jsonDecode($response->body(), true); |
|
341
|
|
|
} |
|
342
|
|
|
|
|
343
|
|
|
/** |
|
344
|
|
|
* create a new webhook |
|
345
|
|
|
* @param string $url the url to receive the webhook events |
|
346
|
|
|
* @param string $identifier a unique identifier to associate with this webhook |
|
347
|
|
|
* @return array associative array containing the response |
|
348
|
|
|
*/ |
|
349
|
|
View Code Duplication |
public function setupWebhook($url, $identifier = null) { |
|
|
|
|
|
|
350
|
|
|
$postData = [ |
|
351
|
|
|
'url' => $url, |
|
352
|
|
|
'identifier' => $identifier |
|
353
|
|
|
]; |
|
354
|
|
|
$response = $this->client->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG); |
|
355
|
|
|
return self::jsonDecode($response->body(), true); |
|
356
|
|
|
} |
|
357
|
|
|
|
|
358
|
|
|
/** |
|
359
|
|
|
* update an existing webhook |
|
360
|
|
|
* @param string $identifier the unique identifier of the webhook to update |
|
361
|
|
|
* @param string $newUrl the new url to receive the webhook events |
|
362
|
|
|
* @param string $newIdentifier a new unique identifier to associate with this webhook |
|
363
|
|
|
* @return array associative array containing the response |
|
364
|
|
|
*/ |
|
365
|
|
|
public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) { |
|
366
|
|
|
$putData = [ |
|
367
|
|
|
'url' => $newUrl, |
|
368
|
|
|
'identifier' => $newIdentifier |
|
369
|
|
|
]; |
|
370
|
|
|
$response = $this->client->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG); |
|
371
|
|
|
return self::jsonDecode($response->body(), true); |
|
372
|
|
|
} |
|
373
|
|
|
|
|
374
|
|
|
/** |
|
375
|
|
|
* deletes an existing webhook and any event subscriptions associated with it |
|
376
|
|
|
* @param string $identifier the unique identifier of the webhook to delete |
|
377
|
|
|
* @return boolean true on success |
|
378
|
|
|
*/ |
|
379
|
|
|
public function deleteWebhook($identifier) { |
|
380
|
|
|
$response = $this->client->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG); |
|
381
|
|
|
return self::jsonDecode($response->body(), true); |
|
382
|
|
|
} |
|
383
|
|
|
|
|
384
|
|
|
/** |
|
385
|
|
|
* get a paginated list of all the events a webhook is subscribed to |
|
386
|
|
|
* @param string $identifier the unique identifier of the webhook |
|
387
|
|
|
* @param integer $page pagination: page number |
|
388
|
|
|
* @param integer $limit pagination: records per page |
|
389
|
|
|
* @return array associative array containing the response |
|
390
|
|
|
*/ |
|
391
|
|
|
public function getWebhookEvents($identifier, $page = 1, $limit = 20) { |
|
392
|
|
|
$queryString = [ |
|
393
|
|
|
'page' => $page, |
|
394
|
|
|
'limit' => $limit |
|
395
|
|
|
]; |
|
396
|
|
|
$response = $this->client->get("webhook/{$identifier}/events", $queryString); |
|
397
|
|
|
return self::jsonDecode($response->body(), true); |
|
398
|
|
|
} |
|
399
|
|
|
|
|
400
|
|
|
/** |
|
401
|
|
|
* subscribes a webhook to transaction events of one particular transaction |
|
402
|
|
|
* @param string $identifier the unique identifier of the webhook to be triggered |
|
403
|
|
|
* @param string $transaction the transaction hash |
|
404
|
|
|
* @param integer $confirmations the amount of confirmations to send. |
|
405
|
|
|
* @return array associative array containing the response |
|
406
|
|
|
*/ |
|
407
|
|
View Code Duplication |
public function subscribeTransaction($identifier, $transaction, $confirmations = 6) { |
|
|
|
|
|
|
408
|
|
|
$postData = [ |
|
409
|
|
|
'event_type' => 'transaction', |
|
410
|
|
|
'transaction' => $transaction, |
|
411
|
|
|
'confirmations' => $confirmations, |
|
412
|
|
|
]; |
|
413
|
|
|
$response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG); |
|
414
|
|
|
return self::jsonDecode($response->body(), true); |
|
415
|
|
|
} |
|
416
|
|
|
|
|
417
|
|
|
/** |
|
418
|
|
|
* subscribes a webhook to transaction events on a particular address |
|
419
|
|
|
* @param string $identifier the unique identifier of the webhook to be triggered |
|
420
|
|
|
* @param string $address the address hash |
|
421
|
|
|
* @param integer $confirmations the amount of confirmations to send. |
|
422
|
|
|
* @return array associative array containing the response |
|
423
|
|
|
*/ |
|
424
|
|
View Code Duplication |
public function subscribeAddressTransactions($identifier, $address, $confirmations = 6) { |
|
|
|
|
|
|
425
|
|
|
$postData = [ |
|
426
|
|
|
'event_type' => 'address-transactions', |
|
427
|
|
|
'address' => $address, |
|
428
|
|
|
'confirmations' => $confirmations, |
|
429
|
|
|
]; |
|
430
|
|
|
$response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG); |
|
431
|
|
|
return self::jsonDecode($response->body(), true); |
|
432
|
|
|
} |
|
433
|
|
|
|
|
434
|
|
|
/** |
|
435
|
|
|
* batch subscribes a webhook to multiple transaction events |
|
436
|
|
|
* |
|
437
|
|
|
* @param string $identifier the unique identifier of the webhook |
|
438
|
|
|
* @param array $batchData A 2D array of event data: |
|
439
|
|
|
* [address => $address, confirmations => $confirmations] |
|
440
|
|
|
* where $address is the address to subscibe to |
|
441
|
|
|
* and optionally $confirmations is the amount of confirmations |
|
442
|
|
|
* @return boolean true on success |
|
443
|
|
|
*/ |
|
444
|
|
|
public function batchSubscribeAddressTransactions($identifier, $batchData) { |
|
445
|
|
|
$postData = []; |
|
446
|
|
|
foreach ($batchData as $record) { |
|
447
|
|
|
$postData[] = [ |
|
448
|
|
|
'event_type' => 'address-transactions', |
|
449
|
|
|
'address' => $record['address'], |
|
450
|
|
|
'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6, |
|
451
|
|
|
]; |
|
452
|
|
|
} |
|
453
|
|
|
$response = $this->client->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG); |
|
454
|
|
|
return self::jsonDecode($response->body(), true); |
|
455
|
|
|
} |
|
456
|
|
|
|
|
457
|
|
|
/** |
|
458
|
|
|
* subscribes a webhook to a new block event |
|
459
|
|
|
* @param string $identifier the unique identifier of the webhook to be triggered |
|
460
|
|
|
* @return array associative array containing the response |
|
461
|
|
|
*/ |
|
462
|
|
View Code Duplication |
public function subscribeNewBlocks($identifier) { |
|
|
|
|
|
|
463
|
|
|
$postData = [ |
|
464
|
|
|
'event_type' => 'block', |
|
465
|
|
|
]; |
|
466
|
|
|
$response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG); |
|
467
|
|
|
return self::jsonDecode($response->body(), true); |
|
468
|
|
|
} |
|
469
|
|
|
|
|
470
|
|
|
/** |
|
471
|
|
|
* removes an transaction event subscription from a webhook |
|
472
|
|
|
* @param string $identifier the unique identifier of the webhook associated with the event subscription |
|
473
|
|
|
* @param string $transaction the transaction hash of the event subscription |
|
474
|
|
|
* @return boolean true on success |
|
475
|
|
|
*/ |
|
476
|
|
|
public function unsubscribeTransaction($identifier, $transaction) { |
|
477
|
|
|
$response = $this->client->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG); |
|
478
|
|
|
return self::jsonDecode($response->body(), true); |
|
479
|
|
|
} |
|
480
|
|
|
|
|
481
|
|
|
/** |
|
482
|
|
|
* removes an address transaction event subscription from a webhook |
|
483
|
|
|
* @param string $identifier the unique identifier of the webhook associated with the event subscription |
|
484
|
|
|
* @param string $address the address hash of the event subscription |
|
485
|
|
|
* @return boolean true on success |
|
486
|
|
|
*/ |
|
487
|
|
|
public function unsubscribeAddressTransactions($identifier, $address) { |
|
488
|
|
|
$response = $this->client->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG); |
|
489
|
|
|
return self::jsonDecode($response->body(), true); |
|
490
|
|
|
} |
|
491
|
|
|
|
|
492
|
|
|
/** |
|
493
|
|
|
* removes a block event subscription from a webhook |
|
494
|
|
|
* @param string $identifier the unique identifier of the webhook associated with the event subscription |
|
495
|
|
|
* @return boolean true on success |
|
496
|
|
|
*/ |
|
497
|
|
|
public function unsubscribeNewBlocks($identifier) { |
|
498
|
|
|
$response = $this->client->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG); |
|
499
|
|
|
return self::jsonDecode($response->body(), true); |
|
500
|
|
|
} |
|
501
|
|
|
|
|
502
|
|
|
/** |
|
503
|
|
|
* create a new wallet |
|
504
|
|
|
* - will generate a new primary seed (with password) and backup seed (without password) |
|
505
|
|
|
* - send the primary seed (BIP39 'encrypted') and backup public key to the server |
|
506
|
|
|
* - receive the blocktrail co-signing public key from the server |
|
507
|
|
|
* |
|
508
|
|
|
* Either takes one argument: |
|
509
|
|
|
* @param array $options |
|
510
|
|
|
* |
|
511
|
|
|
* Or takes three arguments (old, deprecated syntax): |
|
512
|
|
|
* (@nonPHP-doc) @param $identifier |
|
513
|
|
|
* (@nonPHP-doc) @param $password |
|
514
|
|
|
* (@nonPHP-doc) @param int $keyIndex override for the blocktrail cosigning key to use |
|
|
|
|
|
|
515
|
|
|
* |
|
516
|
|
|
* @return array[WalletInterface, array] list($wallet, $backupInfo) |
|
|
|
|
|
|
517
|
|
|
* @throws \Exception |
|
518
|
|
|
*/ |
|
519
|
|
|
public function createNewWallet($options) { |
|
520
|
|
|
if (!is_array($options)) { |
|
521
|
|
|
$args = func_get_args(); |
|
522
|
|
|
$options = [ |
|
523
|
|
|
"identifier" => $args[0], |
|
524
|
|
|
"password" => $args[1], |
|
525
|
|
|
"key_index" => isset($args[2]) ? $args[2] : null, |
|
526
|
|
|
]; |
|
527
|
|
|
} |
|
528
|
|
|
|
|
529
|
|
|
if (isset($options['password'])) { |
|
530
|
|
|
if (isset($options['passphrase'])) { |
|
531
|
|
|
throw new \InvalidArgumentException("Can only provide either passphrase or password"); |
|
532
|
|
|
} else { |
|
533
|
|
|
$options['passphrase'] = $options['password']; |
|
534
|
|
|
} |
|
535
|
|
|
} |
|
536
|
|
|
|
|
537
|
|
|
if (!isset($options['passphrase'])) { |
|
538
|
|
|
$options['passphrase'] = null; |
|
539
|
|
|
} |
|
540
|
|
|
|
|
541
|
|
|
if (!isset($options['key_index'])) { |
|
542
|
|
|
$options['key_index'] = 0; |
|
543
|
|
|
} |
|
544
|
|
|
|
|
545
|
|
|
if (!isset($options['wallet_version'])) { |
|
546
|
|
|
$options['wallet_version'] = Wallet::WALLET_VERSION_V3; |
|
547
|
|
|
} |
|
548
|
|
|
|
|
549
|
|
|
switch ($options['wallet_version']) { |
|
550
|
|
|
case Wallet::WALLET_VERSION_V1: |
|
551
|
|
|
return $this->createNewWalletV1($options); |
|
552
|
|
|
|
|
553
|
|
|
case Wallet::WALLET_VERSION_V2: |
|
554
|
|
|
return $this->createNewWalletV2($options); |
|
555
|
|
|
|
|
556
|
|
|
case Wallet::WALLET_VERSION_V3: |
|
557
|
|
|
return $this->createNewWalletV3($options); |
|
558
|
|
|
|
|
559
|
|
|
default: |
|
560
|
|
|
throw new \InvalidArgumentException("Invalid wallet version"); |
|
561
|
|
|
} |
|
562
|
|
|
} |
|
563
|
|
|
|
|
564
|
|
|
protected function createNewWalletV1($options) { |
|
565
|
|
|
$walletPath = WalletPath::create($options['key_index']); |
|
566
|
|
|
|
|
567
|
|
|
$storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null; |
|
568
|
|
|
|
|
569
|
|
|
if (isset($options['primary_mnemonic']) && $options['primary_private_key']) { |
|
570
|
|
|
throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey"); |
|
571
|
|
|
} |
|
572
|
|
|
|
|
573
|
|
|
$primaryMnemonic = null; |
|
574
|
|
|
$primaryPrivateKey = null; |
|
575
|
|
|
if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) { |
|
576
|
|
|
if (!$options['passphrase']) { |
|
577
|
|
|
throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase"); |
|
578
|
|
|
} else { |
|
579
|
|
|
// create new primary seed |
|
580
|
|
|
/** @var HierarchicalKey $primaryPrivateKey */ |
|
581
|
|
|
list($primaryMnemonic, , $primaryPrivateKey) = $this->newPrimarySeed($options['passphrase']); |
|
582
|
|
|
if ($storePrimaryMnemonic !== false) { |
|
583
|
|
|
$storePrimaryMnemonic = true; |
|
584
|
|
|
} |
|
585
|
|
|
} |
|
586
|
|
|
} elseif (isset($options['primary_mnemonic'])) { |
|
587
|
|
|
$primaryMnemonic = $options['primary_mnemonic']; |
|
588
|
|
|
} elseif (isset($options['primary_private_key'])) { |
|
589
|
|
|
$primaryPrivateKey = $options['primary_private_key']; |
|
590
|
|
|
} |
|
591
|
|
|
|
|
592
|
|
|
if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) { |
|
593
|
|
|
throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase"); |
|
594
|
|
|
} |
|
595
|
|
|
|
|
596
|
|
View Code Duplication |
if ($primaryPrivateKey) { |
|
|
|
|
|
|
597
|
|
|
if (is_string($primaryPrivateKey)) { |
|
598
|
|
|
$primaryPrivateKey = [$primaryPrivateKey, "m"]; |
|
599
|
|
|
} |
|
600
|
|
|
} else { |
|
601
|
|
|
$primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase'])); |
|
602
|
|
|
} |
|
603
|
|
|
|
|
604
|
|
|
if (!$storePrimaryMnemonic) { |
|
605
|
|
|
$primaryMnemonic = false; |
|
606
|
|
|
} |
|
607
|
|
|
|
|
608
|
|
|
// create primary public key from the created private key |
|
609
|
|
|
$path = (string)$walletPath->keyIndexPath()->publicPath(); |
|
610
|
|
|
$primaryPublicKey = BIP32Key::create($primaryPrivateKey->derivePath($path), $path); |
|
611
|
|
|
|
|
612
|
|
|
if (isset($options['backup_mnemonic']) && $options['backup_public_key']) { |
|
613
|
|
|
throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey"); |
|
614
|
|
|
} |
|
615
|
|
|
|
|
616
|
|
|
$backupMnemonic = null; |
|
617
|
|
|
$backupPublicKey = null; |
|
618
|
|
|
if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) { |
|
619
|
|
|
/** @var HierarchicalKey $backupPrivateKey */ |
|
620
|
|
|
list($backupMnemonic, , ) = $this->newBackupSeed(); |
|
621
|
|
|
} else if (isset($options['backup_mnemonic'])) { |
|
622
|
|
|
$backupMnemonic = $options['backup_mnemonic']; |
|
623
|
|
|
} elseif (isset($options['backup_public_key'])) { |
|
624
|
|
|
$backupPublicKey = $options['backup_public_key']; |
|
625
|
|
|
} |
|
626
|
|
|
|
|
627
|
|
View Code Duplication |
if ($backupPublicKey) { |
|
|
|
|
|
|
628
|
|
|
if (is_string($backupPublicKey)) { |
|
629
|
|
|
$backupPublicKey = [$backupPublicKey, "m"]; |
|
630
|
|
|
} |
|
631
|
|
|
} else { |
|
632
|
|
|
$backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, "")); |
|
633
|
|
|
$backupPublicKey = BIP32Key::create($backupPrivateKey->toPublic(), "M"); |
|
634
|
|
|
} |
|
635
|
|
|
|
|
636
|
|
|
// create a checksum of our private key which we'll later use to verify we used the right password |
|
637
|
|
|
$checksum = $primaryPrivateKey->getPublicKey()->getAddress()->getAddress(); |
|
638
|
|
|
|
|
639
|
|
|
// send the public keys to the server to store them |
|
640
|
|
|
// and the mnemonic, which is safe because it's useless without the password |
|
641
|
|
|
$data = $this->storeNewWalletV1($options['identifier'], $primaryPublicKey->tuple(), $backupPublicKey->tuple(), $primaryMnemonic, $checksum, $options['key_index']); |
|
642
|
|
|
|
|
643
|
|
|
// received the blocktrail public keys |
|
644
|
|
View Code Duplication |
$blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) { |
|
|
|
|
|
|
645
|
|
|
return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])]; |
|
646
|
|
|
}, $data['blocktrail_public_keys']); |
|
647
|
|
|
|
|
648
|
|
|
$wallet = new WalletV1( |
|
649
|
|
|
$this, |
|
650
|
|
|
$options['identifier'], |
|
651
|
|
|
$primaryMnemonic, |
|
652
|
|
|
[$options['key_index'] => $primaryPublicKey], |
|
653
|
|
|
$backupPublicKey, |
|
654
|
|
|
$blocktrailPublicKeys, |
|
655
|
|
|
$options['key_index'], |
|
656
|
|
|
$this->network, |
|
657
|
|
|
$this->testnet, |
|
658
|
|
|
$checksum |
|
659
|
|
|
); |
|
660
|
|
|
|
|
661
|
|
|
$wallet->unlock($options); |
|
662
|
|
|
|
|
663
|
|
|
// return wallet and backup mnemonic |
|
664
|
|
|
return [ |
|
665
|
|
|
$wallet, |
|
666
|
|
|
[ |
|
667
|
|
|
'primary_mnemonic' => $primaryMnemonic, |
|
668
|
|
|
'backup_mnemonic' => $backupMnemonic, |
|
669
|
|
|
'blocktrail_public_keys' => $blocktrailPublicKeys, |
|
670
|
|
|
], |
|
671
|
|
|
]; |
|
672
|
|
|
} |
|
673
|
|
|
|
|
674
|
|
|
public static function randomBits($bits) { |
|
675
|
|
|
return self::randomBytes($bits / 8); |
|
676
|
|
|
} |
|
677
|
|
|
|
|
678
|
|
|
public static function randomBytes($bytes) { |
|
679
|
|
|
return (new Random())->bytes($bytes)->getBinary(); |
|
680
|
|
|
} |
|
681
|
|
|
|
|
682
|
|
|
protected function createNewWalletV2($options) { |
|
683
|
|
|
$walletPath = WalletPath::create($options['key_index']); |
|
684
|
|
|
|
|
685
|
|
|
if (isset($options['store_primary_mnemonic'])) { |
|
686
|
|
|
$options['store_data_on_server'] = $options['store_primary_mnemonic']; |
|
687
|
|
|
} |
|
688
|
|
|
|
|
689
|
|
View Code Duplication |
if (!isset($options['store_data_on_server'])) { |
|
|
|
|
|
|
690
|
|
|
if (isset($options['primary_private_key'])) { |
|
691
|
|
|
$options['store_data_on_server'] = false; |
|
692
|
|
|
} else { |
|
693
|
|
|
$options['store_data_on_server'] = true; |
|
694
|
|
|
} |
|
695
|
|
|
} |
|
696
|
|
|
|
|
697
|
|
|
$storeDataOnServer = $options['store_data_on_server']; |
|
698
|
|
|
|
|
699
|
|
|
$secret = null; |
|
700
|
|
|
$encryptedSecret = null; |
|
701
|
|
|
$primarySeed = null; |
|
702
|
|
|
$encryptedPrimarySeed = null; |
|
703
|
|
|
$recoverySecret = null; |
|
704
|
|
|
$recoveryEncryptedSecret = null; |
|
705
|
|
|
$backupSeed = null; |
|
706
|
|
|
|
|
707
|
|
|
if (!isset($options['primary_private_key'])) { |
|
708
|
|
|
$primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : self::randomBits(256); |
|
709
|
|
|
} |
|
710
|
|
|
|
|
711
|
|
|
if ($storeDataOnServer) { |
|
712
|
|
|
if (!isset($options['secret'])) { |
|
713
|
|
|
if (!$options['passphrase']) { |
|
714
|
|
|
throw new \InvalidArgumentException("Can't encrypt data without a passphrase"); |
|
715
|
|
|
} |
|
716
|
|
|
|
|
717
|
|
|
$secret = bin2hex(self::randomBits(256)); // string because we use it as passphrase |
|
718
|
|
|
$encryptedSecret = CryptoJSAES::encrypt($secret, $options['passphrase']); |
|
719
|
|
|
} else { |
|
720
|
|
|
$secret = $options['secret']; |
|
721
|
|
|
} |
|
722
|
|
|
|
|
723
|
|
|
$encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($primarySeed), $secret); |
|
724
|
|
|
$recoverySecret = bin2hex(self::randomBits(256)); |
|
725
|
|
|
|
|
726
|
|
|
$recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret); |
|
727
|
|
|
} |
|
728
|
|
|
|
|
729
|
|
|
if (!isset($options['backup_public_key'])) { |
|
730
|
|
|
$backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : self::randomBits(256); |
|
731
|
|
|
} |
|
732
|
|
|
|
|
733
|
|
View Code Duplication |
if (isset($options['primary_private_key'])) { |
|
|
|
|
|
|
734
|
|
|
$options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']); |
|
735
|
|
|
} else { |
|
736
|
|
|
$options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m"); |
|
737
|
|
|
} |
|
738
|
|
|
|
|
739
|
|
|
// create primary public key from the created private key |
|
740
|
|
|
$options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath()); |
|
741
|
|
|
|
|
742
|
|
View Code Duplication |
if (!isset($options['backup_public_key'])) { |
|
|
|
|
|
|
743
|
|
|
$options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M"); |
|
744
|
|
|
} |
|
745
|
|
|
|
|
746
|
|
|
// create a checksum of our private key which we'll later use to verify we used the right password |
|
747
|
|
|
$checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress(); |
|
748
|
|
|
|
|
749
|
|
|
// send the public keys and encrypted data to server |
|
750
|
|
|
$data = $this->storeNewWalletV2( |
|
751
|
|
|
$options['identifier'], |
|
752
|
|
|
$options['primary_public_key']->tuple(), |
|
753
|
|
|
$options['backup_public_key']->tuple(), |
|
|
|
|
|
|
754
|
|
|
$storeDataOnServer ? $encryptedPrimarySeed : false, |
|
755
|
|
|
$storeDataOnServer ? $encryptedSecret : false, |
|
756
|
|
|
$storeDataOnServer ? $recoverySecret : false, |
|
757
|
|
|
$checksum, |
|
758
|
|
|
$options['key_index'] |
|
759
|
|
|
); |
|
760
|
|
|
|
|
761
|
|
|
// received the blocktrail public keys |
|
762
|
|
View Code Duplication |
$blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) { |
|
|
|
|
|
|
763
|
|
|
return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])]; |
|
764
|
|
|
}, $data['blocktrail_public_keys']); |
|
765
|
|
|
|
|
766
|
|
|
$wallet = new WalletV2( |
|
767
|
|
|
$this, |
|
768
|
|
|
$options['identifier'], |
|
769
|
|
|
$encryptedPrimarySeed, |
|
770
|
|
|
$encryptedSecret, |
|
771
|
|
|
[$options['key_index'] => $options['primary_public_key']], |
|
772
|
|
|
$options['backup_public_key'], |
|
773
|
|
|
$blocktrailPublicKeys, |
|
774
|
|
|
$options['key_index'], |
|
775
|
|
|
$this->network, |
|
776
|
|
|
$this->testnet, |
|
777
|
|
|
$checksum |
|
778
|
|
|
); |
|
779
|
|
|
|
|
780
|
|
|
$wallet->unlock([ |
|
781
|
|
|
'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null, |
|
782
|
|
|
'primary_private_key' => $options['primary_private_key'], |
|
783
|
|
|
'primary_seed' => $primarySeed, |
|
784
|
|
|
'secret' => $secret, |
|
785
|
|
|
]); |
|
786
|
|
|
|
|
787
|
|
|
// return wallet and mnemonics for backup sheet |
|
788
|
|
|
return [ |
|
789
|
|
|
$wallet, |
|
790
|
|
|
[ |
|
791
|
|
|
'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null, |
|
792
|
|
|
'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null, |
|
793
|
|
|
'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null, |
|
794
|
|
|
'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null, |
|
795
|
|
|
'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) { |
|
796
|
|
|
return [$keyIndex, $pubKey->tuple()]; |
|
797
|
|
|
}, $blocktrailPublicKeys), |
|
798
|
|
|
], |
|
799
|
|
|
]; |
|
800
|
|
|
} |
|
801
|
|
|
|
|
802
|
|
|
protected function createNewWalletV3($options) { |
|
803
|
|
|
$walletPath = WalletPath::create($options['key_index']); |
|
804
|
|
|
|
|
805
|
|
|
if (isset($options['store_primary_mnemonic'])) { |
|
806
|
|
|
$options['store_data_on_server'] = $options['store_primary_mnemonic']; |
|
807
|
|
|
} |
|
808
|
|
|
|
|
809
|
|
View Code Duplication |
if (!isset($options['store_data_on_server'])) { |
|
|
|
|
|
|
810
|
|
|
if (isset($options['primary_private_key'])) { |
|
811
|
|
|
$options['store_data_on_server'] = false; |
|
812
|
|
|
} else { |
|
813
|
|
|
$options['store_data_on_server'] = true; |
|
814
|
|
|
} |
|
815
|
|
|
} |
|
816
|
|
|
|
|
817
|
|
|
$storeDataOnServer = $options['store_data_on_server']; |
|
818
|
|
|
|
|
819
|
|
|
$secret = null; |
|
820
|
|
|
$encryptedSecret = null; |
|
821
|
|
|
$primarySeed = null; |
|
822
|
|
|
$encryptedPrimarySeed = null; |
|
823
|
|
|
$recoverySecret = null; |
|
824
|
|
|
$recoveryEncryptedSecret = null; |
|
825
|
|
|
$backupSeed = null; |
|
826
|
|
|
|
|
827
|
|
View Code Duplication |
if (!isset($options['primary_private_key'])) { |
|
|
|
|
|
|
828
|
|
|
if (isset($options['primary_seed'])) { |
|
829
|
|
|
if (!$options['primary_seed'] instanceof BufferInterface) { |
|
830
|
|
|
throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer'); |
|
831
|
|
|
} |
|
832
|
|
|
$primarySeed = $options['primary_seed']; |
|
833
|
|
|
} else { |
|
834
|
|
|
$primarySeed = new Buffer(self::randomBits(256)); |
|
835
|
|
|
} |
|
836
|
|
|
} |
|
837
|
|
|
|
|
838
|
|
|
if ($storeDataOnServer) { |
|
839
|
|
|
if (!isset($options['secret'])) { |
|
840
|
|
|
if (!$options['passphrase']) { |
|
841
|
|
|
throw new \InvalidArgumentException("Can't encrypt data without a passphrase"); |
|
842
|
|
|
} |
|
843
|
|
|
|
|
844
|
|
|
$secret = new Buffer(self::randomBits(256)); |
|
845
|
|
|
$encryptedSecret = Encryption::encrypt($secret, new Buffer($options['passphrase'])); |
|
846
|
|
|
} else { |
|
847
|
|
|
if (!$options['secret'] instanceof Buffer) { |
|
848
|
|
|
throw new \RuntimeException('Secret must be provided as a Buffer'); |
|
849
|
|
|
} |
|
850
|
|
|
|
|
851
|
|
|
$secret = $options['secret']; |
|
852
|
|
|
} |
|
853
|
|
|
|
|
854
|
|
|
$encryptedPrimarySeed = Encryption::encrypt($primarySeed, $secret); |
|
855
|
|
|
$recoverySecret = new Buffer(self::randomBits(256)); |
|
856
|
|
|
|
|
857
|
|
|
$recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret); |
|
858
|
|
|
} |
|
859
|
|
|
|
|
860
|
|
View Code Duplication |
if (!isset($options['backup_public_key'])) { |
|
|
|
|
|
|
861
|
|
|
if (isset($options['backup_seed'])) { |
|
862
|
|
|
if (!$options['backup_seed'] instanceof Buffer) { |
|
863
|
|
|
throw new \RuntimeException('Backup seed must be an instance of Buffer'); |
|
864
|
|
|
} |
|
865
|
|
|
$backupSeed = $options['backup_seed']; |
|
866
|
|
|
} else { |
|
867
|
|
|
$backupSeed = new Buffer(self::randomBits(256)); |
|
868
|
|
|
} |
|
869
|
|
|
} |
|
870
|
|
|
|
|
871
|
|
View Code Duplication |
if (isset($options['primary_private_key'])) { |
|
|
|
|
|
|
872
|
|
|
$options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']); |
|
873
|
|
|
} else { |
|
874
|
|
|
$options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m"); |
|
875
|
|
|
} |
|
876
|
|
|
|
|
877
|
|
|
// create primary public key from the created private key |
|
878
|
|
|
$options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath()); |
|
879
|
|
|
|
|
880
|
|
View Code Duplication |
if (!isset($options['backup_public_key'])) { |
|
|
|
|
|
|
881
|
|
|
$options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m")->buildKey("M"); |
|
882
|
|
|
} |
|
883
|
|
|
|
|
884
|
|
|
// create a checksum of our private key which we'll later use to verify we used the right password |
|
885
|
|
|
$checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress(); |
|
886
|
|
|
|
|
887
|
|
|
// send the public keys and encrypted data to server |
|
888
|
|
|
$data = $this->storeNewWalletV3( |
|
889
|
|
|
$options['identifier'], |
|
890
|
|
|
$options['primary_public_key']->tuple(), |
|
891
|
|
|
$options['backup_public_key']->tuple(), |
|
|
|
|
|
|
892
|
|
|
$storeDataOnServer ? base64_encode($encryptedPrimarySeed->getBinary()) : false, |
|
893
|
|
|
$storeDataOnServer ? base64_encode($encryptedSecret->getBinary()) : false, |
|
894
|
|
|
$storeDataOnServer ? $recoverySecret->getHex() : false, |
|
895
|
|
|
$checksum, |
|
896
|
|
|
$options['key_index'] |
|
897
|
|
|
); |
|
898
|
|
|
|
|
899
|
|
|
// received the blocktrail public keys |
|
900
|
|
|
$blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) { |
|
901
|
|
|
return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])]; |
|
902
|
|
|
}, $data['blocktrail_public_keys']); |
|
903
|
|
|
|
|
904
|
|
|
$wallet = new WalletV3( |
|
905
|
|
|
$this, |
|
906
|
|
|
$options['identifier'], |
|
907
|
|
|
$encryptedPrimarySeed, |
|
|
|
|
|
|
908
|
|
|
$encryptedSecret, |
|
|
|
|
|
|
909
|
|
|
[$options['key_index'] => $options['primary_public_key']], |
|
910
|
|
|
$options['backup_public_key'], |
|
911
|
|
|
$blocktrailPublicKeys, |
|
912
|
|
|
$options['key_index'], |
|
913
|
|
|
$this->network, |
|
914
|
|
|
$this->testnet, |
|
915
|
|
|
$checksum |
|
916
|
|
|
); |
|
917
|
|
|
|
|
918
|
|
|
$wallet->unlock([ |
|
919
|
|
|
'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null, |
|
920
|
|
|
'primary_private_key' => $options['primary_private_key'], |
|
921
|
|
|
'primary_seed' => $primarySeed, |
|
922
|
|
|
'secret' => $secret, |
|
923
|
|
|
]); |
|
924
|
|
|
|
|
925
|
|
|
// return wallet and mnemonics for backup sheet |
|
926
|
|
|
return [ |
|
927
|
|
|
$wallet, |
|
928
|
|
|
[ |
|
929
|
|
|
'encrypted_primary_seed' => $encryptedPrimarySeed ? Mnemonic::encode($encryptedPrimarySeed) : null, |
|
930
|
|
|
'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic($backupSeed) : null, |
|
931
|
|
|
'recovery_encrypted_secret' => $recoveryEncryptedSecret ? Mnemonic::encode($recoveryEncryptedSecret) : null, |
|
932
|
|
|
'encrypted_secret' => $encryptedSecret ? Mnemonic::encode($encryptedSecret) : null, |
|
933
|
|
|
'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) { |
|
934
|
|
|
return [$keyIndex, $pubKey->tuple()]; |
|
935
|
|
|
}, $blocktrailPublicKeys), |
|
936
|
|
|
] |
|
937
|
|
|
]; |
|
938
|
|
|
} |
|
939
|
|
|
|
|
940
|
|
|
/** |
|
941
|
|
|
* create wallet using the API |
|
942
|
|
|
* |
|
943
|
|
|
* @param string $identifier the wallet identifier to create |
|
944
|
|
|
* @param array $primaryPublicKey BIP32 extended public key - [key, path] |
|
945
|
|
|
* @param string $backupPublicKey plain public key |
|
946
|
|
|
* @param string $primaryMnemonic mnemonic to store |
|
947
|
|
|
* @param string $checksum checksum to store |
|
948
|
|
|
* @param int $keyIndex account that we expect to use |
|
949
|
|
|
* @return mixed |
|
950
|
|
|
*/ |
|
951
|
|
|
public function storeNewWalletV1($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex) { |
|
952
|
|
|
$data = [ |
|
953
|
|
|
'identifier' => $identifier, |
|
954
|
|
|
'primary_public_key' => $primaryPublicKey, |
|
955
|
|
|
'backup_public_key' => $backupPublicKey, |
|
956
|
|
|
'primary_mnemonic' => $primaryMnemonic, |
|
957
|
|
|
'checksum' => $checksum, |
|
958
|
|
|
'key_index' => $keyIndex |
|
959
|
|
|
]; |
|
960
|
|
|
|
|
961
|
|
|
$response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG); |
|
962
|
|
|
return self::jsonDecode($response->body(), true); |
|
963
|
|
|
} |
|
964
|
|
|
|
|
965
|
|
|
/** |
|
966
|
|
|
* create wallet using the API |
|
967
|
|
|
* |
|
968
|
|
|
* @param string $identifier the wallet identifier to create |
|
969
|
|
|
* @param array $primaryPublicKey BIP32 extended public key - [key, path] |
|
970
|
|
|
* @param string $backupPublicKey plain public key |
|
971
|
|
|
* @param $encryptedPrimarySeed |
|
972
|
|
|
* @param $encryptedSecret |
|
973
|
|
|
* @param $recoverySecret |
|
974
|
|
|
* @param string $checksum checksum to store |
|
975
|
|
|
* @param int $keyIndex account that we expect to use |
|
976
|
|
|
* @return mixed |
|
977
|
|
|
* @throws \Exception |
|
978
|
|
|
*/ |
|
979
|
|
View Code Duplication |
public function storeNewWalletV2($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex) { |
|
|
|
|
|
|
980
|
|
|
$data = [ |
|
981
|
|
|
'identifier' => $identifier, |
|
982
|
|
|
'wallet_version' => Wallet::WALLET_VERSION_V2, |
|
983
|
|
|
'primary_public_key' => $primaryPublicKey, |
|
984
|
|
|
'backup_public_key' => $backupPublicKey, |
|
985
|
|
|
'encrypted_primary_seed' => $encryptedPrimarySeed, |
|
986
|
|
|
'encrypted_secret' => $encryptedSecret, |
|
987
|
|
|
'recovery_secret' => $recoverySecret, |
|
988
|
|
|
'checksum' => $checksum, |
|
989
|
|
|
'key_index' => $keyIndex |
|
990
|
|
|
]; |
|
991
|
|
|
|
|
992
|
|
|
$response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG); |
|
993
|
|
|
return self::jsonDecode($response->body(), true); |
|
994
|
|
|
} |
|
995
|
|
|
|
|
996
|
|
|
/** |
|
997
|
|
|
* create wallet using the API |
|
998
|
|
|
* |
|
999
|
|
|
* @param string $identifier the wallet identifier to create |
|
1000
|
|
|
* @param array $primaryPublicKey BIP32 extended public key - [key, path] |
|
1001
|
|
|
* @param string $backupPublicKey plain public key |
|
1002
|
|
|
* @param $encryptedPrimarySeed |
|
1003
|
|
|
* @param $encryptedSecret |
|
1004
|
|
|
* @param $recoverySecret |
|
1005
|
|
|
* @param string $checksum checksum to store |
|
1006
|
|
|
* @param int $keyIndex account that we expect to use |
|
1007
|
|
|
* @return mixed |
|
1008
|
|
|
* @throws \Exception |
|
1009
|
|
|
*/ |
|
1010
|
|
View Code Duplication |
public function storeNewWalletV3($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex) { |
|
|
|
|
|
|
1011
|
|
|
$data = [ |
|
1012
|
|
|
'identifier' => $identifier, |
|
1013
|
|
|
'wallet_version' => Wallet::WALLET_VERSION_V3, |
|
1014
|
|
|
'primary_public_key' => $primaryPublicKey, |
|
1015
|
|
|
'backup_public_key' => $backupPublicKey, |
|
1016
|
|
|
'encrypted_primary_seed' => $encryptedPrimarySeed, |
|
1017
|
|
|
'encrypted_secret' => $encryptedSecret, |
|
1018
|
|
|
'recovery_secret' => $recoverySecret, |
|
1019
|
|
|
'checksum' => $checksum, |
|
1020
|
|
|
'key_index' => $keyIndex |
|
1021
|
|
|
]; |
|
1022
|
|
|
|
|
1023
|
|
|
$response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG); |
|
1024
|
|
|
return self::jsonDecode($response->body(), true); |
|
1025
|
|
|
} |
|
1026
|
|
|
|
|
1027
|
|
|
/** |
|
1028
|
|
|
* upgrade wallet to use a new account number |
|
1029
|
|
|
* the account number specifies which blocktrail cosigning key is used |
|
1030
|
|
|
* |
|
1031
|
|
|
* @param string $identifier the wallet identifier to be upgraded |
|
1032
|
|
|
* @param int $keyIndex the new account to use |
|
1033
|
|
|
* @param array $primaryPublicKey BIP32 extended public key - [key, path] |
|
1034
|
|
|
* @return mixed |
|
1035
|
|
|
*/ |
|
1036
|
|
|
public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) { |
|
1037
|
|
|
$data = [ |
|
1038
|
|
|
'key_index' => $keyIndex, |
|
1039
|
|
|
'primary_public_key' => $primaryPublicKey |
|
1040
|
|
|
]; |
|
1041
|
|
|
|
|
1042
|
|
|
$response = $this->client->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG); |
|
1043
|
|
|
return self::jsonDecode($response->body(), true); |
|
1044
|
|
|
} |
|
1045
|
|
|
|
|
1046
|
|
|
/** |
|
1047
|
|
|
* initialize a previously created wallet |
|
1048
|
|
|
* |
|
1049
|
|
|
* Either takes one argument: |
|
1050
|
|
|
* @param array $options |
|
1051
|
|
|
* |
|
1052
|
|
|
* Or takes two arguments (old, deprecated syntax): |
|
1053
|
|
|
* (@nonPHP-doc) @param string $identifier the wallet identifier to be initialized |
|
|
|
|
|
|
1054
|
|
|
* (@nonPHP-doc) @param string $password the password to decrypt the mnemonic with |
|
|
|
|
|
|
1055
|
|
|
* |
|
1056
|
|
|
* @return WalletInterface |
|
1057
|
|
|
* @throws \Exception |
|
1058
|
|
|
*/ |
|
1059
|
|
|
public function initWallet($options) { |
|
1060
|
|
|
if (!is_array($options)) { |
|
1061
|
|
|
$args = func_get_args(); |
|
1062
|
|
|
$options = [ |
|
1063
|
|
|
"identifier" => $args[0], |
|
1064
|
|
|
"password" => $args[1], |
|
1065
|
|
|
]; |
|
1066
|
|
|
} |
|
1067
|
|
|
|
|
1068
|
|
|
$identifier = $options['identifier']; |
|
1069
|
|
|
$readonly = isset($options['readonly']) ? $options['readonly'] : |
|
1070
|
|
|
(isset($options['readOnly']) ? $options['readOnly'] : |
|
1071
|
|
|
(isset($options['read-only']) ? $options['read-only'] : |
|
1072
|
|
|
false)); |
|
1073
|
|
|
|
|
1074
|
|
|
// get the wallet data from the server |
|
1075
|
|
|
$data = $this->getWallet($identifier); |
|
1076
|
|
|
|
|
1077
|
|
|
if (!$data) { |
|
1078
|
|
|
throw new \Exception("Failed to get wallet"); |
|
1079
|
|
|
} |
|
1080
|
|
|
|
|
1081
|
|
|
switch ($data['wallet_version']) { |
|
1082
|
|
|
case Wallet::WALLET_VERSION_V1: |
|
1083
|
|
|
$wallet = new WalletV1( |
|
1084
|
|
|
$this, |
|
1085
|
|
|
$identifier, |
|
1086
|
|
|
isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'], |
|
1087
|
|
|
$data['primary_public_keys'], |
|
1088
|
|
|
$data['backup_public_key'], |
|
1089
|
|
|
$data['blocktrail_public_keys'], |
|
1090
|
|
|
isset($options['key_index']) ? $options['key_index'] : $data['key_index'], |
|
1091
|
|
|
$this->network, |
|
1092
|
|
|
$this->testnet, |
|
1093
|
|
|
$data['checksum'] |
|
1094
|
|
|
); |
|
1095
|
|
|
break; |
|
1096
|
|
|
case Wallet::WALLET_VERSION_V2: |
|
1097
|
|
|
$wallet = new WalletV2( |
|
1098
|
|
|
$this, |
|
1099
|
|
|
$identifier, |
|
1100
|
|
|
isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'], |
|
1101
|
|
|
isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'], |
|
1102
|
|
|
$data['primary_public_keys'], |
|
1103
|
|
|
$data['backup_public_key'], |
|
1104
|
|
|
$data['blocktrail_public_keys'], |
|
1105
|
|
|
isset($options['key_index']) ? $options['key_index'] : $data['key_index'], |
|
1106
|
|
|
$this->network, |
|
1107
|
|
|
$this->testnet, |
|
1108
|
|
|
$data['checksum'] |
|
1109
|
|
|
); |
|
1110
|
|
|
break; |
|
1111
|
|
|
case Wallet::WALLET_VERSION_V3: |
|
1112
|
|
View Code Duplication |
if (isset($options['encrypted_primary_seed'])) { |
|
|
|
|
|
|
1113
|
|
|
if (!$options['encrypted_primary_seed'] instanceof Buffer) { |
|
1114
|
|
|
throw new \InvalidArgumentException('Encrypted PrimarySeed must be provided as a Buffer'); |
|
1115
|
|
|
} |
|
1116
|
|
|
$encryptedPrimarySeed = $data['encrypted_primary_seed']; |
|
1117
|
|
|
} else { |
|
1118
|
|
|
$encryptedPrimarySeed = new Buffer(base64_decode($data['encrypted_primary_seed'])); |
|
1119
|
|
|
} |
|
1120
|
|
|
|
|
1121
|
|
View Code Duplication |
if (isset($options['encrypted_secret'])) { |
|
|
|
|
|
|
1122
|
|
|
if (!$options['encrypted_secret'] instanceof Buffer) { |
|
1123
|
|
|
throw new \InvalidArgumentException('Encrypted secret must be provided as a Buffer'); |
|
1124
|
|
|
} |
|
1125
|
|
|
|
|
1126
|
|
|
$encryptedSecret = $data['encrypted_secret']; |
|
1127
|
|
|
} else { |
|
1128
|
|
|
$encryptedSecret = new Buffer(base64_decode($data['encrypted_secret'])); |
|
1129
|
|
|
} |
|
1130
|
|
|
|
|
1131
|
|
|
$wallet = new WalletV3( |
|
1132
|
|
|
$this, |
|
1133
|
|
|
$identifier, |
|
1134
|
|
|
$encryptedPrimarySeed, |
|
1135
|
|
|
$encryptedSecret, |
|
1136
|
|
|
$data['primary_public_keys'], |
|
1137
|
|
|
$data['backup_public_key'], |
|
1138
|
|
|
$data['blocktrail_public_keys'], |
|
1139
|
|
|
isset($options['key_index']) ? $options['key_index'] : $data['key_index'], |
|
1140
|
|
|
$this->network, |
|
1141
|
|
|
$this->testnet, |
|
1142
|
|
|
$data['checksum'] |
|
1143
|
|
|
); |
|
1144
|
|
|
break; |
|
1145
|
|
|
default: |
|
1146
|
|
|
throw new \InvalidArgumentException("Invalid wallet version"); |
|
1147
|
|
|
} |
|
1148
|
|
|
|
|
1149
|
|
|
if (!$readonly) { |
|
1150
|
|
|
$wallet->unlock($options); |
|
1151
|
|
|
} |
|
1152
|
|
|
|
|
1153
|
|
|
return $wallet; |
|
1154
|
|
|
} |
|
1155
|
|
|
|
|
1156
|
|
|
/** |
|
1157
|
|
|
* get the wallet data from the server |
|
1158
|
|
|
* |
|
1159
|
|
|
* @param string $identifier the identifier of the wallet |
|
1160
|
|
|
* @return mixed |
|
1161
|
|
|
*/ |
|
1162
|
|
|
public function getWallet($identifier) { |
|
1163
|
|
|
$response = $this->client->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG); |
|
1164
|
|
|
return self::jsonDecode($response->body(), true); |
|
1165
|
|
|
} |
|
1166
|
|
|
|
|
1167
|
|
|
/** |
|
1168
|
|
|
* update the wallet data on the server |
|
1169
|
|
|
* |
|
1170
|
|
|
* @param string $identifier |
|
1171
|
|
|
* @param $data |
|
1172
|
|
|
* @return mixed |
|
1173
|
|
|
*/ |
|
1174
|
|
|
public function updateWallet($identifier, $data) { |
|
1175
|
|
|
$response = $this->client->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG); |
|
1176
|
|
|
return self::jsonDecode($response->body(), true); |
|
1177
|
|
|
} |
|
1178
|
|
|
|
|
1179
|
|
|
/** |
|
1180
|
|
|
* delete a wallet from the server |
|
1181
|
|
|
* the checksum address and a signature to verify you ownership of the key of that checksum address |
|
1182
|
|
|
* is required to be able to delete a wallet |
|
1183
|
|
|
* |
|
1184
|
|
|
* @param string $identifier the identifier of the wallet |
|
1185
|
|
|
* @param string $checksumAddress the address for your master private key (and the checksum used when creating the wallet) |
|
1186
|
|
|
* @param string $signature a signature of the checksum address as message signed by the private key matching that address |
|
1187
|
|
|
* @param bool $force ignore warnings (such as a non-zero balance) |
|
1188
|
|
|
* @return mixed |
|
1189
|
|
|
*/ |
|
1190
|
|
|
public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) { |
|
1191
|
|
|
$response = $this->client->delete("wallet/{$identifier}", ['force' => $force], [ |
|
1192
|
|
|
'checksum' => $checksumAddress, |
|
1193
|
|
|
'signature' => $signature |
|
1194
|
|
|
], RestClient::AUTH_HTTP_SIG, 360); |
|
1195
|
|
|
return self::jsonDecode($response->body(), true); |
|
1196
|
|
|
} |
|
1197
|
|
|
|
|
1198
|
|
|
/** |
|
1199
|
|
|
* create new backup key; |
|
1200
|
|
|
* 1) a BIP39 mnemonic |
|
1201
|
|
|
* 2) a seed from that mnemonic with a blank password |
|
1202
|
|
|
* 3) a private key from that seed |
|
1203
|
|
|
* |
|
1204
|
|
|
* @return array [mnemonic, seed, key] |
|
1205
|
|
|
*/ |
|
1206
|
|
|
protected function newBackupSeed() { |
|
1207
|
|
|
list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed(""); |
|
1208
|
|
|
|
|
1209
|
|
|
return [$backupMnemonic, $backupSeed, $backupPrivateKey]; |
|
1210
|
|
|
} |
|
1211
|
|
|
|
|
1212
|
|
|
/** |
|
1213
|
|
|
* create new primary key; |
|
1214
|
|
|
* 1) a BIP39 mnemonic |
|
1215
|
|
|
* 2) a seed from that mnemonic with the password |
|
1216
|
|
|
* 3) a private key from that seed |
|
1217
|
|
|
* |
|
1218
|
|
|
* @param string $passphrase the password to use in the BIP39 creation of the seed |
|
1219
|
|
|
* @return array [mnemonic, seed, key] |
|
1220
|
|
|
* @TODO: require a strong password? |
|
1221
|
|
|
*/ |
|
1222
|
|
|
protected function newPrimarySeed($passphrase) { |
|
1223
|
|
|
list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase); |
|
1224
|
|
|
|
|
1225
|
|
|
return [$primaryMnemonic, $primarySeed, $primaryPrivateKey]; |
|
1226
|
|
|
} |
|
1227
|
|
|
|
|
1228
|
|
|
/** |
|
1229
|
|
|
* create a new key; |
|
1230
|
|
|
* 1) a BIP39 mnemonic |
|
1231
|
|
|
* 2) a seed from that mnemonic with the password |
|
1232
|
|
|
* 3) a private key from that seed |
|
1233
|
|
|
* |
|
1234
|
|
|
* @param string $passphrase the password to use in the BIP39 creation of the seed |
|
1235
|
|
|
* @param string $forceEntropy forced entropy instead of random entropy for testing purposes |
|
1236
|
|
|
* @return array |
|
1237
|
|
|
*/ |
|
1238
|
|
|
protected function generateNewSeed($passphrase = "", $forceEntropy = null) { |
|
1239
|
|
|
// generate master seed, retry if the generated private key isn't valid (FALSE is returned) |
|
1240
|
|
|
do { |
|
1241
|
|
|
$mnemonic = $this->generateNewMnemonic($forceEntropy); |
|
1242
|
|
|
|
|
1243
|
|
|
$seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase); |
|
1244
|
|
|
|
|
1245
|
|
|
$key = null; |
|
1246
|
|
|
try { |
|
1247
|
|
|
$key = HierarchicalKeyFactory::fromEntropy($seed); |
|
1248
|
|
|
} catch (\Exception $e) { |
|
1249
|
|
|
// try again |
|
1250
|
|
|
} |
|
1251
|
|
|
} while (!$key); |
|
1252
|
|
|
|
|
1253
|
|
|
return [$mnemonic, $seed, $key]; |
|
1254
|
|
|
} |
|
1255
|
|
|
|
|
1256
|
|
|
/** |
|
1257
|
|
|
* generate a new mnemonic from some random entropy (512 bit) |
|
1258
|
|
|
* |
|
1259
|
|
|
* @param string $forceEntropy forced entropy instead of random entropy for testing purposes |
|
1260
|
|
|
* @return string |
|
1261
|
|
|
* @throws \Exception |
|
1262
|
|
|
*/ |
|
1263
|
|
|
protected function generateNewMnemonic($forceEntropy = null) { |
|
1264
|
|
|
if ($forceEntropy === null) { |
|
1265
|
|
|
$random = new Random(); |
|
1266
|
|
|
$entropy = $random->bytes(512 / 8); |
|
1267
|
|
|
} else { |
|
1268
|
|
|
$entropy = $forceEntropy; |
|
1269
|
|
|
} |
|
1270
|
|
|
|
|
1271
|
|
|
return MnemonicFactory::bip39()->entropyToMnemonic($entropy); |
|
|
|
|
|
|
1272
|
|
|
} |
|
1273
|
|
|
|
|
1274
|
|
|
/** |
|
1275
|
|
|
* get the balance for the wallet |
|
1276
|
|
|
* |
|
1277
|
|
|
* @param string $identifier the identifier of the wallet |
|
1278
|
|
|
* @return array |
|
1279
|
|
|
*/ |
|
1280
|
|
|
public function getWalletBalance($identifier) { |
|
1281
|
|
|
$response = $this->client->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG); |
|
1282
|
|
|
return self::jsonDecode($response->body(), true); |
|
1283
|
|
|
} |
|
1284
|
|
|
|
|
1285
|
|
|
/** |
|
1286
|
|
|
* do HD wallet discovery for the wallet |
|
1287
|
|
|
* |
|
1288
|
|
|
* this can be REALLY slow, so we've set the timeout to 120s ... |
|
1289
|
|
|
* |
|
1290
|
|
|
* @param string $identifier the identifier of the wallet |
|
1291
|
|
|
* @param int $gap the gap setting to use for discovery |
|
1292
|
|
|
* @return mixed |
|
1293
|
|
|
*/ |
|
1294
|
|
|
public function doWalletDiscovery($identifier, $gap = 200) { |
|
1295
|
|
|
$response = $this->client->get("wallet/{$identifier}/discovery", ['gap' => $gap], RestClient::AUTH_HTTP_SIG, 360.0); |
|
1296
|
|
|
return self::jsonDecode($response->body(), true); |
|
1297
|
|
|
} |
|
1298
|
|
|
|
|
1299
|
|
|
/** |
|
1300
|
|
|
* get a new derivation number for specified parent path |
|
1301
|
|
|
* 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 |
|
1302
|
|
|
* |
|
1303
|
|
|
* returns the path |
|
1304
|
|
|
* |
|
1305
|
|
|
* @param string $identifier the identifier of the wallet |
|
1306
|
|
|
* @param string $path the parent path for which to get a new derivation |
|
1307
|
|
|
* @return string |
|
1308
|
|
|
*/ |
|
1309
|
|
|
public function getNewDerivation($identifier, $path) { |
|
1310
|
|
|
$result = $this->_getNewDerivation($identifier, $path); |
|
1311
|
|
|
return $result['path']; |
|
1312
|
|
|
} |
|
1313
|
|
|
|
|
1314
|
|
|
/** |
|
1315
|
|
|
* get a new derivation number for specified parent path |
|
1316
|
|
|
* 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 |
|
1317
|
|
|
* |
|
1318
|
|
|
* @param string $identifier the identifier of the wallet |
|
1319
|
|
|
* @param string $path the parent path for which to get a new derivation |
|
1320
|
|
|
* @return mixed |
|
1321
|
|
|
*/ |
|
1322
|
|
|
public function _getNewDerivation($identifier, $path) { |
|
1323
|
|
|
$response = $this->client->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG); |
|
1324
|
|
|
return self::jsonDecode($response->body(), true); |
|
1325
|
|
|
} |
|
1326
|
|
|
|
|
1327
|
|
|
/** |
|
1328
|
|
|
* get the path (and redeemScript) to specified address |
|
1329
|
|
|
* |
|
1330
|
|
|
* @param string $identifier |
|
1331
|
|
|
* @param string $address |
|
1332
|
|
|
* @return array |
|
1333
|
|
|
* @throws \Exception |
|
1334
|
|
|
*/ |
|
1335
|
|
|
public function getPathForAddress($identifier, $address) { |
|
1336
|
|
|
$response = $this->client->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG); |
|
1337
|
|
|
return self::jsonDecode($response->body(), true)['path']; |
|
1338
|
|
|
} |
|
1339
|
|
|
|
|
1340
|
|
|
/** |
|
1341
|
|
|
* send the transaction using the API |
|
1342
|
|
|
* |
|
1343
|
|
|
* @param string $identifier the identifier of the wallet |
|
1344
|
|
|
* @param string $rawTransaction raw hex of the transaction (should be partially signed) |
|
1345
|
|
|
* @param array $paths list of the paths that were used for the UTXO |
|
1346
|
|
|
* @param bool $checkFee let the server verify the fee after signing |
|
1347
|
|
|
* @return string the complete raw transaction |
|
1348
|
|
|
* @throws \Exception |
|
1349
|
|
|
*/ |
|
1350
|
|
|
public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false) { |
|
1351
|
|
|
$data = [ |
|
1352
|
|
|
'raw_transaction' => $rawTransaction, |
|
1353
|
|
|
'paths' => $paths |
|
1354
|
|
|
]; |
|
1355
|
|
|
|
|
1356
|
|
|
// dynamic TTL for when we're signing really big transactions |
|
1357
|
|
|
$ttl = max(5.0, count($paths) * 0.25) + 4.0; |
|
1358
|
|
|
|
|
1359
|
|
|
$response = $this->client->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl); |
|
1360
|
|
|
$signed = self::jsonDecode($response->body(), true); |
|
1361
|
|
|
|
|
1362
|
|
|
if (!$signed['complete'] || $signed['complete'] == 'false') { |
|
1363
|
|
|
throw new \Exception("Failed to completely sign transaction"); |
|
1364
|
|
|
} |
|
1365
|
|
|
|
|
1366
|
|
|
// create TX hash from the raw signed hex |
|
1367
|
|
|
return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex(); |
|
1368
|
|
|
} |
|
1369
|
|
|
|
|
1370
|
|
|
/** |
|
1371
|
|
|
* use the API to get the best inputs to use based on the outputs |
|
1372
|
|
|
* |
|
1373
|
|
|
* the return array has the following format: |
|
1374
|
|
|
* [ |
|
1375
|
|
|
* "utxos" => [ |
|
1376
|
|
|
* [ |
|
1377
|
|
|
* "hash" => "<txHash>", |
|
1378
|
|
|
* "idx" => "<index of the output of that <txHash>", |
|
1379
|
|
|
* "scriptpubkey_hex" => "<scriptPubKey-hex>", |
|
1380
|
|
|
* "value" => 32746327, |
|
1381
|
|
|
* "address" => "1address", |
|
1382
|
|
|
* "path" => "m/44'/1'/0'/0/13", |
|
1383
|
|
|
* "redeem_script" => "<redeemScript-hex>", |
|
1384
|
|
|
* ], |
|
1385
|
|
|
* ], |
|
1386
|
|
|
* "fee" => 10000, |
|
1387
|
|
|
* "change"=> 1010109201, |
|
1388
|
|
|
* ] |
|
1389
|
|
|
* |
|
1390
|
|
|
* @param string $identifier the identifier of the wallet |
|
1391
|
|
|
* @param array $outputs the outputs you want to create - array[address => satoshi-value] |
|
1392
|
|
|
* @param bool $lockUTXO when TRUE the UTXOs selected will be locked for a few seconds |
|
1393
|
|
|
* so you have some time to spend them without race-conditions |
|
1394
|
|
|
* @param bool $allowZeroConf |
|
1395
|
|
|
* @param string $feeStrategy |
|
1396
|
|
|
* @param null|int $forceFee |
|
1397
|
|
|
* @return array |
|
1398
|
|
|
* @throws \Exception |
|
1399
|
|
|
*/ |
|
1400
|
|
View Code Duplication |
public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null) { |
|
|
|
|
|
|
1401
|
|
|
$args = [ |
|
1402
|
|
|
'lock' => (int)!!$lockUTXO, |
|
1403
|
|
|
'zeroconf' => (int)!!$allowZeroConf, |
|
1404
|
|
|
'fee_strategy' => $feeStrategy, |
|
1405
|
|
|
]; |
|
1406
|
|
|
|
|
1407
|
|
|
if ($forceFee !== null) { |
|
1408
|
|
|
$args['forcefee'] = (int)$forceFee; |
|
1409
|
|
|
} |
|
1410
|
|
|
|
|
1411
|
|
|
$response = $this->client->post( |
|
1412
|
|
|
"wallet/{$identifier}/coin-selection", |
|
1413
|
|
|
$args, |
|
1414
|
|
|
$outputs, |
|
1415
|
|
|
RestClient::AUTH_HTTP_SIG |
|
1416
|
|
|
); |
|
1417
|
|
|
|
|
1418
|
|
|
return self::jsonDecode($response->body(), true); |
|
1419
|
|
|
} |
|
1420
|
|
|
|
|
1421
|
|
|
/** |
|
1422
|
|
|
* |
|
1423
|
|
|
* @param string $identifier the identifier of the wallet |
|
1424
|
|
|
* @param bool $allowZeroConf |
|
1425
|
|
|
* @param string $feeStrategy |
|
1426
|
|
|
* @param null|int $forceFee |
|
1427
|
|
|
* @param int $outputCnt |
|
1428
|
|
|
* @return array |
|
1429
|
|
|
* @throws \Exception |
|
1430
|
|
|
*/ |
|
1431
|
|
View Code Duplication |
public function walletMaxSpendable($identifier, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) { |
|
|
|
|
|
|
1432
|
|
|
$args = [ |
|
1433
|
|
|
'zeroconf' => (int)!!$allowZeroConf, |
|
1434
|
|
|
'fee_strategy' => $feeStrategy, |
|
1435
|
|
|
'outputs' => $outputCnt, |
|
1436
|
|
|
]; |
|
1437
|
|
|
|
|
1438
|
|
|
if ($forceFee !== null) { |
|
1439
|
|
|
$args['forcefee'] = (int)$forceFee; |
|
1440
|
|
|
} |
|
1441
|
|
|
|
|
1442
|
|
|
$response = $this->client->get( |
|
1443
|
|
|
"wallet/{$identifier}/max-spendable", |
|
1444
|
|
|
$args, |
|
1445
|
|
|
RestClient::AUTH_HTTP_SIG |
|
1446
|
|
|
); |
|
1447
|
|
|
|
|
1448
|
|
|
return self::jsonDecode($response->body(), true); |
|
1449
|
|
|
} |
|
1450
|
|
|
|
|
1451
|
|
|
/** |
|
1452
|
|
|
* @return array ['optimal_fee' => 10000, 'low_priority_fee' => 5000] |
|
1453
|
|
|
*/ |
|
1454
|
|
|
public function feePerKB() { |
|
1455
|
|
|
$response = $this->client->get("fee-per-kb"); |
|
1456
|
|
|
return self::jsonDecode($response->body(), true); |
|
1457
|
|
|
} |
|
1458
|
|
|
|
|
1459
|
|
|
/** |
|
1460
|
|
|
* get the current price index |
|
1461
|
|
|
* |
|
1462
|
|
|
* @return array eg; ['USD' => 287.30] |
|
1463
|
|
|
*/ |
|
1464
|
|
|
public function price() { |
|
1465
|
|
|
$response = $this->client->get("price"); |
|
1466
|
|
|
return self::jsonDecode($response->body(), true); |
|
1467
|
|
|
} |
|
1468
|
|
|
|
|
1469
|
|
|
/** |
|
1470
|
|
|
* setup webhook for wallet |
|
1471
|
|
|
* |
|
1472
|
|
|
* @param string $identifier the wallet identifier for which to create the webhook |
|
1473
|
|
|
* @param string $webhookIdentifier the webhook identifier to use |
|
1474
|
|
|
* @param string $url the url to receive the webhook events |
|
1475
|
|
|
* @return array |
|
1476
|
|
|
*/ |
|
1477
|
|
|
public function setupWalletWebhook($identifier, $webhookIdentifier, $url) { |
|
1478
|
|
|
$response = $this->client->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG); |
|
1479
|
|
|
return self::jsonDecode($response->body(), true); |
|
1480
|
|
|
} |
|
1481
|
|
|
|
|
1482
|
|
|
/** |
|
1483
|
|
|
* delete webhook for wallet |
|
1484
|
|
|
* |
|
1485
|
|
|
* @param string $identifier the wallet identifier for which to delete the webhook |
|
1486
|
|
|
* @param string $webhookIdentifier the webhook identifier to delete |
|
1487
|
|
|
* @return array |
|
1488
|
|
|
*/ |
|
1489
|
|
|
public function deleteWalletWebhook($identifier, $webhookIdentifier) { |
|
1490
|
|
|
$response = $this->client->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG); |
|
1491
|
|
|
return self::jsonDecode($response->body(), true); |
|
1492
|
|
|
} |
|
1493
|
|
|
|
|
1494
|
|
|
/** |
|
1495
|
|
|
* lock a specific unspent output |
|
1496
|
|
|
* |
|
1497
|
|
|
* @param $identifier |
|
1498
|
|
|
* @param $txHash |
|
1499
|
|
|
* @param $txIdx |
|
1500
|
|
|
* @param int $ttl |
|
1501
|
|
|
* @return bool |
|
1502
|
|
|
*/ |
|
1503
|
|
|
public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) { |
|
1504
|
|
|
$response = $this->client->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG); |
|
1505
|
|
|
return self::jsonDecode($response->body(), true)['locked']; |
|
1506
|
|
|
} |
|
1507
|
|
|
|
|
1508
|
|
|
/** |
|
1509
|
|
|
* unlock a specific unspent output |
|
1510
|
|
|
* |
|
1511
|
|
|
* @param $identifier |
|
1512
|
|
|
* @param $txHash |
|
1513
|
|
|
* @param $txIdx |
|
1514
|
|
|
* @return bool |
|
1515
|
|
|
*/ |
|
1516
|
|
|
public function unlockWalletUTXO($identifier, $txHash, $txIdx) { |
|
1517
|
|
|
$response = $this->client->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG); |
|
1518
|
|
|
return self::jsonDecode($response->body(), true)['unlocked']; |
|
1519
|
|
|
} |
|
1520
|
|
|
|
|
1521
|
|
|
/** |
|
1522
|
|
|
* get all transactions for wallet (paginated) |
|
1523
|
|
|
* |
|
1524
|
|
|
* @param string $identifier the wallet identifier for which to get transactions |
|
1525
|
|
|
* @param integer $page pagination: page number |
|
1526
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
1527
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
1528
|
|
|
* @return array associative array containing the response |
|
1529
|
|
|
*/ |
|
1530
|
|
View Code Duplication |
public function walletTransactions($identifier, $page = 1, $limit = 20, $sortDir = 'asc') { |
|
|
|
|
|
|
1531
|
|
|
$queryString = [ |
|
1532
|
|
|
'page' => $page, |
|
1533
|
|
|
'limit' => $limit, |
|
1534
|
|
|
'sort_dir' => $sortDir |
|
1535
|
|
|
]; |
|
1536
|
|
|
$response = $this->client->get("wallet/{$identifier}/transactions", $queryString, RestClient::AUTH_HTTP_SIG); |
|
1537
|
|
|
return self::jsonDecode($response->body(), true); |
|
1538
|
|
|
} |
|
1539
|
|
|
|
|
1540
|
|
|
/** |
|
1541
|
|
|
* get all addresses for wallet (paginated) |
|
1542
|
|
|
* |
|
1543
|
|
|
* @param string $identifier the wallet identifier for which to get addresses |
|
1544
|
|
|
* @param integer $page pagination: page number |
|
1545
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
1546
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
1547
|
|
|
* @return array associative array containing the response |
|
1548
|
|
|
*/ |
|
1549
|
|
View Code Duplication |
public function walletAddresses($identifier, $page = 1, $limit = 20, $sortDir = 'asc') { |
|
|
|
|
|
|
1550
|
|
|
$queryString = [ |
|
1551
|
|
|
'page' => $page, |
|
1552
|
|
|
'limit' => $limit, |
|
1553
|
|
|
'sort_dir' => $sortDir |
|
1554
|
|
|
]; |
|
1555
|
|
|
$response = $this->client->get("wallet/{$identifier}/addresses", $queryString, RestClient::AUTH_HTTP_SIG); |
|
1556
|
|
|
return self::jsonDecode($response->body(), true); |
|
1557
|
|
|
} |
|
1558
|
|
|
|
|
1559
|
|
|
/** |
|
1560
|
|
|
* get all UTXOs for wallet (paginated) |
|
1561
|
|
|
* |
|
1562
|
|
|
* @param string $identifier the wallet identifier for which to get addresses |
|
1563
|
|
|
* @param integer $page pagination: page number |
|
1564
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
1565
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
1566
|
|
|
* @return array associative array containing the response |
|
1567
|
|
|
*/ |
|
1568
|
|
View Code Duplication |
public function walletUTXOs($identifier, $page = 1, $limit = 20, $sortDir = 'asc') { |
|
|
|
|
|
|
1569
|
|
|
$queryString = [ |
|
1570
|
|
|
'page' => $page, |
|
1571
|
|
|
'limit' => $limit, |
|
1572
|
|
|
'sort_dir' => $sortDir |
|
1573
|
|
|
]; |
|
1574
|
|
|
$response = $this->client->get("wallet/{$identifier}/utxos", $queryString, RestClient::AUTH_HTTP_SIG); |
|
1575
|
|
|
return self::jsonDecode($response->body(), true); |
|
1576
|
|
|
} |
|
1577
|
|
|
|
|
1578
|
|
|
/** |
|
1579
|
|
|
* get a paginated list of all wallets associated with the api user |
|
1580
|
|
|
* |
|
1581
|
|
|
* @param integer $page pagination: page number |
|
1582
|
|
|
* @param integer $limit pagination: records per page |
|
1583
|
|
|
* @return array associative array containing the response |
|
1584
|
|
|
*/ |
|
1585
|
|
View Code Duplication |
public function allWallets($page = 1, $limit = 20) { |
|
|
|
|
|
|
1586
|
|
|
$queryString = [ |
|
1587
|
|
|
'page' => $page, |
|
1588
|
|
|
'limit' => $limit |
|
1589
|
|
|
]; |
|
1590
|
|
|
$response = $this->client->get("wallets", $queryString, RestClient::AUTH_HTTP_SIG); |
|
1591
|
|
|
return self::jsonDecode($response->body(), true); |
|
1592
|
|
|
} |
|
1593
|
|
|
|
|
1594
|
|
|
/** |
|
1595
|
|
|
* send raw transaction |
|
1596
|
|
|
* |
|
1597
|
|
|
* @param $txHex |
|
1598
|
|
|
* @return bool |
|
1599
|
|
|
*/ |
|
1600
|
|
|
public function sendRawTransaction($txHex) { |
|
1601
|
|
|
$response = $this->client->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG); |
|
1602
|
|
|
return self::jsonDecode($response->body(), true); |
|
1603
|
|
|
} |
|
1604
|
|
|
|
|
1605
|
|
|
/** |
|
1606
|
|
|
* testnet only ;-) |
|
1607
|
|
|
* |
|
1608
|
|
|
* @param $address |
|
1609
|
|
|
* @param int $amount defaults to 0.0001 BTC, max 0.001 BTC |
|
1610
|
|
|
* @return mixed |
|
1611
|
|
|
* @throws \Exception |
|
1612
|
|
|
*/ |
|
1613
|
|
View Code Duplication |
public function faucetWithdrawl($address, $amount = 10000) { |
|
|
|
|
|
|
1614
|
|
|
$response = $this->client->post("faucet/withdrawl", null, [ |
|
1615
|
|
|
'address' => $address, |
|
1616
|
|
|
'amount' => $amount, |
|
1617
|
|
|
], RestClient::AUTH_HTTP_SIG); |
|
1618
|
|
|
return self::jsonDecode($response->body(), true); |
|
1619
|
|
|
} |
|
1620
|
|
|
|
|
1621
|
|
|
/** |
|
1622
|
|
|
* verify a message signed bitcoin-core style |
|
1623
|
|
|
* |
|
1624
|
|
|
* @param string $message |
|
1625
|
|
|
* @param string $address |
|
1626
|
|
|
* @param string $signature |
|
1627
|
|
|
* @return boolean |
|
1628
|
|
|
*/ |
|
1629
|
|
|
public function verifyMessage($message, $address, $signature) { |
|
1630
|
|
|
// we could also use the API instead of the using BitcoinLib to verify |
|
1631
|
|
|
// $this->client->post("verify_message", null, ['message' => $message, 'address' => $address, 'signature' => $signature])['result']; |
|
|
|
|
|
|
1632
|
|
|
|
|
1633
|
|
|
$adapter = Bitcoin::getEcAdapter(); |
|
1634
|
|
|
$addr = AddressFactory::fromString($address); |
|
1635
|
|
|
if (!$addr instanceof PayToPubKeyHashAddress) { |
|
1636
|
|
|
throw new \RuntimeException('Can only verify a message with a pay-to-pubkey-hash address'); |
|
1637
|
|
|
} |
|
1638
|
|
|
|
|
1639
|
|
|
/** @var CompactSignatureSerializerInterface $csSerializer */ |
|
1640
|
|
|
$csSerializer = EcSerializer::getSerializer(CompactSignatureSerializerInterface::class, $adapter); |
|
|
|
|
|
|
1641
|
|
|
$signedMessage = new SignedMessage($message, $csSerializer->parse(new Buffer(base64_decode($signature)))); |
|
1642
|
|
|
|
|
1643
|
|
|
$signer = new MessageSigner($adapter); |
|
1644
|
|
|
return $signer->verify($signedMessage, $addr); |
|
1645
|
|
|
} |
|
1646
|
|
|
|
|
1647
|
|
|
/** |
|
1648
|
|
|
* convert a Satoshi value to a BTC value |
|
1649
|
|
|
* |
|
1650
|
|
|
* @param int $satoshi |
|
1651
|
|
|
* @return float |
|
1652
|
|
|
*/ |
|
1653
|
|
|
public static function toBTC($satoshi) { |
|
1654
|
|
|
return bcdiv((int)(string)$satoshi, 100000000, 8); |
|
1655
|
|
|
} |
|
1656
|
|
|
|
|
1657
|
|
|
/** |
|
1658
|
|
|
* convert a Satoshi value to a BTC value and return it as a string |
|
1659
|
|
|
|
|
1660
|
|
|
* @param int $satoshi |
|
1661
|
|
|
* @return string |
|
1662
|
|
|
*/ |
|
1663
|
|
|
public static function toBTCString($satoshi) { |
|
1664
|
|
|
return sprintf("%.8f", self::toBTC($satoshi)); |
|
1665
|
|
|
} |
|
1666
|
|
|
|
|
1667
|
|
|
/** |
|
1668
|
|
|
* convert a BTC value to a Satoshi value |
|
1669
|
|
|
* |
|
1670
|
|
|
* @param float $btc |
|
1671
|
|
|
* @return string |
|
1672
|
|
|
*/ |
|
1673
|
|
|
public static function toSatoshiString($btc) { |
|
1674
|
|
|
return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0); |
|
1675
|
|
|
} |
|
1676
|
|
|
|
|
1677
|
|
|
/** |
|
1678
|
|
|
* convert a BTC value to a Satoshi value |
|
1679
|
|
|
* |
|
1680
|
|
|
* @param float $btc |
|
1681
|
|
|
* @return string |
|
1682
|
|
|
*/ |
|
1683
|
|
|
public static function toSatoshi($btc) { |
|
1684
|
|
|
return (int)self::toSatoshiString($btc); |
|
1685
|
|
|
} |
|
1686
|
|
|
|
|
1687
|
|
|
/** |
|
1688
|
|
|
* json_decode helper that throws exceptions when it fails to decode |
|
1689
|
|
|
* |
|
1690
|
|
|
* @param $json |
|
1691
|
|
|
* @param bool $assoc |
|
1692
|
|
|
* @return mixed |
|
1693
|
|
|
* @throws \Exception |
|
1694
|
|
|
*/ |
|
1695
|
|
|
protected static function jsonDecode($json, $assoc = false) { |
|
1696
|
|
|
if (!$json) { |
|
1697
|
|
|
throw new \Exception("Can't json_decode empty string [{$json}]"); |
|
1698
|
|
|
} |
|
1699
|
|
|
|
|
1700
|
|
|
$data = json_decode($json, $assoc); |
|
1701
|
|
|
|
|
1702
|
|
|
if ($data === null) { |
|
1703
|
|
|
throw new \Exception("Failed to json_decode [{$json}]"); |
|
1704
|
|
|
} |
|
1705
|
|
|
|
|
1706
|
|
|
return $data; |
|
1707
|
|
|
} |
|
1708
|
|
|
|
|
1709
|
|
|
/** |
|
1710
|
|
|
* sort public keys for multisig script |
|
1711
|
|
|
* |
|
1712
|
|
|
* @param PublicKeyInterface[] $pubKeys |
|
1713
|
|
|
* @return PublicKeyInterface[] |
|
1714
|
|
|
*/ |
|
1715
|
|
|
public static function sortMultisigKeys(array $pubKeys) { |
|
1716
|
|
|
$result = array_values($pubKeys); |
|
1717
|
|
|
usort($result, function (PublicKeyInterface $a, PublicKeyInterface $b) { |
|
1718
|
|
|
$av = $a->getHex(); |
|
1719
|
|
|
$bv = $b->getHex(); |
|
1720
|
|
|
return $av == $bv ? 0 : $av > $bv ? 1 : -1; |
|
1721
|
|
|
}); |
|
1722
|
|
|
|
|
1723
|
|
|
return $result; |
|
1724
|
|
|
} |
|
1725
|
|
|
|
|
1726
|
|
|
/** |
|
1727
|
|
|
* read and decode the json payload from a webhook's POST request. |
|
1728
|
|
|
* |
|
1729
|
|
|
* @param bool $returnObject flag to indicate if an object or associative array should be returned |
|
1730
|
|
|
* @return mixed|null |
|
1731
|
|
|
* @throws \Exception |
|
1732
|
|
|
*/ |
|
1733
|
|
|
public static function getWebhookPayload($returnObject = false) { |
|
1734
|
|
|
$data = file_get_contents("php://input"); |
|
1735
|
|
|
if ($data) { |
|
1736
|
|
|
return self::jsonDecode($data, !$returnObject); |
|
1737
|
|
|
} else { |
|
1738
|
|
|
return null; |
|
1739
|
|
|
} |
|
1740
|
|
|
} |
|
1741
|
|
|
|
|
1742
|
|
|
public static function normalizeBIP32KeyArray($keys) { |
|
1743
|
|
|
return Util::arrayMapWithIndex(function ($idx, $key) { |
|
1744
|
|
|
return [$idx, self::normalizeBIP32Key($key)]; |
|
1745
|
|
|
}, $keys); |
|
1746
|
|
|
} |
|
1747
|
|
|
|
|
1748
|
|
|
public static function normalizeBIP32Key($key) { |
|
1749
|
|
|
if ($key instanceof BIP32Key) { |
|
1750
|
|
|
return $key; |
|
1751
|
|
|
} |
|
1752
|
|
|
|
|
1753
|
|
|
if (is_array($key)) { |
|
1754
|
|
|
$path = $key[1]; |
|
1755
|
|
|
$key = $key[0]; |
|
1756
|
|
|
|
|
1757
|
|
|
if (!($key instanceof HierarchicalKey)) { |
|
1758
|
|
|
$key = HierarchicalKeyFactory::fromExtended($key); |
|
1759
|
|
|
} |
|
1760
|
|
|
|
|
1761
|
|
|
return BIP32Key::create($key, $path); |
|
1762
|
|
|
} else { |
|
1763
|
|
|
throw new \Exception("Bad Input"); |
|
1764
|
|
|
} |
|
1765
|
|
|
} |
|
1766
|
|
|
} |
|
1767
|
|
|
|
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.