Completed
Pull Request — master (#89)
by thomas
16:20
created

Wallet::getRedeemScriptByPath()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 4
nop 1
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 3
rs 9.4285
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\Network\NetworkInterface;
10
use BitWasp\Bitcoin\Script\P2shScript;
11
use BitWasp\Bitcoin\Script\ScriptFactory;
12
use BitWasp\Bitcoin\Script\ScriptInterface;
13
use BitWasp\Bitcoin\Script\WitnessScript;
14
use BitWasp\Bitcoin\Transaction\Factory\SignData;
15
use BitWasp\Bitcoin\Transaction\Factory\Signer;
16
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
17
use BitWasp\Bitcoin\Transaction\OutPoint;
18
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
19
use BitWasp\Bitcoin\Transaction\Transaction;
20
use BitWasp\Bitcoin\Transaction\TransactionInterface;
21
use BitWasp\Buffertools\Buffer;
22
use Blocktrail\SDK\Bitcoin\BIP32Key;
23
use Blocktrail\SDK\Bitcoin\BIP32Path;
24
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
25
26
/**
27
 * Class Wallet
28
 */
29
abstract class Wallet implements WalletInterface {
30
31
    const WALLET_VERSION_V1 = 'v1';
32
    const WALLET_VERSION_V2 = 'v2';
33
    const WALLET_VERSION_V3 = 'v3';
34
35
    const CHAIN_BTC_DEFAULT = 0;
36
    const CHAIN_BCC_DEFAULT = 1;
37
    const CHAIN_BTC_SEGWIT = 2;
38
39
    const BASE_FEE = 10000;
40
41
    /**
42
     * development / debug setting
43
     *  when getting a new derivation from the API,
44
     *  will verify address / redeeemScript with the values the API provides
45
     */
46
    const VERIFY_NEW_DERIVATION = true;
47
48
    /**
49
     * @var BlocktrailSDKInterface
50
     */
51
    protected $sdk;
52
53
    /**
54
     * @var string
55
     */
56
    protected $identifier;
57
58
    /**
59
     * BIP32 master primary private key (m/)
60
     *
61
     * @var BIP32Key
62
     */
63
    protected $primaryPrivateKey;
64
65
    /**
66
     * @var BIP32Key[]
67
     */
68
    protected $primaryPublicKeys;
69
70
    /**
71
     * BIP32 master backup public key (M/)
72
73
     * @var BIP32Key
74
     */
75
    protected $backupPublicKey;
76
77
    /**
78
     * map of blocktrail BIP32 public keys
79
     *  keyed by key index
80
     *  path should be `M / key_index'`
81
     *
82
     * @var BIP32Key[]
83
     */
84
    protected $blocktrailPublicKeys;
85
86
    /**
87
     * the 'Blocktrail Key Index' that is used for new addresses
88
     *
89
     * @var int
90
     */
91
    protected $keyIndex;
92
93
    /**
94
     * 'bitcoin'
95
     *
96
     * @var string
97
     */
98
    protected $network;
99
100
    /**
101
     * @var NetworkInterface
102
     */
103
    protected $networkParams;
104
105
    /**
106
     * testnet yes / no
107
     *
108
     * @var bool
109
     */
110
    protected $testnet;
111
112
    /**
113
     * cache of public keys, by path
114
     *
115
     * @var BIP32Key[]
116
     */
117
    protected $pubKeys = [];
118
119
    /**
120
     * cache of address / redeemScript, by path
121
     *
122
     * @var string[][]      [[address, redeemScript)], ]
123
     */
124
    protected $derivations = [];
125
126
    /**
127
     * reverse cache of paths by address
128
     *
129
     * @var string[]
130
     */
131
    protected $derivationsByAddress = [];
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
    protected $optimalFeePerKB;
156
    protected $lowPriorityFeePerKB;
157
    protected $feePerKBAge;
158
    protected $allowedSignModes = [SignInfo::MODE_DONTSIGN, SignInfo::MODE_SIGN];
159
160
    /**
161
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
162
     * @param string                        $identifier                 identifier of the wallet
163
     * @param BIP32Key[]                    $primaryPublicKeys
164
     * @param BIP32Key                      $backupPublicKey            should be BIP32 master public key M/
165
     * @param BIP32Key[]                    $blocktrailPublicKeys
166
     * @param int                           $keyIndex
167
     * @param bool                          $segwit
168
     * @param string                        $checksum
169
     * @throws BlocktrailSDKException
170
     */
171 21
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, array $primaryPublicKeys, $backupPublicKey, array $blocktrailPublicKeys, $keyIndex, $segwit, $checksum) {
172 21
        $this->sdk = $sdk;
173 21
        $this->networkParams = $sdk->getNetworkParams();
0 ignored issues
show
Documentation Bug introduced by
It seems like $sdk->getNetworkParams() of type object<Blocktrail\SDK\NetworkParams> is incompatible with the declared type object<BitWasp\Bitcoin\Network\NetworkInterface> of property $networkParams.

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...
174
175 21
        $this->identifier = $identifier;
176 21
        $this->backupPublicKey = BlocktrailSDK::normalizeBIP32Key($backupPublicKey, $this->networkParams->getNetwork());
177 21
        $this->primaryPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($primaryPublicKeys, $this->networkParams->getNetwork());
178 21
        $this->blocktrailPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($blocktrailPublicKeys, $this->networkParams->getNetwork());
179
180 21
        $this->keyIndex = $keyIndex;
181 21
        $this->checksum = $checksum;
182
183 21
        if ($this->networkParams->isNetwork("bitcoin")) {
184 21
            if ($segwit) {
185 3
                $chainIdx = self::CHAIN_BTC_DEFAULT;
186 3
                $changeIdx = self::CHAIN_BTC_SEGWIT;
187
            } else {
188 19
                $chainIdx = self::CHAIN_BTC_DEFAULT;
189 21
                $changeIdx = self::CHAIN_BTC_DEFAULT;
190
            }
191
        } else {
192
            if ($segwit && $this->networkParams->isNetwork("bitcoincash")) {
193
                throw new BlocktrailSDKException("Received segwit flag for bitcoincash - abort");
194
            }
195
            $chainIdx = self::CHAIN_BCC_DEFAULT;
196
            $changeIdx = self::CHAIN_BCC_DEFAULT;
197
        }
198
199 21
        $this->isSegwit = (bool) $segwit;
200 21
        $this->chainIndex = $chainIdx;
201 21
        $this->changeIndex = $changeIdx;
202 21
    }
203
204
    /**
205
     * @param int|null $chainIndex
206
     * @return WalletPath
207
     */
208 14
    protected function getWalletPath($chainIndex = null) {
209 14
        if ($chainIndex === null) {
210 12
            return WalletPath::create($this->keyIndex, $this->chainIndex);
211
        } else {
212 6
            return WalletPath::create($this->keyIndex, $chainIndex);
213
        }
214
    }
215
216
    /**
217
     * @return bool
218
     */
219 3
    public function isSegwit() {
220 3
        return $this->isSegwit;
221
    }
222
223
    /**
224
     * return the wallet identifier
225
     *
226
     * @return string
227
     */
228 10
    public function getIdentifier() {
229 10
        return $this->identifier;
230
    }
231
232
    /**
233
     * Returns the wallets backup public key
234
     *
235
     * @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...
236
     */
237 1
    public function getBackupKey() {
238 1
        return $this->backupPublicKey->tuple();
239
    }
240
241
    /**
242
     * return list of Blocktrail co-sign extended public keys
243
     *
244
     * @return array[]      [ [xpub, path] ]
245
     */
246 5
    public function getBlocktrailPublicKeys() {
247
        return array_map(function (BIP32Key $key) {
248 5
            return $key->tuple();
249 5
        }, $this->blocktrailPublicKeys);
250
    }
251
252
    /**
253
     * check if wallet is locked
254
     *
255
     * @return bool
256
     */
257 10
    public function isLocked() {
258 10
        return $this->locked;
259
    }
260
261
    /**
262
     * upgrade wallet to different blocktrail cosign key
263
     *
264
     * @param $keyIndex
265
     * @return bool
266
     * @throws \Exception
267
     */
268 5
    public function upgradeKeyIndex($keyIndex) {
269 5
        if ($this->locked) {
270 4
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
271
        }
272
273 5
        $walletPath = WalletPath::create($keyIndex);
274
275
        // do the upgrade to the new 'key_index'
276 5
        $primaryPublicKey = $this->primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath());
277
278
        // $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...
279 5
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
280
281 5
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
282 5
        $this->keyIndex = $keyIndex;
283
284
        // update the blocktrail public keys
285 5
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
286 5
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
287 5
                $path = $pubKey[1];
288 5
                $pubKey = $pubKey[0];
289 5
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::fromString($this->networkParams->getNetwork(), $pubKey, $path);
0 ignored issues
show
Bug introduced by
The method getNetwork() does not seem to exist on object<BitWasp\Bitcoin\Network\NetworkInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
290
            }
