Completed
Pull Request — master (#85)
by thomas
15:44
created

Wallet::getRedeemScriptByPath()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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

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

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

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

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

Loading history...
591
592 5
        if ($forceFee !== null) {
593 1
            $txBuilder->setFee($forceFee);
594
        } else {
595 5
            $txBuilder->validateFee($fee);
596
        }
597
598 5
        foreach ($utxos as $utxo) {
599 5
            $signMode = SignInfo::MODE_SIGN;
600 5
            if (isset($utxo['sign_mode'])) {
601
                $signMode = $utxo['sign_mode'];
602
                if (!in_array($signMode, $this->allowedSignModes)) {
603
                    throw new \Exception("Sign mode disallowed by wallet");
604
                }
605
            }
606
607 5
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script'], $utxo['witness_script'], $signMode);
608
        }
609
610 5
        return $txBuilder;
611
    }
612
613
    /**
614
     * build inputs and outputs lists for TransactionBuilder
615
     *
616
     * @param TransactionBuilder $txBuilder
617
     * @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...
618
     * @throws \Exception
619
     */
620 7
    public function buildTx(TransactionBuilder $txBuilder) {
621 7
        $send = $txBuilder->getOutputs();
622 7
        $utxos = $txBuilder->getUtxos();
623 7
        $signInfo = [];
624
625 7
        $txb = new TxBuilder();
626
627 7
        foreach ($utxos as $utxo) {
628 7
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
629 1
                $tx = $this->sdk->transaction($utxo->hash);
630
631 1
                if (!$tx || !isset($tx['outputs'][$utxo->index])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tx of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
632
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
633
                }
634
635 1
                $output = $tx['outputs'][$utxo->index];
636
637 1
                if (!$utxo->address) {
638
                    $utxo->address = AddressFactory::fromString($output['address']);
639
                }
640 1
                if (!$utxo->value) {
641
                    $utxo->value = $output['value'];
642
                }
643 1
                if (!$utxo->scriptPubKey) {
644 1
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
645
                }
646
            }
647
648 7
            if (SignInfo::MODE_SIGN === $utxo->signMode) {
649 7
                if (!$utxo->path) {
650
                    $utxo->path = $this->getPathForAddress($utxo->address->getAddress());
651
                }
652
653 7
                if (!$utxo->redeemScript || !$utxo->witnessScript) {
654 6
                    list(, $redeemScript, $witnessScript) = $this->getRedeemScriptByPath($utxo->path);
0 ignored issues
show
Documentation introduced by
$utxo->path is of type array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
655 6
                    $utxo->redeemScript = $redeemScript;
656 6
                    $utxo->witnessScript = $witnessScript;
657
                }
658
            }
659
660 7
            $signInfo[] = $utxo->getSignInfo();
661
        }
662
663
        $utxoSum = array_sum(array_map(function (UTXO $utxo) {
664 7
            return $utxo->value;
665 7
        }, $utxos));
666 7
        if ($utxoSum < array_sum(array_column($send, 'value'))) {
667 1
            throw new \Exception("Atempting to spend more than sum of UTXOs");
668
        }
669
670 7
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
671
672 7
        if ($txBuilder->getValidateFee() !== null) {
673 5
            if (abs($txBuilder->getValidateFee() - $fee) > Wallet::BASE_FEE) {
674
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
675
            }
676
        }
677
678 7
        if ($change > 0) {
679 6
            $send[] = [
680 6
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewAddress(),
681 6
                'value' => $change
682
            ];
683
        }
684
685 7
        foreach ($utxos as $utxo) {
686 7
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index));
687
        }
688
689
        // outputs should be randomized to make the change harder to detect
690 7
        if ($txBuilder->shouldRandomizeChangeOuput()) {
691 7
            shuffle($send);
692
        }
693
694 7
        foreach ($send as $out) {
695 7
            assert(isset($out['value']));
696
697 7
            if (isset($out['scriptPubKey'])) {
698 2
                $txb->output($out['value'], $out['scriptPubKey']);
699 7
            } elseif (isset($out['address'])) {
700 7
                $txb->payToAddress($out['value'], AddressFactory::fromString($out['address']));
701
            } else {
702 7
                throw new \Exception();
703
            }
704
        }
705
706 7
        return [$txb->get(), $signInfo];
707
    }
708
709 7
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) {
710 7
        $send = $txBuilder->getOutputs();
711 7
        $utxos = $txBuilder->getUtxos();
712
713 7
        $fee = $txBuilder->getFee();
714 7
        $change = null;
0 ignored issues
show
Unused Code introduced by
$change is not used, you could remove the assignment.

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

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

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

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

Loading history...
715
716
        // if the fee is fixed we just need to calculate the change
717 7
        if ($fee !== null) {
718 2
            $change = $this->determineChange($utxos, $send, $fee);
719
720
            // if change is not dust we need to add a change output
721 2
            if ($change > Blocktrail::DUST) {
722 1
                $send[] = ['address' => 'change', 'value' => $change];
723
            } else {
724
                // if change is dust we do nothing (implicitly it's added to the fee)
725 2
                $change = 0;
726
            }
727
        } else {
728 7
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
729
730 7
            $change = $this->determineChange($utxos, $send, $fee);
731
732 7
            if ($change > 0) {
733 6
                $changeIdx = count($send);
734
                // set dummy change output
735 6
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
736
737
                // recaculate fee now that we know that we have a change output
738 6
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
739
740
                // unset dummy change output
741 6
                unset($send[$changeIdx]);
742
743
                // if adding the change output made the fee bump up and the change is smaller than the fee
744
                //  then we're not doing change
745 6
                if ($fee2 > $fee && $fee2 > $change) {
746 1
                    $change = 0;
747
                } else {
748 6
                    $change = $this->determineChange($utxos, $send, $fee2);
749
750
                    // if change is not dust we need to add a change output
751 6
                    if ($change > Blocktrail::DUST) {
752 6
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
753
                    } else {
754
                        // if change is dust we do nothing (implicitly it's added to the fee)
755
                        $change = 0;
756
                    }
757
                }
758
            }
759
        }
760
761 7
        $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
762
763 7
        return [$fee, $change];
764
    }
765
766
    /**
767
     * create, sign and send transction based on TransactionBuilder
768
     *
769
     * @param TransactionBuilder $txBuilder
770
     * @param bool $apiCheckFee     let the API check if the fee is correct
771
     * @return string
772
     * @throws \Exception
773
     */
774 4
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
775 4
        list($tx, $signInfo) = $this->buildTx($txBuilder);
776
777 4
        return $this->_sendTx($tx, $signInfo, $apiCheckFee);
0 ignored issues
show
Compatibility introduced by
$tx of type object<BitWasp\Bitcoin\T...n\TransactionInterface> is not a sub-type of object<BitWasp\Bitcoin\Transaction\Transaction>. It seems like you assume a concrete implementation of the interface BitWasp\Bitcoin\Transaction\TransactionInterface to be always present.

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

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

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

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

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

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

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

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

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

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

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