Completed
Pull Request — master (#118)
by Ruben de
09:49 queued 04:39
created

Wallet::getAddressByPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 1
dl 0
loc 11
ccs 7
cts 7
cp 1
crap 2
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\Base58AddressInterface;
6
use BitWasp\Bitcoin\Address\PayToPubKeyHashAddress;
7
use BitWasp\Bitcoin\Address\ScriptHashAddress;
8
use BitWasp\Bitcoin\Bitcoin;
9
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
10
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
11
use BitWasp\Bitcoin\Script\P2shScript;
12
use BitWasp\Bitcoin\Script\ScriptFactory;
13
use BitWasp\Bitcoin\Script\ScriptInterface;
14
use BitWasp\Bitcoin\Script\ScriptType;
15
use BitWasp\Bitcoin\Script\WitnessScript;
16
use BitWasp\Bitcoin\Transaction\Factory\SignData;
17
use BitWasp\Bitcoin\Transaction\Factory\Signer;
18
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
19
use BitWasp\Bitcoin\Transaction\OutPoint;
20
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
21
use BitWasp\Bitcoin\Transaction\Transaction;
22
use BitWasp\Bitcoin\Transaction\TransactionInterface;
23
use BitWasp\Buffertools\Buffer;
24
use Blocktrail\SDK\Address\AddressReaderBase;
25
use Blocktrail\SDK\Address\CashAddress;
26
use Blocktrail\SDK\Bitcoin\BIP32Key;
27
use Blocktrail\SDK\Bitcoin\BIP32Path;
28
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
29
30
/**
31
 * Class Wallet
32
 */
33
abstract class Wallet implements WalletInterface {
34
35
    const WALLET_VERSION_V1 = 'v1';
36
    const WALLET_VERSION_V2 = 'v2';
37
    const WALLET_VERSION_V3 = 'v3';
38
39
    const CHAIN_BTC_DEFAULT = 0;
40
    const CHAIN_BCC_DEFAULT = 1;
41
    const CHAIN_BTC_SEGWIT = 2;
42
43
    const BASE_FEE = 10000;
44
45
    /**
46
     * development / debug setting
47
     *  when getting a new derivation from the API,
48
     *  will verify address / redeeemScript with the values the API provides
49
     */
50
    const VERIFY_NEW_DERIVATION = true;
51
52
    /**
53
     * @var BlocktrailSDKInterface
54
     */
55
    protected $sdk;
56
57
    /**
58
     * @var string
59
     */
60
    protected $identifier;
61
62
    /**
63
     * BIP32 master primary private key (m/)
64
     *
65
     * @var BIP32Key
66
     */
67
    protected $primaryPrivateKey;
68
69
    /**
70
     * @var BIP32Key[]
71
     */
72
    protected $primaryPublicKeys;
73
74
    /**
75
     * BIP32 master backup public key (M/)
76
77
     * @var BIP32Key
78
     */
79
    protected $backupPublicKey;
80
81
    /**
82
     * map of blocktrail BIP32 public keys
83
     *  keyed by key index
84
     *  path should be `M / key_index'`
85
     *
86
     * @var BIP32Key[]
87
     */
88
    protected $blocktrailPublicKeys;
89
90
    /**
91
     * the 'Blocktrail Key Index' that is used for new addresses
92
     *
93
     * @var int
94
     */
95
    protected $keyIndex;
96
97
    /**
98
     * 'bitcoin'
99
     *
100
     * @var string
101
     */
102
    protected $network;
103
104
    /**
105
     * testnet yes / no
106
     *
107
     * @var bool
108
     */
109
    protected $testnet;
110
111
    /**
112
     * cache of public keys, by path
113
     *
114
     * @var BIP32Key[]
115
     */
116
    protected $pubKeys = [];
117
118
    /**
119
     * cache of address / redeemScript, by path
120
     *
121
     * @var string[][]      [[address, redeemScript)], ]
122
     */
123
    protected $derivations = [];
124
125
    /**
126
     * reverse cache of paths by address
127
     *
128
     * @var string[]
129
     */
130
    protected $derivationsByAddress = [];
131
132
    /**
133
     * @var string
134
     */
135
    protected $checksum;
136
137
    /**
138
     * @var bool
139
     */
140
    protected $locked = true;
141
142
    /**
143
     * @var bool
144
     */
145
    protected $isSegwit = false;
146
147
    /**
148
     * @var int
149
     */
150
    protected $chainIndex;
151
152
    /**
153
     * @var int
154
     */
155
    protected $changeIndex;
156
157
    /**
158
     * @var AddressReaderBase
159
     */
160
    protected $addressReader;
161
162
    protected $highPriorityFeePerKB;
163
    protected $optimalFeePerKB;
164
    protected $lowPriorityFeePerKB;
165
    protected $feePerKBAge;
166
    protected $allowedSignModes = [SignInfo::MODE_DONTSIGN, SignInfo::MODE_SIGN];
167
168
    /**
169
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
170
     * @param string                        $identifier                 identifier of the wallet
171
     * @param BIP32Key[]                    $primaryPublicKeys
172
     * @param BIP32Key                      $backupPublicKey            should be BIP32 master public key M/
173
     * @param BIP32Key[]                    $blocktrailPublicKeys
174
     * @param int                           $keyIndex
175
     * @param string                        $network
176
     * @param bool                          $testnet
177
     * @param bool                          $segwit
178
     * @param string                        $checksum
179
     * @throws BlocktrailSDKException
180
     */
181 31
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, array $primaryPublicKeys, $backupPublicKey, array $blocktrailPublicKeys, $keyIndex, $network, $testnet, $segwit, AddressReaderBase $addressReader, $checksum) {
182 31
        $this->sdk = $sdk;
183
184 31
        $this->identifier = $identifier;
185 31
        $this->backupPublicKey = BlocktrailSDK::normalizeBIP32Key($backupPublicKey);
186 31
        $this->primaryPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($primaryPublicKeys);
187 31
        $this->blocktrailPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($blocktrailPublicKeys);
188
189 31
        $this->network = $network;
190 31
        $this->testnet = $testnet;
191 31
        $this->keyIndex = $keyIndex;
192 31
        $this->checksum = $checksum;
193
194 31
        if ($network === "bitcoin") {
195 28
            if ($segwit) {
196 2
                $chainIdx = self::CHAIN_BTC_DEFAULT;
197 2
                $changeIdx = self::CHAIN_BTC_SEGWIT;
198
            } else {
199 26
                $chainIdx = self::CHAIN_BTC_DEFAULT;
200 28
                $changeIdx = self::CHAIN_BTC_DEFAULT;
201
            }
202
        } else {
203 3
            if ($segwit && $network === "bitcoincash") {
204
                throw new BlocktrailSDKException("Received segwit flag for bitcoincash - abort");
205
            }
206 3
            $chainIdx = self::CHAIN_BCC_DEFAULT;
207 3
            $changeIdx = self::CHAIN_BCC_DEFAULT;
208
        }
209
210 31
        $this->addressReader = $addressReader;
211 31
        $this->isSegwit = (bool) $segwit;
212 31
        $this->chainIndex = $chainIdx;
213 31
        $this->changeIndex = $changeIdx;
214 31
    }
215
216
    /**
217
     * @return AddressReaderBase
218
     */
219 8
    public function getAddressReader() {
220 8
        return $this->addressReader;
221
    }
222
223
    /**
224
     * @param int|null $chainIndex
225
     * @return WalletPath
226
     * @throws BlocktrailSDKException
227
     */
228 10
    protected function getWalletPath($chainIndex = null) {
229 10
        if ($chainIndex === null) {
230 6
            return WalletPath::create($this->keyIndex, $this->chainIndex);
231
        } else {
232 4
            if (!is_int($chainIndex)) {
233 1
                throw new BlocktrailSDKException("Chain index is invalid - should be an integer");
234
            }
235 3
            return WalletPath::create($this->keyIndex, $chainIndex);
236
        }
237
    }
238
239
    /**
240
     * @return bool
241
     */
242 4
    public function isSegwit() {
243 4
        return $this->isSegwit;
244
    }
245
246
    /**
247
     * return the wallet identifier
248
     *
249
     * @return string
250
     */
251 3
    public function getIdentifier() {
252 3
        return $this->identifier;
253
    }
254
255
    /**
256
     * Returns the wallets backup public key
257
     *
258
     * @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...
259
     */
260
    public function getBackupKey() {
261
        return $this->backupPublicKey->tuple();
262
    }
263
264
    /**
265
     * return list of Blocktrail co-sign extended public keys
266
     *
267
     * @return array[]      [ [xpub, path] ]
268
     */
269
    public function getBlocktrailPublicKeys() {
270 3
        return array_map(function (BIP32Key $key) {
271 3
            return $key->tuple();
272 3
        }, $this->blocktrailPublicKeys);
273
    }
274
275
    /**
276
     * check if wallet is locked
277
     *
278
     * @return bool
279
     */
280
    public function isLocked() {
281
        return $this->locked;
282
    }
283
284
    /**
285
     * upgrade wallet to different blocktrail cosign key
286
     *
287
     * @param $keyIndex
288
     * @return bool
289
     * @throws \Exception
290
     */
291 2
    public function upgradeKeyIndex($keyIndex) {
292 2
        if ($this->locked) {
293 1
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
294
        }
295
296 1
        $walletPath = WalletPath::create($keyIndex);
297
298
        // do the upgrade to the new 'key_index'
299 1
        $primaryPublicKey = $this->primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath());
300
301
        // $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...
302 1
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
303
304 1
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
305 1
        $this->keyIndex = $keyIndex;
306
307
        // update the blocktrail public keys
308 1
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
309 1
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
310 1
                $path = $pubKey[1];
311 1
                $pubKey = $pubKey[0];
312 1
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKey), $path);
313
            }