291
        }
292
293 5
        return true;
294
    }
295
296
    /**
297
     * get a new BIP32 derivation for the next (unused) address
298
     *  by requesting it from the API
299
     *
300
     * @return string
301
     * @param int|null $chainIndex
302
     * @throws \Exception
303
     */
304 14
    protected function getNewDerivation($chainIndex = null) {
305 14
        $path = $this->getWalletPath($chainIndex)->path()->last("*");
306
307 14
        if (self::VERIFY_NEW_DERIVATION) {
308 14
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
309
310 14
            $path = $new['path'];
311 14
            $address = $new['address'];
312 14
            $redeemScript = $new['redeem_script'];
313 14
            $witnessScript = array_key_exists('witness_script', $new) ? $new['witness_script'] : null;
314
315
            /** @var ScriptInterface $checkRedeemScript */
316
            /** @var ScriptInterface $checkWitnessScript */
317 14
            list($checkAddress, $checkRedeemScript, $checkWitnessScript) = $this->getRedeemScriptByPath($path);
318
319 14
            if ($checkAddress != $address) {
320
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
321
            }
322
323 14
            if ($checkRedeemScript && $checkRedeemScript->getHex() != $redeemScript) {
324
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript->getHex()}]");
325
            }
326
327 14
            if ($checkWitnessScript && $checkWitnessScript->getHex() != $witnessScript) {
328 14
                throw new \Exception("Failed to verify that witnessScript from API [{$witnessScript}] matches address locally [{$checkWitnessScript->getHex()}]");
329
            }
