Completed
Pull Request — master (#87)
by thomas
72:13
created

Wallet::transactions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 3
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\AddressFactory;
6
use BitWasp\Bitcoin\Bitcoin;
7
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
8
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
9
use BitWasp\Bitcoin\Script\P2shScript;
10
use BitWasp\Bitcoin\Script\ScriptFactory;
11
use BitWasp\Bitcoin\Script\ScriptInterface;
12
use BitWasp\Bitcoin\Script\WitnessScript;
13
use BitWasp\Bitcoin\Transaction\Factory\SignData;
14
use BitWasp\Bitcoin\Transaction\Factory\Signer;
15
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
16
use BitWasp\Bitcoin\Transaction\OutPoint;
17
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
18
use BitWasp\Bitcoin\Transaction\Transaction;
19
use BitWasp\Bitcoin\Transaction\TransactionInterface;
20
use BitWasp\Buffertools\Buffer;
21
use Blocktrail\SDK\Bitcoin\BIP32Key;
22
use Blocktrail\SDK\Bitcoin\BIP32Path;
23
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
24
25
/**
26
 * Class Wallet
27
 */
28
abstract class Wallet implements WalletInterface {
29
30
    const WALLET_VERSION_V1 = 'v1';
31
    const WALLET_VERSION_V2 = 'v2';
32
    const WALLET_VERSION_V3 = 'v3';
33
34
    const CHAIN_BTC_DEFAULT = 0;
35
    const CHAIN_BCC_DEFAULT = 1;
36
    const CHAIN_BTC_SEGWIT = 2;
37
38
    const BASE_FEE = 10000;
39
40
    /**
41
     * development / debug setting
42
     *  when getting a new derivation from the API,
43
     *  will verify address / redeeemScript with the values the API provides
44
     */
45
    const VERIFY_NEW_DERIVATION = true;
46
47
    /**
48
     * @var BlocktrailSDKInterface
49
     */
50
    protected $sdk;
51
52
    /**
53
     * @var string
54
     */
55
    protected $identifier;
56
57
    /**
58
     * BIP32 master primary private key (m/)
59
     *
60
     * @var BIP32Key
61
     */
62
    protected $primaryPrivateKey;
63
64
    /**
65
     * @var BIP32Key[]
66
     */
67
    protected $primaryPublicKeys;
68
69
    /**
70
     * BIP32 master backup public key (M/)
71
72
     * @var BIP32Key
73
     */
74
    protected $backupPublicKey;
75
76
    /**
77
     * map of blocktrail BIP32 public keys
78
     *  keyed by key index
79
     *  path should be `M / key_index'`
80
     *
81
     * @var BIP32Key[]
82
     */
83
    protected $blocktrailPublicKeys;
84
85
    /**
86
     * the 'Blocktrail Key Index' that is used for new addresses
87
     *
88
     * @var int
89
     */
90
    protected $keyIndex;
91
92
    /**
93
     * 'bitcoin'
94
     *
95
     * @var string
96
     */
97
    protected $network;
98
99
    /**
100
     * testnet yes / no
101
     *
102
     * @var bool
103
     */
104
    protected $testnet;
105
106
    /**
107
     * cache of public keys, by path
108
     *
109
     * @var BIP32Key[]
110
     */
111
    protected $pubKeys = [];
112
113
    /**
114
     * cache of address / redeemScript, by path
115
     *
116
     * @var string[][]      [[address, redeemScript)], ]
117
     */
118
    protected $derivations = [];
119
120
    /**
121
     * reverse cache of paths by address
122
     *
123
     * @var string[]
124
     */
125
    protected $derivationsByAddress = [];
126
127
    /**
128
     * @var WalletPath
129
     * @deprecated
130
     */
131
    protected $walletPath;
132
133
    protected $checksum;
134
135
    /**
136
     * @var bool
137
     */
138
    protected $locked = true;
139
140
    /**
141
     * @var bool
142
     */
143
    protected $isSegwit = false;
144
145
    /**
146
     * @var int
147
     */
148
    protected $chainIndex;
149
150
    /**
151
     * @var int
152
     */
153
    protected $changeIndex;
154
155 19
    protected $optimalFeePerKB;
156 19
    protected $lowPriorityFeePerKB;
157
    protected $feePerKBAge;
158 19
    protected $allowedSignModes = [SignInfo::MODE_DONTSIGN, SignInfo::MODE_SIGN];
159 19
160 19
    /**
161
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
162 19
     * @param string                        $identifier                 identifier of the wallet
163
     * @param BIP32Key[]                    $primaryPublicKeys
164 19
     * @param BIP32Key                      $backupPublicKey            should be BIP32 master public key M/
165 19
     * @param BIP32Key[]                    $blocktrailPublicKeys
166 19
     * @param int                           $keyIndex
167 19
     * @param string                        $network
168
     * @param bool                          $testnet
169 19
     * @param bool                          $segwit
170 19
     * @param string                        $checksum
171 2
     * @throws BlocktrailSDKException
172
     */
173 19
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, array $primaryPublicKeys, $backupPublicKey, array $blocktrailPublicKeys, $keyIndex, $network, $testnet, $segwit, $checksum) {
174
        $this->sdk = $sdk;
175
176
        $this->identifier = $identifier;
177
        $this->backupPublicKey = BlocktrailSDK::normalizeBIP32Key($backupPublicKey);
178
        $this->primaryPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($primaryPublicKeys);
179
        $this->blocktrailPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($blocktrailPublicKeys);
180
181
        $this->network = $network;
182 19
        $this->testnet = $testnet;
183 19
        $this->keyIndex = $keyIndex;
184
        $this->checksum = $checksum;
185
186
        if ($network === "bitcoin") {
187
            if ($segwit) {
188
                $chainIdx = self::CHAIN_BTC_DEFAULT;
189
                $changeIdx = self::CHAIN_BTC_SEGWIT;
190 3
            } else {
191 3
                $chainIdx = self::CHAIN_BTC_DEFAULT;
192
                $changeIdx = self::CHAIN_BTC_DEFAULT;
193
            }
194
        } else {
195
            if ($segwit && $network === "bitcoincash") {
196
                throw new BlocktrailSDKException("Received segwit flag for bitcoincash - abort");
197 3
            }
198 3
            $chainIdx = self::CHAIN_BCC_DEFAULT;
199
            $changeIdx = self::CHAIN_BCC_DEFAULT;
200
        }
201
202
        $this->isSegwit = (bool) $segwit;
203
        $this->chainIndex = $chainIdx;
204
        $this->changeIndex = $changeIdx;
205
        $this->walletPath = $this->getWalletPath(null);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getWalletPath(null) of type object<Blocktrail\SDK\Bitcoin\BIP32Path> is incompatible with the declared type object<Blocktrail\SDK\WalletPath> of property $walletPath.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
Deprecated Code introduced by
The property Blocktrail\SDK\Wallet::$walletPath has been deprecated.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
206 10
    }
207 10
208
    /**
209
     * @param null $chainIndex
210
     * @return BIP32Path
211
     */
212
    protected function getWalletPath($chainIndex = null) {
213
        if ($chainIndex === null) {
214
            return WalletPath::create($this->keyIndex, $this->chainIndex)->path()->last("*");
215 1
        } else {
216 1
            return WalletPath::create($this->keyIndex, $chainIndex)->path()->last("*");
217
        }
218
    }
219
220
    /**
221
     * @return bool
222
     */
223
    public function isSegwit() {
224 5
        return $this->isSegwit;
225
    }
226 5
227 5
    /**
228
     * return the wallet identifier
229
     *
230
     * @return string
231
     */
232
    public function getIdentifier() {
233
        return $this->identifier;
234
    }
235 10
236 10
    /**
237
     * Returns the wallets backup public key
238
     *
239
     * @return [xpub, path]
0 ignored issues
show
Documentation introduced by
The doc-type xpub,">[xpub, could not be parsed: Unknown type name "[" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
240
     */
241
    public function getBackupKey() {
242
        return $this->backupPublicKey->tuple();
243
    }
244
245
    /**
246 5
     * return list of Blocktrail co-sign extended public keys
247 5
     *
248 4
     * @return array[]      [ [xpub, path] ]
249
     */
250
    public function getBlocktrailPublicKeys() {
251 5
        return array_map(function (BIP32Key $key) {
252
            return $key->tuple();
253
        }, $this->blocktrailPublicKeys);
254 5
    }
255
256
    /**
257 5
     * check if wallet is locked
258
     *
259 5
     * @return bool
260
     */
261 5
    public function isLocked() {
262 5
        return $this->locked;
263
    }
264
265 5
    /**
266 5
     * upgrade wallet to different blocktrail cosign key
267 5
     *
268 5
     * @param $keyIndex
269 5
     * @return bool
270
     * @throws \Exception
271
     */
272
    public function upgradeKeyIndex($keyIndex) {
273 5
        if ($this->locked) {
274
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
275
        }
276
277
        $walletPath = WalletPath::create($keyIndex);
278
279
        // do the upgrade to the new 'key_index'
280
        $primaryPublicKey = $this->primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath());
281
282
        // $primaryPublicKey = BIP32::extended_private_to_public(BIP32::build_key($this->primaryPrivateKey->tuple(), (string)$walletPath->keyIndexPath()));
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
283 13
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
284 13
285 13
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
286 13
        $this->keyIndex = $keyIndex;
287
288 13
        // reset wallet path to new state
289 13
        $this->walletPath = $this->getWalletPath(null);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getWalletPath(null) of type object<Blocktrail\SDK\Bitcoin\BIP32Path> is incompatible with the declared type object<Blocktrail\SDK\WalletPath> of property $walletPath.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
Deprecated Code introduced by
The property Blocktrail\SDK\Wallet::$walletPath has been deprecated.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
290 13
291 13
        // update the blocktrail public keys
292
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
293
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
294
                $path = $pubKey[1];
295 13
                $pubKey = $pubKey[0];
296 13
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKey), $path);
297
            }
