Passed
Pull Request — master (#129)
by thomas
26:40 queued 15:56
created

Wallet::pay()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
489
            $witnessScript = new WitnessScript($multisig);
490
            $redeemScript = new P2shScript($witnessScript);
491
            $scriptPubKey = $redeemScript->getOutputScript();
492
        } else if ($type === Wallet::CHAIN_BTC_DEFAULT || $type === Wallet::CHAIN_BCC_DEFAULT) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
493
            $witnessScript = null;
494
            $redeemScript = new P2shScript($multisig);
495
            $scriptPubKey = $redeemScript->getOutputScript();
496
        } else {
497
            throw new BlocktrailSDKException("Unsupported chain in path");
498
        }
499
500
        $address = $this->addressReader->fromOutputScript($scriptPubKey);
501
502
        return new WalletScript($path, $scriptPubKey, $redeemScript, $witnessScript, $address);
503
    }
504
505
    /**
506
     * get the path (and redeemScript) to specified address
507
     *
508
     * @param string $address
509
     * @return array
510
     */
511
    public function getPathForAddress($address) {
512
        $decoded = $this->addressReader->fromString($address);
513
        if ($decoded instanceof CashAddress) {
514
            $address = $decoded->getLegacyAddress();
515
        }
516
517
        return $this->sdk->getPathForAddress($this->identifier, $address);
0 ignored issues
show
Bug introduced by
It seems like $address can also be of type BitWasp\Bitcoin\Address\PayToPubKeyHashAddress and BitWasp\Bitcoin\Address\ScriptHashAddress; however, parameter $address of Blocktrail\SDK\Blocktrai...ce::getPathForAddress() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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

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

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

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

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
600
            throw new \InvalidArgumentException("feeStrategy should be set to force_fee to set a forced fee");
601
        }
602
603
        $outputs = (new OutputsNormalizer($this->getAddressReader()))->normalize($outputs);
604
605
        $txBuilder = new TransactionBuilder($this->addressReader);
606
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
607
        $txBuilder->setFeeStrategy($feeStrategy);
608
        $txBuilder->setChangeAddress($changeAddress);
609
610
        foreach ($outputs as $output) {
611
            $txBuilder->addOutput($output);
612
        }
613
614
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
615
616
        if ($forceFee !== null) {
617
            $apiCheckFee = true;
618
        }
619
620
        return $this->sendTx($txBuilder, $apiCheckFee);
621
    }
622
623
    /**
624
     * determine max spendable from wallet after fees
625
     *
626
     * @param bool     $allowZeroConf
627
     * @param string   $feeStrategy
628
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
629
     * @param int      $outputCnt
630
     * @return string
631
     * @throws BlocktrailSDKException
632
     */
633
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
634
        return $this->sdk->walletMaxSpendable($this->identifier, $allowZeroConf, $feeStrategy, $forceFee, $outputCnt);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->sdk->walle... $forceFee, $outputCnt) returns the type array which is incompatible with the documented return type string.
Loading history...
635
    }
636
637
    /**
638
     * parse outputs into normalized struct
639
     *
640
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
641 1
     * @return array            [['address' => address, 'value' => value], ]
642 1
     */
643
    public static function normalizeOutputsStruct(array $outputs) {
644 1
        $result = [];
645 1
646 1
        foreach ($outputs as $k => $v) {
647
            if (is_numeric($k)) {
648
                if (!is_array($v)) {
649
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
650 1
                }
651 1
652 1
                if (isset($v['address']) && isset($v['value'])) {
653 1
                    $address = $v['address'];
654 1
                    $value = $v['value'];
655 1
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
656
                    $address = $v[0];
657 1
                    $value = $v[1];
658
                } else {
659
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
660 1
                }
661 1
            } else {
662
                $address = $k;
663
                $value = $v;
664 1
            }
665
666
            $result[] = ['address' => $address, 'value' => $value];
667 1
        }
668
669
        return $result;
670
    }
671
672
    /**
673
     * 'fund' the txBuilder with UTXOs (modified in place)
674
     *
675
     * @param TransactionBuilder    $txBuilder
676
     * @param bool|true             $lockUTXOs
677
     * @param bool|false            $allowZeroConf
678
     * @param null|int              $forceFee
679
     * @return TransactionBuilder
680
     */
681
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
682
683
        // get the data we should use for this transaction