330
        } else {
331
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
332
        }
333
334 14
        return (string)$path;
335
    }
336
337
    /**
338
     * @param string|BIP32Path  $path
339
     * @return BIP32Key|false
340
     * @throws \Exception
341
     *
342
     * @TODO: hmm?
343
     */
344 16
    protected function getParentPublicKey($path) {
345 16
        $path = BIP32Path::path($path)->parent()->publicPath();
346
347 16
        if ($path->count() <= 2) {
348
            return false;
349
        }
350
351 16
        if ($path->isHardened()) {
352
            return false;
353
        }
354
355 16
        if (!isset($this->pubKeys[(string)$path])) {
356 16
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
357
        }
358
359 16
        return $this->pubKeys[(string)$path];
360
    }
361
362
    /**
363
     * get address for the specified path
364
     *
365
     * @param string|BIP32Path  $path
366
     * @return string
367
     */
368 14
    public function getAddressByPath($path) {
369 14
        $path = (string)BIP32Path::path($path)->privatePath();
370 14
        if (!isset($this->derivations[$path])) {
371 14
            list($address, ) = $this->getRedeemScriptByPath($path);
372
373 14
            $this->derivations[$path] = $address;
374 14
            $this->derivationsByAddress[$address] = $path;
375
        }
376
377 14
        return $this->derivations[$path];
378
    }
379
380
    /**
381
     * @param string $path
382
     * @return WalletScript
383
     */
384 16
    public function getWalletScriptByPath($path) {
385 16
        $path = BIP32Path::path($path);
386
387
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
388 16
        if ($pubKey = $this->getParentPublicKey($path)) {
389 16
            $key = $pubKey->buildKey($path->publicPath());
390
        } else {
391
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
392
        }
393
394 16
        return $this->getWalletScriptFromKey($key, $path);
395
    }
396
397
    /**
398
     * get address and redeemScript for specified path
399
     *
400
     * @param string    $path
401
     * @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...
402
     */
403 15
    public function getRedeemScriptByPath($path) {
404 15
        $walletScript = $this->getWalletScriptByPath($path);
405
406 15
        $redeemScript = $walletScript->isP2SH() ? $walletScript->getRedeemScript() : null;
407 15
        $witnessScript = $walletScript->isP2WSH() ? $walletScript->getWitnessScript() : null;
408 15
        return [$walletScript->getAddress()->getAddress($this->networkParams->getNetwork()), $redeemScript, $witnessScript];
0 ignored issues
show
Bug introduced by
The method getNetwork() does not seem to exist on object<BitWasp\Bitcoin\Network\NetworkInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
409
    }
410
411
    /**
412
     * @param BIP32Key          $key
413
     * @param string|BIP32Path  $path
414
     * @return string
415
     */
416
    protected function getAddressFromKey(BIP32Key $key, $path) {
417
        return $this->getWalletScriptFromKey($key, $path)->getAddress()->getAddress($this->networkParams->getNetwork());
0 ignored issues
show
Bug introduced by
The method getNetwork() does not seem to exist on object<BitWasp\Bitcoin\Network\NetworkInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
418
    }
419
420
    /**
421
     * @param BIP32Key          $key
422
     * @param string|BIP32Path  $path
423
     * @return WalletScript
424
     * @throws \Exception
425
     */
426 16
    protected function getWalletScriptFromKey(BIP32Key $key, $path) {
427 16
        $path = BIP32Path::path($path)->publicPath();
428
429 16
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
430
431 16
        $multisig = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
432 16
            $key->buildKey($path)->publicKey(),
433 16
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
434 16
            $blocktrailPublicKey->buildKey($path)->publicKey()
435 16
        ]), false);
436
437 16
        $type = (int)$key->path()[2];
438 16
        if ($this->isSegwit && $type === Wallet::CHAIN_BTC_SEGWIT) {
439 3
            $witnessScript = new WitnessScript($multisig);
440 3
            $redeemScript = new P2shScript($witnessScript);
441 3
            $scriptPubKey = $redeemScript->getOutputScript();
442 16
        } else if ($type === Wallet::CHAIN_BTC_DEFAULT || $type === Wallet::CHAIN_BCC_DEFAULT) {
443 15
            $witnessScript = null;
444 15
            $redeemScript = new P2shScript($multisig);
445 15
            $scriptPubKey = $redeemScript->getOutputScript();
446
        } else {
447 1
            throw new BlocktrailSDKException("Unsupported chain in path");
448
        }