298
        }
299
300 13
        return true;
301
    }
302
303
    /**
304 13
     * get a new BIP32 derivation for the next (unused) address
305 13
     *  by requesting it from the API
306
     *
307
     * @return string
308
     * @param int|null $chainIndex
309
     * @throws \Exception
310
     */
311 13
    protected function getNewDerivation($chainIndex = null) {
312
        $path = $this->getWalletPath($chainIndex);
0 ignored issues
show
Bug introduced by
It seems like $chainIndex defined by parameter $chainIndex on line 311 can also be of type integer; however, Blocktrail\SDK\Wallet::getWalletPath() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
313
314
        if (self::VERIFY_NEW_DERIVATION) {
315
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
316
317
            $path = $new['path'];
318
            $address = $new['address'];
319
            $redeemScript = $new['redeem_script'];
320
            $witnessScript = array_key_exists('witness_script', $new) ? $new['witness_script'] : null;
321 14
322 14
            /** @var ScriptInterface $checkRedeemScript */
323
            /** @var ScriptInterface $checkWitnessScript */
324 14
            list($checkAddress, $checkRedeemScript, $checkWitnessScript) = $this->getRedeemScriptByPath($path);
325
326
            if ($checkAddress != $address) {
327
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
328 14
            }
329
330
            if ($checkRedeemScript && $checkRedeemScript->getHex() != $redeemScript) {
331
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript->getHex()}]");
332 14
            }
