Completed
Pull Request — master (#85)
by thomas
71:20
created

Wallet::estimateSize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 3
ccs 1
cts 1
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\Script\P2shScript;
10
use BitWasp\Bitcoin\Script\ScriptFactory;
11
use BitWasp\Bitcoin\Script\ScriptInterface;
12
use BitWasp\Bitcoin\Script\WitnessScript;
13
use BitWasp\Bitcoin\Transaction\Factory\SignData;
14
use BitWasp\Bitcoin\Transaction\Factory\Signer;
15
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
16
use BitWasp\Bitcoin\Transaction\OutPoint;
17
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
18
use BitWasp\Bitcoin\Transaction\Transaction;
19
use BitWasp\Bitcoin\Transaction\TransactionInterface;
20
use BitWasp\Bitcoin\Transaction\TransactionOutput;
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
     * testnet yes / no
102
     *
103
     * @var bool
104
     */
105
    protected $testnet;
106
107
    /**
108
     * cache of public keys, by path
109
     *
110
     * @var BIP32Key[]
111
     */
112
    protected $pubKeys = [];
113
114
    /**
115
     * cache of address / redeemScript, by path
116
     *
117
     * @var string[][]      [[address, redeemScript)], ]
118
     */
119
    protected $derivations = [];
120
121
    /**
122
     * reverse cache of paths by address
123
     *
124
     * @var string[]
125
     */
126
    protected $derivationsByAddress = [];
127
128
    /**
129
     * @var WalletPath
130
     */
131
    protected $walletPath;
132
133
    protected $checksum;
134
135
    protected $locked = true;
136
137
    protected $optimalFeePerKB;
138
    protected $lowPriorityFeePerKB;
139
    protected $feePerKBAge;
140
141
    /**
142
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
143
     * @param string                        $identifier                 identifier of the wallet
144
     * @param BIP32Key[]                    $primaryPublicKeys
145
     * @param BIP32Key                      $backupPublicKey            should be BIP32 master public key M/
146
     * @param BIP32Key[]                    $blocktrailPublicKeys
147 14
     * @param int                           $keyIndex
148 14
     * @param string                        $network
149
     * @param bool                          $testnet
150 14
     * @param string                        $checksum
151 14
     */
152 14
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, array $primaryPublicKeys, $backupPublicKey, array $blocktrailPublicKeys, $keyIndex, $network, $testnet, $checksum) {
153 14
        $this->sdk = $sdk;
154
155 14
        $this->identifier = $identifier;
156 14
        $this->backupPublicKey = BlocktrailSDK::normalizeBIP32Key($backupPublicKey);
157 14
        $this->primaryPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($primaryPublicKeys);
158 14
        ;
159
        $this->blocktrailPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($blocktrailPublicKeys);
160 14
161 14
        $this->network = $network;
162
        $this->testnet = $testnet;
163
        $this->keyIndex = $keyIndex;
164
        $this->checksum = $checksum;
165
166
        $this->setChainIndex();
167
    }
168 10
169 10
    /**
170
     * @return int
171
     */
172
    public function getDefaultChainIdx() {
173
        if ($this->network !== "bitcoincash") {
174
            return self::CHAIN_BTC_SEGWIT;
175
        } else {
176
            return self::CHAIN_BCC_DEFAULT;
177 5
        }
178
    }
179 5
180 5
    /**
181
     * Returns the current chain index, usually
182
     * indicating what type of scripts to derive.
183
     * @return int
184
     */
185
    public function getChainIndex() {
186
        return $this->walletPath->path()[2];
187
    }
188 10
189 10
    /**
190
     * @param null $chainIdx
191
     * @return $this
192
     */
193
    public function setChainIndex($chainIdx = null) {
194
        if (null === $chainIdx) {
195
            $chainIdx = $this->getDefaultChainIdx();
196
        }
197
198
        if (!in_array($chainIdx, [self::CHAIN_BTC_SEGWIT, self::CHAIN_BCC_DEFAULT, self::CHAIN_BTC_DEFAULT])) {
199 5
            throw new \RuntimeException("Unsupported chain index");
200 5
        }
201 4
202
        $this->walletPath = WalletPath::create($this->keyIndex, $chainIdx);
203
        return $this;
204 5
    }