314
        }
315
316 1
        return true;
317
    }
318
319
    /**
320
     * get a new BIP32 derivation for the next (unused) address
321
     *  by requesting it from the API
322
     *
323
     * @return string
324
     * @param int|null $chainIndex
325
     * @throws \Exception
326
     */
327 10
    protected function getNewDerivation($chainIndex = null) {
328 10
        $path = $this->getWalletPath($chainIndex)->path()->last("*");
329
330 9
        if (self::VERIFY_NEW_DERIVATION) {
331 9
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
332
333 9
            $path = $new['path'];
334 9
            $address = $new['address'];
335
336 9
            $serverDecoded = $this->addressReader->fromString($address);
337
338 9
            $redeemScript = $new['redeem_script'];
339 9
            $witnessScript = array_key_exists('witness_script', $new) ? $new['witness_script'] : null;
340
341
            /** @var ScriptInterface $checkRedeemScript */
342
            /** @var ScriptInterface $checkWitnessScript */
343 9
            list($checkAddress, $checkRedeemScript, $checkWitnessScript) = $this->getRedeemScriptByPath($path);
344
345 9
            $oursDecoded = $this->addressReader->fromString($checkAddress);
346
347 9
            if ($this->network === "bitcoincash" &&
348 9
                $serverDecoded instanceof Base58AddressInterface &&
349 9
                $oursDecoded instanceof CashAddress
350
            ) {
351
                // our address is a cashaddr, server gave us base58.
352
353 1
                if (!$oursDecoded->getHash()->equals($serverDecoded->getHash())) {
354
                    throw new BlocktrailSDKException("Failed to verify legacy address from server [hash mismatch]");
355
                }
356
357 1
                $matchedP2PKH = $serverDecoded instanceof PayToPubKeyHashAddress && $oursDecoded->getType() === ScriptType::P2PKH;
358 1
                $matchedP2SH = $serverDecoded instanceof ScriptHashAddress && $oursDecoded->getType() === ScriptType::P2SH;
359 1
                if (!($matchedP2PKH || $matchedP2SH)) {
360
                    throw new BlocktrailSDKException("Failed to verify legacy address from server [prefix mismatch]");
361
                }
362
363
                // promote the legacy address to our cashaddr, as they are equivalent.
364 1
                $address = $checkAddress;
365
            }
366
367 9
            if ($checkAddress != $address) {
368
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
369
            }
370
371 9
            if ($checkRedeemScript && $checkRedeemScript->getHex() != $redeemScript) {
372
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript->getHex()}]");
373
            }
