Completed
Pull Request — master (#99)
by thomas
70:33
created

Wallet   F

Complexity

Total Complexity 157

Size/Duplication

Total Lines 1245
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 36

Test Coverage

Coverage 89.95%

Importance

Changes 0
Metric Value
dl 0
loc 1245
rs 0.5217
c 0
b 0
f 0
ccs 358
cts 398
cp 0.8995
wmc 157
lcom 1
cbo 36

54 Methods

Rating   Name   Duplication   Size   Complexity  
A getAddressReader() 0 3 1
A getBlocktrailPublicKeys() 0 5 1
A isLocked() 0 3 1
A getNewAddress() 0 3 1
B coinSelectionForTxBuilder() 0 29 5
A estimateFee() 0 5 1
B __construct() 0 34 5
A getWalletPath() 0 10 3
A isSegwit() 0 3 1
A getIdentifier() 0 3 1
A getBackupKey() 0 3 1
B upgradeKeyIndex() 0 27 4
C getNewDerivation() 0 57 16
A getParentPublicKey() 0 17 4
A getAddressByPath() 0 11 2
A getWalletScriptByPath() 0 12 2
A getRedeemScriptByPath() 0 7 3
A getAddressFromKey() 0 3 1
B getWalletScriptFromKey() 0 28 5
A getPathForAddress() 0 8 2
A getBlocktrailPublicKey() 0 11 2
A getNewAddressPair() 0 6 1
A getNewChangeAddress() 0 3 1
A getBalance() 0 5 1
A doDiscovery() 0 5 1
A pay() 0 22 3
A getMaxSpendable() 0 3 1
D normalizeOutputsStruct() 0 28 9
F buildTx() 0 89 24
B determineFeeAndChange() 0 56 7
A sendTx() 0 5 1
A _sendTx() 0 22 2
A baseFeeForSize() 0 5 1
A estimateSize() 0 3 1
A estimateSizeOutputs() 0 3 1
B estimateSizeUTXOs() 0 36 5
B determineFee() 0 21 5
A determineChange() 0 8 1
B signTransaction() 0 32 6
A sendTransaction() 0 3 1
A coinSelection() 0 10 1
A getHighPriorityFeePerKB() 0 7 3
A getOptimalFeePerKB() 0 7 3
A getLowPriorityFeePerKB() 0 7 3
A updateFeePerKB() 0 9 1
A deleteWallet() 0 8 2
A createChecksumVerificationSignature() 0 11 1
A setupWebhook() 0 4 2
A deleteWebhook() 0 4 2
A lockUTXO() 0 3 1
A unlockUTXO() 0 3 1
A transactions() 0 3 1
A addresses() 0 3 1
A utxos() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Wallet often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Wallet, and based on these observations, apply Extract Interface, too.

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\BitcoinCashAddressReader;
26
use Blocktrail\SDK\Address\CashAddress;
27
use Blocktrail\SDK\Bitcoin\BIP32Key;
28
use Blocktrail\SDK\Bitcoin\BIP32Path;
29
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
30
31
/**
32
 * Class Wallet
33
 */
34
abstract class Wallet implements WalletInterface {
35
36
    const WALLET_VERSION_V1 = 'v1';
37
    const WALLET_VERSION_V2 = 'v2';
38
    const WALLET_VERSION_V3 = 'v3';
39
40
    const CHAIN_BTC_DEFAULT = 0;
41
    const CHAIN_BCC_DEFAULT = 1;
42
    const CHAIN_BTC_SEGWIT = 2;
43
44
    const BASE_FEE = 10000;
45
46
    /**
47
     * development / debug setting
48
     *  when getting a new derivation from the API,
49
     *  will verify address / redeeemScript with the values the API provides
50
     */
51
    const VERIFY_NEW_DERIVATION = true;
52
53
    /**
54
     * @var BlocktrailSDKInterface
55
     */
56
    protected $sdk;
57
58
    /**
59
     * @var string
60
     */
61
    protected $identifier;
62
63
    /**
64
     * BIP32 master primary private key (m/)
65
     *
66
     * @var BIP32Key
67
     */
68
    protected $primaryPrivateKey;
69
70
    /**
71
     * @var BIP32Key[]
72
     */
73
    protected $primaryPublicKeys;
74
75
    /**
76
     * BIP32 master backup public key (M/)
77
78
     * @var BIP32Key
79
     */
80
    protected $backupPublicKey;
81
82
    /**
83
     * map of blocktrail BIP32 public keys
84
     *  keyed by key index
85
     *  path should be `M / key_index'`
86
     *
87
     * @var BIP32Key[]
88
     */
89
    protected $blocktrailPublicKeys;
90
91
    /**
92
     * the 'Blocktrail Key Index' that is used for new addresses
93
     *
94
     * @var int
95
     */
96
    protected $keyIndex;
97
98
    /**
99
     * 'bitcoin'
100
     *
101
     * @var string
102
     */
103
    protected $network;
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
    /**
134
     * @var string
135
     */
136
    protected $checksum;
137
138
    /**
139
     * @var bool
140
     */
141
    protected $locked = true;
142
143
    /**
144
     * @var bool
145
     */
146
    protected $isSegwit = false;
147
148
    /**
149
     * @var int
150
     */
151
    protected $chainIndex;
152
153
    /**
154
     * @var int
155
     */
156
    protected $changeIndex;
157
158
    /**
159
     * @var AddressReaderBase
160
     */
161
    protected $addressReader;
162
163
    protected $highPriorityFeePerKB;
164
    protected $optimalFeePerKB;
165
    protected $lowPriorityFeePerKB;
166
    protected $feePerKBAge;
167
    protected $allowedSignModes = [SignInfo::MODE_DONTSIGN, SignInfo::MODE_SIGN];
168 22
169 22
    /**
170
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
171 22
     * @param string                        $identifier                 identifier of the wallet
172 22
     * @param BIP32Key[]                    $primaryPublicKeys
173 22
     * @param BIP32Key                      $backupPublicKey            should be BIP32 master public key M/
174 22
     * @param BIP32Key[]                    $blocktrailPublicKeys
175
     * @param int                           $keyIndex
176 22
     * @param string                        $network
177 22
     * @param bool                          $testnet
178 22
     * @param bool                          $segwit
179 22
     * @param string                        $checksum
180
     * @throws BlocktrailSDKException
181 22
     */
182 22
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, array $primaryPublicKeys, $backupPublicKey, array $blocktrailPublicKeys, $keyIndex, $network, $testnet, $segwit, AddressReaderBase $addressReader, $checksum) {
183 3
        $this->sdk = $sdk;
184 3
185
        $this->identifier = $identifier;
186 20
        $this->backupPublicKey = BlocktrailSDK::normalizeBIP32Key($backupPublicKey);
187 22
        $this->primaryPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($primaryPublicKeys);
188
        $this->blocktrailPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($blocktrailPublicKeys);
189
190
        $this->network = $network;
191
        $this->testnet = $testnet;
192
        $this->keyIndex = $keyIndex;
193
        $this->checksum = $checksum;
194
195
        if ($network === "bitcoin") {
196
            if ($segwit) {
197 22
                $chainIdx = self::CHAIN_BTC_DEFAULT;
198 22
                $changeIdx = self::CHAIN_BTC_SEGWIT;
199 22
            } else {
200 22
                $chainIdx = self::CHAIN_BTC_DEFAULT;
201
                $changeIdx = self::CHAIN_BTC_DEFAULT;
202
            }
203
        } else {
204
            if ($segwit && $network === "bitcoincash") {
205
                throw new BlocktrailSDKException("Received segwit flag for bitcoincash - abort");
206
            }
207 14
            $chainIdx = self::CHAIN_BCC_DEFAULT;
208 14
            $changeIdx = self::CHAIN_BCC_DEFAULT;
209 12
        }
210
211 6
        $this->addressReader = $addressReader;
212 1
        $this->isSegwit = (bool) $segwit;
213
        $this->chainIndex = $chainIdx;
214 5
        $this->changeIndex = $changeIdx;
215
    }
216
217
    /**
218
     * @return AddressReaderBase
219
     */
220
    public function getAddressReader() {
221 3
        return $this->addressReader;
222 3
    }
223
224
    /**
225
     * @param int|null $chainIndex
226
     * @return WalletPath
227
     * @throws BlocktrailSDKException
228
     */
229
    protected function getWalletPath($chainIndex = null) {
230 10
        if ($chainIndex === null) {
231 10
            return WalletPath::create($this->keyIndex, $this->chainIndex);
232
        } else {
233
            if (!is_int($chainIndex)) {
234
                throw new BlocktrailSDKException("Chain index is invalid - should be an integer");
235
            }
236
            return WalletPath::create($this->keyIndex, $chainIndex);
237
        }
238
    }
239 1
240 1
    /**
241
     * @return bool
242
     */
243
    public function isSegwit() {
244
        return $this->isSegwit;
245
    }
246
247
    /**
248 5
     * return the wallet identifier
249
     *
250 5
     * @return string
251 5
     */
252
    public function getIdentifier() {
253
        return $this->identifier;
254
    }
255
256
    /**
257
     * Returns the wallets backup public key
258
     *
259 10
     * @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...
260 10
     */
261
    public function getBackupKey() {
262
        return $this->backupPublicKey->tuple();
263
    }
264
265
    /**
266
     * return list of Blocktrail co-sign extended public keys
267
     *
268
     * @return array[]      [ [xpub, path] ]
269
     */
270 5
    public function getBlocktrailPublicKeys() {
271 5
        return array_map(function (BIP32Key $key) {
272 4
            return $key->tuple();
273
        }, $this->blocktrailPublicKeys);
274
    }
275 5
276
    /**
277
     * check if wallet is locked
278 5
     *
279
     * @return bool
280
     */
281 5
    public function isLocked() {
282
        return $this->locked;
283 5
    }
284 5
285
    /**
286
     * upgrade wallet to different blocktrail cosign key
287 5
     *
288 5
     * @param $keyIndex
289 5
     * @return bool
290 5
     * @throws \Exception
291 5
     */
292
    public function upgradeKeyIndex($keyIndex) {
293
        if ($this->locked) {
294
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
295 5
        }
296
297
        $walletPath = WalletPath::create($keyIndex);
298
299
        // do the upgrade to the new 'key_index'
300
        $primaryPublicKey = $this->primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath());
301
302
        // $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...
303
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
304
305
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
306 14
        $this->keyIndex = $keyIndex;
307 14
308
        // update the blocktrail public keys
309 13
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
310 13
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
311
                $path = $pubKey[1];
312 13
                $pubKey = $pubKey[0];
313 13
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKey), $path);
314 13
            }