333 14
334
            if ($checkWitnessScript && $checkWitnessScript->getHex() != $witnessScript) {
335
                throw new \Exception("Failed to verify that witnessScript from API [{$witnessScript}] matches address locally [{$checkWitnessScript->getHex()}]");
336 14
            }
337
        } else {
338
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
339
        }
340
341
        return (string)$path;
342
    }
343
344
    /**
345 13
     * @param string|BIP32Path  $path
346 13
     * @return BIP32Key|false
347 13
     * @throws \Exception
348 13
     *
349
     * @TODO: hmm?
350 13
     */
351 13
    protected function getParentPublicKey($path) {
352
        $path = BIP32Path::path($path)->parent()->publicPath();
353
354 13
        if ($path->count() <= 2) {
355
            return false;
356
        }
357
358
        if ($path->isHardened()) {
359
            return false;
360
        }
361 14
362 14
        if (!isset($this->pubKeys[(string)$path])) {
363
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
364
        }
365 14
366 14
        return $this->pubKeys[(string)$path];
367
    }
368
369
    /**
370
     * get address for the specified path
371 14
     *
372
     * @param string|BIP32Path  $path
373
     * @return string
374
     */
375
    public function getAddressByPath($path) {
376
        $path = (string)BIP32Path::path($path)->privatePath();
377
        if (!isset($this->derivations[$path])) {
378
            list($address, ) = $this->getRedeemScriptByPath($path);
379
380 14
            $this->derivations[$path] = $address;
381 14
            $this->derivationsByAddress[$address] = $path;
382
        }
383 14
384 14
        return $this->derivations[$path];
385 14
    }
386
387
    /**
388
     * @param string $path
389
     * @return WalletScript
390
     */
391
    public function getWalletScriptByPath($path) {
392
        $path = BIP32Path::path($path);
393
394
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
395
        if ($pubKey = $this->getParentPublicKey($path)) {
396
            $key = $pubKey->buildKey($path->publicPath());
397
        } else {
398
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
399
        }
400
401
        return $this->getWalletScriptFromKey($key, $path);
402
    }
403 14
404 14
    /**
405
     * get address and redeemScript for specified path
406 14
     *
407
     * @param string    $path
408 14
     * @return array[string, ScriptInterface, ScriptInterface|null]     [address, redeemScript, witnessScript]
0 ignored issues
show
Documentation introduced by
The doc-type array[string, could not be parsed: Expected "]" at position 2, but found "string". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
409 14
     */
410 14
    public function getRedeemScriptByPath($path) {
411 14
        $walletScript = $this->getWalletScriptByPath($path);
412 14
413
        $redeemScript = $walletScript->isP2SH() ? $walletScript->getRedeemScript() : null;
414 14
        $witnessScript = $walletScript->isP2WSH() ? $walletScript->getWitnessScript() : null;
415 14
        return [$walletScript->getAddress()->getAddress(), $redeemScript, $witnessScript];
416 3
    }
417 3
418 3
    /**
419
     * @param BIP32Key          $key
420 14
     * @param string|BIP32Path  $path
421 14
     * @return string
422 14
     */
423
    protected function getAddressFromKey(BIP32Key $key, $path) {
424
        return $this->getWalletScriptFromKey($key, $path)->getAddress()->getAddress();
425 14
    }
426
427
    /**
428
     * @param BIP32Key          $key
429
     * @param string|BIP32Path  $path
430
     * @return WalletScript
431
     * @throws \Exception
432
     */
433
    protected function getWalletScriptFromKey(BIP32Key $key, $path) {
434
        $path = BIP32Path::path($path)->publicPath();
435
436
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
437
438
        $multisig = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
439
            $key->buildKey($path)->publicKey(),
440
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
441
            $blocktrailPublicKey->buildKey($path)->publicKey()
442
        ]), false);
443 14
444 14
        $type = (int)$key->path()[2];