374
375 9
            if ($checkWitnessScript && $checkWitnessScript->getHex() != $witnessScript) {
376 9
                throw new \Exception("Failed to verify that witnessScript from API [{$witnessScript}] matches address locally [{$checkWitnessScript->getHex()}]");
377
            }
378
        } else {
379
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
380
        }
381
382 9
        return (string)$path;
383
    }
384
385
    /**
386
     * @param string|BIP32Path  $path
387
     * @return BIP32Key|false
388
     * @throws \Exception
389
     *
390
     * @TODO: hmm?
391
     */
392 18
    protected function getParentPublicKey($path) {
393 18
        $path = BIP32Path::path($path)->parent()->publicPath();
394
395 18
        if ($path->count() <= 2) {
396
            return false;
397
        }
398
399 18
        if ($path->isHardened()) {
400
            return false;
401
        }
402
403 18
        if (!isset($this->pubKeys[(string)$path])) {
404 18
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
405
        }
406
407 18
        return $this->pubKeys[(string)$path];
408
    }
409
410
    /**
411
     * get address for the specified path
412
     *
413
     * @param string|BIP32Path  $path
414
     * @return string
415
     */
416 9
    public function getAddressByPath($path) {
417 9
        $path = (string)BIP32Path::path($path)->privatePath();
418 9
        if (!isset($this->derivations[$path])) {
419 9
            list($address, ) = $this->getRedeemScriptByPath($path);
420
421 9
            $this->derivations[$path] = $address;
422 9
            $this->derivationsByAddress[$address] = $path;
423
        }
424
425 9
        return $this->derivations[$path];
426
    }
427
428
    /**
429
     * @param string $path
430
     * @return WalletScript
431
     */
432 18
    public function getWalletScriptByPath($path) {
433 18
        $path = BIP32Path::path($path);
434
435
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
436 18
        if ($pubKey = $this->getParentPublicKey($path)) {
437 18
            $key = $pubKey->buildKey($path->publicPath());
438
        } else {
439
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
440
        }
441
442 18
        return $this->getWalletScriptFromKey($key, $path);
443
    }
444
445
    /**
446
     * get address and redeemScript for specified path
447
     *
448
     * @param string    $path
449
     * @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...
450
     */
451 17
    public function getRedeemScriptByPath($path) {
452 17
        $walletScript = $this->getWalletScriptByPath($path);
453
454 16
        $redeemScript = $walletScript->isP2SH() ? $walletScript->getRedeemScript() : null;
455 16
        $witnessScript = $walletScript->isP2WSH() ? $walletScript->getWitnessScript() : null;
456 16
        return [$walletScript->getAddress()->getAddress(), $redeemScript, $witnessScript];
457
    }
458
459
    /**
460
     * @param BIP32Key          $key
461
     * @param string|BIP32Path  $path
462
     * @return string
463
     */
464
    protected function getAddressFromKey(BIP32Key $key, $path) {
465
        return $this->getWalletScriptFromKey($key, $path)->getAddress()->getAddress();
466
    }
467
468
    /**
469
     * @param BIP32Key          $key
470
     * @param string|BIP32Path  $path
471
     * @return WalletScript
472
     * @throws \Exception
473
     */
474 18
    protected function getWalletScriptFromKey(BIP32Key $key, $path) {
475 18
        $path = BIP32Path::path($path)->publicPath();
476
477 18
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
478
479 18
        $multisig = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
480 18
            $key->buildKey($path)->publicKey(),
481 18
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
482 18
            $blocktrailPublicKey->buildKey($path)->publicKey()
483 18
        ]), false);
