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