315 13
        }
316
317
        return true;
318
    }
319 13
320
    /**
321 13
     * get a new BIP32 derivation for the next (unused) address
322
     *  by requesting it from the API
323
     *
324
     * @return string
325 13
     * @param int|null $chainIndex
326
     * @throws \Exception
327
     */
328
    protected function getNewDerivation($chainIndex = null) {
329 13
        $path = $this->getWalletPath($chainIndex)->path()->last("*");
330 13
331
        if (self::VERIFY_NEW_DERIVATION) {
332
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
333
334
            $path = $new['path'];
335
            $address = $new['address'];
336 13
337
            $serverDecoded = $this->addressReader->fromString($address);
338
339
            $redeemScript = $new['redeem_script'];
340
            $witnessScript = array_key_exists('witness_script', $new) ? $new['witness_script'] : null;
341
342
            /** @var ScriptInterface $checkRedeemScript */
343
            /** @var ScriptInterface $checkWitnessScript */
344
            list($checkAddress, $checkRedeemScript, $checkWitnessScript) = $this->getRedeemScriptByPath($path);
345
346 16
            $oursDecoded = $this->addressReader->fromString($checkAddress);
347 16
348
            if ($this->network === "bitcoincash" &&
349 16
                $serverDecoded instanceof Base58AddressInterface &&
350
                $oursDecoded instanceof CashAddress
351
            ) {
352
                // our address is a cashaddr, server gave us base58.
353 16
354
                if (!$oursDecoded->getHash()->equals($serverDecoded->getHash())) {
355
                    throw new BlocktrailSDKException("Failed to verify legacy address from server [hash mismatch]");
356
                }
357 16
358 16
                $matchedP2PKH = $serverDecoded instanceof PayToPubKeyHashAddress && $oursDecoded->getType() === ScriptType::P2PKH;
359
                $matchedP2SH = $serverDecoded instanceof ScriptHashAddress && $oursDecoded->getType() === ScriptType::P2SH;
360
                if (!($matchedP2PKH || $matchedP2SH)) {
361 16
                    throw new BlocktrailSDKException("Failed to verify legacy address from server [prefix mismatch]");
362
                }
363
364
                // promote the legacy address to our cashaddr, as they are equivalent.
365
                $address = $checkAddress;
366
            }
367
368
            if ($checkAddress != $address) {
369
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
370 13
            }
371 13
372 13
            if ($checkRedeemScript && $checkRedeemScript->getHex() != $redeemScript) {
373 13
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript->getHex()}]");
374
            }