484
485 18
        $type = (int)$key->path()[2];
486 18
        if ($this->isSegwit && $type === Wallet::CHAIN_BTC_SEGWIT) {
487 1
            $witnessScript = new WitnessScript($multisig);
488 1
            $redeemScript = new P2shScript($witnessScript);
489 1
            $scriptPubKey = $redeemScript->getOutputScript();
490 18
        } else if ($type === Wallet::CHAIN_BTC_DEFAULT || $type === Wallet::CHAIN_BCC_DEFAULT) {
491 16
            $witnessScript = null;
492 16
            $redeemScript = new P2shScript($multisig);
493 16
            $scriptPubKey = $redeemScript->getOutputScript();
494
        } else {
495 2
            throw new BlocktrailSDKException("Unsupported chain in path");
496
        }
497
498 16
        $address = $this->addressReader->fromOutputScript($scriptPubKey);
499
500 16
        return new WalletScript($path, $scriptPubKey, $redeemScript, $witnessScript, $address);
501
    }
502
503
    /**
504
     * get the path (and redeemScript) to specified address
505
     *
506
     * @param string $address
507
     * @return array
508
     */
509
    public function getPathForAddress($address) {
510
        $decoded = $this->addressReader->fromString($address);
511
        if ($decoded instanceof CashAddress) {
512
            $address = $decoded->getLegacyAddress();
513
        }
514
515
        return $this->sdk->getPathForAddress($this->identifier, $address);
0 ignored issues
show
Bug introduced by
It seems like $address defined by $decoded->getLegacyAddress() on line 512 can also be of type object<BitWasp\Bitcoin\A...PayToPubKeyHashAddress> or object<BitWasp\Bitcoin\Address\ScriptHashAddress>; however, Blocktrail\SDK\Blocktrai...ce::getPathForAddress() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
516
    }
517
518
    /**
519
     * @param string|BIP32Path  $path
520
     * @return BIP32Key
521
     * @throws \Exception
522
     */
523 18
    public function getBlocktrailPublicKey($path) {
524 18
        $path = BIP32Path::path($path);
525
526 18
        $keyIndex = str_replace("'", "", $path[1]);
527
528 18
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
529
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
530
        }
531
532 18
        return $this->blocktrailPublicKeys[$keyIndex];
533
    }
534
535
    /**
536
     * generate a new derived key and return the new path and address for it
537
     *
538
     * @param int|null $chainIndex
539
     * @return string[]     [path, address]
540
     */
541 10
    public function getNewAddressPair($chainIndex = null) {
542 10
        $path = $this->getNewDerivation($chainIndex);
543 9
        $address = $this->getAddressByPath($path);
544
545 9
        return [$path, $address];
546
    }
547
548
    /**
549
     * generate a new derived private key and return the new address for it
550
     *
551
     * @param int|null $chainIndex
552
     * @return string
553
     */
554 1
    public function getNewAddress($chainIndex = null) {
555 1
        return $this->getNewAddressPair($chainIndex)[1];
556
    }
557
558
    /**
559
     * generate a new derived private key and return the new address for it
560
     *
561
     * @return string
562
     */
563
    public function getNewChangeAddress() {
564
        return $this->getNewAddressPair($this->changeIndex)[1];
565
    }
566
567
    /**
568
     * get the balance for the wallet
569
     *
570
     * @return int[]            [confirmed, unconfirmed]
571
     */
572
    public function getBalance() {
573
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
574
575
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
576
    }
577
578
    /**
579
     * create, sign and send a transaction
580
     *
581
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
582
     *                                      value should be INT
583
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
584
     * @param bool     $allowZeroConf
585
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
586
     * @param string   $feeStrategy
587
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
588
     * @param bool     $apiCheckFee         let the API apply sanity checks to the fee
589
     * @return string the txid / transaction hash
590
     * @throws \Exception
591
     */
592 7
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $apiCheckFee = true) {
593 7
        if ($this->locked) {
594 1
            throw new \Exception("Wallet needs to be unlocked to pay");
595
        }
596
597 6
        if ($forceFee && $feeStrategy !== self::FEE_STRATEGY_FORCE_FEE) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $forceFee of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
598
            throw new \InvalidArgumentException("feeStrategy should be set to force_fee to set a forced fee");
599
        }
600
601 6
        $outputs = (new OutputsNormalizer($this->getAddressReader()))->normalize($outputs);
602
603 6
        $txBuilder = new TransactionBuilder($this->addressReader);
604 6
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
605 6
        $txBuilder->setFeeStrategy($feeStrategy);
606 6
        $txBuilder->setChangeAddress($changeAddress);
607
608 6
        foreach ($outputs as $output) {
609 6
            $txBuilder->addOutput($output);
610
        }
611
612 6
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
613
614 6
        if ($forceFee !== null) {
615 1
            $apiCheckFee = true;
616
        }
617
618 6
        return $this->sendTx($txBuilder, $apiCheckFee);
619
    }
620
621
    /**
622
     * determine max spendable from wallet after fees
623
     *
624
     * @param bool     $allowZeroConf
625
     * @param string   $feeStrategy
626
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
627
     * @param int      $outputCnt
628
     * @return string
629
     * @throws BlocktrailSDKException
630
     */
