Completed
Pull Request — master (#87)
by thomas
18:16 queued 16:00
created

Wallet::getWalletPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
189 21
        $this->changeIndex = $changeIdx;
0 ignored issues
show
Documentation Bug introduced by
The property $changeIndex was declared of type boolean, but $changeIdx is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
190 21
        $this->walletPath = $this->getWalletPath(null);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getWalletPath(null) of type object<Blocktrail\SDK\Bitcoin\BIP32Path> is incompatible with the declared type object<Blocktrail\SDK\WalletPath> of property $walletPath.

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

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

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

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

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

Loading history...
191 21
    }
192
193
    /**
194
     * @param null $chainIndex
195
     * @return BIP32Path
196
     */
197 21
    protected function getWalletPath($chainIndex = null) {
198 21
        if ($chainIndex === null) {
199 21
            return WalletPath::create($this->keyIndex, $this->chainIndex)->path()->last("*");
0 ignored issues
show
Documentation introduced by
$this->chainIndex is of type boolean, but the function expects a integer.

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...
200
        } else {
201 6
            return WalletPath::create($this->keyIndex, $chainIndex)->path()->last("*");
202
        }
203
    }
204
205
    /**
206
     * @return bool
207
     */
208 3
    public function isSegwit() {
209 3
        return $this->isSegwit;
210
    }
211
212
    /**
213
     * return the wallet identifier
214
     *
215
     * @return string
216
     */
217 10
    public function getIdentifier() {
218 10
        return $this->identifier;
219
    }
220
221
    /**
222
     * Returns the wallets backup public key
223
     *
224
     * @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...
225
     */
226 1
    public function getBackupKey() {
227 1
        return $this->backupPublicKey->tuple();
228
    }
229
230
    /**
231
     * return list of Blocktrail co-sign extended public keys
232
     *
233
     * @return array[]      [ [xpub, path] ]
234
     */
235 5
    public function getBlocktrailPublicKeys() {
236
        return array_map(function (BIP32Key $key) {
237 5
            return $key->tuple();
238 5
        }, $this->blocktrailPublicKeys);
239
    }
240
241
    /**
242
     * check if wallet is locked
243
     *
244
     * @return bool
245
     */
246 10
    public function isLocked() {
247 10
        return $this->locked;
248
    }
249
250
    /**
251
     * upgrade wallet to different blocktrail cosign key
252
     *
253
     * @param $keyIndex
254
     * @return bool
255
     * @throws \Exception
256
     */
257 5
    public function upgradeKeyIndex($keyIndex) {
258 5
        if ($this->locked) {
259 4
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
260
        }
261
262 5
        $walletPath = WalletPath::create($keyIndex);
263
264
        // do the upgrade to the new 'key_index'
265 5
        $primaryPublicKey = $this->primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath());
266
267
        // $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...
268 5
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
269
270 5
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
271 5
        $this->keyIndex = $keyIndex;
272
273
        // reset wallet path to new state
274 5
        $this->walletPath = $this->getWalletPath(null);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getWalletPath(null) of type object<Blocktrail\SDK\Bitcoin\BIP32Path> is incompatible with the declared type object<Blocktrail\SDK\WalletPath> of property $walletPath.

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

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

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

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

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

Loading history...
275
276
        // update the blocktrail public keys
277 5
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
278 5
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
279 5
                $path = $pubKey[1];
280 5
                $pubKey = $pubKey[0];
281 5
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKey), $path);
282
            }
283
        }
284
285 5
        return true;
286
    }
287
288
    /**
289
     * get a new BIP32 derivation for the next (unused) address
290
     *  by requesting it from the API
291
     *
292
     * @return string
293
     * @param int|null $chainIndex
294
     * @throws \Exception
295
     */