205
206
    /**
207 5
     * return the wallet identifier
208
     *
209
     * @return string
210 5
     */
211
    public function getIdentifier() {
212 5
        return $this->identifier;
213
    }
214 5
215 5
    /**
216
     * return list of Blocktrail co-sign extended public keys
217
     *
218 5
     * @return array[]      [ [xpub, path] ]
219 5
     */
220 5
    public function getBlocktrailPublicKeys() {
221 5
        return array_map(function (BIP32Key $key) {
222 5
            return $key->tuple();
223
        }, $this->blocktrailPublicKeys);
224
    }
225
226 5
    /**
227
     * check if wallet is locked
228
     *
229
     * @return bool
230
     */
231
    public function isLocked() {
232
        return $this->locked;
233
    }
234
235
    /**
236 10
     * upgrade wallet to different blocktrail cosign key
237 10
     *
238
     * @param $keyIndex
239 10
     * @return bool
240 10
     * @throws \Exception
241
     */
242 10
    public function upgradeKeyIndex($keyIndex) {
243 10
        if ($this->locked) {
244 10
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
245
        }
246
247 10
        $walletPath = WalletPath::create($keyIndex);
248
249 10
        // do the upgrade to the new 'key_index'
250
        $primaryPublicKey = $this->primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath());
251
252
        // $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...
253 10
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
254 10
255
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
256
257
        $this->keyIndex = $keyIndex;
258
        $this->walletPath = $walletPath;
259
260 10
        // update the blocktrail public keys
261
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
262
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
263
                $path = $pubKey[1];
264
                $pubKey = $pubKey[0];
265
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKey), $path);
266
            }
267
        }
268
269
        return true;
270 10
    }
271 10
272
    /**
273 10
     * get a new BIP32 derivation for the next (unused) address
274
     *  by requesting it from the API
275
     *
276
     * @return string
277 10
     * @throws \Exception
278
     */
279
    protected function getNewDerivation() {
280
        $path = $this->walletPath->path()->last("*");
281 10
        if (self::VERIFY_NEW_DERIVATION) {
282 10
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
283
284
            $path = $new['path'];
285 10
            $address = $new['address'];
286
            $redeemScript = $new['redeem_script'];
287
            $witnessScript = array_key_exists('witness_script', $new) ? $new['witness_script'] : null;
288
289
            /** @var ScriptInterface $checkRedeemScript */
290
            /** @var ScriptInterface $checkWitnessScript */
291
            list($checkAddress, $checkRedeemScript, $checkWitnessScript) = $this->getRedeemScriptByPath($path);
292
            if ($checkAddress != $address) {
293
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
294 10
            }
295 10
296 10
            if ($checkRedeemScript && $checkRedeemScript->getHex() != $redeemScript) {
297 10
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript->getHex()}]");
298
            }
299 10
300 10
            if ($checkWitnessScript && $checkWitnessScript->getHex() != $witnessScript) {
301
                throw new \Exception("Failed to verify that witnessScript from API [{$witnessScript}] matches address locally [{$checkWitnessScript->getHex()}]");
302
            }
303 10
        } else {
304
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
305
        }
306
307
        return (string)$path;
308
    }
309
310
    /**
311
     * @param string|BIP32Path  $path
312 10
     * @return BIP32Key|false
313 10
     * @throws \Exception
314
     *
315
     * @TODO: hmm?
316 10
     */
317 10
    protected function getParentPublicKey($path) {
318
        $path = BIP32Path::path($path)->parent()->publicPath();
319
320
        if ($path->count() <= 2) {
321
            return false;
322 10
        }
323
324
        if ($path->isHardened()) {
325
            return false;
326
        }
327
328
        if (!isset($this->pubKeys[(string)$path])) {
329
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
330
        }
331
332
        return $this->pubKeys[(string)$path];
333
    }
334
335
    /**
336
     * get address for the specified path
337
     *
338
     * @param string|BIP32Path  $path
339
     * @return string
340 10
     */
341 10
    public function getAddressByPath($path) {
342
        $path = (string)BIP32Path::path($path)->privatePath();
343 10
        if (!isset($this->derivations[$path])) {
344
            list($address, ) = $this->getRedeemScriptByPath($path);
345 10
346 10
            $this->derivations[$path] = $address;
347 10
            $this->derivationsByAddress[$address] = $path;
348 10
        }
349 10
350
        return $this->derivations[$path];
351 10
    }
