|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Blocktrail\SDK; |
|
4
|
|
|
|
|
5
|
|
|
use BitWasp\BitcoinLib\BIP32; |
|
6
|
|
|
use BitWasp\BitcoinLib\BIP39\BIP39; |
|
7
|
|
|
use BitWasp\BitcoinLib\BitcoinLib; |
|
8
|
|
|
use BitWasp\BitcoinLib\RawTransaction; |
|
9
|
|
|
use Blocktrail\SDK\Bitcoin\BIP32Key; |
|
10
|
|
|
use Blocktrail\SDK\Bitcoin\BIP32Path; |
|
11
|
|
|
use Blocktrail\SDK\Exceptions\BlocktrailSDKException; |
|
12
|
|
|
|
|
13
|
|
|
/** |
|
14
|
|
|
* Class Wallet |
|
15
|
|
|
*/ |
|
16
|
|
|
class Wallet implements WalletInterface { |
|
17
|
|
|
|
|
18
|
|
|
const BASE_FEE = 10000; |
|
19
|
|
|
|
|
20
|
|
|
/** |
|
21
|
|
|
* development / debug setting |
|
22
|
|
|
* when getting a new derivation from the API, |
|
23
|
|
|
* will verify address / redeeemScript with the values the API provides |
|
24
|
|
|
*/ |
|
25
|
|
|
const VERIFY_NEW_DERIVATION = true; |
|
26
|
|
|
|
|
27
|
|
|
/** |
|
28
|
|
|
* @var BlocktrailSDKInterface |
|
29
|
|
|
*/ |
|
30
|
|
|
protected $sdk; |
|
31
|
|
|
|
|
32
|
|
|
/** |
|
33
|
|
|
* @var string |
|
34
|
|
|
*/ |
|
35
|
|
|
protected $identifier; |
|
36
|
|
|
|
|
37
|
|
|
/** |
|
38
|
|
|
* BIP39 Mnemonic for the master primary private key |
|
39
|
|
|
* |
|
40
|
|
|
* @var string |
|
41
|
|
|
*/ |
|
42
|
|
|
protected $primaryMnemonic; |
|
43
|
|
|
|
|
44
|
|
|
/** |
|
45
|
|
|
* BIP32 master primary private key (m/) |
|
46
|
|
|
* |
|
47
|
|
|
* @var BIP32Key |
|
48
|
|
|
*/ |
|
49
|
|
|
protected $primaryPrivateKey; |
|
50
|
|
|
|
|
51
|
|
|
/** |
|
52
|
|
|
* @var BIP32Key[] |
|
53
|
|
|
*/ |
|
54
|
|
|
protected $primaryPublicKeys; |
|
55
|
|
|
|
|
56
|
|
|
/** |
|
57
|
|
|
* BIP32 master backup public key (M/) |
|
58
|
|
|
|
|
59
|
|
|
* @var BIP32Key |
|
60
|
|
|
*/ |
|
61
|
|
|
protected $backupPublicKey; |
|
62
|
|
|
|
|
63
|
|
|
/** |
|
64
|
|
|
* map of blocktrail BIP32 public keys |
|
65
|
|
|
* keyed by key index |
|
66
|
|
|
* path should be `M / key_index'` |
|
67
|
|
|
* |
|
68
|
|
|
* @var BIP32Key[] |
|
69
|
|
|
*/ |
|
70
|
|
|
protected $blocktrailPublicKeys; |
|
71
|
|
|
|
|
72
|
|
|
/** |
|
73
|
|
|
* the 'Blocktrail Key Index' that is used for new addresses |
|
74
|
|
|
* |
|
75
|
|
|
* @var int |
|
76
|
|
|
*/ |
|
77
|
|
|
protected $keyIndex; |
|
78
|
|
|
|
|
79
|
|
|
/** |
|
80
|
|
|
* 'bitcoin' |
|
81
|
|
|
* |
|
82
|
|
|
* @var string |
|
83
|
|
|
*/ |
|
84
|
|
|
protected $network; |
|
85
|
|
|
|
|
86
|
|
|
/** |
|
87
|
|
|
* testnet yes / no |
|
88
|
|
|
* |
|
89
|
|
|
* @var bool |
|
90
|
|
|
*/ |
|
91
|
|
|
protected $testnet; |
|
92
|
|
|
|
|
93
|
|
|
/** |
|
94
|
|
|
* cache of public keys, by path |
|
95
|
|
|
* |
|
96
|
|
|
* @var BIP32Key[] |
|
97
|
|
|
*/ |
|
98
|
|
|
protected $pubKeys = []; |
|
99
|
|
|
|
|
100
|
|
|
/** |
|
101
|
|
|
* cache of address / redeemScript, by path |
|
102
|
|
|
* |
|
103
|
|
|
* @var string[][] [[address, redeemScript)], ] |
|
104
|
|
|
*/ |
|
105
|
|
|
protected $derivations = []; |
|
106
|
|
|
|
|
107
|
|
|
/** |
|
108
|
|
|
* reverse cache of paths by address |
|
109
|
|
|
* |
|
110
|
|
|
* @var string[] |
|
111
|
|
|
*/ |
|
112
|
|
|
protected $derivationsByAddress = []; |
|
113
|
|
|
|
|
114
|
|
|
/** |
|
115
|
|
|
* @var WalletPath |
|
116
|
|
|
*/ |
|
117
|
|
|
protected $walletPath; |
|
118
|
|
|
|
|
119
|
|
|
private $checksum; |
|
120
|
|
|
|
|
121
|
|
|
private $locked = true; |
|
122
|
|
|
|
|
123
|
|
|
protected $optimalFeePerKB; |
|
124
|
|
|
protected $lowPriorityFeePerKB; |
|
125
|
|
|
protected $feePerKBAge; |
|
126
|
|
|
|
|
127
|
|
|
/** |
|
128
|
|
|
* @param BlocktrailSDKInterface $sdk SDK instance used to do requests |
|
129
|
|
|
* @param string $identifier identifier of the wallet |
|
130
|
|
|
* @param string $primaryMnemonic |
|
131
|
|
|
* @param array[string, string] $primaryPublicKeys |
|
132
|
|
|
* @param array[string, string] $backupPublicKey should be BIP32 master public key M/ |
|
133
|
|
|
* @param array[array[string, string]] $blocktrailPublicKeys |
|
134
|
|
|
* @param int $keyIndex |
|
135
|
|
|
* @param string $network |
|
136
|
|
|
* @param bool $testnet |
|
137
|
|
|
* @param string $checksum |
|
138
|
|
|
*/ |
|
139
|
|
|
public function __construct(BlocktrailSDKInterface $sdk, $identifier, $primaryMnemonic, $primaryPublicKeys, $backupPublicKey, $blocktrailPublicKeys, $keyIndex, $network, $testnet, $checksum) { |
|
140
|
|
|
$this->sdk = $sdk; |
|
141
|
|
|
|
|
142
|
|
|
$this->identifier = $identifier; |
|
143
|
|
|
|
|
144
|
|
|
$this->primaryMnemonic = $primaryMnemonic; |
|
145
|
|
|
$this->backupPublicKey = BIP32Key::create($backupPublicKey); |
|
146
|
|
|
$this->primaryPublicKeys = array_map(function ($key) { |
|
147
|
|
|
return BIP32Key::create($key); |
|
148
|
|
|
}, $primaryPublicKeys); |
|
149
|
|
|
$this->blocktrailPublicKeys = array_map(function ($key) { |
|
150
|
|
|
return BIP32Key::create($key); |
|
151
|
|
|
}, $blocktrailPublicKeys); |
|
152
|
|
|
|
|
153
|
|
|
$this->network = $network; |
|
154
|
|
|
$this->testnet = $testnet; |
|
155
|
|
|
$this->keyIndex = $keyIndex; |
|
156
|
|
|
$this->checksum = $checksum; |
|
157
|
|
|
|
|
158
|
|
|
$this->walletPath = WalletPath::create($this->keyIndex); |
|
159
|
|
|
} |
|
160
|
|
|
|
|
161
|
|
|
/** |
|
162
|
|
|
* return the wallet identifier |
|
163
|
|
|
* |
|
164
|
|
|
* @return string |
|
165
|
|
|
*/ |
|
166
|
|
|
public function getIdentifier() { |
|
167
|
|
|
return $this->identifier; |
|
168
|
|
|
} |
|
169
|
|
|
|
|
170
|
|
|
/** |
|
171
|
|
|
* return the wallet primary mnemonic (for backup purposes) |
|
172
|
|
|
* |
|
173
|
|
|
* @return string |
|
174
|
|
|
*/ |
|
175
|
|
|
public function getPrimaryMnemonic() { |
|
176
|
|
|
return $this->primaryMnemonic; |
|
177
|
|
|
} |
|
178
|
|
|
|
|
179
|
|
|
/** |
|
180
|
|
|
* return list of Blocktrail co-sign extended public keys |
|
181
|
|
|
* |
|
182
|
|
|
* @return array[] [ [xpub, path] ] |
|
183
|
|
|
*/ |
|
184
|
|
|
public function getBlocktrailPublicKeys() { |
|
185
|
|
|
return array_map(function (BIP32Key $key) { |
|
186
|
|
|
return $key->tuple(); |
|
187
|
|
|
}, $this->blocktrailPublicKeys); |
|
188
|
|
|
} |
|
189
|
|
|
|
|
190
|
|
|
/** |
|
191
|
|
|
* unlock wallet so it can be used for payments |
|
192
|
|
|
* |
|
193
|
|
|
* @param $options ['primary_private_key' => key] OR ['passphrase' => pass] |
|
194
|
|
|
* @param callable $fn |
|
195
|
|
|
* @return bool |
|
196
|
|
|
* @throws \Exception |
|
197
|
|
|
*/ |
|
198
|
|
|
public function unlock($options, callable $fn = null) { |
|
199
|
|
|
// explode the wallet data |
|
200
|
|
|
$password = isset($options['passphrase']) ? $options['passphrase'] : (isset($options['password']) ? $options['password'] : null); |
|
201
|
|
|
$primaryMnemonic = $this->primaryMnemonic; |
|
202
|
|
|
$primaryPrivateKey = isset($options['primary_private_key']) ? $options['primary_private_key'] : null; |
|
203
|
|
|
|
|
204
|
|
|
if ($primaryMnemonic && $primaryPrivateKey) { |
|
205
|
|
|
throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey"); |
|
206
|
|
|
} |
|
207
|
|
|
|
|
208
|
|
|
if (!$primaryMnemonic && !$primaryPrivateKey) { |
|
209
|
|
|
throw new \InvalidArgumentException("Can't init wallet with Primary Mnemonic or Primary PrivateKey"); |
|
210
|
|
|
} |
|
211
|
|
|
|
|
212
|
|
|
if ($primaryMnemonic && !$password) { |
|
213
|
|
|
throw new \InvalidArgumentException("Can't init wallet with Primary Mnemonic without a passphrase"); |
|
214
|
|
|
} |
|
215
|
|
|
|
|
216
|
|
View Code Duplication |
if ($primaryPrivateKey) { |
|
|
|
|
|
|
217
|
|
|
if (is_string($primaryPrivateKey)) { |
|
218
|
|
|
$primaryPrivateKey = [$primaryPrivateKey, "m"]; |
|
219
|
|
|
} |
|
220
|
|
|
} else { |
|
221
|
|
|
// convert the mnemonic to a seed using BIP39 standard |
|
222
|
|
|
$primarySeed = BIP39::mnemonicToSeedHex($primaryMnemonic, $password); |
|
223
|
|
|
// create BIP32 private key from the seed |
|
224
|
|
|
$primaryPrivateKey = BIP32::master_key($primarySeed, $this->network, $this->testnet); |
|
|
|
|
|
|
225
|
|
|
} |
|
226
|
|
|
|
|
227
|
|
|
$this->primaryPrivateKey = BIP32Key::create($primaryPrivateKey); |
|
228
|
|
|
|
|
229
|
|
|
// create checksum (address) of the primary privatekey to compare to the stored checksum |
|
230
|
|
|
$checksum = BIP32::key_to_address($primaryPrivateKey[0]); |
|
231
|
|
|
if ($checksum != $this->checksum) { |
|
232
|
|
|
throw new \Exception("Checksum [{$checksum}] does not match [{$this->checksum}], most likely due to incorrect password"); |
|
233
|
|
|
} |
|
234
|
|
|
|
|
235
|
|
|
$this->locked = false; |
|
236
|
|
|
|
|
237
|
|
|
// if the response suggests we should upgrade to a different blocktrail cosigning key then we should |
|
238
|
|
|
if (isset($data['upgrade_key_index'])) { |
|
|
|
|
|
|
239
|
|
|
$this->upgradeKeyIndex($data['upgrade_key_index']); |
|
240
|
|
|
} |
|
241
|
|
|
|
|
242
|
|
|
if ($fn) { |
|
243
|
|
|
$fn($this); |
|
244
|
|
|
$this->lock(); |
|
245
|
|
|
} |
|
246
|
|
|
} |
|
247
|
|
|
|
|
248
|
|
|
/** |
|
249
|
|
|
* lock the wallet (unsets primary private key) |
|
250
|
|
|
* |
|
251
|
|
|
* @return void |
|
252
|
|
|
*/ |
|
253
|
|
|
public function lock() { |
|
254
|
|
|
$this->primaryPrivateKey = null; |
|
255
|
|
|
$this->locked = true; |
|
256
|
|
|
} |
|
257
|
|
|
|
|
258
|
|
|
/** |
|
259
|
|
|
* check if wallet is locked |
|
260
|
|
|
* |
|
261
|
|
|
* @return bool |
|
262
|
|
|
*/ |
|
263
|
|
|
public function isLocked() { |
|
264
|
|
|
return $this->locked; |
|
265
|
|
|
} |
|
266
|
|
|
|
|
267
|
|
|
/** |
|
268
|
|
|
* upgrade wallet to different blocktrail cosign key |
|
269
|
|
|
* |
|
270
|
|
|
* @param $keyIndex |
|
271
|
|
|
* @return bool |
|
272
|
|
|
* @throws \Exception |
|
273
|
|
|
*/ |
|
274
|
|
|
public function upgradeKeyIndex($keyIndex) { |
|
275
|
|
|
if ($this->locked) { |
|
276
|
|
|
throw new \Exception("Wallet needs to be unlocked to upgrade key index"); |
|
277
|
|
|
} |
|
278
|
|
|
|
|
279
|
|
|
$walletPath = WalletPath::create($keyIndex); |
|
280
|
|
|
|
|
281
|
|
|
// do the upgrade to the new 'key_index' |
|
282
|
|
|
$primaryPublicKey = BIP32::extended_private_to_public(BIP32::build_key($this->primaryPrivateKey->tuple(), (string)$walletPath->keyIndexPath())); |
|
|
|
|
|
|
283
|
|
|
$result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey); |
|
|
|
|
|
|
284
|
|
|
|
|
285
|
|
|
$this->primaryPublicKeys[$keyIndex] = BIP32Key::create($primaryPublicKey); |
|
286
|
|
|
|
|
287
|
|
|
$this->keyIndex = $keyIndex; |
|
288
|
|
|
$this->walletPath = $walletPath; |
|
289
|
|
|
|
|
290
|
|
|
// update the blocktrail public keys |
|
291
|
|
|
foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) { |
|
292
|
|
|
if (!isset($this->blocktrailPublicKeys[$keyIndex])) { |
|
293
|
|
|
$this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create($pubKey); |
|
294
|
|
|
} |
|
295
|
|
|
} |
|
296
|
|
|
|
|
297
|
|
|
return true; |
|
298
|
|
|
} |
|
299
|
|
|
|
|
300
|
|
|
/** |
|
301
|
|
|
* get a new BIP32 derivation for the next (unused) address |
|
302
|
|
|
* by requesting it from the API |
|
303
|
|
|
* |
|
304
|
|
|
* @return string |
|
305
|
|
|
* @throws \Exception |
|
306
|
|
|
*/ |
|
307
|
|
|
protected function getNewDerivation() { |
|
308
|
|
|
$path = $this->walletPath->path()->last("*"); |
|
309
|
|
|
|
|
310
|
|
|
if (self::VERIFY_NEW_DERIVATION) { |
|
311
|
|
|
$new = $this->sdk->_getNewDerivation($this->identifier, (string)$path); |
|
312
|
|
|
|
|
313
|
|
|
$path = $new['path']; |
|
314
|
|
|
$address = $new['address']; |
|
315
|
|
|
$redeemScript = $new['redeem_script']; |
|
316
|
|
|
|
|
317
|
|
|
list($checkAddress, $checkRedeemScript) = $this->getRedeemScriptByPath($path); |
|
318
|
|
|
|
|
319
|
|
|
if ($checkAddress != $address) { |
|
320
|
|
|
throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]"); |
|
321
|
|
|
} |
|
322
|
|
|
|
|
323
|
|
|
if ($checkRedeemScript != $redeemScript) { |
|
324
|
|
|
throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript}]"); |
|
325
|
|
|
} |
|
326
|
|
|
} else { |
|
327
|
|
|
$path = $this->sdk->getNewDerivation($this->identifier, (string)$path); |
|
328
|
|
|
} |
|
329
|
|
|
|
|
330
|
|
|
return (string)$path; |
|
331
|
|
|
} |
|
332
|
|
|
|
|
333
|
|
|
/** |
|
334
|
|
|
* @param string|BIP32Path $path |
|
335
|
|
|
* @return BIP32Key|false |
|
336
|
|
|
* @throws \Exception |
|
337
|
|
|
* |
|
338
|
|
|
* @TODO: hmm? |
|
339
|
|
|
*/ |
|
340
|
|
|
protected function getParentPublicKey($path) { |
|
341
|
|
|
$path = BIP32Path::path($path)->parent()->publicPath(); |
|
342
|
|
|
|
|
343
|
|
|
if ($path->count() <= 2) { |
|
344
|
|
|
return false; |
|
345
|
|
|
} |
|
346
|
|
|
|
|
347
|
|
|
if ($path->isHardened()) { |
|
348
|
|
|
return false; |
|
349
|
|
|
} |
|
350
|
|
|
|
|
351
|
|
|
if (!isset($this->pubKeys[(string)$path])) { |
|
352
|
|
|
$this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path); |
|
353
|
|
|
} |
|
354
|
|
|
|
|
355
|
|
|
return $this->pubKeys[(string)$path]; |
|
356
|
|
|
} |
|
357
|
|
|
|
|
358
|
|
|
/** |
|
359
|
|
|
* get address for the specified path |
|
360
|
|
|
* |
|
361
|
|
|
* @param string|BIP32Path $path |
|
362
|
|
|
* @return string |
|
363
|
|
|
*/ |
|
364
|
|
|
public function getAddressByPath($path) { |
|
365
|
|
|
$path = (string)BIP32Path::path($path)->privatePath(); |
|
366
|
|
|
if (!isset($this->derivations[$path])) { |
|
367
|
|
|
list($address, ) = $this->getRedeemScriptByPath($path); |
|
368
|
|
|
|
|
369
|
|
|
$this->derivations[$path] = $address; |
|
370
|
|
|
$this->derivationsByAddress[$address] = $path; |
|
371
|
|
|
} |
|
372
|
|
|
|
|
373
|
|
|
return $this->derivations[$path]; |
|
|
|
|
|
|
374
|
|
|
} |
|
375
|
|
|
|
|
376
|
|
|
/** |
|
377
|
|
|
* get address and redeemScript for specified path |
|
378
|
|
|
* |
|
379
|
|
|
* @param string $path |
|
380
|
|
|
* @return array[string, string] [address, redeemScript] |
|
|
|
|
|
|
381
|
|
|
*/ |
|
382
|
|
|
public function getRedeemScriptByPath($path) { |
|
383
|
|
|
$path = BIP32Path::path($path); |
|
384
|
|
|
|
|
385
|
|
|
// optimization to avoid doing BitcoinLib::private_key_to_public_key too much |
|
386
|
|
|
if ($pubKey = $this->getParentPublicKey($path)) { |
|
387
|
|
|
$key = $pubKey->buildKey($path->publicPath()); |
|
388
|
|
|
} else { |
|
389
|
|
|
$key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path); |
|
390
|
|
|
} |
|
391
|
|
|
|
|
392
|
|
|
return $this->getRedeemScriptFromKey($key, $path); |
|
393
|
|
|
} |
|
394
|
|
|
|
|
395
|
|
|
/** |
|
396
|
|
|
* @param BIP32Key $key |
|
397
|
|
|
* @param string|BIP32Path $path |
|
398
|
|
|
* @return string |
|
399
|
|
|
*/ |
|
400
|
|
|
protected function getAddressFromKey(BIP32Key $key, $path) { |
|
401
|
|
|
return $this->getRedeemScriptFromKey($key, $path)[0]; |
|
402
|
|
|
} |
|
403
|
|
|
|
|
404
|
|
|
/** |
|
405
|
|
|
* @param BIP32Key $key |
|
406
|
|
|
* @param string|BIP32Path $path |
|
407
|
|
|
* @return string[] [address, redeemScript] |
|
408
|
|
|
* @throws \Exception |
|
409
|
|
|
*/ |
|
410
|
|
|
protected function getRedeemScriptFromKey(BIP32Key $key, $path) { |
|
411
|
|
|
$path = BIP32Path::path($path)->publicPath(); |
|
412
|
|
|
|
|
413
|
|
|
$blocktrailPublicKey = $this->getBlocktrailPublicKey($path); |
|
414
|
|
|
|
|
415
|
|
|
$multiSig = RawTransaction::create_multisig( |
|
416
|
|
|
2, |
|
417
|
|
|
BlocktrailSDK::sortMultisigKeys([ |
|
|
|
|
|
|
418
|
|
|
$key->buildKey($path)->publicKey(), |
|
419
|
|
|
$this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(), |
|
420
|
|
|
$blocktrailPublicKey->buildKey($path)->publicKey() |
|
421
|
|
|
]) |
|
422
|
|
|
); |
|
423
|
|
|
|
|
424
|
|
|
return [$multiSig['address'], $multiSig['redeemScript']]; |
|
425
|
|
|
} |
|
426
|
|
|
|
|
427
|
|
|
/** |
|
428
|
|
|
* get the path (and redeemScript) to specified address |
|
429
|
|
|
* |
|
430
|
|
|
* @param string $address |
|
431
|
|
|
* @return array |
|
432
|
|
|
*/ |
|
433
|
|
|
public function getPathForAddress($address) { |
|
434
|
|
|
return $this->sdk->getPathForAddress($this->identifier, $address); |
|
435
|
|
|
} |
|
436
|
|
|
|
|
437
|
|
|
/** |
|
438
|
|
|
* @param string|BIP32Path $path |
|
439
|
|
|
* @return BIP32Key |
|
440
|
|
|
* @throws \Exception |
|
441
|
|
|
*/ |
|
442
|
|
View Code Duplication |
public function getBlocktrailPublicKey($path) { |
|
|
|
|
|
|
443
|
|
|
$path = BIP32Path::path($path); |
|
444
|
|
|
|
|
445
|
|
|
$keyIndex = str_replace("'", "", $path[1]); |
|
446
|
|
|
|
|
447
|
|
|
if (!isset($this->blocktrailPublicKeys[$keyIndex])) { |
|
448
|
|
|
throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]"); |
|
449
|
|
|
} |
|
450
|
|
|
|
|
451
|
|
|
return $this->blocktrailPublicKeys[$keyIndex]; |
|
452
|
|
|
} |
|
453
|
|
|
|
|
454
|
|
|
/** |
|
455
|
|
|
* generate a new derived key and return the new path and address for it |
|
456
|
|
|
* |
|
457
|
|
|
* @return string[] [path, address] |
|
458
|
|
|
*/ |
|
459
|
|
|
public function getNewAddressPair() { |
|
460
|
|
|
$path = $this->getNewDerivation(); |
|
461
|
|
|
$address = $this->getAddressByPath($path); |
|
462
|
|
|
|
|
463
|
|
|
return [$path, $address]; |
|
|
|
|
|
|
464
|
|
|
} |
|
465
|
|
|
|
|
466
|
|
|
/** |
|
467
|
|
|
* generate a new derived private key and return the new address for it |
|
468
|
|
|
* |
|
469
|
|
|
* @return string |
|
470
|
|
|
*/ |
|
471
|
|
|
public function getNewAddress() { |
|
472
|
|
|
return $this->getNewAddressPair()[1]; |
|
|
|
|
|
|
473
|
|
|
} |
|
474
|
|
|
|
|
475
|
|
|
/** |
|
476
|
|
|
* get the balance for the wallet |
|
477
|
|
|
* |
|
478
|
|
|
* @return int[] [confirmed, unconfirmed] |
|
479
|
|
|
*/ |
|
480
|
|
|
public function getBalance() { |
|
481
|
|
|
$balanceInfo = $this->sdk->getWalletBalance($this->identifier); |
|
482
|
|
|
|
|
483
|
|
|
return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']]; |
|
484
|
|
|
} |
|
485
|
|
|
|
|
486
|
|
|
/** |
|
487
|
|
|
* do wallet discovery (slow) |
|
488
|
|
|
* |
|
489
|
|
|
* @param int $gap the gap setting to use for discovery |
|
490
|
|
|
* @return int[] [confirmed, unconfirmed] |
|
491
|
|
|
*/ |
|
492
|
|
|
public function doDiscovery($gap = 200) { |
|
493
|
|
|
$balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap); |
|
494
|
|
|
|
|
495
|
|
|
return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']]; |
|
496
|
|
|
} |
|
497
|
|
|
|
|
498
|
|
|
/** |
|
499
|
|
|
* create, sign and send a transaction |
|
500
|
|
|
* |
|
501
|
|
|
* @param array $outputs [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send |
|
502
|
|
|
* value should be INT |
|
503
|
|
|
* @param string $changeAddress change address to use (autogenerated if NULL) |
|
504
|
|
|
* @param bool $allowZeroConf |
|
505
|
|
|
* @param bool $randomizeChangeIdx randomize the location of the change (for increased privacy / anonimity) |
|
506
|
|
|
* @param string $feeStrategy |
|
507
|
|
|
* @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended! |
|
508
|
|
|
* @return string the txid / transaction hash |
|
509
|
|
|
* @throws \Exception |
|
510
|
|
|
*/ |
|
511
|
|
|
public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) { |
|
512
|
|
|
if ($this->locked) { |
|
513
|
|
|
throw new \Exception("Wallet needs to be unlocked to pay"); |
|
514
|
|
|
} |
|
515
|
|
|
|
|
516
|
|
|
$outputs = self::normalizeOutputsStruct($outputs); |
|
517
|
|
|
|
|
518
|
|
|
$txBuilder = new TransactionBuilder(); |
|
519
|
|
|
$txBuilder->randomizeChangeOutput($randomizeChangeIdx); |
|
520
|
|
|
$txBuilder->setFeeStrategy($feeStrategy); |
|
521
|
|
|
|
|
522
|
|
|
foreach ($outputs as $output) { |
|
523
|
|
|
$txBuilder->addRecipient($output['address'], $output['value']); |
|
524
|
|
|
} |
|
525
|
|
|
|
|
526
|
|
|
$this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee); |
|
527
|
|
|
|
|
528
|
|
|
$apiCheckFee = $forceFee === null; |
|
529
|
|
|
|
|
530
|
|
|
return $this->sendTx($txBuilder, $apiCheckFee); |
|
531
|
|
|
} |
|
532
|
|
|
|
|
533
|
|
|
/** |
|
534
|
|
|
* determine max spendable from wallet after fees |
|
535
|
|
|
* |
|
536
|
|
|
* @param bool $allowZeroConf |
|
537
|
|
|
* @param string $feeStrategy |
|
538
|
|
|
* @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended! |
|
539
|
|
|
* @param int $outputCnt |
|
540
|
|
|
* @return string |
|
541
|
|
|
* @throws BlocktrailSDKException |
|
542
|
|
|
*/ |
|
543
|
|
|
public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) { |
|
544
|
|
|
return $this->sdk->walletMaxSpendable($this->identifier, $allowZeroConf, $feeStrategy, $forceFee, $outputCnt); |
|
|
|
|
|
|
545
|
|
|
} |
|
546
|
|
|
|
|
547
|
|
|
/** |
|
548
|
|
|
* parse outputs into normalized struct |
|
549
|
|
|
* |
|
550
|
|
|
* @param array $outputs [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] |
|
551
|
|
|
* @return array [['address' => address, 'value' => value], ] |
|
552
|
|
|
*/ |
|
553
|
|
|
public static function normalizeOutputsStruct(array $outputs) { |
|
554
|
|
|
$result = []; |
|
555
|
|
|
|
|
556
|
|
|
foreach ($outputs as $k => $v) { |
|
557
|
|
|
if (is_numeric($k)) { |
|
558
|
|
|
if (!is_array($v)) { |
|
559
|
|
|
throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]"); |
|
560
|
|
|
} |
|
561
|
|
|
|
|
562
|
|
|
if (isset($v['address']) && isset($v['value'])) { |
|
563
|
|
|
$address = $v['address']; |
|
564
|
|
|
$value = $v['value']; |
|
565
|
|
|
} else if (count($v) == 2 && isset($v[0]) && isset($v[1])) { |
|
566
|
|
|
$address = $v[0]; |
|
567
|
|
|
$value = $v[1]; |
|
568
|
|
|
} else { |
|
569
|
|
|
throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]"); |
|
570
|
|
|
} |
|
571
|
|
|
} else { |
|
572
|
|
|
$address = $k; |
|
573
|
|
|
$value = $v; |
|
574
|
|
|
} |
|
575
|
|
|
|
|
576
|
|
|
$result[] = ['address' => $address, 'value' => $value]; |
|
577
|
|
|
} |
|
578
|
|
|
|
|
579
|
|
|
return $result; |
|
580
|
|
|
} |
|
581
|
|
|
|
|
582
|
|
|
/** |
|
583
|
|
|
* 'fund' the txBuilder with UTXOs (modified in place) |
|
584
|
|
|
* |
|
585
|
|
|
* @param TransactionBuilder $txBuilder |
|
586
|
|
|
* @param bool|true $lockUTXOs |
|
587
|
|
|
* @param bool|false $allowZeroConf |
|
588
|
|
|
* @param null|int $forceFee |
|
589
|
|
|
* @return TransactionBuilder |
|
590
|
|
|
*/ |
|
591
|
|
|
public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) { |
|
592
|
|
|
// get the data we should use for this transaction |
|
593
|
|
|
$coinSelection = $this->coinSelection($txBuilder->getOutputs(), $lockUTXOs, $allowZeroConf, $txBuilder->getFeeStrategy(), $forceFee); |
|
594
|
|
|
$utxos = $coinSelection['utxos']; |
|
595
|
|
|
$fee = $coinSelection['fee']; |
|
596
|
|
|
$change = $coinSelection['change']; |
|
|
|
|
|
|
597
|
|
|
|
|
598
|
|
|
if ($forceFee !== null) { |
|
599
|
|
|
$txBuilder->setFee($forceFee); |
|
600
|
|
|
} else { |
|
601
|
|
|
$txBuilder->validateFee($fee); |
|
602
|
|
|
} |
|
603
|
|
|
|
|
604
|
|
|
foreach ($utxos as $utxo) { |
|
605
|
|
|
$txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script']); |
|
606
|
|
|
} |
|
607
|
|
|
|
|
608
|
|
|
return $txBuilder; |
|
609
|
|
|
} |
|
610
|
|
|
|
|
611
|
|
|
/** |
|
612
|
|
|
* build inputs and outputs lists for TransactionBuilder |
|
613
|
|
|
* |
|
614
|
|
|
* @param TransactionBuilder $txBuilder |
|
615
|
|
|
* @return array |
|
616
|
|
|
* @throws \Exception |
|
617
|
|
|
*/ |
|
618
|
|
|
public function buildTx(TransactionBuilder $txBuilder) { |
|
619
|
|
|
$send = $txBuilder->getOutputs(); |
|
620
|
|
|
|
|
621
|
|
|
$utxos = $txBuilder->getUtxos(); |
|
622
|
|
|
|
|
623
|
|
|
foreach ($utxos as $utxo) { |
|
624
|
|
|
if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKeyHex) { |
|
625
|
|
|
$tx = $this->sdk->transaction($utxo->hash); |
|
626
|
|
|
|
|
627
|
|
|
if (!$tx || !isset($tx['outputs'][$utxo->index])) { |
|
|
|
|
|
|
628
|
|
|
throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]"); |
|
629
|
|
|
} |
|
630
|
|
|
|
|
631
|
|
|
$output = $tx['outputs'][$utxo->index]; |
|
632
|
|
|
|
|
633
|
|
|
if (!$utxo->address) { |
|
634
|
|
|
$utxo->address = $output['address']; |
|
635
|
|
|
} |
|
636
|
|
|
if (!$utxo->value) { |
|
637
|
|
|
$utxo->value = $output['value']; |
|
638
|
|
|
} |
|
639
|
|
|
if (!$utxo->scriptPubKeyHex) { |
|
640
|
|
|
$utxo->scriptPubKeyHex = $output['script_hex']; |
|
641
|
|
|
} |
|
642
|
|
|
} |
|
643
|
|
|
|
|
644
|
|
|
if (!$utxo->path) { |
|
645
|
|
|
$address = $utxo->address; |
|
646
|
|
|
if (!BitcoinLib::validate_address($address)) { |
|
647
|
|
|
throw new \Exception("Invalid address [{$address}]"); |
|
648
|
|
|
} |
|
649
|
|
|
|
|
650
|
|
|
$utxo->path = $this->getPathForAddress($address); |
|
651
|
|
|
} |
|
652
|
|
|
|
|
653
|
|
|
if (!$utxo->redeemScript) { |
|
654
|
|
|
list(, $redeemScript) = $this->getRedeemScriptByPath($utxo->path); |
|
655
|
|
|
$utxo->redeemScript = $redeemScript; |
|
656
|
|
|
} |
|
657
|
|
|
} |
|
658
|
|
|
|
|
659
|
|
|
if (array_sum(array_map(function (UTXO $utxo) { return $utxo->value; }, $utxos)) < array_sum(array_column($send, 'value'))) { |
|
660
|
|
|
throw new \Exception("Atempting to spend more than sum of UTXOs"); |
|
661
|
|
|
} |
|
662
|
|
|
|
|
663
|
|
|
list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB()); |
|
664
|
|
|
|
|
665
|
|
|
if ($txBuilder->getValidateFee() !== null) { |
|
666
|
|
|
if (abs($txBuilder->getValidateFee() - $fee) > Wallet::BASE_FEE) { |
|
667
|
|
|
throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})"); |
|
668
|
|
|
} |
|
669
|
|
|
} |
|
670
|
|
|
|
|
671
|
|
|
if ($change > 0) { |
|
672
|
|
|
$send[] = [ |
|
673
|
|
|
'address' => $txBuilder->getChangeAddress() ?: $this->getNewAddress(), |
|
674
|
|
|
'value' => $change |
|
675
|
|
|
]; |
|
676
|
|
|
} |
|
677
|
|
|
|
|
678
|
|
|
// create raw transaction |
|
679
|
|
|
$inputs = array_map(function (UTXO $utxo) { |
|
680
|
|
|
return [ |
|
681
|
|
|
'txid' => $utxo->hash, |
|
682
|
|
|
'vout' => (int)$utxo->index, |
|
683
|
|
|
'address' => $utxo->address, |
|
684
|
|
|
'scriptPubKey' => $utxo->scriptPubKeyHex, |
|
685
|
|
|
'value' => $utxo->value, |
|
686
|
|
|
'path' => $utxo->path, |
|
687
|
|
|
'redeemScript' => $utxo->redeemScript |
|
688
|
|
|
]; |
|
689
|
|
|
}, $utxos); |
|
690
|
|
|
|
|
691
|
|
|
|
|
692
|
|
|
// outputs should be randomized to make the change harder to detect |
|
693
|
|
|
if ($txBuilder->shouldRandomizeChangeOuput()) { |
|
694
|
|
|
shuffle($send); |
|
695
|
|
|
} |
|
696
|
|
|
|
|
697
|
|
|
return [$inputs, $send]; |
|
698
|
|
|
} |
|
699
|
|
|
|
|
700
|
|
|
public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) { |
|
701
|
|
|
$send = $txBuilder->getOutputs(); |
|
702
|
|
|
$utxos = $txBuilder->getUtxos(); |
|
703
|
|
|
|
|
704
|
|
|
$fee = $txBuilder->getFee(); |
|
705
|
|
|
$change = null; |
|
|
|
|
|
|
706
|
|
|
|
|
707
|
|
|
// if the fee is fixed we just need to calculate the change |
|
708
|
|
|
if ($fee !== null) { |
|
709
|
|
|
$change = $this->determineChange($utxos, $send, $fee); |
|
710
|
|
|
|
|
711
|
|
|
// if change is not dust we need to add a change output |
|
712
|
|
View Code Duplication |
if ($change > Blocktrail::DUST) { |
|
|
|
|
|
|
713
|
|
|
$send[] = ['address' => 'change', 'value' => $change]; |
|
714
|
|
|
} else { |
|
715
|
|
|
// if change is dust we do nothing (implicitly it's added to the fee) |
|
716
|
|
|
$change = 0; |
|
717
|
|
|
} |
|
718
|
|
|
} else { |
|
719
|
|
|
$fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB); |
|
720
|
|
|
|
|
721
|
|
|
$change = $this->determineChange($utxos, $send, $fee); |
|
722
|
|
|
|
|
723
|
|
|
if ($change > 0) { |
|
724
|
|
|
$changeIdx = count($send); |
|
725
|
|
|
// set dummy change output |
|
726
|
|
|
$send[$changeIdx] = ['address' => 'change', 'value' => $change]; |
|
727
|
|
|
|
|
728
|
|
|
// recaculate fee now that we know that we have a change output |
|
729
|
|
|
$fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB); |
|
730
|
|
|
|
|
731
|
|
|
// unset dummy change output |
|
732
|
|
|
unset($send[$changeIdx]); |
|
733
|
|
|
|
|
734
|
|
|
// if adding the change output made the fee bump up and the change is smaller than the fee |
|
735
|
|
|
// then we're not doing change |
|
736
|
|
|
if ($fee2 > $fee && $fee2 > $change) { |
|
737
|
|
|
$change = 0; |
|
738
|
|
|
} else { |
|
739
|
|
|
$change = $this->determineChange($utxos, $send, $fee2); |
|
740
|
|
|
|
|
741
|
|
|
// if change is not dust we need to add a change output |
|
742
|
|
View Code Duplication |
if ($change > Blocktrail::DUST) { |
|
|
|
|
|
|
743
|
|
|
$send[$changeIdx] = ['address' => 'change', 'value' => $change]; |
|
744
|
|
|
} else { |
|
745
|
|
|
// if change is dust we do nothing (implicitly it's added to the fee) |
|
746
|
|
|
$change = 0; |
|
747
|
|
|
} |
|
748
|
|
|
} |
|
749
|
|
|
} |
|
750
|
|
|
} |
|
751
|
|
|
|
|
752
|
|
|
$fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB); |
|
753
|
|
|
|
|
754
|
|
|
return [$fee, $change]; |
|
755
|
|
|
} |
|
756
|
|
|
|
|
757
|
|
|
/** |
|
758
|
|
|
* create, sign and send transction based on TransactionBuilder |
|
759
|
|
|
* |
|
760
|
|
|
* @param TransactionBuilder $txBuilder |
|
761
|
|
|
* @param bool $apiCheckFee let the API check if the fee is correct |
|
762
|
|
|
* @return string |
|
763
|
|
|
* @throws \Exception |
|
764
|
|
|
*/ |
|
765
|
|
|
public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) { |
|
766
|
|
|
list($inputs, $outputs) = $this->buildTx($txBuilder); |
|
767
|
|
|
|
|
768
|
|
|
return $this->_sendTx($inputs, $outputs, $apiCheckFee); |
|
769
|
|
|
} |
|
770
|
|
|
|
|
771
|
|
|
/** |
|
772
|
|
|
* !! INTERNAL METHOD, public for testing purposes !! |
|
773
|
|
|
* create, sign and send transction based on inputs and outputs |
|
774
|
|
|
* |
|
775
|
|
|
* @param $inputs |
|
776
|
|
|
* @param $outputs |
|
777
|
|
|
* @param bool $apiCheckFee let the API check if the fee is correct |
|
778
|
|
|
* @return string |
|
779
|
|
|
* @throws \Exception |
|
780
|
|
|
* @internal |
|
781
|
|
|
*/ |
|
782
|
|
|
public function _sendTx($inputs, $outputs, $apiCheckFee = true) { |
|
783
|
|
|
if ($this->locked) { |
|
784
|
|
|
throw new \Exception("Wallet needs to be unlocked to pay"); |
|
785
|
|
|
} |
|
786
|
|
|
|
|
787
|
|
|
// create raw unsigned TX |
|
788
|
|
|
$raw_transaction = RawTransaction::create($inputs, $outputs); |
|
789
|
|
|
|
|
790
|
|
|
if (!$raw_transaction) { |
|
791
|
|
|
throw new \Exception("Failed to create raw transaction"); |
|
792
|
|
|
} |
|
793
|
|
|
|
|
794
|
|
|
// sign the transaction with our keys |
|
795
|
|
|
$signed = $this->signTransaction($raw_transaction, $inputs); |
|
796
|
|
|
|
|
797
|
|
|
if (!$signed['sign_count']) { |
|
798
|
|
|
throw new \Exception("Failed to partially sign transaction"); |
|
799
|
|
|
} |
|
800
|
|
|
|
|
801
|
|
|
// send the transaction |
|
802
|
|
|
$finished = $this->sendTransaction($signed['hex'], array_column($inputs, 'path'), $apiCheckFee); |
|
803
|
|
|
|
|
804
|
|
|
return $finished; |
|
805
|
|
|
} |
|
806
|
|
|
|
|
807
|
|
|
/** |
|
808
|
|
|
* only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs |
|
809
|
|
|
* |
|
810
|
|
|
* @param int $utxoCnt number of unspent inputs in transaction |
|
811
|
|
|
* @param int $outputCnt number of outputs in transaction |
|
812
|
|
|
* @return float |
|
813
|
|
|
* @access public reminder that people might use this! |
|
814
|
|
|
*/ |
|
815
|
|
|
public static function estimateFee($utxoCnt, $outputCnt) { |
|
816
|
|
|
$size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt)); |
|
817
|
|
|
|
|
818
|
|
|
return self::baseFeeForSize($size); |
|
819
|
|
|
} |
|
820
|
|
|
|
|
821
|
|
|
/** |
|
822
|
|
|
* @param int $size size in bytes |
|
823
|
|
|
* @return int fee in satoshi |
|
824
|
|
|
*/ |
|
825
|
|
|
public static function baseFeeForSize($size) { |
|
826
|
|
|
$sizeKB = (int)ceil($size / 1000); |
|
827
|
|
|
|
|
828
|
|
|
return $sizeKB * self::BASE_FEE; |
|
829
|
|
|
} |
|
830
|
|
|
|
|
831
|
|
|
/** |
|
832
|
|
|
* @param int $txinSize |
|
833
|
|
|
* @param int $txoutSize |
|
834
|
|
|
* @return float |
|
835
|
|
|
*/ |
|
836
|
|
|
public static function estimateSize($txinSize, $txoutSize) { |
|
837
|
|
|
return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime |
|
838
|
|
|
} |
|
839
|
|
|
|
|
840
|
|
|
/** |
|
841
|
|
|
* only supports estimating size for P2PKH/P2SH outputs |
|
842
|
|
|
* |
|
843
|
|
|
* @param int $outputCnt number of outputs in transaction |
|
844
|
|
|
* @return float |
|
845
|
|
|
*/ |
|
846
|
|
|
public static function estimateSizeOutputs($outputCnt) { |
|
847
|
|
|
return ($outputCnt * 34); |
|
848
|
|
|
} |
|
849
|
|
|
|
|
850
|
|
|
/** |
|
851
|
|
|
* only supports estimating size for 2of3 multsig UTXOs |
|
852
|
|
|
* |
|
853
|
|
|
* @param int $utxoCnt number of unspent inputs in transaction |
|
854
|
|
|
* @return float |
|
855
|
|
|
*/ |
|
856
|
|
|
public static function estimateSizeUTXOs($utxoCnt) { |
|
857
|
|
|
$txinSize = 0; |
|
858
|
|
|
|
|
859
|
|
|
for ($i=0; $i<$utxoCnt; $i++) { |
|
860
|
|
|
// @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ... |
|
861
|
|
|
$multisig = "2of3"; |
|
862
|
|
|
|
|
863
|
|
|
if ($multisig) { |
|
864
|
|
|
$sigCnt = 2; |
|
865
|
|
|
$msig = explode("of", $multisig); |
|
866
|
|
|
if (count($msig) == 2 && is_numeric($msig[0])) { |
|
867
|
|
|
$sigCnt = $msig[0]; |
|
868
|
|
|
} |
|
869
|
|
|
|
|
870
|
|
|
$txinSize += array_sum([ |
|
871
|
|
|
32, // txhash |
|
872
|
|
|
4, // idx |
|
873
|
|
|
3, // scriptVarInt[>=253] |
|
|
|
|
|
|
874
|
|
|
((1 + 72) * $sigCnt), // (OP_PUSHDATA[<75] + 72) * sigCnt |
|
|
|
|
|
|
875
|
|
|
(2 + 105) + // OP_PUSHDATA[>=75] + script |
|
876
|
|
|
4, // sequence |
|
877
|
|
|
]); |
|
878
|
|
|
} else { |
|
879
|
|
|
$txinSize += array_sum([ |
|
880
|
|
|
32, // txhash |
|
881
|
|
|
4, // idx |
|
882
|
|
|
73, // sig |
|
883
|
|
|
34, // script |
|
884
|
|
|
4, // sequence |
|
885
|
|
|
]); |
|
886
|
|
|
} |
|
887
|
|
|
} |
|
888
|
|
|
|
|
889
|
|
|
return $txinSize; |
|
890
|
|
|
} |
|
891
|
|
|
|
|
892
|
|
|
/** |
|
893
|
|
|
* determine how much fee is required based on the inputs and outputs |
|
894
|
|
|
* this is an estimation, not a proper 100% correct calculation |
|
895
|
|
|
* |
|
896
|
|
|
* @param UTXO[] $utxos |
|
897
|
|
|
* @param array[] $outputs |
|
898
|
|
|
* @param $feeStrategy |
|
899
|
|
|
* @param $optimalFeePerKB |
|
900
|
|
|
* @param $lowPriorityFeePerKB |
|
901
|
|
|
* @return int |
|
902
|
|
|
* @throws BlocktrailSDKException |
|
903
|
|
|
*/ |
|
904
|
|
|
protected function determineFee($utxos, $outputs, $feeStrategy, $optimalFeePerKB, $lowPriorityFeePerKB) { |
|
905
|
|
|
$outputSize = 0; |
|
906
|
|
|
foreach ($outputs as $output) { |
|
907
|
|
|
if (isset($output['scriptPubKey'])) { |
|
908
|
|
|
$outputSize += strlen($output['scriptPubKey']) / 2; // asume HEX |
|
909
|
|
|
} else { |
|
910
|
|
|
$outputSize += 34; |
|
911
|
|
|
} |
|
912
|
|
|
} |
|
913
|
|
|
|
|
914
|
|
|
$size = self::estimateSize(self::estimateSizeUTXOs(count($utxos)), $outputSize); |
|
915
|
|
|
|
|
916
|
|
|
switch ($feeStrategy) { |
|
917
|
|
|
case self::FEE_STRATEGY_BASE_FEE: |
|
918
|
|
|
return self::baseFeeForSize($size); |
|
919
|
|
|
|
|
920
|
|
|
case self::FEE_STRATEGY_OPTIMAL: |
|
921
|
|
|
return (int)round(($size / 1000) * $optimalFeePerKB); |
|
922
|
|
|
|
|
923
|
|
|
case self::FEE_STRATEGY_LOW_PRIORITY: |
|
924
|
|
|
return (int)round(($size / 1000) * $lowPriorityFeePerKB); |
|
925
|
|
|
|
|
926
|
|
|
default: |
|
927
|
|
|
throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]"); |
|
928
|
|
|
} |
|
929
|
|
|
} |
|
930
|
|
|
|
|
931
|
|
|
/** |
|
932
|
|
|
* determine how much change is left over based on the inputs and outputs and the fee |
|
933
|
|
|
* |
|
934
|
|
|
* @param UTXO[] $utxos |
|
935
|
|
|
* @param array[] $outputs |
|
936
|
|
|
* @param int $fee |
|
937
|
|
|
* @return int |
|
938
|
|
|
*/ |
|
939
|
|
|
protected function determineChange($utxos, $outputs, $fee) { |
|
940
|
|
|
$inputsTotal = array_sum(array_map(function (UTXO $utxo) { |
|
941
|
|
|
return $utxo->value; |
|
942
|
|
|
}, $utxos)); |
|
943
|
|
|
$outputsTotal = array_sum(array_column($outputs, 'value')); |
|
944
|
|
|
|
|
945
|
|
|
return $inputsTotal - $outputsTotal - $fee; |
|
946
|
|
|
} |
|
947
|
|
|
|
|
948
|
|
|
/** |
|
949
|
|
|
* sign a raw transaction with the private keys that we have |
|
950
|
|
|
* |
|
951
|
|
|
* @param string $raw_transaction |
|
952
|
|
|
* @param array[] $inputs |
|
953
|
|
|
* @return array response from RawTransaction::sign |
|
954
|
|
|
* @throws \Exception |
|
955
|
|
|
*/ |
|
956
|
|
|
protected function signTransaction($raw_transaction, array $inputs) { |
|
957
|
|
|
$wallet = []; |
|
958
|
|
|
$keys = []; |
|
959
|
|
|
$redeemScripts = []; |
|
960
|
|
|
|
|
961
|
|
|
foreach ($inputs as $input) { |
|
962
|
|
|
$redeemScript = null; |
|
|
|
|
|
|
963
|
|
|
$key = null; |
|
|
|
|
|
|
964
|
|
|
|
|
965
|
|
|
if (isset($input['redeemScript'], $input['path'])) { |
|
966
|
|
|
$redeemScript = $input['redeemScript']; |
|
967
|
|
|
$path = BIP32Path::path($input['path'])->privatePath(); |
|
968
|
|
|
$key = $this->primaryPrivateKey->buildKey($path); |
|
969
|
|
|
$address = $this->getAddressFromKey($key, $path); |
|
970
|
|
|
|
|
971
|
|
|
if ($address != $input['address']) { |
|
972
|
|
|
throw new \Exception("Generated address does not match expected address!"); |
|
973
|
|
|
} |
|
974
|
|
|
} else { |
|
975
|
|
|
throw new \Exception("No redeemScript/path for input"); |
|
976
|
|
|
} |
|
977
|
|
|
|
|
978
|
|
|
if ($redeemScript && $key) { |
|
979
|
|
|
$keys[] = $key; |
|
980
|
|
|
$redeemScripts[] = $redeemScript; |
|
981
|
|
|
} |
|
982
|
|
|
} |
|
983
|
|
|
|
|
984
|
|
|
BIP32::bip32_keys_to_wallet($wallet, array_map(function (BIP32Key $key) { |
|
985
|
|
|
return $key->tuple(); |
|
986
|
|
|
}, $keys)); |
|
987
|
|
|
RawTransaction::redeem_scripts_to_wallet($wallet, $redeemScripts); |
|
988
|
|
|
|
|
989
|
|
|
return RawTransaction::sign($wallet, $raw_transaction, json_encode($inputs)); |
|
990
|
|
|
} |
|
991
|
|
|
|
|
992
|
|
|
/** |
|
993
|
|
|
* send the transaction using the API |
|
994
|
|
|
* |
|
995
|
|
|
* @param string $signed |
|
996
|
|
|
* @param string[] $paths |
|
997
|
|
|
* @param bool $checkFee |
|
998
|
|
|
* @return string the complete raw transaction |
|
999
|
|
|
* @throws \Exception |
|
1000
|
|
|
*/ |
|
1001
|
|
|
protected function sendTransaction($signed, $paths, $checkFee = false) { |
|
1002
|
|
|
return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee); |
|
1003
|
|
|
} |
|
1004
|
|
|
|
|
1005
|
|
|
/** |
|
1006
|
|
|
* use the API to get the best inputs to use based on the outputs |
|
1007
|
|
|
* |
|
1008
|
|
|
* @param array[] $outputs |
|
1009
|
|
|
* @param bool $lockUTXO |
|
1010
|
|
|
* @param bool $allowZeroConf |
|
1011
|
|
|
* @param string $feeStrategy |
|
1012
|
|
|
* @param null|int $forceFee |
|
1013
|
|
|
* @return array |
|
1014
|
|
|
*/ |
|
1015
|
|
|
public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) { |
|
1016
|
|
|
$result = $this->sdk->coinSelection($this->identifier, $outputs, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee); |
|
1017
|
|
|
|
|
1018
|
|
|
$this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL]; |
|
1019
|
|
|
$this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY]; |
|
1020
|
|
|
$this->feePerKBAge = time(); |
|
1021
|
|
|
|
|
1022
|
|
|
return $result; |
|
1023
|
|
|
} |
|
1024
|
|
|
|
|
1025
|
|
|
public function getOptimalFeePerKB() { |
|
1026
|
|
|
if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) { |
|
1027
|
|
|
$this->updateFeePerKB(); |
|
1028
|
|
|
} |
|
1029
|
|
|
|
|
1030
|
|
|
return $this->optimalFeePerKB; |
|
1031
|
|
|
} |
|
1032
|
|
|
|
|
1033
|
|
|
public function getLowPriorityFeePerKB() { |
|
1034
|
|
|
if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) { |
|
1035
|
|
|
$this->updateFeePerKB(); |
|
1036
|
|
|
} |
|
1037
|
|
|
|
|
1038
|
|
|
return $this->lowPriorityFeePerKB; |
|
1039
|
|
|
} |
|
1040
|
|
|
|
|
1041
|
|
|
public function updateFeePerKB() { |
|
1042
|
|
|
$result = $this->sdk->feePerKB(); |
|
1043
|
|
|
|
|
1044
|
|
|
$this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL]; |
|
1045
|
|
|
$this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY]; |
|
1046
|
|
|
|
|
1047
|
|
|
$this->feePerKBAge = time(); |
|
1048
|
|
|
} |
|
1049
|
|
|
|
|
1050
|
|
|
/** |
|
1051
|
|
|
* delete the wallet |
|
1052
|
|
|
* |
|
1053
|
|
|
* @param bool $force ignore warnings (such as non-zero balance) |
|
1054
|
|
|
* @return mixed |
|
1055
|
|
|
* @throws \Exception |
|
1056
|
|
|
*/ |
|
1057
|
|
|
public function deleteWallet($force = false) { |
|
1058
|
|
|
if ($this->locked) { |
|
1059
|
|
|
throw new \Exception("Wallet needs to be unlocked to delete wallet"); |
|
1060
|
|
|
} |
|
1061
|
|
|
|
|
1062
|
|
|
list($checksumAddress, $signature) = $this->createChecksumVerificationSignature(); |
|
1063
|
|
|
return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted']; |
|
1064
|
|
|
} |
|
1065
|
|
|
|
|
1066
|
|
|
/** |
|
1067
|
|
|
* create checksum to verify ownership of the master primary key |
|
1068
|
|
|
* |
|
1069
|
|
|
* @return string[] [address, signature] |
|
1070
|
|
|
*/ |
|
1071
|
|
|
protected function createChecksumVerificationSignature() { |
|
1072
|
|
|
$import = BIP32::import($this->primaryPrivateKey->key()); |
|
1073
|
|
|
|
|
1074
|
|
|
$public = $this->primaryPrivateKey->publicKey(); |
|
1075
|
|
|
$address = BitcoinLib::public_key_to_address($public, $import['version']); |
|
|
|
|
|
|
1076
|
|
|
|
|
1077
|
|
|
return [$address, BitcoinLib::signMessage($address, $import)]; |
|
1078
|
|
|
} |
|
1079
|
|
|
|
|
1080
|
|
|
/** |
|
1081
|
|
|
* setup a webhook for our wallet |
|
1082
|
|
|
* |
|
1083
|
|
|
* @param string $url URL to receive webhook events |
|
1084
|
|
|
* @param string $identifier identifier for the webhook, defaults to WALLET-{$this->identifier} |
|
1085
|
|
|
* @return array |
|
1086
|
|
|
*/ |
|
1087
|
|
|
public function setupWebhook($url, $identifier = null) { |
|
1088
|
|
|
$identifier = $identifier ?: "WALLET-{$this->identifier}"; |
|
1089
|
|
|
return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url); |
|
1090
|
|
|
} |
|
1091
|
|
|
|
|
1092
|
|
|
/** |
|
1093
|
|
|
* @param string $identifier identifier for the webhook, defaults to WALLET-{$this->identifier} |
|
1094
|
|
|
* @return mixed |
|
1095
|
|
|
*/ |
|
1096
|
|
|
public function deleteWebhook($identifier = null) { |
|
1097
|
|
|
$identifier = $identifier ?: "WALLET-{$this->identifier}"; |
|
1098
|
|
|
return $this->sdk->deleteWalletWebhook($this->identifier, $identifier); |
|
1099
|
|
|
} |
|
1100
|
|
|
|
|
1101
|
|
|
/** |
|
1102
|
|
|
* lock a specific unspent output |
|
1103
|
|
|
* |
|
1104
|
|
|
* @param $txHash |
|
1105
|
|
|
* @param $txIdx |
|
1106
|
|
|
* @param int $ttl |
|
1107
|
|
|
* @return bool |
|
1108
|
|
|
*/ |
|
1109
|
|
|
public function lockUTXO($txHash, $txIdx, $ttl = 3) { |
|
1110
|
|
|
return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl); |
|
1111
|
|
|
} |
|
1112
|
|
|
|
|
1113
|
|
|
/** |
|
1114
|
|
|
* unlock a specific unspent output |
|
1115
|
|
|
* |
|
1116
|
|
|
* @param $txHash |
|
1117
|
|
|
* @param $txIdx |
|
1118
|
|
|
* @return bool |
|
1119
|
|
|
*/ |
|
1120
|
|
|
public function unlockUTXO($txHash, $txIdx) { |
|
1121
|
|
|
return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx); |
|
1122
|
|
|
} |
|
1123
|
|
|
|
|
1124
|
|
|
/** |
|
1125
|
|
|
* get all transactions for the wallet (paginated) |
|
1126
|
|
|
* |
|
1127
|
|
|
* @param integer $page pagination: page number |
|
1128
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
1129
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
1130
|
|
|
* @return array associative array containing the response |
|
1131
|
|
|
*/ |
|
1132
|
|
|
public function transactions($page = 1, $limit = 20, $sortDir = 'asc') { |
|
1133
|
|
|
return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir); |
|
1134
|
|
|
} |
|
1135
|
|
|
|
|
1136
|
|
|
/** |
|
1137
|
|
|
* get all addresses for the wallet (paginated) |
|
1138
|
|
|
* |
|
1139
|
|
|
* @param integer $page pagination: page number |
|
1140
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
1141
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
1142
|
|
|
* @return array associative array containing the response |
|
1143
|
|
|
*/ |
|
1144
|
|
|
public function addresses($page = 1, $limit = 20, $sortDir = 'asc') { |
|
1145
|
|
|
return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir); |
|
1146
|
|
|
} |
|
1147
|
|
|
|
|
1148
|
|
|
/** |
|
1149
|
|
|
* get all UTXOs for the wallet (paginated) |
|
1150
|
|
|
* |
|
1151
|
|
|
* @param integer $page pagination: page number |
|
1152
|
|
|
* @param integer $limit pagination: records per page (max 500) |
|
1153
|
|
|
* @param string $sortDir pagination: sort direction (asc|desc) |
|
1154
|
|
|
* @return array associative array containing the response |
|
1155
|
|
|
*/ |
|
1156
|
|
|
public function utxos($page = 1, $limit = 20, $sortDir = 'asc') { |
|
1157
|
|
|
return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir); |
|
1158
|
|
|
} |
|
1159
|
|
|
} |
|
1160
|
|
|
|
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.