296 14
    protected function getNewDerivation($chainIndex = null) {
297 14
        $path = $this->getWalletPath($chainIndex);
0 ignored issues
show
Bug introduced by
It seems like $chainIndex defined by parameter $chainIndex on line 296 can also be of type integer; however, Blocktrail\SDK\Wallet::getWalletPath() does only seem to accept null, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

Loading history...
298
299 14
        if (self::VERIFY_NEW_DERIVATION) {
300 14
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
301
302 14
            $path = $new['path'];
303 14
            $address = $new['address'];
304 14
            $redeemScript = $new['redeem_script'];
305 14
            $witnessScript = array_key_exists('witness_script', $new) ? $new['witness_script'] : null;
306
307
            /** @var ScriptInterface $checkRedeemScript */
308
            /** @var ScriptInterface $checkWitnessScript */
309 14
            list($checkAddress, $checkRedeemScript, $checkWitnessScript) = $this->getRedeemScriptByPath($path);
310
311 14
            if ($checkAddress != $address) {
312
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
313
            }
314
315 14
            if ($checkRedeemScript && $checkRedeemScript->getHex() != $redeemScript) {
316
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript->getHex()}]");
317
            }
318
319 14
            if ($checkWitnessScript && $checkWitnessScript->getHex() != $witnessScript) {
320 14
                throw new \Exception("Failed to verify that witnessScript from API [{$witnessScript}] matches address locally [{$checkWitnessScript->getHex()}]");
321
            }
322
        } else {
323
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
324
        }
325
326 14
        return (string)$path;
327
    }
328
329
    /**
330
     * @param string|BIP32Path  $path
331
     * @return BIP32Key|false
332
     * @throws \Exception
333
     *
334
     * @TODO: hmm?
335
     */
336 16
    protected function getParentPublicKey($path) {
337 16
        $path = BIP32Path::path($path)->parent()->publicPath();
338
339 16
        if ($path->count() <= 2) {
340
            return false;
341
        }
342
343 16
        if ($path->isHardened()) {
344
            return false;
345
        }
346
347 16
        if (!isset($this->pubKeys[(string)$path])) {
348 16
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
349
        }
350
351 16
        return $this->pubKeys[(string)$path];
352
    }
353
354
    /**
355
     * get address for the specified path
356
     *
357
     * @param string|BIP32Path  $path
358
     * @return string
359
     */
360 14
    public function getAddressByPath($path) {
361 14
        $path = (string)BIP32Path::path($path)->privatePath();
362 14
        if (!isset($this->derivations[$path])) {
363 14
            list($address, ) = $this->getRedeemScriptByPath($path);
364
365 14
            $this->derivations[$path] = $address;
366 14
            $this->derivationsByAddress[$address] = $path;
367
        }
368
369 14
        return $this->derivations[$path];
370
    }
371
372
    /**
373
     * @param string $path
374
     * @return WalletScript
375
     */
376 16
    public function getWalletScriptByPath($path) {
377 16
        $path = BIP32Path::path($path);
378
379
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
380 16
        if ($pubKey = $this->getParentPublicKey($path)) {
381 16
            $key = $pubKey->buildKey($path->publicPath());
382
        } else {
383
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
384
        }
385
386 16
        return $this->getWalletScriptFromKey($key, $path);
387
    }
388
389
    /**
390
     * get address and redeemScript for specified path
391
     *
392
     * @param string    $path
393
     * @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...
394
     */
395 15
    public function getRedeemScriptByPath($path) {
396 15
        $walletScript = $this->getWalletScriptByPath($path);
397
398 15
        $redeemScript = $walletScript->isP2SH() ? $walletScript->getRedeemScript() : null;
399 15
        $witnessScript = $walletScript->isP2WSH() ? $walletScript->getWitnessScript() : null;
400 15
        return [$walletScript->getAddress()->getAddress(), $redeemScript, $witnessScript];
401
    }
402
403
    /**
404
     * @param BIP32Key          $key
405
     * @param string|BIP32Path  $path
406
     * @return string
407
     */