445
446 14
        if ($this->isSegwit && $type === Wallet::CHAIN_BTC_SEGWIT) {
447
            $witnessScript = new WitnessScript($multisig);
448 14
            $redeemScript = new P2shScript($witnessScript);
449
            $scriptPubKey = $redeemScript->getOutputScript();
450
        } else if ($type === Wallet::CHAIN_BTC_DEFAULT || $type === Wallet::CHAIN_BCC_DEFAULT) {
451
            $witnessScript = null;
452 14
            $redeemScript = new P2shScript($multisig);
453
            $scriptPubKey = $redeemScript->getOutputScript();
454
        } else {
455
            throw new BlocktrailSDKException("Unsupported chain in path");
456
        }
457
458
        return new WalletScript($path, $scriptPubKey, $redeemScript, $witnessScript);
459
    }
460 13
461 13
    /**
462 13
     * get the path (and redeemScript) to specified address
463
     *
464 13
     * @param string $address
465
     * @return array
466
     */
467
    public function getPathForAddress($address) {
468
        return $this->sdk->getPathForAddress($this->identifier, $address);
469
    }
470
471
    /**
472 9
     * @param string|BIP32Path  $path
473 9
     * @return BIP32Key
474
     * @throws \Exception
475
     */
476
    public function getBlocktrailPublicKey($path) {
477
        $path = BIP32Path::path($path);
478
479
        $keyIndex = str_replace("'", "", $path[1]);
480
481 9
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
482 9
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
483
        }
484 9
485
        return $this->blocktrailPublicKeys[$keyIndex];
486
    }
487
488
    /**
489
     * generate a new derived key and return the new path and address for it
490
     *
491
     * @param int|null $chainIndex
492
     * @return string[]     [path, address]
493 2
     */
494 2
    public function getNewAddressPair($chainIndex = null) {
495
        $path = $this->getNewDerivation($chainIndex);
496 2
        $address = $this->getAddressByPath($path);
497
498
        return [$path, $address];
499
    }
500
501
    /**
502
     * generate a new derived private key and return the new address for it
503
     *
504
     * @param int|null $chainIndex
505
     * @return string
506
     */
507
    public function getNewAddress($chainIndex = null) {
508
        return $this->getNewAddressPair($chainIndex)[1];
509
    }
510
511
    /**
512 9
     * get the balance for the wallet
513 9
     *
514 4
     * @return int[]            [confirmed, unconfirmed]
515
     */
516
    public function getBalance() {
517 9
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
518
519 9
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
520 9
    }
521 9
522 9
    /**
523
     * do wallet discovery (slow)
524 9
     *
525 9
     * @param int   $gap        the gap setting to use for discovery
526
     * @return int[]            [confirmed, unconfirmed]
527
     */
528 9
    public function doDiscovery($gap = 200) {
529
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
530 3
531
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
532 3
    }
533
534
    /**
535
     * create, sign and send a transaction
536
     *
537
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
538
     *                                      value should be INT
539
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
540
     * @param bool     $allowZeroConf
541
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
542
     * @param string   $feeStrategy
543
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
544
     * @return string the txid / transaction hash
545
     * @throws \Exception
546
     */
547
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
548
        if ($this->locked) {
549
            throw new \Exception("Wallet needs to be unlocked to pay");
550
        }
551
552
        $outputs = self::normalizeOutputsStruct($outputs);
553
554
        $txBuilder = new TransactionBuilder();
555 10
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
556 10
        $txBuilder->setFeeStrategy($feeStrategy);
557
        $txBuilder->setChangeAddress($changeAddress);
558 10
559 10
        foreach ($outputs as $output) {
560 1
            $txBuilder->addRecipient($output['address'], $output['value']);
561
        }
562
563
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
564 1
565 1
        $apiCheckFee = $forceFee === null;
566 1
567 1
        return $this->sendTx($txBuilder, $apiCheckFee);
568 1
    }
569 1
570
    /**
571 1
     * determine max spendable from wallet after fees
572
     *
573
     * @param bool     $allowZeroConf
574 10
     * @param string   $feeStrategy
575 10
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
576
     * @param int      $outputCnt
577
     * @return string
578 10
     * @throws BlocktrailSDKException
579
     */
580
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
581 10
        return $this->sdk->walletMaxSpendable($this->identifier, $allowZeroConf, $feeStrategy, $forceFee, $outputCnt);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->sdk->walle...$forceFee, $outputCnt); (array) is incompatible with the return type declared by the interface Blocktrail\SDK\WalletInterface::getMaxSpendable of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
582
    }
583
584
    /**
585
     * parse outputs into normalized struct
586
     *
587
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
588
     * @return array            [['address' => address, 'value' => value], ]
589
     */
590
    public static function normalizeOutputsStruct(array $outputs) {
591
        $result = [];
592
593 11
        foreach ($outputs as $k => $v) {
594
            if (is_numeric($k)) {
595 11
                if (!is_array($v)) {
596
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
597 5
                }
598 5
599 5
                if (isset($v['address']) && isset($v['value'])) {
600
                    $address = $v['address'];
601 5
                    $value = $v['value'];
602 1
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
603
                    $address = $v[0];
604 5
                    $value = $v[1];
605
                } else {
606
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
607 5
                }
608 5
            } else {
609 5
                $address = $k;
610
                $value = $v;
611
            }
612
613
            $result[] = ['address' => $address, 'value' => $value];
614
        }
615
616 5
        return $result;
617
    }