375 13
376 13
            if ($checkWitnessScript && $checkWitnessScript->getHex() != $witnessScript) {
377
                throw new \Exception("Failed to verify that witnessScript from API [{$witnessScript}] matches address locally [{$checkWitnessScript->getHex()}]");
378
            }
379 13
        } else {
380
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
381
        }
382
383
        return (string)$path;
384
    }
385
386 16
    /**
387 16
     * @param string|BIP32Path  $path
388
     * @return BIP32Key|false
389
     * @throws \Exception
390 16
     *
391 16
     * @TODO: hmm?
392
     */
393
    protected function getParentPublicKey($path) {
394
        $path = BIP32Path::path($path)->parent()->publicPath();
395
396 16
        if ($path->count() <= 2) {
397
            return false;
398
        }
399
400
        if ($path->isHardened()) {
401
            return false;
402
        }
403
404
        if (!isset($this->pubKeys[(string)$path])) {
405 15
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
406 15
        }
407
408 15
        return $this->pubKeys[(string)$path];
409 15
    }
410 15
411
    /**
412
     * get address for the specified path
413
     *
414
     * @param string|BIP32Path  $path
415
     * @return string
416
     */
417
    public function getAddressByPath($path) {
418
        $path = (string)BIP32Path::path($path)->privatePath();
419
        if (!isset($this->derivations[$path])) {
420
            list($address, ) = $this->getRedeemScriptByPath($path);
421
422
            $this->derivations[$path] = $address;
423
            $this->derivationsByAddress[$address] = $path;
424
        }
425
426
        return $this->derivations[$path];
427
    }