408
    protected function getAddressFromKey(BIP32Key $key, $path) {
409
        return $this->getWalletScriptFromKey($key, $path)->getAddress()->getAddress();
410
    }
411
412
    /**
413
     * @param BIP32Key          $key
414
     * @param string|BIP32Path  $path
415
     * @return WalletScript
416
     * @throws \Exception
417
     */
418 16
    protected function getWalletScriptFromKey(BIP32Key $key, $path) {
419 16
        $path = BIP32Path::path($path)->publicPath();
420
421 16
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
422
423 16
        $multisig = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
424 16
            $key->buildKey($path)->publicKey(),
425 16
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
426 16
            $blocktrailPublicKey->buildKey($path)->publicKey()
427 16
        ]), false);
428
429 16
        $type = (int)$key->path()[2];
430
431 16
        if ($this->isSegwit && $type === Wallet::CHAIN_BTC_SEGWIT) {
432 3
            $witnessScript = new WitnessScript($multisig);
433 3
            $redeemScript = new P2shScript($witnessScript);
434 3
            $scriptPubKey = $redeemScript->getOutputScript();
435 16
        } else if ($type === Wallet::CHAIN_BTC_DEFAULT || $type === Wallet::CHAIN_BCC_DEFAULT) {
436 15
            $witnessScript = null;
437 15
            $redeemScript = new P2shScript($multisig);
438 15
            $scriptPubKey = $redeemScript->getOutputScript();
439
        } else {
440 1
            throw new BlocktrailSDKException("Unsupported chain in path");
441
        }
442
443 15
        return new WalletScript($path, $scriptPubKey, $redeemScript, $witnessScript);
444
    }
445
446
    /**
447
     * get the path (and redeemScript) to specified address
448
     *
449
     * @param string $address
450
     * @return array
451
     */
452 1
    public function getPathForAddress($address) {
453 1
        return $this->sdk->getPathForAddress($this->identifier, $address);
454
    }
455
456
    /**
457
     * @param string|BIP32Path  $path
458
     * @return BIP32Key
459
     * @throws \Exception
460
     */
461 16
    public function getBlocktrailPublicKey($path) {
462 16
        $path = BIP32Path::path($path);
463
464 16
        $keyIndex = str_replace("'", "", $path[1]);
465
466 16
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
467
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
468
        }
469
470 16
        return $this->blocktrailPublicKeys[$keyIndex];
471
    }
472
473
    /**
474
     * generate a new derived key and return the new path and address for it
475
     *
476
     * @param int|null $chainIndex
477
     * @return string[]     [path, address]
478
     */
479 14
    public function getNewAddressPair($chainIndex = null) {
480 14
        $path = $this->getNewDerivation($chainIndex);
481 14
        $address = $this->getAddressByPath($path);
482
483 14
        return [$path, $address];
484
    }
485
486
    /**
487
     * generate a new derived private key and return the new address for it
488
     *
489
     * @param int|null $chainIndex
490
     * @return string
491
     */
492 10
    public function getNewAddress($chainIndex = null) {
493 10
        return $this->getNewAddressPair($chainIndex)[1];
494
    }
495
496
    /**
497
     * get the balance for the wallet
498
     *
499
     * @return int[]            [confirmed, unconfirmed]
500
     */
501 9
    public function getBalance() {
502 9
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
503
504 9
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
505
    }
506
507
    /**
508
     * do wallet discovery (slow)
509
     *
510
     * @param int   $gap        the gap setting to use for discovery
511
     * @return int[]            [confirmed, unconfirmed]
512
     */
513 2
    public function doDiscovery($gap = 200) {
514 2
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
515
516 2
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
517
    }
518
519
    /**
520
     * create, sign and send a transaction
521
     *
522
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
523
     *                                      value should be INT
524
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
525
     * @param bool     $allowZeroConf
526
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
527
     * @param string   $feeStrategy
528
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
529
     * @return string the txid / transaction hash
530
     * @throws \Exception
531
     */
