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

Wallet::createTransaction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

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

Loading history...
174
175 22
        $this->identifier = $identifier;
176 22
        $this->backupPublicKey = BlocktrailSDK::normalizeBIP32Key($backupPublicKey, $this->networkParams->getNetwork());
177 22
        $this->primaryPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($primaryPublicKeys, $this->networkParams->getNetwork());
178 22
        $this->blocktrailPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($blocktrailPublicKeys, $this->networkParams->getNetwork());
179
180 22
        $this->keyIndex = $keyIndex;
181 22
        $this->checksum = $checksum;
182
183 22
        if ($this->networkParams->isNetwork("bitcoin")) {
184 22
            if ($segwit) {
185 3
                $chainIdx = self::CHAIN_BTC_DEFAULT;
186 3
                $changeIdx = self::CHAIN_BTC_SEGWIT;
187
            } else {
188 20
                $chainIdx = self::CHAIN_BTC_DEFAULT;
189 22
                $changeIdx = self::CHAIN_BTC_DEFAULT;
190
            }
191
        } else {
192
            if ($segwit && $this->networkParams->isNetwork("bitcoincash")) {
193
                throw new BlocktrailSDKException("Received segwit flag for bitcoincash - abort");
194
            }
195
            $chainIdx = self::CHAIN_BCC_DEFAULT;
196
            $changeIdx = self::CHAIN_BCC_DEFAULT;
197
        }
198
199 22
        $this->isSegwit = (bool) $segwit;
200 22
        $this->chainIndex = $chainIdx;
201 22
        $this->changeIndex = $changeIdx;
202 22
    }
203
204
    /**
205
     * @param int|null $chainIndex
206
     * @return WalletPath
207
     * @throws BlocktrailSDKException
208
     */
209 14
    protected function getWalletPath($chainIndex = null) {
210 14
        if ($chainIndex === null) {
211 12
            return WalletPath::create($this->keyIndex, $this->chainIndex);
212
        } else {
213 6
            if (!is_int($chainIndex)) {
214 1
                throw new BlocktrailSDKException("Chain index is invalid - should be an integer");
215
            }
216 5
            return WalletPath::create($this->keyIndex, $chainIndex);
217
        }
218
    }
219
220
    /**
221
     * @return bool
222
     */
223 3
    public function isSegwit() {
224 3
        return $this->isSegwit;
225
    }
226
227
    /**
228
     * return the wallet identifier
229
     *
230
     * @return string
231
     */
232 10
    public function getIdentifier() {
233 10
        return $this->identifier;
234
    }
235
236
    /**
237
     * Returns the wallets backup public key
238
     *
239
     * @return [xpub, path]
0 ignored issues
show
Documentation introduced by
The doc-type xpub,">[xpub, could not be parsed: Unknown type name "[" at position 0. [(view supported doc-types)

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

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

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

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

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

Loading history...
283 5
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
284
285 5
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
286 5
        $this->keyIndex = $keyIndex;
287
288
        // update the blocktrail public keys
289 5
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
290 5
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
291 5
                $path = $pubKey[1];
292 5
                $pubKey = $pubKey[0];
293 5
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::fromString($this->networkParams->getNetwork(), $pubKey, $path);
0 ignored issues
show
Bug introduced by
The method getNetwork() does not seem to exist on object<BitWasp\Bitcoin\Network\NetworkInterface>.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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