352
353
    /**
354
     * @param string $path
355
     * @return WalletScript
356
     */
357
    public function getWalletScriptByPath($path) {
358
        $path = BIP32Path::path($path);
359
360
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
361
        if ($pubKey = $this->getParentPublicKey($path)) {
362
            $key = $pubKey->buildKey($path->publicPath());
363
        } else {
364
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
365
        }
366
367
        return $this->getWalletScriptFromKey($key, $path);
368
    }
369 10
370 10
    /**
371
     * get address and redeemScript for specified path
372 10
     *
373
     * @param string    $path
374 10
     * @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...
375
     */
376
    public function getRedeemScriptByPath($path) {
377
        $walletScript = $this->getWalletScriptByPath($path);
378 10
379
        $redeemScript = $walletScript->isP2SH() ? $walletScript->getRedeemScript() : null;
380
        $witnessScript = $walletScript->isP2WSH() ? $walletScript->getWitnessScript() : null;
381
        return [$walletScript->getAddress()->getAddress(), $redeemScript, $witnessScript];
382
    }
383
384
    /**
385
     * @param BIP32Key          $key
386 10
     * @param string|BIP32Path  $path
387 10
     * @return string
388 10
     */
389
    protected function getAddressFromKey(BIP32Key $key, $path) {
390 10
        return $this->getWalletScriptFromKey($key, $path)->getAddress()->getAddress();
391
    }
392
393
    /**
394
     * @param BIP32Key          $key
395
     * @param string|BIP32Path  $path
396
     * @return WalletScript
397
     * @throws \Exception
398 7
     */
399 7
    protected function getWalletScriptFromKey(BIP32Key $key, $path) {
400
        $path = BIP32Path::path($path)->publicPath();
401
402
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
403
404
        $multisig = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
405
            $key->buildKey($path)->publicKey(),
406
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
407 9
            $blocktrailPublicKey->buildKey($path)->publicKey()
408 9
        ]), false);
409
410 9
        $type = (int)$key->path()[2];
411
        if ($this->network !== "bitcoincash" && $type === 2) {
412
            $witnessScript = new WitnessScript($multisig);
413
            $redeemScript = new P2shScript($witnessScript);
414
            $scriptPubKey = $redeemScript->getOutputScript();
415
        } else {
416
            $witnessScript = null;
417
            $redeemScript = new P2shScript($multisig);
418
            $scriptPubKey = $redeemScript->getOutputScript();
419 2
        }
420 2
421
        return new WalletScript($path, $scriptPubKey, $redeemScript, $witnessScript);
422 2
    }
423
424
    /**
425
     * get the path (and redeemScript) to specified address
426
     *
427
     * @param string $address
428
     * @return array
429
     */
430
    public function getPathForAddress($address) {
431
        return $this->sdk->getPathForAddress($this->identifier, $address);
432
    }
433
434
    /**
435
     * @param string|BIP32Path  $path
436
     * @return BIP32Key
437
     * @throws \Exception
438 8
     */
439 8
    public function getBlocktrailPublicKey($path) {
440 4
        $path = BIP32Path::path($path);
441
442
        $keyIndex = str_replace("'", "", $path[1]);
443 8
444
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
445 8
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
446 8
        }
447 8
448 8
        return $this->blocktrailPublicKeys[$keyIndex];
449
    }
450 8
451 8
    /**
452
     * generate a new derived key and return the new path and address for it
453
     *
454 8
     * @return string[]     [path, address]
455
     */
456 2
    public function getNewAddressPair() {
457
        $path = $this->getNewDerivation();
458 2
        $address = $this->getAddressByPath($path);
459
460
        return [$path, $address];
461
    }
462
463
    /**
464
     * generate a new derived private key and return the new address for it
465
     *
466
     * @return string
467
     */
468
    public function getNewAddress() {
469
        return $this->getNewAddressPair()[1];
470
    }
471
472
    /**
473
     * get the balance for the wallet
474
     *
475
     * @return int[]            [confirmed, unconfirmed]
476
     */
477
    public function getBalance() {
478
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
479
480
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
481 9
    }
482 9
483
    /**
484 9
     * do wallet discovery (slow)
485 9
     *
486 1
     * @param int   $gap        the gap setting to use for discovery
487
     * @return int[]            [confirmed, unconfirmed]
488
     */