532 9
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
533 9
        if ($this->locked) {
534 4
            throw new \Exception("Wallet needs to be unlocked to pay");
535
        }
536
537 9
        $outputs = self::normalizeOutputsStruct($outputs);
538
539 9
        $txBuilder = new TransactionBuilder();
540 9
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
541 9
        $txBuilder->setFeeStrategy($feeStrategy);
542 9
        $txBuilder->setChangeAddress($changeAddress);
543
544 9
        foreach ($outputs as $output) {
545 9
            $txBuilder->addRecipient($output['address'], $output['value']);
546
        }
547
548 9
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
549
550 3
        $apiCheckFee = $forceFee === null;
551
552 3
        return $this->sendTx($txBuilder, $apiCheckFee);
553
    }
554
555
    /**
556
     * determine max spendable from wallet after fees
557
     *
558
     * @param bool     $allowZeroConf
559
     * @param string   $feeStrategy
560
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
561
     * @param int      $outputCnt
562
     * @return string
563
     * @throws BlocktrailSDKException
564
     */
565
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
566
        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...
567
    }
568
569
    /**
570
     * parse outputs into normalized struct
571
     *
572
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
573
     * @return array            [['address' => address, 'value' => value], ]
574
     */
575 10
    public static function normalizeOutputsStruct(array $outputs) {
576 10
        $result = [];
577
578 10
        foreach ($outputs as $k => $v) {
579 10
            if (is_numeric($k)) {
580 1
                if (!is_array($v)) {
581
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
582
                }
583
584 1
                if (isset($v['address']) && isset($v['value'])) {
585 1
                    $address = $v['address'];
586 1
                    $value = $v['value'];
587 1
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
588 1
                    $address = $v[0];
589 1
                    $value = $v[1];
590
                } else {
591 1
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
592
                }
593
            } else {
594 10
                $address = $k;
595 10
                $value = $v;
596
            }
597
598 10
            $result[] = ['address' => $address, 'value' => $value];
599
        }
600
601 10
        return $result;
602
    }
603
604
    /**
605
     * 'fund' the txBuilder with UTXOs (modified in place)
606
     *
607
     * @param TransactionBuilder    $txBuilder
608
     * @param bool|true             $lockUTXOs
609
     * @param bool|false            $allowZeroConf
610
     * @param null|int              $forceFee
611
     * @return TransactionBuilder
612
     */
613 11
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
614
        // get the data we should use for this transaction
615 11
        $coinSelection = $this->coinSelection($txBuilder->getOutputs(/* $json = */true), $lockUTXOs, $allowZeroConf, $txBuilder->getFeeStrategy(), $forceFee);
0 ignored issues
show
Bug introduced by
It seems like $forceFee defined by parameter $forceFee on line 613 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...
616
        
617 5
        $utxos = $coinSelection['utxos'];
618 5
        $fee = $coinSelection['fee'];
619 5
        $change = $coinSelection['change'];
0 ignored issues
show
Unused Code introduced by
$change is not used, you could remove the assignment.

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

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

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

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

Loading history...
620
621 5
        if ($forceFee !== null) {
622
            $txBuilder->setFee($forceFee);
623
        } else {
624 5
            $txBuilder->validateFee($fee);
625
        }
626
627 5
        foreach ($utxos as $utxo) {
628 5
            $signMode = SignInfo::MODE_SIGN;
629 5
            if (isset($utxo['sign_mode'])) {
630
                $signMode = $utxo['sign_mode'];
631
                if (!in_array($signMode, $this->allowedSignModes)) {
632
                    throw new \Exception("Sign mode disallowed by wallet");
633
                }
634
            }
635
636 5
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script'], $utxo['witness_script'], $signMode);
637
        }
638
639 5
        return $txBuilder;
640
    }