449
450 15
        return new WalletScript($path, $scriptPubKey, $redeemScript, $witnessScript);
451
    }
452
453
    /**
454
     * get the path (and redeemScript) to specified address
455
     *
456
     * @param string $address
457
     * @return array
458
     */
459 1
    public function getPathForAddress($address) {
460 1
        return $this->sdk->getPathForAddress($this->identifier, $address);
461
    }
462
463
    /**
464
     * @param string|BIP32Path  $path
465
     * @return BIP32Key
466
     * @throws \Exception
467
     */
468 16
    public function getBlocktrailPublicKey($path) {
469 16
        $path = BIP32Path::path($path);
470
471 16
        $keyIndex = str_replace("'", "", $path[1]);
472
473 16
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
474
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
475
        }
476
477 16
        return $this->blocktrailPublicKeys[$keyIndex];
478
    }
479
480
    /**
481
     * generate a new derived key and return the new path and address for it
482
     *
483
     * @param int|null $chainIndex
484
     * @return string[]     [path, address]
485
     */
486 14
    public function getNewAddressPair($chainIndex = null) {
487 14
        $path = $this->getNewDerivation($chainIndex);
488 14
        $address = $this->getAddressByPath($path);
489
490 14
        return [$path, $address];
491
    }
492
493
    /**
494
     * generate a new derived private key and return the new address for it
495
     *
496
     * @param int|null $chainIndex
497
     * @return string
498
     */
499 10
    public function getNewAddress($chainIndex = null) {
500 10
        return $this->getNewAddressPair($chainIndex)[1];
501
    }
502
503
    /**
504
     * get the balance for the wallet
505
     *
506
     * @return int[]            [confirmed, unconfirmed]
507
     */
508 9
    public function getBalance() {
509 9
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
510
511 9
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
512
    }
513
514
    /**
515
     * do wallet discovery (slow)
516
     *
517
     * @param int   $gap        the gap setting to use for discovery
518
     * @return int[]            [confirmed, unconfirmed]
519
     */
520 2
    public function doDiscovery($gap = 200) {
521 2
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
522
523 2
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
524
    }
525
526
    /**
527
     * @return TransactionBuilder
528
     */
529 13
    public function createTransaction() {
530 13
        return new TransactionBuilder($this->networkParams->getNetwork());
0 ignored issues
show
Bug introduced by
The method getNetwork() does not seem to exist on object<BitWasp\Bitcoin\Network\NetworkInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
531
    }
532
533
    /**
534
     * create, sign and send a transaction
535
     *
536
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
537
     *                                      value should be INT
538
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
539
     * @param bool     $allowZeroConf
540
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
541
     * @param string   $feeStrategy
542
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
543
     * @return string the txid / transaction hash
544
     * @throws \Exception
545
     */
546 9
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
547 9
        if ($this->locked) {
548 4
            throw new \Exception("Wallet needs to be unlocked to pay");
549
        }
550
551 9
        $outputs = self::normalizeOutputsStruct($outputs);
552
553 9
        $txBuilder = $this->createTransaction();
554 9
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
555 9
        $txBuilder->setFeeStrategy($feeStrategy);
556 9
        $txBuilder->setChangeAddress($changeAddress);
557
558 9
        foreach ($outputs as $output) {
559 9
            $txBuilder->addRecipient($output['address'], $output['value']);
560
        }
561
562 9
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
563
564 3
        $apiCheckFee = $forceFee === null;
565
566 3
        return $this->sendTx($txBuilder, $apiCheckFee);
567
    }
568
569
    /**
570
     * determine max spendable from wallet after fees
571
     *
572
     * @param bool     $allowZeroConf
573
     * @param string   $feeStrategy
574
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
575
     * @param int      $outputCnt
576
     * @return string
577
     * @throws BlocktrailSDKException
578
     */
579
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
580
        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...
581
    }
582
583
    /**
584
     * parse outputs into normalized struct
585
     *
586
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
587
     * @return array            [['address' => address, 'value' => value], ]
588
     */
589 10
    public static function normalizeOutputsStruct(array $outputs) {
590 10
        $result = [];
591
592 10
        foreach ($outputs as $k => $v) {
593 10
            if (is_numeric($k)) {
594 1
                if (!is_array($v)) {
595
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
596
                }
597
598 1
                if (isset($v['address']) && isset($v['value'])) {
599 1
                    $address = $v['address'];
600 1
                    $value = $v['value'];
601 1
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
602 1
                    $address = $v[0];
603 1
                    $value = $v[1];
604
                } else {
605 1
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
606
                }
607
            } else {
608 10
                $address = $k;
609 10
                $value = $v;
610
            }
611
612 10
            $result[] = ['address' => $address, 'value' => $value];
613
        }
614
615 10
        return $result;
616
    }