618
619 5
    /**
620
     * 'fund' the txBuilder with UTXOs (modified in place)
621
     *
622
     * @param TransactionBuilder    $txBuilder
623
     * @param bool|true             $lockUTXOs
624
     * @param bool|false            $allowZeroConf
625
     * @param null|int              $forceFee
626
     * @return TransactionBuilder
627
     */
628
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
629 7
        // get the data we should use for this transaction
630 7
        $coinSelection = $this->coinSelection($txBuilder->getOutputs(/* $json = */true), $lockUTXOs, $allowZeroConf, $txBuilder->getFeeStrategy(), $forceFee);
0 ignored issues
show
Bug introduced by
It seems like $forceFee defined by parameter $forceFee on line 628 can also be of type integer; however, Blocktrail\SDK\Wallet::coinSelection() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
631 7
        
632 7
        $utxos = $coinSelection['utxos'];
633
        $fee = $coinSelection['fee'];
634 7
        $change = $coinSelection['change'];
0 ignored issues
show
Unused Code introduced by
$change is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
635
636 7
        if ($forceFee !== null) {
637 7
            $txBuilder->setFee($forceFee);
638 1
        } else {
639
            $txBuilder->validateFee($fee);
640 1
        }
641
642
        foreach ($utxos as $utxo) {
643
            $signMode = SignInfo::MODE_SIGN;
644 1
            if (isset($utxo['sign_mode'])) {
645
                $signMode = $utxo['sign_mode'];
646 1
                if (!in_array($signMode, $this->allowedSignModes)) {
647
                    throw new \Exception("Sign mode disallowed by wallet");
648
                }
649 1
            }
650
651
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script'], $utxo['witness_script'], $signMode);
652 1
        }
653 1
654
        return $txBuilder;
655
    }
656
657 7
    /**
658 7
     * build inputs and outputs lists for TransactionBuilder
659
     *
660
     * @param TransactionBuilder $txBuilder
661
     * @return [TransactionInterface, SignInfo[]]
0 ignored issues
show
Documentation introduced by
The doc-type TransactionInterface,">[TransactionInterface, could not be parsed: Unknown type name "[" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
662 7
     * @throws \Exception
663 6
     */
664 6
    public function buildTx(TransactionBuilder $txBuilder) {
665 6
        $send = $txBuilder->getOutputs();
666
        $utxos = $txBuilder->getUtxos();
667
        $signInfo = [];
668
669 7
        $txb = new TxBuilder();
670
671
        foreach ($utxos as $utxo) {
672
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
673 7
                $tx = $this->sdk->transaction($utxo->hash);
674 7
675 7
                if (!$tx || !isset($tx['outputs'][$utxo->index])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tx of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
676 1
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
677
                }
678
679 7
                $output = $tx['outputs'][$utxo->index];
680
681 7
                if (!$utxo->address) {
682 5
                    $utxo->address = AddressFactory::fromString($output['address']);
683
                }
684
                if (!$utxo->value) {
685
                    $utxo->value = $output['value'];
686
                }
687 7
                if (!$utxo->scriptPubKey) {
688 5
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
689 5
                }
690 5
            }
691
692
            if (SignInfo::MODE_SIGN === $utxo->signMode) {
693
                if (!$utxo->path) {
694 7
                    $utxo->path = $this->getPathForAddress($utxo->address->getAddress());
695 7
                }
696
697
                if (!$utxo->redeemScript || !$utxo->witnessScript) {
698
                    list(, $redeemScript, $witnessScript) = $this->getRedeemScriptByPath($utxo->path);
0 ignored issues
show
Documentation introduced by
$utxo->path is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
699 7
                    $utxo->redeemScript = $redeemScript;
700 7
                    $utxo->witnessScript = $witnessScript;
701
                }
702
            }
703 7
704 7
            $signInfo[] = $utxo->getSignInfo();
705
        }
706 7
707 2
        $utxoSum = array_sum(array_map(function (UTXO $utxo) {
708 7
            return $utxo->value;
709 7
        }, $utxos));
710
        if ($utxoSum < array_sum(array_column($send, 'value'))) {
711 7
            throw new \Exception("Atempting to spend more than sum of UTXOs");
712
        }
713
714
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
715 7
716
        if ($txBuilder->getValidateFee() !== null) {
717
            if (abs($txBuilder->getValidateFee() - $fee) > Wallet::BASE_FEE) {
718 7
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
719 7
            }
720 7
        }
721
722 7
        if ($change > 0) {
723 7
            $send[] = [
724
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewAddress($this->changeIndex),
725
                'value' => $change
726 7
            ];
727 2
        }
728
729
        foreach ($utxos as $utxo) {
730 2
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index));
731 1
        }
732
733
        // outputs should be randomized to make the change harder to detect
734 2
        if ($txBuilder->shouldRandomizeChangeOuput()) {
735
            shuffle($send);
736
        }