684
        $coinSelection = $this->coinSelection($txBuilder->getOutputs(/* $json = */true), $lockUTXOs, $allowZeroConf, $txBuilder->getFeeStrategy(), $forceFee);
0 ignored issues
show
Bug introduced by
It seems like $forceFee can also be of type integer; however, parameter $forceFee of Blocktrail\SDK\Wallet::coinSelection() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

684
        $coinSelection = $this->coinSelection($txBuilder->getOutputs(/* $json = */true), $lockUTXOs, $allowZeroConf, $txBuilder->getFeeStrategy(), /** @scrutinizer ignore-type */ $forceFee);
Loading history...
685
        
686
        $utxos = $coinSelection['utxos'];
687
        $fee = $coinSelection['fee'];
688
        $change = $coinSelection['change'];
0 ignored issues
show
Unused Code introduced by
The assignment to $change is dead and can be removed.
Loading history...
689
690
        if ($forceFee !== null) {
691
            $txBuilder->setFee($forceFee);
692
        } else {
693
            $txBuilder->validateFee($fee);
694
        }
695
696
        foreach ($utxos as $utxo) {
697
            $signMode = SignInfo::MODE_SIGN;
698
            if (isset($utxo['sign_mode'])) {
699
                $signMode = $utxo['sign_mode'];
700
                if (!in_array($signMode, $this->allowedSignModes)) {
701
                    throw new \Exception("Sign mode disallowed by wallet");
702
                }
703
            }
704
705
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script'], $utxo['witness_script'], $signMode);
706
        }
707
708
        return $txBuilder;
709
    }
710
711
    /**
712
     * build inputs and outputs lists for TransactionBuilder
713
     *
714
     * @param TransactionBuilder $txBuilder
715
     * @return [TransactionInterface, SignInfo[]]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [TransactionInterface, SignInfo[]] at position 0 could not be parsed: Unknown type name '[' at position 0 in [TransactionInterface, SignInfo[]].
Loading history...
716
     * @throws \Exception
717
     */
718
    public function buildTx(TransactionBuilder $txBuilder) {
719
        $send = $txBuilder->getOutputs();
720
        $utxos = $txBuilder->getUtxos();
721
        $signInfo = [];
722
723
        $txb = new TxBuilder();
724
725
        foreach ($utxos as $utxo) {
726
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
727
                $tx = $this->sdk->transaction($utxo->hash);
728
729
                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...
730
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
731
                }
732
733
                $output = $tx['outputs'][$utxo->index];
734
735
                if (!$utxo->address) {
736
                    $utxo->address = $this->addressReader->fromString($output['address']);
737
                }
738
                if (!$utxo->value) {
739
                    $utxo->value = $output['value'];
740
                }
741
                if (!$utxo->scriptPubKey) {
742
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
743
                }
744
            }
745
746
            if (SignInfo::MODE_SIGN === $utxo->signMode) {
747
                if (!$utxo->path) {
748
                    $utxo->path = $this->getPathForAddress($utxo->address->getAddress())['path'];
749
                }
750
751
                if (!$utxo->redeemScript || !$utxo->witnessScript) {
752
                    list(, $redeemScript, $witnessScript) = $this->getRedeemScriptByPath($utxo->path);
753
                    $utxo->redeemScript = $redeemScript;
754
                    $utxo->witnessScript = $witnessScript;
755
                }
756
            }
757
758
            $signInfo[] = $utxo->getSignInfo();
759
        }
760
761
        $utxoSum = array_sum(array_map(function (UTXO $utxo) {
762
            return $utxo->value;
763
        }, $utxos));
764
        if ($utxoSum < array_sum(array_column($send, 'value'))) {
765
            throw new \Exception("Atempting to spend more than sum of UTXOs");
766
        }
767
768
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getHighPriorityFeePerKB(), $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
769
770
        if ($txBuilder->getValidateFee() !== null) {
771
            // sanity check to make sure the API isn't giving us crappy data
772
            if (abs($txBuilder->getValidateFee() - $fee) > (Wallet::BASE_FEE * 5)) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
773
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
774
            }
775
        }
776
777
        if ($change > 0) {
778
            $send[] = [
779
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewChangeAddress(),
780
                'value' => $change
781
            ];
782
        }