617
618
    /**
619
     * 'fund' the txBuilder with UTXOs (modified in place)
620
     *
621
     * @param TransactionBuilder    $txBuilder
622
     * @param bool|true             $lockUTXOs
623
     * @param bool|false            $allowZeroConf
624
     * @param null|int              $forceFee
625
     * @return TransactionBuilder
626
     */
627 11
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
628
        // get the data we should use for this transaction
629 11
        $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 627 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...
630
        
631 5
        $utxos = $coinSelection['utxos'];
632 5
        $fee = $coinSelection['fee'];
633 5
        $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...
634
635 5
        if ($forceFee !== null) {
636 1
            $txBuilder->setFee($forceFee);
637
        } else {
638 5
            $txBuilder->validateFee($fee);
639
        }
640
641 5
        foreach ($utxos as $utxo) {
642 5
            $signMode = SignInfo::MODE_SIGN;
643 5
            if (isset($utxo['sign_mode'])) {
644
                $signMode = $utxo['sign_mode'];
645
                if (!in_array($signMode, $this->allowedSignModes)) {
646
                    throw new \Exception("Sign mode disallowed by wallet");
647
                }
648
            }
649
650 5
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script'], $utxo['witness_script'], $signMode);
651
        }
652
653 5
        return $txBuilder;
654
    }
655
656
    /**
657
     * build inputs and outputs lists for TransactionBuilder
658
     *
659
     * @param TransactionBuilder $txBuilder
660
     * @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...
661
     * @throws \Exception
662
     */
663 7
    public function buildTx(TransactionBuilder $txBuilder) {
664 7
        $send = $txBuilder->getOutputs();
665 7
        $utxos = $txBuilder->getUtxos();
666 7
        $signInfo = [];
667
668 7
        $txb = new TxBuilder();
669
670 7
        foreach ($utxos as $utxo) {
671 7
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
672 1
                $tx = $this->sdk->transaction($utxo->hash);
673
674 1
                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...
675
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
676
                }
677
678 1
                $output = $tx['outputs'][$utxo->index];
679
680 1
                if (!$utxo->address) {
681
                    $utxo->address = AddressFactory::fromString($output['address']);
682
                }
683 1
                if (!$utxo->value) {
684
                    $utxo->value = $output['value'];
685
                }
686 1
                if (!$utxo->scriptPubKey) {
687 1
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
688
                }
689
            }
690
691 7
            if (SignInfo::MODE_SIGN === $utxo->signMode) {
692 7
                if (!$utxo->path) {
693
                    $utxo->path = $this->getPathForAddress($utxo->address->getAddress($this->networkParams->getNetwork()));
0 ignored issues
show
Bug introduced by
The method getNetwork() does not seem to exist on object<BitWasp\Bitcoin\Network\NetworkInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
694
                }
695
696 7
                if (!$utxo->redeemScript || !$utxo->witnessScript) {
697 6
                    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...
698 6
                    $utxo->redeemScript = $redeemScript;
699 6
                    $utxo->witnessScript = $witnessScript;
700
                }
701
            }
702
703 7
            $signInfo[] = $utxo->getSignInfo();
704
        }
705
706
        $utxoSum = array_sum(array_map(function (UTXO $utxo) {
707 7
            return $utxo->value;
708 7
        }, $utxos));
709 7
        if ($utxoSum < array_sum(array_column($send, 'value'))) {
710 1
            throw new \Exception("Atempting to spend more than sum of UTXOs");
711
        }
712
713 7
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
714
715 7
        if ($txBuilder->getValidateFee() !== null) {
716 5
            if (abs($txBuilder->getValidateFee() - $fee) > Wallet::BASE_FEE) {
717
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
718
            }
719
        }
720
721 7
        if ($change > 0) {
722 6
            $send[] = [
723 6
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewAddress($this->changeIndex),
724 6
                'value' => $change
725
            ];
726
        }
727
728 7
        foreach ($utxos as $utxo) {
729 7
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index));
730
        }
731
732
        // outputs should be randomized to make the change harder to detect
733 7
        if ($txBuilder->shouldRandomizeChangeOuput()) {
734 7
            shuffle($send);
735
        }
736
737 7
        foreach ($send as $out) {
738 7
            assert(isset($out['value']));
739
740 7
            if (isset($out['scriptPubKey'])) {
741 2
                $txb->output($out['value'], $out['scriptPubKey']);
742 7
            } elseif (isset($out['address'])) {
743 7
                $txb->payToAddress($out['value'], AddressFactory::fromString($out['address'], $this->networkParams->getNetwork()));
0 ignored issues
show
Bug introduced by
The method getNetwork() does not seem to exist on object<BitWasp\Bitcoin\Network\NetworkInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
744
            } else {
745 7
                throw new \Exception();
746
            }
747
        }
748
749 7
        return [$txb->get(), $signInfo];