489
    public function doDiscovery($gap = 200) {
490 1
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
491 1
492 1
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
493 1
    }
494 1
495 1
    /**
496
     * create, sign and send a transaction
497 1
     *
498
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
499
     *                                      value should be INT
500 9
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
501 9
     * @param bool     $allowZeroConf
502
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
503
     * @param string   $feeStrategy
504 9
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
505
     * @return string the txid / transaction hash
506
     * @throws \Exception
507 9
     */
508
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
509
        if ($this->locked) {
510
            throw new \Exception("Wallet needs to be unlocked to pay");
511
        }
512
513
        $outputs = self::normalizeOutputsStruct($outputs);
514
515
        $txBuilder = new TransactionBuilder();
516
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
517
        $txBuilder->setFeeStrategy($feeStrategy);
518
        $txBuilder->setChangeAddress($changeAddress);
519 8
520
        foreach ($outputs as $output) {
521 8
            $txBuilder->addRecipient($output['address'], $output['value']);
522 2
        }
523 2
524 2
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
525
526 2
        $apiCheckFee = $forceFee === null;
527 1
528
        return $this->sendTx($txBuilder, $apiCheckFee);
529 2
    }
530
531
    /**
532 2
     * determine max spendable from wallet after fees
533 2
     *
534 2
     * @param bool     $allowZeroConf
535 2
     * @param string   $feeStrategy
536 2
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
537
     * @param int      $outputCnt
538
     * @return string
539 2
     * @throws BlocktrailSDKException
540
     */
541
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
542
        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...
543
    }
544
545
    /**
546
     * parse outputs into normalized struct
547
     *
548
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
549 3
     * @return array            [['address' => address, 'value' => value], ]
550 3
     */
551 3
    public static function normalizeOutputsStruct(array $outputs) {
552 3
        $result = [];
553
554 3
        foreach ($outputs as $k => $v) {
555
            if (is_numeric($k)) {
556 3
                if (!is_array($v)) {
557 3
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
558
                }
559
560
                if (isset($v['address']) && isset($v['value'])) {
561
                    $address = $v['address'];
562
                    $value = $v['value'];
563
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
564
                    $address = $v[0];
565
                    $value = $v[1];
566
                } else {
567
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
568
                }
569
            } else {
570
                $address = $k;
571
                $value = $v;
572
            }
573
574
            $result[] = ['address' => $address, 'value' => $value];
575
        }
576
577 3
        return $result;
578
    }
579
580
    /**
581 3
     * 'fund' the txBuilder with UTXOs (modified in place)
582
     *
583
     * @param TransactionBuilder    $txBuilder
584
     * @param bool|true             $lockUTXOs
585
     * @param bool|false            $allowZeroConf
586 3
     * @param null|int              $forceFee
587
     * @return TransactionBuilder
588
     */
589
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
590 3
        // get the data we should use for this transaction
591 3
        $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 589 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...
592 3
        $utxos = $coinSelection['utxos'];
593 1
        $fee = $coinSelection['fee'];
594
        $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...
595
596 3
        if ($forceFee !== null) {
597
            $txBuilder->setFee($forceFee);
598 3
        } else {
599 2
            $txBuilder->validateFee($fee);
600
        }
601
602
        foreach ($utxos as $utxo) {
603
            $scriptPubKey = ScriptFactory::fromHex($utxo['scriptpubkey_hex']);
604 3
            $redeemScript = ScriptFactory::fromHex($utxo['redeem_script']);
605 3
            $address = AddressFactory::fromString($utxo['address']);
606 3
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $address, $scriptPubKey, $utxo['path'], $redeemScript);
607 3
        }
608
609
        return $txBuilder;
610
    }
611 3
612 3
    /**
613
     * build inputs and outputs lists for TransactionBuilder
614
     *
615
     * @param TransactionBuilder $txBuilder
616 3
     * @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...
617 3
     * @throws \Exception
618
     */