631
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
632
        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...
633
    }
634
635
    /**
636
     * parse outputs into normalized struct
637
     *
638
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
639
     * @return array            [['address' => address, 'value' => value], ]
640
     */
641 1
    public static function normalizeOutputsStruct(array $outputs) {
642 1
        $result = [];
643
644 1
        foreach ($outputs as $k => $v) {
645 1
            if (is_numeric($k)) {
646 1
                if (!is_array($v)) {
647
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
648
                }
649
650 1
                if (isset($v['address']) && isset($v['value'])) {
651 1
                    $address = $v['address'];
652 1
                    $value = $v['value'];
653 1
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
654 1
                    $address = $v[0];
655 1
                    $value = $v[1];
656
                } else {
657 1
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
658
                }
659
            } else {
660 1
                $address = $k;
661 1
                $value = $v;
662
            }
663
664 1
            $result[] = ['address' => $address, 'value' => $value];
665
        }
666
667 1
        return $result;
668
    }
669
670
    /**
671
     * 'fund' the txBuilder with UTXOs (modified in place)
672
     *
673
     * @param TransactionBuilder    $txBuilder
674
     * @param bool|true             $lockUTXOs
675
     * @param bool|false            $allowZeroConf
676
     * @param null|int              $forceFee
677
     * @return TransactionBuilder
678
     */
679 8
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
680
681
        // get the data we should use for this transaction
682 8
        $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 679 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...
683
        
684 8
        $utxos = $coinSelection['utxos'];
685 8
        $fee = $coinSelection['fee'];
686 8
        $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...
687
688 8
        if ($forceFee !== null) {
689 1
            $txBuilder->setFee($forceFee);
690
        } else {
691 7
            $txBuilder->validateFee($fee);
692
        }
693
694 8
        foreach ($utxos as $utxo) {
695 8
            $signMode = SignInfo::MODE_SIGN;
696 8
            if (isset($utxo['sign_mode'])) {
697
                $signMode = $utxo['sign_mode'];
698
                if (!in_array($signMode, $this->allowedSignModes)) {
699
                    throw new \Exception("Sign mode disallowed by wallet");
700
                }
701
            }
702
703 8
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script'], $utxo['witness_script'], $signMode);
704
        }
705
706 8
        return $txBuilder;
707
    }
708
709
    /**
710
     * build inputs and outputs lists for TransactionBuilder
711
     *
712
     * @param TransactionBuilder $txBuilder
713
     * @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...
714
     * @throws \Exception
715
     */
716 8
    public function buildTx(TransactionBuilder $txBuilder) {
717 8
        $send = $txBuilder->getOutputs();
718 8
        $utxos = $txBuilder->getUtxos();
719 8
        $signInfo = [];
720
721 8
        $txb = new TxBuilder();
722
723 8
        foreach ($utxos as $utxo) {
724 8
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
725
                $tx = $this->sdk->transaction($utxo->hash);
726
727
                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...
728
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
729
                }
730
731
                $output = $tx['outputs'][$utxo->index];
732
733
                if (!$utxo->address) {
734
                    $utxo->address = $this->addressReader->fromString($output['address']);
735
                }
736
                if (!$utxo->value) {
737
                    $utxo->value = $output['value'];
738
                }
739
                if (!$utxo->scriptPubKey) {
740
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
741
                }
742
            }
743
744 8
            if (SignInfo::MODE_SIGN === $utxo->signMode) {
745 8
                if (!$utxo->path) {
746
                    $utxo->path = $this->getPathForAddress($utxo->address->getAddress());
747
                }
748
749 8
                if (!$utxo->redeemScript || !$utxo->witnessScript) {
750 7
                    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...
751 7
                    $utxo->redeemScript = $redeemScript;
752 7
                    $utxo->witnessScript = $witnessScript;
753
                }
754
            }
755
756 8
            $signInfo[] = $utxo->getSignInfo();
757
        }
758
759 8
        $utxoSum = array_sum(array_map(function (UTXO $utxo) {
760 8
            return $utxo->value;
761 8
        }, $utxos));
762 8
        if ($utxoSum < array_sum(array_column($send, 'value'))) {
763
            throw new \Exception("Atempting to spend more than sum of UTXOs");
764
        }
765
766 8
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getHighPriorityFeePerKB(), $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
767
768 8
        if ($txBuilder->getValidateFee() !== null) {
769
            // sanity check to make sure the API isn't giving us crappy data
770 7
            if (abs($txBuilder->getValidateFee() - $fee) > (Wallet::BASE_FEE * 5)) {
771
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
772
            }
773
        }
774
775 8
        if ($change > 0) {
776 8
            $send[] = [
777 8
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewChangeAddress(),
778 8
                'value' => $change
779
            ];
780
        }
781
782 8
        foreach ($utxos as $utxo) {
783 8
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index));
784
        }
785
786
        // outputs should be randomized to make the change harder to detect
787 8
        if ($txBuilder->shouldRandomizeChangeOuput()) {
788 3
            $this->sdk->shuffle($send);
789
        }