737 7
738
        foreach ($send as $out) {
739 7
            assert(isset($out['value']));
740
741 7
            if (isset($out['scriptPubKey'])) {
742 6
                $txb->output($out['value'], $out['scriptPubKey']);
743
            } elseif (isset($out['address'])) {
744 6
                $txb->payToAddress($out['value'], AddressFactory::fromString($out['address']));
745
            } else {
746
                throw new \Exception();
747 6
            }
748
        }
749
750 6
        return [$txb->get(), $signInfo];
751
    }
752
753
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) {
754 6
        $send = $txBuilder->getOutputs();
755 4
        $utxos = $txBuilder->getUtxos();
756
757 5
        $fee = $txBuilder->getFee();
758
        $change = null;
0 ignored issues
show
Unused Code introduced by
$change is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
759
760 5
        // if the fee is fixed we just need to calculate the change
761 5
        if ($fee !== null) {
762
            $change = $this->determineChange($utxos, $send, $fee);
763
764
            // if change is not dust we need to add a change output
765
            if ($change > Blocktrail::DUST) {
766
                $send[] = ['address' => 'change', 'value' => $change];
767
            } else {
768
                // if change is dust we do nothing (implicitly it's added to the fee)
769
                $change = 0;
770 7
            }
771
        } else {
772 7
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
773
774
            $change = $this->determineChange($utxos, $send, $fee);
775
776
            if ($change > 0) {
777
                $changeIdx = count($send);
778
                // set dummy change output
779
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
780
781
                // recaculate fee now that we know that we have a change output
782
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
783 4
784 4
                // unset dummy change output
785
                unset($send[$changeIdx]);
786 4
787
                // if adding the change output made the fee bump up and the change is smaller than the fee
788
                //  then we're not doing change
789
                if ($fee2 > $fee && $fee2 > $change) {
790
                    $change = 0;
791
                } else {
792
                    $change = $this->determineChange($utxos, $send, $fee2);
793
794
                    // if change is not dust we need to add a change output
795
                    if ($change > Blocktrail::DUST) {
796
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
797
                    } else {
798
                        // if change is dust we do nothing (implicitly it's added to the fee)
799
                        $change = 0;
800 4
                    }
801 4
                }
802
            }
803
        }
804
805
        $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
806 4
807 4
        return [$fee, $change];
808
    }
809
810 4
    /**
811
     * create, sign and send transction based on TransactionBuilder
812
     *
813 4
     * @param TransactionBuilder $txBuilder
814 4
     * @param bool $apiCheckFee     let the API check if the fee is correct
815
     * @return string
816
     * @throws \Exception
817
     */
818
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
819 4
        list($tx, $signInfo) = $this->buildTx($txBuilder);
820 4
821
        return $this->_sendTx($tx, $signInfo, $apiCheckFee);
0 ignored issues
show
Compatibility introduced by
$tx of type object<BitWasp\Bitcoin\T...n\TransactionInterface> is not a sub-type of object<BitWasp\Bitcoin\Transaction\Transaction>. It seems like you assume a concrete implementation of the interface BitWasp\Bitcoin\Transaction\TransactionInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
822
    }
823
824
    /**
825
     * !! INTERNAL METHOD, public for testing purposes !!
826
     * create, sign and send transction based on inputs and outputs
827
     *
828
     * @param Transaction $tx
829
     * @param SignInfo[]  $signInfo
830
     * @param bool $apiCheckFee     let the API check if the fee is correct
831
     * @return string
832 1
     * @throws \Exception
833 1
     * @internal
834
     */
835 1
    public function _sendTx(Transaction $tx, array $signInfo, $apiCheckFee = true) {
836
        if ($this->locked) {
837
            throw new \Exception("Wallet needs to be unlocked to pay");
838
        }
839
840
        assert(Util::all(function ($signInfo) {
841
            return $signInfo instanceof SignInfo;
842 5
        }, $signInfo), '$signInfo should be SignInfo[]');
843 5
844
        // sign the transaction with our keys
845 5
        $signed = $this->signTransaction($tx, $signInfo);
846
847
        $txs = [
848
            'signed_transaction' => $signed->getHex(),
849
            'base_transaction' => $signed->getBaseSerialization()->getHex(),
850
        ];
851
852
        // send the transaction
853
        return $this->sendTransaction($txs, array_map(function (SignInfo $r) {
854 9
            return (string)$r->path;
855 9
        }, $signInfo), $apiCheckFee);
856
    }
857
858
    /**
859
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
860
     *
861
     * @todo: mark this as deprecated, insist on the utxo's or qualified scripts.
862
     * @param int $utxoCnt      number of unspent inputs in transaction
863
     * @param int $outputCnt    number of outputs in transaction
864 2
     * @return float
865 2
     * @access public           reminder that people might use this!
866
     */
867
    public static function estimateFee($utxoCnt, $outputCnt) {
868
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
869
870
        return self::baseFeeForSize($size);
871
    }
872
873
    /**
874 3
     * @param int $size     size in bytes
875 3
     * @return int          fee in satoshi
876
     */