619
    public function buildTx(TransactionBuilder $txBuilder) {
620 3
        $send = $txBuilder->getOutputs();
621 3
        $utxos = $txBuilder->getUtxos();
622
        $signInfo = [];
623 3
624 1
        $txb = new TxBuilder();
625 3
626 3
        foreach ($utxos as $utxo) {
627
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
628 3
                $tx = $this->sdk->transaction($utxo->hash);
629
630
                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...
631
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
632 3
                }
633
634
                $output = $tx['outputs'][$utxo->index];
635 3
636 3
                if (!$utxo->address) {
637 3
                    $utxo->address = AddressFactory::fromString($output['address']);
638
                }
639 3
                if (!$utxo->value) {
640 3
                    $utxo->value = $output['value'];
641
                }
642
                if (!$utxo->scriptPubKey) {
643 3
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
644 2
                }
645
            }
646
647 2
            if (!$utxo->path) {
648
                $utxo->path = $this->getPathForAddress($utxo->address->getAddress());
649
            }
650
651 2
            if (!$utxo->redeemScript || !$utxo->witnessScript) {
652
                list(, $redeemScript, $witnessScript) = $this->getRedeemScriptByPath($utxo->path);
653
                $utxo->redeemScript = $redeemScript;
654 3
                $utxo->witnessScript = $witnessScript;
0 ignored issues
show
Documentation Bug introduced by
It seems like $witnessScript can also be of type object<BitWasp\Bitcoin\Script\WitnessScript>. However, the property $witnessScript is declared as type null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
655
            }
656 3
657
            $signInfo[] = new SignInfo($utxo->path, $utxo->redeemScript, $utxo->witnessScript, new TransactionOutput($utxo->value, $utxo->scriptPubKey));
0 ignored issues
show
Bug introduced by
It seems like $utxo->redeemScript can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
658 3
        }
659 3
660
        $utxoSum = array_sum(array_map(function (UTXO $utxo) {
661 3
            return $utxo->value;
662
        }, $utxos));
663
        if ($utxoSum < array_sum(array_column($send, 'value'))) {
664 3
            throw new \Exception("Atempting to spend more than sum of UTXOs");
665
        }
666
667 3
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
668
669
        if ($txBuilder->getValidateFee() !== null) {
670
            if (abs($txBuilder->getValidateFee() - $fee) > Wallet::BASE_FEE) {
671 3
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
672 1
            }
673
        }
674 3
675
        if ($change > 0) {
676
            $send[] = [
677 3
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewAddress(),
678 3
                'value' => $change
679
            ];
680
        }
681
682
        foreach ($utxos as $utxo) {
683
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index), $utxo->scriptPubKey);
684
        }
685
686
        // outputs should be randomized to make the change harder to detect
687 3
        if ($txBuilder->shouldRandomizeChangeOuput()) {
688
            shuffle($send);
689 3
        }
690
691
        foreach ($send as $out) {
692
            assert(isset($out['value']));
693
694
            if (isset($out['scriptPubKey'])) {
695
                $txb->output($out['value'], $out['scriptPubKey']);
696
            } elseif (isset($out['address'])) {
697
                $txb->payToAddress($out['value'], AddressFactory::fromString($out['address']));
698
            } else {
699
                throw new \Exception();
700 2
            }
701 2
        }
702
703 2
        return [$txb->get(), $signInfo];
704
    }
705
706
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) {
707
        $send = $txBuilder->getOutputs();
708
        $utxos = $txBuilder->getUtxos();
709
710
        $fee = $txBuilder->getFee();
711
        $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...
712
713
        // if the fee is fixed we just need to calculate the change
714
        if ($fee !== null) {
715
            $change = $this->determineChange($utxos, $send, $fee);
716
717 2
            // if change is not dust we need to add a change output
718 2
            if ($change > Blocktrail::DUST) {
719
                $send[] = ['address' => 'change', 'value' => $change];
720
            } else {
721
                // if change is dust we do nothing (implicitly it's added to the fee)
722
                $change = 0;
723 2
            }
724 2
        } else {
725
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
726
727 2
            $change = $this->determineChange($utxos, $send, $fee);
728
729
            if ($change > 0) {
730
                $changeIdx = count($send);
731 2
                // set dummy change output
732 2
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
733
734 2
                // recaculate fee now that we know that we have a change output
735
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
736
737
                // unset dummy change output
738
                unset($send[$changeIdx]);
739
740
                // if adding the change output made the fee bump up and the change is smaller than the fee
741
                //  then we're not doing change
742
                if ($fee2 > $fee && $fee2 > $change) {
743
                    $change = 0;
744
                } else {
745 1
                    $change = $this->determineChange($utxos, $send, $fee2);
746 1
747
                    // if change is not dust we need to add a change output
748 1
                    if ($change > Blocktrail::DUST) {
749
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
750
                    } else {
751
                        // if change is dust we do nothing (implicitly it's added to the fee)
752
                        $change = 0;
753
                    }
754
                }
755 3
            }
756 3
        }
757
758 3
        $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
759
760
        return [$fee, $change];
761
    }