790
791 8
        foreach ($send as $out) {
792 8
            assert(isset($out['value']));
793
794 8
            if (isset($out['scriptPubKey'])) {
795 8
                $txb->output($out['value'], $out['scriptPubKey']);
796 8
            } elseif (isset($out['address'])) {
797 8
                $txb->output($out['value'], $this->addressReader->fromString($out['address'])->getScriptPubKey());
798
            } else {
799 8
                throw new \Exception();
800
            }
801
        }
802
803 8
        return [$txb->get(), $signInfo];
804
    }
805
806 8
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB) {
807 8
        $send = (new OutputsNormalizer($this->addressReader))->normalize($txBuilder->getOutputs());
808 8
        $utxos = $txBuilder->getUtxos();
809
810 8
        $fee = $txBuilder->getFee();
811 8
        $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...
812
813
        // if the fee is fixed we just need to calculate the change
814 8
        if ($fee !== null) {
815 1
            $change = $this->determineChange($utxos, $send, $fee);
816
817
            // if change is not dust we need to add a change output
818 1
            if ($change > Blocktrail::DUST) {
819 1
                $send[] = ['address' => 'change', 'value' => $change];
820
            } else {
821
                // if change is dust we add it to the fee
822
                $fee += $change;
823
                $change = 0;
824
            }
825
826 1
            return [$fee, $change];
827
        } else {
828 7
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB);
829
830 7
            $change = $this->determineChange($utxos, $send, $fee);
831
832 7
            if ($change > 0) {
833 7
                $changeIdx = count($send);
834
                // set dummy change output
835 7
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
836
837
                // recaculate fee now that we know that we have a change output
838 7
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB);
839
840
                // unset dummy change output
841 7
                unset($send[$changeIdx]);
842
843
                // if adding the change output made the fee bump up and the change is smaller than the fee
844
                //  then we're not doing change
845 7
                if ($fee2 > $fee && $fee2 > $change) {
846
                    $change = 0;
847
                } else {
848 7
                    $change = $this->determineChange($utxos, $send, $fee2);
849
850
                    // if change is not dust we need to add a change output
851 7
                    if ($change > Blocktrail::DUST) {
852 7
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
853
                    } else {
854
                        // if change is dust we do nothing (implicitly it's added to the fee)
855
                        $change = 0;
856
                    }
857
                }
858
            }
859
860
861 7
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB);
862
863 7
            return [$fee, $change];
864
        }
865
    }
866
867
    /**
868
     * create, sign and send transction based on TransactionBuilder
869
     *
870
     * @param TransactionBuilder $txBuilder
871
     * @param bool $apiCheckFee     let the API check if the fee is correct
872
     * @return string
873
     * @throws \Exception
874
     */
875 6
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
876 6
        list($tx, $signInfo) = $this->buildTx($txBuilder);
877
878 6
        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...
879
    }
880
881
    /**
882
     * !! INTERNAL METHOD, public for testing purposes !!
883
     * create, sign and send transction based on inputs and outputs
884
     *
885
     * @param Transaction $tx
886
     * @param SignInfo[]  $signInfo
887
     * @param bool $apiCheckFee     let the API check if the fee is correct
888
     * @return string
889
     * @throws \Exception
890
     * @internal
891
     */
892 6
    public function _sendTx(Transaction $tx, array $signInfo, $apiCheckFee = true) {
893 6
        if ($this->locked) {
894
            throw new \Exception("Wallet needs to be unlocked to pay");
895
        }
896
897 6
        assert(Util::all(function ($signInfo) {
898 6
            return $signInfo instanceof SignInfo;
899 6
        }, $signInfo), '$signInfo should be SignInfo[]');
900
901
        // sign the transaction with our keys
902 6
        $signed = $this->signTransaction($tx, $signInfo);
903
904
        $txs = [
905 6
            'signed_transaction' => $signed->getHex(),
906 6
            'base_transaction' => $signed->getBaseSerialization()->getHex(),
907
        ];
908
909
        // send the transaction
910 6
        return $this->sendTransaction($txs, array_map(function (SignInfo $r) {
911 6
            return (string)$r->path;
912 6
        }, $signInfo), $apiCheckFee);
913
    }
914
915
    /**
916
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
917
     *
918
     * @todo: mark this as deprecated, insist on the utxo's or qualified scripts.
919
     * @param int $utxoCnt      number of unspent inputs in transaction
920
     * @param int $outputCnt    number of outputs in transaction
921
     * @return float
922
     * @access public           reminder that people might use this!
923
     */
924
    public static function estimateFee($utxoCnt, $outputCnt) {
925
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
926
927
        return self::baseFeeForSize($size);
928
    }
929
930
    /**
931
     * @param int $size     size in bytes
932
     * @return int          fee in satoshi
933
     */
934
    public static function baseFeeForSize($size) {
935
        $sizeKB = (int)ceil($size / 1000);
936
937
        return $sizeKB * self::BASE_FEE;
938
    }
939
940
    /**
941
     * @todo: variable varint
942
     * @todo: deprecate
943
     * @param int $txinSize
944
     * @param int $txoutSize
945
     * @return float
946
     */
947
    public static function estimateSize($txinSize, $txoutSize) {
948
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
949
    }
950
951
    /**
952
     * only supports estimating size for P2PKH/P2SH outputs
953
     *
954
     * @param int $outputCnt    number of outputs in transaction
955
     * @return float
956
     */
957
    public static function estimateSizeOutputs($outputCnt) {
958
        return ($outputCnt * 34);
959
    }