641
642
    /**
643
     * build inputs and outputs lists for TransactionBuilder
644
     *
645
     * @param TransactionBuilder $txBuilder
646
     * @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...
647
     * @throws \Exception
648
     */
649 7
    public function buildTx(TransactionBuilder $txBuilder) {
650 7
        $send = $txBuilder->getOutputs();
651 7
        $utxos = $txBuilder->getUtxos();
652 7
        $signInfo = [];
653
654 7
        $txb = new TxBuilder();
655
656 7
        foreach ($utxos as $utxo) {
657 7
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
658 1
                $tx = $this->sdk->transaction($utxo->hash);
659
660 1
                if (!$tx || !isset($tx['outputs'][$utxo->index])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tx of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
661
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
662
                }
663
664 1
                $output = $tx['outputs'][$utxo->index];
665
666 1
                if (!$utxo->address) {
667
                    $utxo->address = AddressFactory::fromString($output['address']);
668
                }
669 1
                if (!$utxo->value) {
670
                    $utxo->value = $output['value'];
671
                }
672 1
                if (!$utxo->scriptPubKey) {
673 1
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
674
                }
675
            }
676
677 7
            if (SignInfo::MODE_SIGN === $utxo->signMode) {
678 7
                if (!$utxo->path) {
679
                    $utxo->path = $this->getPathForAddress($utxo->address->getAddress());
680
                }
681
682 7
                if (!$utxo->redeemScript || !$utxo->witnessScript) {
683 6
                    list(, $redeemScript, $witnessScript) = $this->getRedeemScriptByPath($utxo->path);
0 ignored issues
show
Documentation introduced by
$utxo->path is of type array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
684 6
                    $utxo->redeemScript = $redeemScript;
685 6
                    $utxo->witnessScript = $witnessScript;
686
                }
687
            }
688
689 7
            $signInfo[] = $utxo->getSignInfo();
690
        }
691
692
        $utxoSum = array_sum(array_map(function (UTXO $utxo) {
693 7
            return $utxo->value;
694 7
        }, $utxos));
695 7
        if ($utxoSum < array_sum(array_column($send, 'value'))) {
696 1
            throw new \Exception("Atempting to spend more than sum of UTXOs");
697
        }
698
699 7
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
700
701 7
        if ($txBuilder->getValidateFee() !== null) {
702 5
            if (abs($txBuilder->getValidateFee() - $fee) > Wallet::BASE_FEE) {
703
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
704
            }
705
        }
706
707 7
        if ($change > 0) {
708 6
            $send[] = [
709 6
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewAddress($this->changeIndex),
0 ignored issues
show
Documentation introduced by
$this->changeIndex is of type boolean, but the function expects a integer|null.

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...
710 6
                'value' => $change
711
            ];
712
        }
713
714 7
        foreach ($utxos as $utxo) {
715 7
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index));
716
        }
717
718
        // outputs should be randomized to make the change harder to detect
719 7
        if ($txBuilder->shouldRandomizeChangeOuput()) {
720 7
            shuffle($send);
721
        }
722
723 7
        foreach ($send as $out) {
724 7
            assert(isset($out['value']));
725
726 7
            if (isset($out['scriptPubKey'])) {
727 1
                $txb->output($out['value'], $out['scriptPubKey']);
728 7
            } elseif (isset($out['address'])) {
729 7
                $txb->payToAddress($out['value'], AddressFactory::fromString($out['address']));
730
            } else {
731 7
                throw new \Exception();
732
            }
733
        }
734
735 7
        return [$txb->get(), $signInfo];
736
    }
737
738 7
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) {
739 7
        $send = $txBuilder->getOutputs();
740 7
        $utxos = $txBuilder->getUtxos();
741
742 7
        $fee = $txBuilder->getFee();
743 7
        $change = null;
0 ignored issues
show
Unused Code introduced by
$change is not used, you could remove the assignment.

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

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

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

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

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

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

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

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

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

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

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

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