783
784
        foreach ($utxos as $utxo) {
785
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index));
786
        }
787
788
        // outputs should be randomized to make the change harder to detect
789
        if ($txBuilder->shouldRandomizeChangeOuput()) {
790
            $this->sdk->shuffle($send);
791
        }
792
793
        foreach ($send as $out) {
794
            assert(isset($out['value']));
795
796
            if (isset($out['scriptPubKey'])) {
797
                $txb->output($out['value'], $out['scriptPubKey']);
798
            } elseif (isset($out['address'])) {
799
                $txb->output($out['value'], $this->addressReader->fromString($out['address'])->getScriptPubKey());
800
            } else {
801
                throw new \Exception();
802
            }
803
        }
804
805
        return [$txb->get(), $signInfo];
806
    }
807
808
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB) {
809
        $send = (new OutputsNormalizer($this->addressReader))->normalize($txBuilder->getOutputs());
810
        $utxos = $txBuilder->getUtxos();
811
812
        $fee = $txBuilder->getFee();
813
        $change = null;
814
815
        // if the fee is fixed we just need to calculate the change
816
        if ($fee !== null) {
817
            $change = $this->determineChange($utxos, $send, $fee);
818
819
            // if change is not dust we need to add a change output
820
            if ($change > Blocktrail::DUST) {
821
                $send[] = ['address' => 'change', 'value' => $change];
822
            } else {
823
                // if change is dust we add it to the fee
824
                $fee += $change;
825
                $change = 0;
826
            }
827
828
            return [$fee, $change];
829
        } else {
830
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB);
831
832
            $change = $this->determineChange($utxos, $send, $fee);
833
834
            if ($change > 0) {
835
                $changeIdx = count($send);
836
                // set dummy change output
837
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
838
839
                // recaculate fee now that we know that we have a change output
840
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB);
841
842
                // unset dummy change output
843
                unset($send[$changeIdx]);
844
845
                // if adding the change output made the fee bump up and the change is smaller than the fee
846
                //  then we're not doing change
847
                if ($fee2 > $fee && $fee2 > $change) {
848
                    $change = 0;
849
                } else {
850
                    $change = $this->determineChange($utxos, $send, $fee2);
851
852
                    // if change is not dust we need to add a change output
853
                    if ($change > Blocktrail::DUST) {
854
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
855
                    } else {
856
                        // if change is dust we do nothing (implicitly it's added to the fee)
857
                        $change = 0;
858
                    }
859
                }
860
            }
861
862
863
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB);
864
865
            return [$fee, $change];
866
        }
867
    }
868
869
    /**
870
     * create, sign and send transction based on TransactionBuilder
871
     *
872
     * @param TransactionBuilder $txBuilder
873
     * @param bool $apiCheckFee     let the API check if the fee is correct
874
     * @return string
875
     * @throws \Exception
876
     */
877
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
878
        list($tx, $signInfo) = $this->buildTx($txBuilder);
879
880
        return $this->_sendTx($tx, $signInfo, $apiCheckFee);
881
    }
882
883
    /**
884
     * !! INTERNAL METHOD, public for testing purposes !!
885
     * create, sign and send transction based on inputs and outputs
886
     *
887
     * @param Transaction $tx
888
     * @param SignInfo[]  $signInfo
889
     * @param bool $apiCheckFee     let the API check if the fee is correct
890
     * @return string
891
     * @throws \Exception
892
     * @internal
893
     */
894
    public function _sendTx(Transaction $tx, array $signInfo, $apiCheckFee = true) {
895
        if ($this->locked) {
896
            throw new \Exception("Wallet needs to be unlocked to pay");
897
        }
898
899
        assert(Util::all(function ($signInfo) {
900
            return $signInfo instanceof SignInfo;
901
        }, $signInfo), '$signInfo should be SignInfo[]');
902
903
        // sign the transaction with our keys
904
        $signed = $this->signTransaction($tx, $signInfo);
905
906
        $txs = [
907
            'signed_transaction' => $signed->getHex(),
908
            'base_transaction' => $signed->getBaseSerialization()->getHex(),
909
        ];
910
911
        // send the transaction
912
        return $this->sendTransaction($txs, array_map(function (SignInfo $r) {
913
            return (string)$r->path;
914
        }, $signInfo), $apiCheckFee);
915
    }