960
961
    /**
962
     * only supports estimating size for 2of3 multsig UTXOs
963
     *
964
     * @param int $utxoCnt      number of unspent inputs in transaction
965
     * @return float
966
     */
967
    public static function estimateSizeUTXOs($utxoCnt) {
968
        $txinSize = 0;
969
970
        for ($i=0; $i<$utxoCnt; $i++) {
971
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
972
            $multisig = "2of3";
973
974
            if ($multisig) {
975
                $sigCnt = 2;
976
                $msig = explode("of", $multisig);
977
                if (count($msig) == 2 && is_numeric($msig[0])) {
978
                    $sigCnt = $msig[0];
979
                }
980
981
                $txinSize += array_sum([
982
                    32, // txhash
983
                    4, // idx
984
                    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...
985
                    ((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...
986
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
987
                    1, // OP_0
988
                    4, // sequence
989
                ]);
990
            } else {
991
                $txinSize += array_sum([
992
                    32, // txhash
993
                    4, // idx
994
                    73, // sig
995
                    34, // script
996
                    4, // sequence
997
                ]);
998
            }
999
        }
1000
1001
        return $txinSize;
1002
    }
1003
1004
    /**
1005
     * determine how much fee is required based on the inputs and outputs
1006
     *  this is an estimation, not a proper 100% correct calculation
1007
     *
1008
     * @param UTXO[]  $utxos
1009
     * @param array[] $outputs
1010
     * @param         $feeStrategy
1011
     * @param         $highPriorityFeePerKB
1012
     * @param         $optimalFeePerKB
1013
     * @param         $lowPriorityFeePerKB
1014
     * @return int
1015
     * @throws BlocktrailSDKException
1016
     */
1017 7
    protected function determineFee($utxos, $outputs, $feeStrategy, $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB) {
1018
1019 7
        $size = SizeEstimation::estimateVsize($utxos, $outputs);
1020
1021
        switch ($feeStrategy) {
1022 7
            case self::FEE_STRATEGY_BASE_FEE:
1023
                return self::baseFeeForSize($size);
1024
1025 7
            case self::FEE_STRATEGY_HIGH_PRIORITY:
1026
                return (int)round(($size / 1000) * $highPriorityFeePerKB);
1027
1028 7
            case self::FEE_STRATEGY_OPTIMAL:
1029 6
                return (int)round(($size / 1000) * $optimalFeePerKB);
1030
1031 1
            case self::FEE_STRATEGY_LOW_PRIORITY:
1032 1
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
1033
1034
            case self::FEE_STRATEGY_FORCE_FEE:
1035
                throw new BlocktrailSDKException("Can't determine when for force_fee");
1036
1037
            default:
1038
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
1039
        }
1040
    }
1041
1042
    /**
1043
     * determine how much change is left over based on the inputs and outputs and the fee
1044
     *
1045
     * @param UTXO[]    $utxos
1046
     * @param array[]   $outputs
1047
     * @param int       $fee
1048
     * @return int
1049
     */
1050
    protected function determineChange($utxos, $outputs, $fee) {
1051 8
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
1052 8
            return $utxo->value;
1053 8
        }, $utxos));
1054 8
        $outputsTotal = array_sum(array_column($outputs, 'value'));
1055
1056 8
        return $inputsTotal - $outputsTotal - $fee;
1057
    }
1058
1059
    /**
1060
     * sign a raw transaction with the private keys that we have
1061
     *
1062
     * @param Transaction $tx
1063
     * @param SignInfo[]  $signInfo
1064
     * @return TransactionInterface
1065
     * @throws \Exception
1066
     */
1067 6
    protected function signTransaction(Transaction $tx, array $signInfo) {
1068 6
        $signer = new Signer($tx, Bitcoin::getEcAdapter());
1069
1070 6
        assert(Util::all(function ($signInfo) {
1071 6
            return $signInfo instanceof SignInfo;
1072 6
        }, $signInfo), '$signInfo should be SignInfo[]');
1073
1074 6
        $sigHash = SigHash::ALL;
1075 6
        if ($this->network === "bitcoincash") {
1076
            $sigHash |= SigHash::BITCOINCASH;
1077
            $signer->redeemBitcoinCash(true);
1078
        }
1079
1080 6
        foreach ($signInfo as $idx => $info) {
1081 6
            if ($info->mode === SignInfo::MODE_SIGN) {
1082
                // required SignInfo: path, redeemScript|witnessScript, output
1083 6
                $path = BIP32Path::path($info->path)->privatePath();
1084 6
                $key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey();
1085 6
                $signData = new SignData();
1086 6
                if ($info->redeemScript) {
1087 6
                    $signData->p2sh($info->redeemScript);
1088
                }
1089 6
                if ($info->witnessScript) {
1090 2
                    $signData->p2wsh($info->witnessScript);
1091
                }
1092 6
                $input = $signer->input($idx, $info->output, $signData);
1093 6
                $input->sign($key, $sigHash);
1094
            }
1095
        }
1096
1097 6
        return $signer->get();
1098
    }
1099
1100
    /**
1101
     * send the transaction using the API
1102
     *
1103
     * @param string|array  $signed
1104
     * @param string[]      $paths
1105
     * @param bool          $checkFee
1106
     * @return string           the complete raw transaction
1107
     * @throws \Exception
1108
     */