762
763
    /**
764
     * create, sign and send transction based on TransactionBuilder
765
     *
766 5
     * @param TransactionBuilder $txBuilder
767 5
     * @param bool $apiCheckFee     let the API check if the fee is correct
768
     * @return string
769
     * @throws \Exception
770
     */
771
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
772
        list($tx, $signInfo) = $this->buildTx($txBuilder);
773
774
        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...
775
    }
776 2
777 2
    /**
778
     * !! INTERNAL METHOD, public for testing purposes !!
779
     * create, sign and send transction based on inputs and outputs
780
     *
781
     * @param Transaction $tx
782
     * @param SignInfo[]  $signInfo
783
     * @param bool $apiCheckFee     let the API check if the fee is correct
784
     * @return string
785
     * @throws \Exception
786 5
     * @internal
787 5
     */
788
    public function _sendTx(Transaction $tx, array $signInfo, $apiCheckFee = true) {
789 5
        if ($this->locked) {
790
            throw new \Exception("Wallet needs to be unlocked to pay");
791 5
        }
792
793 5
        assert(Util::all(function ($signInfo) {
794 5
            return $signInfo instanceof SignInfo;
795 5
        }, $signInfo), '$signInfo should be SignInfo[]');
796 5
797 5
        // sign the transaction with our keys
798
        $signed = $this->signTransaction($tx, $signInfo);
799
800 5
        $txs = [
801 5
            'signed_transaction' => $signed->getHex(),
802 5
            'base_transaction' => $signed->getBaseSerialization()->getHex(),
803 5
        ];
804 5
805
        // send the transaction
806
        $finished = $this->sendTransaction($txs, array_map(function (SignInfo $r) {
807 5
            return (string)$r->path;
808
        }, $signInfo), $apiCheckFee);
809
810
        return $finished;
811
    }
812
813
    /**
814
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
815
     *
816
     * @todo: mark this as deprecated, insist on the utxo's or qualified scripts.
817
     * @param int $utxoCnt      number of unspent inputs in transaction
818
     * @param int $outputCnt    number of outputs in transaction
819
     * @return float
820 5
     * @access public           reminder that people might use this!
821
     */
822
    public static function estimateFee($utxoCnt, $outputCnt) {
823
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
824
825
        return self::baseFeeForSize($size);
826
    }
827
828
    /**
829
     * @param int $size     size in bytes
830
     * @return int          fee in satoshi
831
     */
832
    public static function baseFeeForSize($size) {
833
        $sizeKB = (int)ceil($size / 1000);
834
835 3
        return $sizeKB * self::BASE_FEE;
836 3
    }
837 3
838 3
    /**
839 1
     * @todo: variable varint
840 1
     * @param int $txinSize
841
     * @param int $txoutSize
842 1
     * @return float
843
     */
844
    public static function estimateSize($txinSize, $txoutSize) {
845 3
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
846
    }
847
848
    /**
849 3
     * only supports estimating size for P2PKH/P2SH outputs
850
     *
851
     * @param int $outputCnt    number of outputs in transaction
852 3
     * @return float
853 2
     */
854
    public static function estimateSizeOutputs($outputCnt) {
855 2
        return ($outputCnt * 34);
856 2
    }
857
858 1
    /**
859 1
     * only supports estimating size for 2of3 multsig UTXOs
860
     *
861
     * @param int $utxoCnt      number of unspent inputs in transaction
862
     * @return float
863
     */