750
    }
751
752 7
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) {
753 7
        $send = $txBuilder->getOutputs();
754 7
        $utxos = $txBuilder->getUtxos();
755
756 7
        $fee = $txBuilder->getFee();
757 7
        $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...
758
759
        // if the fee is fixed we just need to calculate the change
760 7
        if ($fee !== null) {
761 2
            $change = $this->determineChange($utxos, $send, $fee);
762
763
            // if change is not dust we need to add a change output
764 2
            if ($change > Blocktrail::DUST) {
765 1
                $send[] = ['address' => 'change', 'value' => $change];
766
            } else {
767
                // if change is dust we do nothing (implicitly it's added to the fee)
768 2
                $change = 0;
769
            }
770
        } else {
771 7
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
772
773 7
            $change = $this->determineChange($utxos, $send, $fee);
774
775 7
            if ($change > 0) {
776 6
                $changeIdx = count($send);
777
                // set dummy change output
778 6
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
779
780
                // recaculate fee now that we know that we have a change output
781 6
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
782
783
                // unset dummy change output
784 6
                unset($send[$changeIdx]);
785
786
                // if adding the change output made the fee bump up and the change is smaller than the fee
787
                //  then we're not doing change
788 6
                if ($fee2 > $fee && $fee2 > $change) {
789 1
                    $change = 0;
790
                } else {
791 6
                    $change = $this->determineChange($utxos, $send, $fee2);
792
793
                    // if change is not dust we need to add a change output
794 6
                    if ($change > Blocktrail::DUST) {
795 6
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
796
                    } else {
797
                        // if change is dust we do nothing (implicitly it's added to the fee)
798
                        $change = 0;
799
                    }
800
                }
801
            }
802
        }
803
804 7
        $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
805
806 7
        return [$fee, $change];
807
    }
808
809
    /**
810
     * create, sign and send transction based on TransactionBuilder
811
     *
812
     * @param TransactionBuilder $txBuilder
813
     * @param bool $apiCheckFee     let the API check if the fee is correct
814
     * @return string
815
     * @throws \Exception
816
     */
817 4
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
818 4
        list($tx, $signInfo) = $this->buildTx($txBuilder);
819
820 4
        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...
821
    }
822
823
    /**
824
     * !! INTERNAL METHOD, public for testing purposes !!
825
     * create, sign and send transction based on inputs and outputs
826
     *
827
     * @param Transaction $tx
828
     * @param SignInfo[]  $signInfo
829
     * @param bool $apiCheckFee     let the API check if the fee is correct
830
     * @return string
831
     * @throws \Exception
832
     * @internal
833
     */
834 4
    public function _sendTx(Transaction $tx, array $signInfo, $apiCheckFee = true) {
835 4
        if ($this->locked) {
836
            throw new \Exception("Wallet needs to be unlocked to pay");
837
        }
838
839
        assert(Util::all(function ($signInfo) {
840 4
            return $signInfo instanceof SignInfo;
841 4
        }, $signInfo), '$signInfo should be SignInfo[]');
842
843
        // sign the transaction with our keys
844 4
        $signed = $this->signTransaction($tx, $signInfo);
845
846
        $txs = [
847 4
            'signed_transaction' => $signed->getHex(),
848 4
            'base_transaction' => $signed->getBaseSerialization()->getHex(),
849
        ];
850
851
        // send the transaction
852
        return $this->sendTransaction($txs, array_map(function (SignInfo $r) {
853 4
            return (string)$r->path;
854 4
        }, $signInfo), $apiCheckFee);
855
    }
856
857
    /**
858
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
859
     *
860
     * @todo: mark this as deprecated, insist on the utxo's or qualified scripts.
861
     * @param int $utxoCnt      number of unspent inputs in transaction
862
     * @param int $outputCnt    number of outputs in transaction
863
     * @return float
864
     * @access public           reminder that people might use this!
865
     */
866 1
    public static function estimateFee($utxoCnt, $outputCnt) {
867 1
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
868
869 1
        return self::baseFeeForSize($size);
870
    }
871
872
    /**
873
     * @param int $size     size in bytes
874
     * @return int          fee in satoshi
875
     */
876 5
    public static function baseFeeForSize($size) {
877 5
        $sizeKB = (int)ceil($size / 1000);
878
879 5
        return $sizeKB * self::BASE_FEE;
880
    }
881
882
    /**
883
     * @todo: variable varint
884
     * @param int $txinSize
885
     * @param int $txoutSize
886
     * @return float
887
     */
888 9
    public static function estimateSize($txinSize, $txoutSize) {
889 9
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
890
    }
891
892
    /**
893
     * only supports estimating size for P2PKH/P2SH outputs
894
     *
895
     * @param int $outputCnt    number of outputs in transaction
896
     * @return float
897
     */
898 2
    public static function estimateSizeOutputs($outputCnt) {
899 2
        return ($outputCnt * 34);
900
    }