916
917
    /**
918
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
919
     *
920
     * @todo: mark this as deprecated, insist on the utxo's or qualified scripts.
921
     * @param int $utxoCnt      number of unspent inputs in transaction
922
     * @param int $outputCnt    number of outputs in transaction
923
     * @return float
924 1
     * @access public           reminder that people might use this!
925 1
     */
926
    public static function estimateFee($utxoCnt, $outputCnt) {
927 1
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
0 ignored issues
show
Bug introduced by
self::estimateSizeOutputs($outputCnt) of type double is incompatible with the type integer expected by parameter $txoutSize of Blocktrail\SDK\Wallet::estimateSize(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

927
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), /** @scrutinizer ignore-type */ self::estimateSizeOutputs($outputCnt));
Loading history...
Bug introduced by
self::estimateSizeUTXOs($utxoCnt) of type double is incompatible with the type integer expected by parameter $txinSize of Blocktrail\SDK\Wallet::estimateSize(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

927
        $size = self::estimateSize(/** @scrutinizer ignore-type */ self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
Loading history...
928
929
        return self::baseFeeForSize($size);
0 ignored issues
show
Bug introduced by
$size of type double is incompatible with the type integer expected by parameter $size of Blocktrail\SDK\Wallet::baseFeeForSize(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

929
        return self::baseFeeForSize(/** @scrutinizer ignore-type */ $size);
Loading history...
930
    }
931
932
    /**
933
     * @param int $size     size in bytes
934 1
     * @return int          fee in satoshi
935 1
     */
936
    public static function baseFeeForSize($size) {
937 1
        $sizeKB = (int)ceil($size / 1000);
938
939
        return $sizeKB * self::BASE_FEE;
940
    }
941
942
    /**
943
     * @todo: variable varint
944
     * @todo: deprecate
945
     * @param int $txinSize
946
     * @param int $txoutSize
947 2
     * @return float
948 2
     */
949
    public static function estimateSize($txinSize, $txoutSize) {
950
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
951
    }
952
953
    /**
954
     * only supports estimating size for P2PKH/P2SH outputs
955
     *
956
     * @param int $outputCnt    number of outputs in transaction
957 2
     * @return float
958 2
     */
959
    public static function estimateSizeOutputs($outputCnt) {
960
        return ($outputCnt * 34);
961
    }
962
963
    /**
964
     * only supports estimating size for 2of3 multsig UTXOs
965
     *
966
     * @param int $utxoCnt      number of unspent inputs in transaction
967 3
     * @return float
968 3
     */
969
    public static function estimateSizeUTXOs($utxoCnt) {
970 3
        $txinSize = 0;
971
972 3
        for ($i=0; $i<$utxoCnt; $i++) {
973
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
974 3
            $multisig = "2of3";
975 3
976 3
            if ($multisig) {
977 3
                $sigCnt = 2;
978 3
                $msig = explode("of", $multisig);
979
                if (count($msig) == 2 && is_numeric($msig[0])) {
980
                    $sigCnt = $msig[0];
981 3
                }
982 3
983 3
                $txinSize += array_sum([
984 3
                    32, // txhash
985 3
                    4, // idx
986
                    3, // scriptVarInt[>=253]
987
                    ((1 + 72) * $sigCnt), // (OP_PUSHDATA[<75] + 72) * sigCnt
988 3
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
989
                    1, // OP_0
990
                    4, // sequence
991
                ]);
992
            } else {
993
                $txinSize += array_sum([
994
                    32, // txhash
995
                    4, // idx
996
                    73, // sig
997
                    34, // script
998
                    4, // sequence
999
                ]);
1000
            }
1001 3
        }
1002
1003
        return $txinSize;
1004
    }
1005
1006
    /**
1007
     * determine how much fee is required based on the inputs and outputs
1008
     *  this is an estimation, not a proper 100% correct calculation
1009
     *
1010
     * @param UTXO[]  $utxos
1011
     * @param array[] $outputs
1012
     * @param         $feeStrategy
1013
     * @param         $highPriorityFeePerKB
1014
     * @param         $optimalFeePerKB
1015
     * @param         $lowPriorityFeePerKB
1016
     * @return int
1017
     * @throws BlocktrailSDKException
1018
     */
1019
    protected function determineFee($utxos, $outputs, $feeStrategy, $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB) {
1020
1021
        $size = SizeEstimation::estimateVsize($utxos, $outputs);
1022
1023
        switch ($feeStrategy) {
1024
            case self::FEE_STRATEGY_BASE_FEE:
1025
                return self::baseFeeForSize($size);
1026
1027
            case self::FEE_STRATEGY_HIGH_PRIORITY:
1028
                return (int)round(($size / 1000) * $highPriorityFeePerKB);
1029
1030
            case self::FEE_STRATEGY_OPTIMAL:
1031
                return (int)round(($size / 1000) * $optimalFeePerKB);
1032
1033
            case self::FEE_STRATEGY_LOW_PRIORITY:
1034
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
1035
1036
            case self::FEE_STRATEGY_FORCE_FEE:
1037
                throw new BlocktrailSDKException("Can't determine when for force_fee");
1038
1039
            default:
1040
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
1041
        }
1042
    }
1043
1044
    /**
1045
     * determine how much change is left over based on the inputs and outputs and the fee
1046
     *
1047
     * @param UTXO[]    $utxos
1048
     * @param array[]   $outputs
1049
     * @param int       $fee
1050
     * @return int
1051
     */
1052
    protected function determineChange($utxos, $outputs, $fee) {
1053
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
1054
            return $utxo->value;
1055
        }, $utxos));