864
    public static function estimateSizeUTXOs($utxoCnt) {
865
        $txinSize = 0;
866
867
        for ($i=0; $i<$utxoCnt; $i++) {
868
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
869
            $multisig = "2of3";
870
871
            if ($multisig) {
872
                $sigCnt = 2;
873
                $msig = explode("of", $multisig);
874 3
                if (count($msig) == 2 && is_numeric($msig[0])) {
875
                    $sigCnt = $msig[0];
876 3
                }
877 3
878 3
                $txinSize += array_sum([
879
                    32, // txhash
880 3
                    4, // idx
881
                    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...
882
                    ((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...
883
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
884
                    1, // OP_0
885
                    4, // sequence
886
                ]);
887
            } else {
888
                $txinSize += array_sum([
889
                    32, // txhash
890
                    4, // idx
891 2
                    73, // sig
892 2
                    34, // script
893
                    4, // sequence
894 2
                ]);
895 2
            }
896 2
        }
897
898 2
        return $txinSize;
899 2
    }
900
901
    /**
902
     * determine how much fee is required based on the inputs and outputs
903
     *  this is an estimation, not a proper 100% correct calculation
904 2
     *
905 2
     * @param UTXO[]  $utxos
906 2
     * @param array[] $outputs
907 2
     * @param         $feeStrategy
908
     * @param         $optimalFeePerKB
909 2
     * @param         $lowPriorityFeePerKB
910
     * @return int
911 2
     * @throws BlocktrailSDKException
912 2
     */
913
    protected function determineFee($utxos, $outputs, $feeStrategy, $optimalFeePerKB, $lowPriorityFeePerKB) {
914
915 2
        $size = SizeEstimation::estimateVsize($utxos, $outputs);
916
917
        switch ($feeStrategy) {
918
            case self::FEE_STRATEGY_BASE_FEE:
919
                return self::baseFeeForSize($size);
920
921
            case self::FEE_STRATEGY_OPTIMAL:
922
                return (int)round(($size / 1000) * $optimalFeePerKB);
923
924
            case self::FEE_STRATEGY_LOW_PRIORITY:
925
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
926
927 2
            default:
928 2
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
929
        }
930
    }
931
932
    /**
933
     * determine how much change is left over based on the inputs and outputs and the fee
934
     *
935
     * @param UTXO[]    $utxos
936
     * @param array[]   $outputs
937
     * @param int       $fee
938
     * @return int
939 8
     */
940 8
    protected function determineChange($utxos, $outputs, $fee) {
941
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
942 2
            return $utxo->value;
943 2
        }, $utxos));
944 2
        $outputsTotal = array_sum(array_column($outputs, 'value'));
945
946 2
        return $inputsTotal - $outputsTotal - $fee;
947
    }
948
949 3
    /**
950 3
     * sign a raw transaction with the private keys that we have
951 1
     *
952
     * @param Transaction $tx
953
     * @param SignInfo[]  $signInfo
954 3
     * @return TransactionInterface
955
     * @throws \Exception
956
     */
957 3
    protected function signTransaction(Transaction $tx, array $signInfo) {
958 3
        $signer = new Signer($tx, Bitcoin::getEcAdapter());
959
960
        assert(Util::all(function ($signInfo) {
961
            return $signInfo instanceof SignInfo;
962 3
        }, $signInfo), '$signInfo should be SignInfo[]');
963
964
        $sigHash = SigHash::ALL;
965 1
        if ($this->network === "bitcoincash") {
966 1
            $sigHash |= SigHash::BITCOINCASH;
967
            $signer->redeemBitcoinCash(true);
968 1
        }
969 1
970
        foreach ($signInfo as $idx => $info) {
971 1
            $path = BIP32Path::path($info->path)->privatePath();
972 1
            $key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey();
973
974
            $signData = new SignData();
975
            if ($info->redeemScript) {
976
                $signData->p2sh($info->redeemScript);
977
            }
978
            if ($info->witnessScript) {
979
                $signData->p2wsh($info->witnessScript);
980
            }
981 10
982 10
            $input = $signer->input($idx, $info->output, $signData);
983
            $input->sign($key, $sigHash);
984
        }
985
986 10
        return $signer->get();
987 10
    }
988
989
    /**
990
     * send the transaction using the API
991
     *
992
     * @param string|array  $signed
993
     * @param string[]      $paths
994
     * @param bool          $checkFee
995 10
     * @return string           the complete raw transaction
996 10
     * @throws \Exception
997
     */
998 10
    protected function sendTransaction($signed, $paths, $checkFee = false) {
999 10
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
1000
    }