901
902
    /**
903
     * only supports estimating size for 2of3 multsig UTXOs
904
     *
905
     * @param int $utxoCnt      number of unspent inputs in transaction
906
     * @return float
907
     */
908 3
    public static function estimateSizeUTXOs($utxoCnt) {
909 3
        $txinSize = 0;
910
911 3
        for ($i=0; $i<$utxoCnt; $i++) {
912
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
913 3
            $multisig = "2of3";
914
915 3
            if ($multisig) {
916 3
                $sigCnt = 2;
917 3
                $msig = explode("of", $multisig);
918 3
                if (count($msig) == 2 && is_numeric($msig[0])) {
919 3
                    $sigCnt = $msig[0];
920
                }
921
922 3
                $txinSize += array_sum([
923 3
                    32, // txhash
924 3
                    4, // idx
925 3
                    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...
926 3
                    ((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...
927
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
928
                    1, // OP_0
929 3
                    4, // sequence
930
                ]);
931
            } else {
932
                $txinSize += array_sum([
933
                    32, // txhash
934
                    4, // idx
935
                    73, // sig
936
                    34, // script
937
                    4, // sequence
938
                ]);
939
            }
940
        }
941
942 3
        return $txinSize;
943
    }
944
945
    /**
946
     * determine how much fee is required based on the inputs and outputs
947
     *  this is an estimation, not a proper 100% correct calculation
948
     *
949
     * @param UTXO[]  $utxos
950
     * @param array[] $outputs
951
     * @param         $feeStrategy
952
     * @param         $optimalFeePerKB
953
     * @param         $lowPriorityFeePerKB
954
     * @return int
955
     * @throws BlocktrailSDKException
956
     */
957 7
    protected function determineFee($utxos, $outputs, $feeStrategy, $optimalFeePerKB, $lowPriorityFeePerKB) {
958
959 7
        $size = SizeEstimation::estimateVsize($utxos, $outputs);
960
961
        switch ($feeStrategy) {
962 7
            case self::FEE_STRATEGY_BASE_FEE:
963 4
                return self::baseFeeForSize($size);
964
965 4
            case self::FEE_STRATEGY_OPTIMAL:
966 4
                return (int)round(($size / 1000) * $optimalFeePerKB);
967
968 1
            case self::FEE_STRATEGY_LOW_PRIORITY:
969 1
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
970
971
            default:
972
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
973
        }
974
    }
975
976
    /**
977
     * determine how much change is left over based on the inputs and outputs and the fee
978
     *
979
     * @param UTXO[]    $utxos
980
     * @param array[]   $outputs
981
     * @param int       $fee
982
     * @return int
983
     */
984 7
    protected function determineChange($utxos, $outputs, $fee) {
985
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
986 7
            return $utxo->value;
987 7
        }, $utxos));
988 7
        $outputsTotal = array_sum(array_column($outputs, 'value'));
989
990 7
        return $inputsTotal - $outputsTotal - $fee;
991
    }
992
993
    /**
994
     * sign a raw transaction with the private keys that we have
995
     *
996
     * @param Transaction $tx
997
     * @param SignInfo[]  $signInfo
998
     * @return TransactionInterface
999
     * @throws \Exception
1000
     */
1001 4
    protected function signTransaction(Transaction $tx, array $signInfo) {
1002 4
        $signer = new Signer($tx, Bitcoin::getEcAdapter());
1003
1004 4
        assert(Util::all(function ($signInfo) {
1005 4
            return $signInfo instanceof SignInfo;
1006 4
        }, $signInfo), '$signInfo should be SignInfo[]');
1007
1008 4
        $sigHash = SigHash::ALL;
1009 4
        if ($this->networkParams->isNetwork("bitcoincash")) {
0 ignored issues
show
Bug introduced by
The method isNetwork() does not seem to exist on object<BitWasp\Bitcoin\Network\NetworkInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1010
            $sigHash |= SigHash::BITCOINCASH;
1011
            $signer->redeemBitcoinCash(true);
1012
        }
1013
1014 4
        foreach ($signInfo as $idx => $info) {
1015 4
            if ($info->mode === SignInfo::MODE_SIGN) {
1016
                // required SignInfo: path, redeemScript|witnessScript, output
1017 4
                $path = BIP32Path::path($info->path)->privatePath();
1018 4
                $key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey();
1019 4
                $signData = new SignData();
1020 4
                if ($info->redeemScript) {
1021 4
                    $signData->p2sh($info->redeemScript);
1022
                }
1023 4
                if ($info->witnessScript) {
1024
                    $signData->p2wsh($info->witnessScript);
1025
                }
1026 4
                $input = $signer->input($idx, $info->output, $signData);
1027 4
                $input->sign($key, $sigHash);
1028
            }
1029
        }
1030
1031 4
        return $signer->get();
1032
    }