1056
        $outputsTotal = array_sum(array_column($outputs, 'value'));
1057
1058
        return $inputsTotal - $outputsTotal - $fee;
1059
    }
1060
1061
    /**
1062
     * sign a raw transaction with the private keys that we have
1063
     *
1064
     * @param Transaction $tx
1065
     * @param SignInfo[]  $signInfo
1066
     * @return TransactionInterface
1067
     * @throws \Exception
1068
     */
1069
    protected function signTransaction(Transaction $tx, array $signInfo) {
1070
        $ecAdapter = Bitcoin::getEcAdapter();
1071
        $signer = new Signer($tx, $ecAdapter);
1072
1073
        assert(Util::all(function ($signInfo) {
1074
            return $signInfo instanceof SignInfo;
1075
        }, $signInfo), '$signInfo should be SignInfo[]');
1076
1077
        $sigHash = SigHash::ALL;
1078
        if ($this->network === "bitcoincash") {
1079
            $sigHash |= BchSigHash::BITCOINCASH;
1080
            $signer->setCheckerCreator(BchCheckerCreator::fromEcAdapter($ecAdapter));
1081
        }
1082
1083
        foreach ($signInfo as $idx => $info) {
1084
            if ($info->mode === SignInfo::MODE_SIGN) {
1085
                // required SignInfo: path, redeemScript|witnessScript, output
1086
                $path = BIP32Path::path($info->path)->privatePath();
1087
                $key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey();
1088
                $signData = new SignData();
1089
                if ($info->redeemScript) {
1090
                    $signData->p2sh($info->redeemScript);
1091
                }
1092
                if ($info->witnessScript) {
1093
                    $signData->p2wsh($info->witnessScript);
1094
                }
1095
                $input = $signer->input($idx, $info->output, $signData);
1096
                $input->sign($key, $sigHash);
1097
            }
1098
        }
1099
1100
        return $signer->get();
1101
    }
1102
1103
    /**
1104
     * send the transaction using the API
1105
     *
1106
     * @param string|array  $signed
1107
     * @param string[]      $paths
1108
     * @param bool          $checkFee
1109
     * @return string           the complete raw transaction
1110
     * @throws \Exception
1111
     */
1112
    protected function sendTransaction($signed, $paths, $checkFee = false) {
1113
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
1114
    }
1115
1116
    /**
1117
     * @param \array[] $outputs
1118
     * @param bool $lockUTXO
1119
     * @param bool $allowZeroConf
1120
     * @param int|null|string $feeStrategy
1121
     * @param null $forceFee
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $forceFee is correct as it would always require null to be passed?
Loading history...
1122
     * @return array
1123
     */
1124
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1125
        $send = [];
1126
        foreach ((new OutputsNormalizer($this->addressReader))->normalize($outputs) as $output) {
1127
            $send[] = [
1128
                "value" => $output['value'],
1129
                "scriptPubKey" => $output['scriptPubKey']->getHex(),
1130
            ];
1131
        }