877 3
    public static function baseFeeForSize($size) {
878
        $sizeKB = (int)ceil($size / 1000);
879 3
880
        return $sizeKB * self::BASE_FEE;
881 3
    }
882 3
883 3
    /**
884 3
     * @todo: variable varint
885 3
     * @param int $txinSize
886
     * @param int $txoutSize
887
     * @return float
888 3
     */
889 3
    public static function estimateSize($txinSize, $txoutSize) {
890 3
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
891 3
    }
892 3
893
    /**
894
     * only supports estimating size for P2PKH/P2SH outputs
895 3
     *
896
     * @param int $outputCnt    number of outputs in transaction
897
     * @return float
898
     */
899
    public static function estimateSizeOutputs($outputCnt) {
900
        return ($outputCnt * 34);
901
    }
902
903
    /**
904
     * only supports estimating size for 2of3 multsig UTXOs
905
     *
906
     * @param int $utxoCnt      number of unspent inputs in transaction
907
     * @return float
908 3
     */
909
    public static function estimateSizeUTXOs($utxoCnt) {
910
        $txinSize = 0;
911
912
        for ($i=0; $i<$utxoCnt; $i++) {
913
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
914
            $multisig = "2of3";
915
916
            if ($multisig) {
917
                $sigCnt = 2;
918
                $msig = explode("of", $multisig);
919
                if (count($msig) == 2 && is_numeric($msig[0])) {
920
                    $sigCnt = $msig[0];
921
                }
922
923 7
                $txinSize += array_sum([
924
                    32, // txhash
925 7
                    4, // idx
926
                    3, // scriptVarInt[>=253]
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
927
                    ((1 + 72) * $sigCnt), // (OP_PUSHDATA[<75] + 72) * sigCnt
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
928 7
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
929 4
                    1, // OP_0
930
                    4, // sequence
931 4
                ]);
932 4
            } else {
933
                $txinSize += array_sum([
934 1
                    32, // txhash
935 1
                    4, // idx
936
                    73, // sig
937
                    34, // script
938
                    4, // sequence
939
                ]);
940
            }
941
        }
942
943
        return $txinSize;
944
    }
945
946
    /**
947
     * determine how much fee is required based on the inputs and outputs
948
     *  this is an estimation, not a proper 100% correct calculation
949
     *
950 7
     * @param UTXO[]  $utxos
951
     * @param array[] $outputs
952 7
     * @param         $feeStrategy
953 7
     * @param         $optimalFeePerKB
954 7
     * @param         $lowPriorityFeePerKB
955
     * @return int
956 7
     * @throws BlocktrailSDKException
957
     */
958
    protected function determineFee($utxos, $outputs, $feeStrategy, $optimalFeePerKB, $lowPriorityFeePerKB) {
959
960
        $size = SizeEstimation::estimateVsize($utxos, $outputs);
961
962
        switch ($feeStrategy) {
963
            case self::FEE_STRATEGY_BASE_FEE:
964
                return self::baseFeeForSize($size);
965
966
            case self::FEE_STRATEGY_OPTIMAL:
967 4
                return (int)round(($size / 1000) * $optimalFeePerKB);
968 4
969
            case self::FEE_STRATEGY_LOW_PRIORITY:
970 4
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
971 4
972 4
            default:
973
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
974 4
        }
975 4
    }
976
977
    /**
978
     * determine how much change is left over based on the inputs and outputs and the fee
979
     *
980 4
     * @param UTXO[]    $utxos
981 4
     * @param array[]   $outputs
982
     * @param int       $fee
983 4
     * @return int
984 4
     */
985 4
    protected function determineChange($utxos, $outputs, $fee) {
986 4
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
987 4
            return $utxo->value;
988
        }, $utxos));
989 4
        $outputsTotal = array_sum(array_column($outputs, 'value'));
990 1
991
        return $inputsTotal - $outputsTotal - $fee;
992 4
    }
993 4
994
    /**
995
     * sign a raw transaction with the private keys that we have
996
     *
997 4
     * @param Transaction $tx
998
     * @param SignInfo[]  $signInfo
999
     * @return TransactionInterface
1000
     * @throws \Exception
1001
     */
1002
    protected function signTransaction(Transaction $tx, array $signInfo) {
1003
        $signer = new Signer($tx, Bitcoin::getEcAdapter());
1004
1005
        assert(Util::all(function ($signInfo) {
1006
            return $signInfo instanceof SignInfo;
1007
        }, $signInfo), '$signInfo should be SignInfo[]');
1008
1009 4
        $sigHash = SigHash::ALL;
1010 4
        if ($this->network === "bitcoincash") {
1011
            $sigHash |= SigHash::BITCOINCASH;
1012
            $signer->redeemBitcoinCash(true);
1013
        }
1014
1015
        foreach ($signInfo as $idx => $info) {
1016
            if ($info->mode === SignInfo::MODE_SIGN) {
1017
                // required SignInfo: path, redeemScript|witnessScript, output
1018
                $path = BIP32Path::path($info->path)->privatePath();
1019
                $key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey();
1020
                $signData = new SignData();
1021 11
                if ($info->redeemScript) {
1022 11
                    $signData->p2sh($info->redeemScript);
1023
                }
1024 5
                if ($info->witnessScript) {
1025 5
                    $signData->p2wsh($info->witnessScript);
1026 5
                }
1027
                $input = $signer->input($idx, $info->output, $signData);
1028 5
                $input->sign($key, $sigHash);
1029
            }
1030
        }
1031 7
1032 7
        return $signer->get();
1033 3
    }