1033
1034
    /**
1035
     * send the transaction using the API
1036
     *
1037
     * @param string|array  $signed
1038
     * @param string[]      $paths
1039
     * @param bool          $checkFee
1040
     * @return string           the complete raw transaction
1041
     * @throws \Exception
1042
     */
1043 4
    protected function sendTransaction($signed, $paths, $checkFee = false) {
1044 4
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
1045
    }
1046
1047
    /**
1048
     * @param \array[] $outputs
1049
     * @param bool $lockUTXO
1050
     * @param bool $allowZeroConf
1051
     * @param int|null|string $feeStrategy
1052
     * @param null $forceFee
1053
     * @return array
1054
     */
1055 11
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1056 11
        $result = $this->sdk->coinSelection($this->identifier, $outputs, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
1057
1058 5
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
1059 5
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
1060 5
        $this->feePerKBAge = time();
1061
1062 5
        return $result;
1063
    }
1064
1065 7
    public function getOptimalFeePerKB() {
1066 7
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
1067 3
            $this->updateFeePerKB();
1068
        }
1069
1070 7
        return $this->optimalFeePerKB;
1071
    }
1072
1073 7
    public function getLowPriorityFeePerKB() {
1074 7
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1075
            $this->updateFeePerKB();
1076
        }
1077
1078 7
        return $this->lowPriorityFeePerKB;
1079
    }
1080
1081 3
    public function updateFeePerKB() {
1082 3
        $result = $this->sdk->feePerKB();
1083
1084 3
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
1085 3
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
1086
1087 3
        $this->feePerKBAge = time();
1088 3
    }
1089
1090
    /**
1091
     * delete the wallet
1092
     *
1093
     * @param bool $force ignore warnings (such as non-zero balance)
1094
     * @return mixed
1095
     * @throws \Exception
1096
     */
1097 10
    public function deleteWallet($force = false) {
1098 10
        if ($this->locked) {
1099
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
1100
        }
1101
1102 10
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
1103 10
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
1104
    }
1105
1106
    /**
1107
     * create checksum to verify ownership of the master primary key
1108
     *
1109
     * @return string[]     [address, signature]
1110
     */
1111 10
    protected function createChecksumVerificationSignature() {
1112 10
        $privKey = $this->primaryPrivateKey->key();
1113
1114 10
        $pubKey = $this->primaryPrivateKey->publicKey();
1115 10
        $address = $pubKey->getAddress()->getAddress($this->networkParams->getNetwork());
0 ignored issues
show
Bug introduced by
The method getNetwork() does not seem to exist on object<BitWasp\Bitcoin\Network\NetworkInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1116
1117 10
        $signer = new MessageSigner(Bitcoin::getEcAdapter());
1118 10
        $signed = $signer->sign($address, $privKey->getPrivateKey());
1119
1120 10
        return [$address, base64_encode($signed->getCompactSignature()->getBuffer()->getBinary())];
1121
    }
1122
1123
    /**
1124
     * setup a webhook for our wallet
1125
     *
1126
     * @param string    $url            URL to receive webhook events
1127
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1128
     * @return array
1129
     */
1130 1
    public function setupWebhook($url, $identifier = null) {
1131 1
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1132 1
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1133
    }
1134
1135
    /**
1136
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1137
     * @return mixed
1138
     */
1139 1
    public function deleteWebhook($identifier = null) {
1140 1
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1141 1
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1142
    }
1143
1144
    /**
1145
     * lock a specific unspent output
1146
     *
1147
     * @param     $txHash
1148
     * @param     $txIdx
1149
     * @param int $ttl
1150
     * @return bool
1151
     */
1152
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1153
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1154
    }
1155
1156
    /**
1157
     * unlock a specific unspent output
1158
     *
1159
     * @param     $txHash
1160
     * @param     $txIdx
1161
     * @return bool
1162
     */
1163
    public function unlockUTXO($txHash, $txIdx) {
1164
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1165
    }
1166
1167
    /**
1168
     * get all transactions for the wallet (paginated)
1169
     *
1170
     * @param  integer $page    pagination: page number
1171
     * @param  integer $limit   pagination: records per page (max 500)
1172
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1173
     * @return array            associative array containing the response
1174
     */
1175 1
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1176 1
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1177
    }
1178
1179
    /**
1180
     * get all addresses for the wallet (paginated)
1181
     *
1182
     * @param  integer $page    pagination: page number
1183
     * @param  integer $limit   pagination: records per page (max 500)
1184
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1185
     * @return array            associative array containing the response
1186
     */
1187 1
    public function addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1188 1
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1189
    }
1190
1191
    /**
1192
     * get all UTXOs for the wallet (paginated)
1193
     *
1194
     * @param  integer $page        pagination: page number
1195
     * @param  integer $limit       pagination: records per page (max 500)
1196
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1197
     * @param  boolean $zeroconf    include zero confirmation transactions
1198
     * @return array                associative array containing the response
1199
     */
1200 1
    public function utxos($page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1201 1
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir, $zeroconf);
1202
    }
1203
}
1204