1109 6
    protected function sendTransaction($signed, $paths, $checkFee = false) {
1110 6
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
1111
    }
1112
1113
    /**
1114
     * @param \array[] $outputs
1115
     * @param bool $lockUTXO
1116
     * @param bool $allowZeroConf
1117
     * @param int|null|string $feeStrategy
1118
     * @param null $forceFee
1119
     * @return array
1120
     */
1121 8
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1122 8
        $send = [];
1123 8
        foreach ((new OutputsNormalizer($this->addressReader))->normalize($outputs) as $output) {
1124 8
            $send[] = [
1125 8
                "value" => $output['value'],
1126 8
                "scriptPubKey" => $output['scriptPubKey']->getHex(),
1127
            ];
1128
        }
1129
1130 8
        $result = $this->sdk->coinSelection($this->identifier, $send, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
1131
1132 8
        $this->highPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_HIGH_PRIORITY];
1133 8
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
1134 8
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
1135 8
        $this->feePerKBAge = time();
1136
1137 8
        return $result;
1138
    }
1139
1140 8
    public function getHighPriorityFeePerKB() {
1141 8
        if (!$this->highPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1142
            $this->updateFeePerKB();
1143
        }
1144
1145 8
        return $this->highPriorityFeePerKB;
1146
    }
1147
1148 8
    public function getOptimalFeePerKB() {
1149 8
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
1150
            $this->updateFeePerKB();
1151
        }
1152
1153 8
        return $this->optimalFeePerKB;
1154
    }
1155
1156 8
    public function getLowPriorityFeePerKB() {
1157 8
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1158
            $this->updateFeePerKB();
1159
        }
1160
1161 8
        return $this->lowPriorityFeePerKB;
1162
    }
1163
1164
    public function updateFeePerKB() {
1165
        $result = $this->sdk->feePerKB();
1166
1167
        $this->highPriorityFeePerKB = $result[self::FEE_STRATEGY_HIGH_PRIORITY];
1168
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
1169
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
1170
1171
        $this->feePerKBAge = time();
1172
    }
1173
1174
    /**
1175
     * delete the wallet
1176
     *
1177
     * @param bool $force ignore warnings (such as non-zero balance)
1178
     * @return mixed
1179
     * @throws \Exception
1180
     */
1181
    public function deleteWallet($force = false) {
1182
        if ($this->locked) {
1183
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
1184
        }
1185
1186
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
1187
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
1188
    }
1189
1190
    /**
1191
     * create checksum to verify ownership of the master primary key
1192
     *
1193
     * @return string[]     [address, signature]
1194
     */
1195
    protected function createChecksumVerificationSignature() {
1196
        $privKey = $this->primaryPrivateKey->key();
1197
1198
        $pubKey = $this->primaryPrivateKey->publicKey();
1199
        $address = $pubKey->getAddress()->getAddress();
1200
1201
        $signer = new MessageSigner(Bitcoin::getEcAdapter());
1202
        $signed = $signer->sign($address, $privKey->getPrivateKey());
1203
1204
        return [$address, base64_encode($signed->getCompactSignature()->getBuffer()->getBinary())];
1205
    }
1206
1207
    /**
1208
     * setup a webhook for our wallet
1209
     *
1210
     * @param string    $url            URL to receive webhook events
1211
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1212
     * @return array
1213
     */
1214 2
    public function setupWebhook($url, $identifier = null) {
1215 2
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1216 2
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1217
    }
1218
1219
    /**
1220
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1221
     * @return mixed
1222
     */
1223 2
    public function deleteWebhook($identifier = null) {
1224 2
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1225 2
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1226
    }
1227
1228
    /**
1229
     * lock a specific unspent output
1230
     *
1231
     * @param     $txHash
1232
     * @param     $txIdx
1233
     * @param int $ttl
1234
     * @return bool
1235
     */
1236
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1237
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1238
    }
1239
1240
    /**
1241
     * unlock a specific unspent output
1242
     *
1243
     * @param     $txHash
1244
     * @param     $txIdx
1245
     * @return bool
1246
     */
1247
    public function unlockUTXO($txHash, $txIdx) {
1248
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1249
    }
1250
1251
    /**
1252
     * get all transactions for the wallet (paginated)
1253
     *
1254
     * @param  integer $page    pagination: page number
1255
     * @param  integer $limit   pagination: records per page (max 500)
1256
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1257
     * @return array            associative array containing the response
1258
     */
1259 1
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1260 1
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1261
    }
1262
1263
    /**
1264
     * get all addresses for the wallet (paginated)
1265
     *
1266
     * @param  integer $page    pagination: page number
1267
     * @param  integer $limit   pagination: records per page (max 500)
1268
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1269
     * @return array            associative array containing the response
1270
     */
1271
    public function addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1272
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1273
    }
1274
1275
    /**
1276
     * get all UTXOs for the wallet (paginated)
1277
     *
1278
     * @param  integer $page        pagination: page number
1279
     * @param  integer $limit       pagination: records per page (max 500)
1280
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1281
     * @param  boolean $zeroconf    include zero confirmation transactions
1282
     * @return array                associative array containing the response
1283
     */
1284
    public function utxos($page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1285
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir, $zeroconf);
1286
    }
1287
}
1288