1034
1035
    /**
1036 7
     * send the transaction using the API
1037
     *
1038
     * @param string|array  $signed
1039 7
     * @param string[]      $paths
1040 7
     * @param bool          $checkFee
1041
     * @return string           the complete raw transaction
1042
     * @throws \Exception
1043
     */
1044 7
    protected function sendTransaction($signed, $paths, $checkFee = false) {
1045
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
1046
    }
1047 3
1048 3
    /**
1049
     * @param \array[] $outputs
1050 3
     * @param bool $lockUTXO
1051 3
     * @param bool $allowZeroConf
1052
     * @param int|null|string $feeStrategy
1053 3
     * @param null $forceFee
1054 3
     * @return array
1055
     */
1056
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1057
        $result = $this->sdk->coinSelection($this->identifier, $outputs, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
1058
1059
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
1060
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
1061
        $this->feePerKBAge = time();
1062
1063 10
        return $result;
1064 10
    }
1065
1066
    public function getOptimalFeePerKB() {
1067
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
1068 10
            $this->updateFeePerKB();
1069 10
        }
1070
1071
        return $this->optimalFeePerKB;
1072
    }
1073
1074
    public function getLowPriorityFeePerKB() {
1075
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1076
            $this->updateFeePerKB();
1077 10
        }
1078 10
1079
        return $this->lowPriorityFeePerKB;
1080 10
    }
1081 10
1082
    public function updateFeePerKB() {
1083 10
        $result = $this->sdk->feePerKB();
1084 10
1085
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
1086 10
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
1087
1088
        $this->feePerKBAge = time();
1089
    }
1090
1091
    /**
1092
     * delete the wallet
1093
     *
1094
     * @param bool $force ignore warnings (such as non-zero balance)
1095
     * @return mixed
1096 1
     * @throws \Exception
1097 1
     */
1098 1
    public function deleteWallet($force = false) {
1099
        if ($this->locked) {
1100
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
1101
        }
1102
1103
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
1104
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
1105 1
    }
1106 1
1107 1
    /**
1108
     * create checksum to verify ownership of the master primary key
1109
     *
1110
     * @return string[]     [address, signature]
1111
     */
1112
    protected function createChecksumVerificationSignature() {
1113
        $privKey = $this->primaryPrivateKey->key();
1114
1115
        $pubKey = $this->primaryPrivateKey->publicKey();
1116
        $address = $pubKey->getAddress()->getAddress();
1117
1118
        $signer = new MessageSigner(Bitcoin::getEcAdapter());
1119
        $signed = $signer->sign($address, $privKey->getPrivateKey());
1120
1121
        return [$address, base64_encode($signed->getCompactSignature()->getBuffer()->getBinary())];
1122
    }
1123
1124
    /**
1125
     * setup a webhook for our wallet
1126
     *
1127
     * @param string    $url            URL to receive webhook events
1128
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1129
     * @return array
1130
     */
1131
    public function setupWebhook($url, $identifier = null) {
1132
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1133
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1134
    }
1135
1136
    /**
1137
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1138
     * @return mixed
1139
     */
1140
    public function deleteWebhook($identifier = null) {
1141 1
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1142 1
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1143
    }
1144
1145
    /**
1146
     * lock a specific unspent output
1147
     *
1148
     * @param     $txHash
1149
     * @param     $txIdx
1150
     * @param int $ttl
1151
     * @return bool
1152
     */
1153 1
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1154 1
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1155
    }
1156
1157
    /**
1158
     * unlock a specific unspent output
1159
     *
1160
     * @param     $txHash
1161
     * @param     $txIdx
1162
     * @return bool
1163
     */
1164
    public function unlockUTXO($txHash, $txIdx) {
1165
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1166 1
    }
1167 1
1168
    /**
1169
     * get all transactions for the wallet (paginated)
1170
     *
1171
     * @param  integer $page    pagination: page number
1172
     * @param  integer $limit   pagination: records per page (max 500)
1173
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1174
     * @return array            associative array containing the response
1175
     */
1176
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1177
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1178
    }
1179
1180
    /**
1181
     * get all addresses for the wallet (paginated)
1182
     *
1183
     * @param  integer $page    pagination: page number
1184
     * @param  integer $limit   pagination: records per page (max 500)
1185
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1186
     * @return array            associative array containing the response
1187
     */
1188
    public function addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1189
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1190
    }
1191
1192
    /**
1193
     * get all UTXOs for the wallet (paginated)
1194
     *
1195
     * @param  integer $page        pagination: page number
1196
     * @param  integer $limit       pagination: records per page (max 500)
1197
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1198
     * @param  boolean $zeroconf    include zero confirmation transactions
1199
     * @return array                associative array containing the response
1200
     */
1201
    public function utxos($page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1202
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir, $zeroconf);
1203
    }
1204
}
1205