428 16
429 16
    /**
430
     * @param string $path
431 16
     * @return WalletScript
432
     */
433 16
    public function getWalletScriptByPath($path) {
434 16
        $path = BIP32Path::path($path);
435 16
436 16
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
437 16
        if ($pubKey = $this->getParentPublicKey($path)) {
438
            $key = $pubKey->buildKey($path->publicPath());
439 16
        } else {
440 16
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
441 3
        }
442 3
443 3
        return $this->getWalletScriptFromKey($key, $path);
444 16
    }
445 15
446 15
    /**
447 15
     * get address and redeemScript for specified path
448
     *
449 1
     * @param string    $path
450
     * @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...
451
     */
452 15
    public function getRedeemScriptByPath($path) {
453
        $walletScript = $this->getWalletScriptByPath($path);
454
455
        $redeemScript = $walletScript->isP2SH() ? $walletScript->getRedeemScript() : null;
456
        $witnessScript = $walletScript->isP2WSH() ? $walletScript->getWitnessScript() : null;
457
        return [$walletScript->getAddress()->getAddress(), $redeemScript, $witnessScript];
458
    }
459
460
    /**
461 1
     * @param BIP32Key          $key
462 1
     * @param string|BIP32Path  $path
463
     * @return string
464
     */
465
    protected function getAddressFromKey(BIP32Key $key, $path) {
466
        return $this->getWalletScriptFromKey($key, $path)->getAddress()->getAddress();
467
    }
468
469
    /**
470 16
     * @param BIP32Key          $key
471 16
     * @param string|BIP32Path  $path
472
     * @return WalletScript
473 16
     * @throws \Exception
474
     */