1132
1133
        $result = $this->sdk->coinSelection($this->identifier, $send, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
1134
1135
        $this->highPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_HIGH_PRIORITY];
1136
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
1137
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
1138
        $this->feePerKBAge = time();
1139
1140
        return $result;
1141
    }
1142
1143
    public function getHighPriorityFeePerKB() {
1144
        if (!$this->highPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1145
            $this->updateFeePerKB();
1146
        }
1147
1148
        return $this->highPriorityFeePerKB;
1149
    }
1150
1151
    public function getOptimalFeePerKB() {
1152
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
1153
            $this->updateFeePerKB();
1154
        }
1155
1156
        return $this->optimalFeePerKB;
1157
    }
1158
1159
    public function getLowPriorityFeePerKB() {
1160
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1161
            $this->updateFeePerKB();
1162
        }
1163
1164
        return $this->lowPriorityFeePerKB;
1165
    }
1166
1167
    public function updateFeePerKB() {
1168
        $result = $this->sdk->feePerKB();
1169
1170
        $this->highPriorityFeePerKB = $result[self::FEE_STRATEGY_HIGH_PRIORITY];
1171
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
1172
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
1173
1174
        $this->feePerKBAge = time();
1175
    }
1176
1177
    /**
1178
     * delete the wallet
1179
     *
1180
     * @param bool $force ignore warnings (such as non-zero balance)
1181
     * @return mixed
1182
     * @throws \Exception
1183
     */
1184
    public function deleteWallet($force = false) {
1185
        if ($this->locked) {
1186
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
1187
        }
1188
1189
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
1190
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
1191
    }
1192
1193
    /**
1194
     * create checksum to verify ownership of the master primary key
1195
     *
1196
     * @return string[]     [address, signature]
1197
     */
1198
    protected function createChecksumVerificationSignature() {
1199
        $privKey = $this->primaryPrivateKey->key();
1200
        $address = $this->primaryPrivateKey->key()->getAddress(new BitcoinAddressCreator())->getAddress();
1201
1202
        $signer = new MessageSigner(Bitcoin::getEcAdapter());
1203
        $signed = $signer->sign($address, $privKey->getPrivateKey());
1204
1205
        return [$address, base64_encode($signed->getCompactSignature()->getBuffer()->getBinary())];
1206
    }
1207
1208
    /**
1209
     * setup a webhook for our wallet
1210
     *
1211
     * @param string    $url            URL to receive webhook events
1212
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1213
     * @return array
1214
     */
1215
    public function setupWebhook($url, $identifier = null) {
1216
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1217
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1218
    }
1219
1220
    /**
1221
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1222
     * @return mixed
1223
     */
1224
    public function deleteWebhook($identifier = null) {
1225
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1226
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1227
    }
1228
1229
    /**
1230
     * lock a specific unspent output
1231
     *
1232
     * @param     $txHash
1233
     * @param     $txIdx
1234
     * @param int $ttl
1235
     * @return bool
1236
     */
1237
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1238
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1239
    }
1240
1241
    /**
1242
     * unlock a specific unspent output
1243
     *
1244
     * @param     $txHash
1245
     * @param     $txIdx
1246
     * @return bool
1247
     */
1248
    public function unlockUTXO($txHash, $txIdx) {
1249
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1250
    }
1251
1252
    /**
1253
     * get all transactions for the wallet (paginated)
1254
     *
1255
     * @param  integer $page    pagination: page number
1256
     * @param  integer $limit   pagination: records per page (max 500)
1257
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1258
     * @return array            associative array containing the response
1259
     */
1260
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1261
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1262
    }
1263
1264
    /**
1265
     * get all addresses for the wallet (paginated)
1266
     *
1267
     * @param  integer $page    pagination: page number
1268
     * @param  integer $limit   pagination: records per page (max 500)
1269
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1270
     * @return array            associative array containing the response
1271
     */
1272
    public function addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1273
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1274
    }
1275
1276
    /**
1277
     * get all UTXOs for the wallet (paginated)
1278
     *
1279
     * @param  integer $page        pagination: page number
1280
     * @param  integer $limit       pagination: records per page (max 500)
1281
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1282
     * @param  boolean $zeroconf    include zero confirmation transactions
1283
     * @return array                associative array containing the response
1284
     */
1285
    public function utxos($page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1286
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir, $zeroconf);
1287
    }
1288
}
1289