1001 10
1002 10
    /**
1003
     * @param \array[] $outputs
1004 10
     * @param bool $lockUTXO
1005
     * @param bool $allowZeroConf
1006
     * @param int|null|string $feeStrategy
1007
     * @param null $forceFee
1008
     * @return array
1009
     */
1010
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1011
        $result = $this->sdk->coinSelection($this->identifier, $outputs, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
1012
1013
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
1014 1
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
1015 1
        $this->feePerKBAge = time();
1016 1
1017
        return $result;
1018
    }
1019
1020
    public function getOptimalFeePerKB() {
1021
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
1022
            $this->updateFeePerKB();
1023 1
        }
1024 1
1025 1
        return $this->optimalFeePerKB;
1026
    }
1027
1028
    public function getLowPriorityFeePerKB() {
1029
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1030
            $this->updateFeePerKB();
1031
        }
1032
1033
        return $this->lowPriorityFeePerKB;
1034
    }
1035
1036
    public function updateFeePerKB() {
1037
        $result = $this->sdk->feePerKB();
1038
1039
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
1040
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
1041
1042
        $this->feePerKBAge = time();
1043
    }
1044
1045
    /**
1046
     * delete the wallet
1047
     *
1048
     * @param bool $force ignore warnings (such as non-zero balance)
1049
     * @return mixed
1050
     * @throws \Exception
1051
     */
1052
    public function deleteWallet($force = false) {
1053
        if ($this->locked) {
1054
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
1055
        }
1056
1057
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
1058
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
1059 1
    }
1060 1
1061
    /**
1062
     * create checksum to verify ownership of the master primary key
1063
     *
1064
     * @return string[]     [address, signature]
1065
     */
1066
    protected function createChecksumVerificationSignature() {
1067
        $privKey = $this->primaryPrivateKey->key();
1068
1069
        $pubKey = $this->primaryPrivateKey->publicKey();
1070
        $address = $pubKey->getAddress()->getAddress();
1071 1
1072 1
        $signer = new MessageSigner(Bitcoin::getEcAdapter());
1073
        $signed = $signer->sign($address, $privKey->getPrivateKey());
1074
1075
        return [$address, base64_encode($signed->getCompactSignature()->getBuffer()->getBinary())];
1076
    }
1077
1078
    /**
1079
     * setup a webhook for our wallet
1080
     *
1081
     * @param string    $url            URL to receive webhook events
1082
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1083
     * @return array
1084 1
     */
1085 1
    public function setupWebhook($url, $identifier = null) {
1086
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1087
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1088
    }
1089
1090
    /**
1091
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1092
     * @return mixed
1093
     */
1094
    public function deleteWebhook($identifier = null) {
1095
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1096
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1097
    }
1098
1099
    /**
1100
     * lock a specific unspent output
1101
     *
1102
     * @param     $txHash
1103
     * @param     $txIdx
1104
     * @param int $ttl
1105
     * @return bool
1106
     */
1107
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1108
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1109
    }
1110
1111
    /**
1112
     * unlock a specific unspent output
1113
     *
1114
     * @param     $txHash
1115
     * @param     $txIdx
1116
     * @return bool
1117
     */
1118
    public function unlockUTXO($txHash, $txIdx) {
1119
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1120
    }
1121
1122
    /**
1123
     * get all transactions for the wallet (paginated)
1124
     *
1125
     * @param  integer $page    pagination: page number
1126
     * @param  integer $limit   pagination: records per page (max 500)
1127
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1128
     * @return array            associative array containing the response
1129
     */
1130
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1131
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1132
    }
1133
1134
    /**
1135
     * get all addresses for the wallet (paginated)
1136
     *
1137
     * @param  integer $page    pagination: page number
1138
     * @param  integer $limit   pagination: records per page (max 500)
1139
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1140
     * @return array            associative array containing the response
1141
     */
1142
    public function addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1143
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1144
    }
1145
1146
    /**
1147
     * get all UTXOs for the wallet (paginated)
1148
     *
1149
     * @param  integer $page        pagination: page number
1150
     * @param  integer $limit       pagination: records per page (max 500)
1151
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1152
     * @param  boolean $zeroconf    include zero confirmation transactions
1153
     * @return array                associative array containing the response
1154
     */
1155
    public function utxos($page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1156
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir, $zeroconf);
1157
    }
1158
}
1159