475 16
    protected function getWalletScriptFromKey(BIP32Key $key, $path) {
476
        $path = BIP32Path::path($path)->publicPath();
477
478
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
479 16
480
        $multisig = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
481
            $key->buildKey($path)->publicKey(),
482
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
483
            $blocktrailPublicKey->buildKey($path)->publicKey()
484
        ]), false);
485
486
        $type = (int)$key->path()[2];
487
        if ($this->isSegwit && $type === Wallet::CHAIN_BTC_SEGWIT) {
488 14
            $witnessScript = new WitnessScript($multisig);
489 14
            $redeemScript = new P2shScript($witnessScript);
490 13
            $scriptPubKey = $redeemScript->getOutputScript();
491
        } else if ($type === Wallet::CHAIN_BTC_DEFAULT || $type === Wallet::CHAIN_BCC_DEFAULT) {
492 13
            $witnessScript = null;
493
            $redeemScript = new P2shScript($multisig);
494
            $scriptPubKey = $redeemScript->getOutputScript();
495
        } else {
496
            throw new BlocktrailSDKException("Unsupported chain in path");
497
        }
498
499
        $address = $this->addressReader->fromOutputScript($scriptPubKey);
500
501 7
        return new WalletScript($path, $scriptPubKey, $redeemScript, $witnessScript, $address);
502 7
    }
503
504
    /**
505
     * get the path (and redeemScript) to specified address
506
     *
507
     * @param string $address
508
     * @return array
509
     */
510 4
    public function getPathForAddress($address) {
511 4
        $decoded = $this->addressReader->fromString($address);
512
        if ($decoded instanceof CashAddress) {
513
            $address = $decoded->getLegacyAddress();
514
        }
515
516
        return $this->sdk->getPathForAddress($this->identifier, $address);
0 ignored issues
show
Bug introduced by
It seems like $address defined by $decoded->getLegacyAddress() on line 513 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...
517
    }
518
519 9
    /**
520 9
     * @param string|BIP32Path  $path
521
     * @return BIP32Key
522 9
     * @throws \Exception
523
     */
524
    public function getBlocktrailPublicKey($path) {
525
        $path = BIP32Path::path($path);
526
527
        $keyIndex = str_replace("'", "", $path[1]);
528
529
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
530
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
531 2
        }
532 2
533
        return $this->blocktrailPublicKeys[$keyIndex];
534 2
    }
535
536
    /**
537
     * generate a new derived key and return the new path and address for it
538
     *
539
     * @param int|null $chainIndex
540
     * @return string[]     [path, address]
541
     */
542
    public function getNewAddressPair($chainIndex = null) {
543
        $path = $this->getNewDerivation($chainIndex);
544
        $address = $this->getAddressByPath($path);
545
546
        return [$path, $address];
547
    }
548
549
    /**
550 9
     * generate a new derived private key and return the new address for it
551 9
     *
552 4
     * @param int|null $chainIndex
553
     * @return string
554
     */
555 9
    public function getNewAddress($chainIndex = null) {
556
        return $this->getNewAddressPair($chainIndex)[1];
557 9
    }
558 9
559 9
    /**
560 9
     * generate a new derived private key and return the new address for it
561
     *
562 9
     * @return string
563 9
     */
564
    public function getNewChangeAddress() {
565
        return $this->getNewAddressPair($this->changeIndex)[1];
566 9
    }
567
568 3
    /**
569
     * get the balance for the wallet
570 3
     *
571
     * @return int[]            [confirmed, unconfirmed]
572
     */
573
    public function getBalance() {
574
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
575
576
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
577
    }
578
579
    /**
580
     * do wallet discovery (slow)
581
     *
582
     * @param int   $gap        the gap setting to use for discovery
583
     * @return int[]            [confirmed, unconfirmed]
584
     */
585
    public function doDiscovery($gap = 200) {
586
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
587
588
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
589
    }
590
591
    /**
592
     * create, sign and send a transaction
593 10
     *
594 10
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
595
     *                                      value should be INT
596 10
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
597 10
     * @param bool     $allowZeroConf
598 1
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
599
     * @param string   $feeStrategy
600
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
601
     * @return string the txid / transaction hash
602 1
     * @throws \Exception
603 1
     */
604 1
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
605 1
        if ($this->locked) {
606 1
            throw new \Exception("Wallet needs to be unlocked to pay");
607 1
        }
608
609 1
        $outputs = (new OutputsNormalizer($this->getAddressReader()))->normalize($outputs);
610
611
        $txBuilder = new TransactionBuilder($this->addressReader);
612 10
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
613 10
        $txBuilder->setFeeStrategy($feeStrategy);
614
        $txBuilder->setChangeAddress($changeAddress);
615
616 10
        foreach ($outputs as $output) {
617
            $txBuilder->addOutput($output);
618
        }
619 10
620
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
621
622
        $apiCheckFee = $forceFee === null;
623
624
        return $this->sendTx($txBuilder, $apiCheckFee);
625
    }
626
627
    /**
628
     * determine max spendable from wallet after fees
629
     *
630
     * @param bool     $allowZeroConf
631 11
     * @param string   $feeStrategy
632
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
633 11
     * @param int      $outputCnt
634
     * @return string
635 5
     * @throws BlocktrailSDKException
636 5
     */
637 5
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
638
        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...
639 5
    }
640 1
641
    /**
642 5
     * parse outputs into normalized struct
643
     *
644
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
645 5
     * @return array            [['address' => address, 'value' => value], ]
646 5
     */
647 5
    public static function normalizeOutputsStruct(array $outputs) {
648
        $result = [];
649
650
        foreach ($outputs as $k => $v) {
651
            if (is_numeric($k)) {
652
                if (!is_array($v)) {
653
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
654 5
                }
655
656
                if (isset($v['address']) && isset($v['value'])) {
657 5
                    $address = $v['address'];
658
                    $value = $v['value'];
659
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
660
                    $address = $v[0];
661
                    $value = $v[1];
662
                } else {
663
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
664
                }
665
            } else {
666
                $address = $k;
667 7
                $value = $v;
668 7
            }
669 7
670 7
            $result[] = ['address' => $address, 'value' => $value];
671
        }
672 7
673
        return $result;
674 7
    }
675 7
676 1
    /**
677
     * 'fund' the txBuilder with UTXOs (modified in place)
678 1
     *
679
     * @param TransactionBuilder    $txBuilder
680
     * @param bool|true             $lockUTXOs
681
     * @param bool|false            $allowZeroConf
682 1
     * @param null|int              $forceFee
683
     * @return TransactionBuilder
684 1
     */
685
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
686
687 1
        // get the data we should use for this transaction
688
        $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 685 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...
689
        
690 1
        $utxos = $coinSelection['utxos'];
691 1
        $fee = $coinSelection['fee'];
692
        $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...
693
694
        if ($forceFee !== null) {
695 7
            $txBuilder->setFee($forceFee);
696 7
        } else {
697
            $txBuilder->validateFee($fee);
698
        }
699
700 7
        foreach ($utxos as $utxo) {
701 6
            $signMode = SignInfo::MODE_SIGN;
702 6
            if (isset($utxo['sign_mode'])) {
703 6
                $signMode = $utxo['sign_mode'];
704
                if (!in_array($signMode, $this->allowedSignModes)) {
705
                    throw new \Exception("Sign mode disallowed by wallet");
706
                }
707 7
            }
708
709
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script'], $utxo['witness_script'], $signMode);
710
        }
711 7
712 7
        return $txBuilder;
713 7
    }
714 1
715
    /**
716
     * build inputs and outputs lists for TransactionBuilder
717 7
     *
718
     * @param TransactionBuilder $txBuilder
719 7
     * @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...
720
     * @throws \Exception
721 5
     */
722
    public function buildTx(TransactionBuilder $txBuilder) {
723
        $send = $txBuilder->getOutputs();
724
        $utxos = $txBuilder->getUtxos();
725
        $signInfo = [];
726 7
727 5
        $txb = new TxBuilder();
728 5
729 5
        foreach ($utxos as $utxo) {
730
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
731
                $tx = $this->sdk->transaction($utxo->hash);
732
733 7
                if (!$tx || !isset($tx['outputs'][$utxo->index])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tx of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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