Completed
Branch master (fe036c)
by
unknown
02:15
created

Wallet::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 2
Metric Value
dl 0
loc 21
rs 9.3142
c 2
b 0
f 2
cc 1
eloc 16
nc 1
nop 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\BitcoinLib\BIP32;
6
use BitWasp\BitcoinLib\BIP39\BIP39;
7
use BitWasp\BitcoinLib\BitcoinLib;
8
use BitWasp\BitcoinLib\RawTransaction;
9
use Blocktrail\SDK\Bitcoin\BIP32Key;
10
use Blocktrail\SDK\Bitcoin\BIP32Path;
11
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
12
13
/**
14
 * Class Wallet
15
 */
16
class Wallet implements WalletInterface {
17
18
    const BASE_FEE = 10000;
19
20
    /**
21
     * development / debug setting
22
     *  when getting a new derivation from the API,
23
     *  will verify address / redeeemScript with the values the API provides
24
     */
25
    const VERIFY_NEW_DERIVATION = true;
26
27
    /**
28
     * @var BlocktrailSDKInterface
29
     */
30
    protected $sdk;
31
32
    /**
33
     * @var string
34
     */
35
    protected $identifier;
36
37
    /**
38
     * BIP39 Mnemonic for the master primary private key
39
     *
40
     * @var string
41
     */
42
    protected $primaryMnemonic;
43
44
    /**
45
     * BIP32 master primary private key (m/)
46
     *
47
     * @var BIP32Key
48
     */
49
    protected $primaryPrivateKey;
50
51
    /**
52
     * @var BIP32Key[]
53
     */
54
    protected $primaryPublicKeys;
55
56
    /**
57
     * BIP32 master backup public key (M/)
58
59
     * @var BIP32Key
60
     */
61
    protected $backupPublicKey;
62
63
    /**
64
     * map of blocktrail BIP32 public keys
65
     *  keyed by key index
66
     *  path should be `M / key_index'`
67
     *
68
     * @var BIP32Key[]
69
     */
70
    protected $blocktrailPublicKeys;
71
72
    /**
73
     * the 'Blocktrail Key Index' that is used for new addresses
74
     *
75
     * @var int
76
     */
77
    protected $keyIndex;
78
79
    /**
80
     * 'bitcoin'
81
     *
82
     * @var string
83
     */
84
    protected $network;
85
86
    /**
87
     * testnet yes / no
88
     *
89
     * @var bool
90
     */
91
    protected $testnet;
92
93
    /**
94
     * cache of public keys, by path
95
     *
96
     * @var BIP32Key[]
97
     */
98
    protected $pubKeys = [];
99
100
    /**
101
     * cache of address / redeemScript, by path
102
     *
103
     * @var string[][]      [[address, redeemScript)], ]
104
     */
105
    protected $derivations = [];
106
107
    /**
108
     * reverse cache of paths by address
109
     *
110
     * @var string[]
111
     */
112
    protected $derivationsByAddress = [];
113
114
    /**
115
     * @var WalletPath
116
     */
117
    protected $walletPath;
118
119
    private $checksum;
120
121
    private $locked = true;
122
123
    protected $optimalFeePerKB;
124
    protected $lowPriorityFeePerKB;
125
    protected $feePerKBAge;
126
127
    /**
128
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
129
     * @param string                        $identifier                 identifier of the wallet
130
     * @param string                        $primaryMnemonic
131
     * @param array[string, string]         $primaryPublicKeys
132
     * @param array[string, string]         $backupPublicKey            should be BIP32 master public key M/
133
     * @param array[array[string, string]]  $blocktrailPublicKeys
134
     * @param int                           $keyIndex
135
     * @param string                        $network
136
     * @param bool                          $testnet
137
     * @param string                        $checksum
138
     */
139
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, $primaryMnemonic, $primaryPublicKeys, $backupPublicKey, $blocktrailPublicKeys, $keyIndex, $network, $testnet, $checksum) {
140
        $this->sdk = $sdk;
141
142
        $this->identifier = $identifier;
143
144
        $this->primaryMnemonic = $primaryMnemonic;
145
        $this->backupPublicKey = BIP32Key::create($backupPublicKey);
146
        $this->primaryPublicKeys = array_map(function ($key) {
147
            return BIP32Key::create($key);
148
        }, $primaryPublicKeys);
149
        $this->blocktrailPublicKeys = array_map(function ($key) {
150
            return BIP32Key::create($key);
151
        }, $blocktrailPublicKeys);
152
153
        $this->network = $network;
154
        $this->testnet = $testnet;
155
        $this->keyIndex = $keyIndex;
156
        $this->checksum = $checksum;
157
158
        $this->walletPath = WalletPath::create($this->keyIndex);
159
    }
160
161
    /**
162
     * return the wallet identifier
163
     *
164
     * @return string
165
     */
166
    public function getIdentifier() {
167
        return $this->identifier;
168
    }
169
170
    /**
171
     * return the wallet primary mnemonic (for backup purposes)
172
     *
173
     * @return string
174
     */
175
    public function getPrimaryMnemonic() {
176
        return $this->primaryMnemonic;
177
    }
178
179
    /**
180
     * return list of Blocktrail co-sign extended public keys
181
     *
182
     * @return array[]      [ [xpub, path] ]
183
     */
184
    public function getBlocktrailPublicKeys() {
185
        return array_map(function (BIP32Key $key) {
186
            return $key->tuple();
187
        }, $this->blocktrailPublicKeys);
188
    }
189
190
    /**
191
     * unlock wallet so it can be used for payments
192
     *
193
     * @param          $options ['primary_private_key' => key] OR ['passphrase' => pass]
194
     * @param callable $fn
195
     * @return bool
196
     * @throws \Exception
197
     */
198
    public function unlock($options, callable $fn = null) {
199
        // explode the wallet data
200
        $password = isset($options['passphrase']) ? $options['passphrase'] : (isset($options['password']) ? $options['password'] : null);
201
        $primaryMnemonic = $this->primaryMnemonic;
202
        $primaryPrivateKey = isset($options['primary_private_key']) ? $options['primary_private_key'] : null;
203
204
        if ($primaryMnemonic && $primaryPrivateKey) {
205
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
206
        }
207
208
        if (!$primaryMnemonic && !$primaryPrivateKey) {
209
            throw new \InvalidArgumentException("Can't init wallet with Primary Mnemonic or Primary PrivateKey");
210
        }
211
212
        if ($primaryMnemonic && !$password) {
213
            throw new \InvalidArgumentException("Can't init wallet with Primary Mnemonic without a passphrase");
214
        }
215
216 View Code Duplication
        if ($primaryPrivateKey) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
217
            if (is_string($primaryPrivateKey)) {
218
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
219
            }
220
        } else {
221
            // convert the mnemonic to a seed using BIP39 standard
222
            $primarySeed = BIP39::mnemonicToSeedHex($primaryMnemonic, $password);
223
            // create BIP32 private key from the seed
224
            $primaryPrivateKey = BIP32::master_key($primarySeed, $this->network, $this->testnet);
0 ignored issues
show
Bug introduced by
It seems like $primarySeed defined by \BitWasp\BitcoinLib\BIP3...aryMnemonic, $password) on line 222 can also be of type false or null; however, BitWasp\BitcoinLib\BIP32::master_key() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
225
        }
226
227
        $this->primaryPrivateKey = BIP32Key::create($primaryPrivateKey);
228
229
        // create checksum (address) of the primary privatekey to compare to the stored checksum
230
        $checksum = BIP32::key_to_address($primaryPrivateKey[0]);
231
        if ($checksum != $this->checksum) {
232
            throw new \Exception("Checksum [{$checksum}] does not match [{$this->checksum}], most likely due to incorrect password");
233
        }
234
235
        $this->locked = false;
236
237
        // if the response suggests we should upgrade to a different blocktrail cosigning key then we should
238
        if (isset($data['upgrade_key_index'])) {
0 ignored issues
show
Bug introduced by
The variable $data seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
239
            $this->upgradeKeyIndex($data['upgrade_key_index']);
240
        }
241
242
        if ($fn) {
243
            $fn($this);
244
            $this->lock();
245
        }
246
    }
247
248
    /**
249
     * lock the wallet (unsets primary private key)
250
     *
251
     * @return void
252
     */
253
    public function lock() {
254
        $this->primaryPrivateKey = null;
255
        $this->locked = true;
256
    }
257
258
    /**
259
     * check if wallet is locked
260
     *
261
     * @return bool
262
     */
263
    public function isLocked() {
264
        return $this->locked;
265
    }
266
267
    /**
268
     * upgrade wallet to different blocktrail cosign key
269
     *
270
     * @param $keyIndex
271
     * @return bool
272
     * @throws \Exception
273
     */
274
    public function upgradeKeyIndex($keyIndex) {
275
        if ($this->locked) {
276
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
277
        }
278
279
        $walletPath = WalletPath::create($keyIndex);
280
281
        // do the upgrade to the new 'key_index'
282
        $primaryPublicKey = BIP32::extended_private_to_public(BIP32::build_key($this->primaryPrivateKey->tuple(), (string)$walletPath->keyIndexPath()));
0 ignored issues
show
Documentation introduced by
$this->primaryPrivateKey->tuple() is of type array<integer,string,{"0":"string","1":"string"}>, 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...
283
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey);
0 ignored issues
show
Bug introduced by
It seems like $primaryPublicKey defined by \BitWasp\BitcoinLib\BIP3...tPath->keyIndexPath())) on line 282 can also be of type string; however, Blocktrail\SDK\Blocktrai...face::upgradeKeyIndex() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
284
285
        $this->primaryPublicKeys[$keyIndex] = BIP32Key::create($primaryPublicKey);
286
287
        $this->keyIndex = $keyIndex;
288
        $this->walletPath = $walletPath;
289
290
        // update the blocktrail public keys
291
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
292
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
293
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create($pubKey);
294
            }
295
        }
296
297
        return true;
298
    }
299
300
    /**
301
     * get a new BIP32 derivation for the next (unused) address
302
     *  by requesting it from the API
303
     *
304
     * @return string
305
     * @throws \Exception
306
     */
307
    protected function getNewDerivation() {
308
        $path = $this->walletPath->path()->last("*");
309
310
        if (self::VERIFY_NEW_DERIVATION) {
311
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
312
313
            $path = $new['path'];
314
            $address = $new['address'];
315
            $redeemScript = $new['redeem_script'];
316
317
            list($checkAddress, $checkRedeemScript) = $this->getRedeemScriptByPath($path);
318
319
            if ($checkAddress != $address) {
320
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
321
            }
322
323
            if ($checkRedeemScript != $redeemScript) {
324
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript}]");
325
            }
326
        } else {
327
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
328
        }
329
330
        return (string)$path;
331
    }
332
333
    /**
334
     * @param string|BIP32Path  $path
335
     * @return BIP32Key|false
336
     * @throws \Exception
337
     *
338
     * @TODO: hmm?
339
     */
340
    protected function getParentPublicKey($path) {
341
        $path = BIP32Path::path($path)->parent()->publicPath();
342
343
        if ($path->count() <= 2) {
344
            return false;
345
        }
346
347
        if ($path->isHardened()) {
348
            return false;
349
        }
350
351
        if (!isset($this->pubKeys[(string)$path])) {
352
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
353
        }
354
355
        return $this->pubKeys[(string)$path];
356
    }
357
358
    /**
359
     * get address for the specified path
360
     *
361
     * @param string|BIP32Path  $path
362
     * @return string
363
     */
364
    public function getAddressByPath($path) {
365
        $path = (string)BIP32Path::path($path)->privatePath();
366
        if (!isset($this->derivations[$path])) {
367
            list($address, ) = $this->getRedeemScriptByPath($path);
368
369
            $this->derivations[$path] = $address;
370
            $this->derivationsByAddress[$address] = $path;
371
        }
372
373
        return $this->derivations[$path];
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->derivations[$path]; of type string[]|string adds the type string[] to the return on line 373 which is incompatible with the return type declared by the interface Blocktrail\SDK\WalletInterface::getAddressByPath of type string.
Loading history...
374
    }
375
376
    /**
377
     * get address and redeemScript for specified path
378
     *
379
     * @param string    $path
380
     * @return array[string, string]     [address, redeemScript]
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...
381
     */
382
    public function getRedeemScriptByPath($path) {
383
        $path = BIP32Path::path($path);
384
385
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
386
        if ($pubKey = $this->getParentPublicKey($path)) {
387
            $key = $pubKey->buildKey($path->publicPath());
388
        } else {
389
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
390
        }
391
392
        return $this->getRedeemScriptFromKey($key, $path);
393
    }
394
395
    /**
396
     * @param BIP32Key          $key
397
     * @param string|BIP32Path  $path
398
     * @return string
399
     */
400
    protected function getAddressFromKey(BIP32Key $key, $path) {
401
        return $this->getRedeemScriptFromKey($key, $path)[0];
402
    }
403
404
    /**
405
     * @param BIP32Key          $key
406
     * @param string|BIP32Path  $path
407
     * @return string[]                 [address, redeemScript]
408
     * @throws \Exception
409
     */
410
    protected function getRedeemScriptFromKey(BIP32Key $key, $path) {
411
        $path = BIP32Path::path($path)->publicPath();
412
413
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
414
415
        $multiSig = RawTransaction::create_multisig(
416
            2,
417
            BlocktrailSDK::sortMultisigKeys([
0 ignored issues
show
Documentation introduced by
array($key->buildKey($pa...ey($path)->publicKey()) is of type array<integer,string|fal...e","2":"string|false"}>, but the function expects a array<integer,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...
418
                $key->buildKey($path)->publicKey(),
419
                $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
420
                $blocktrailPublicKey->buildKey($path)->publicKey()
421
            ])
422
        );
423
424
        return [$multiSig['address'], $multiSig['redeemScript']];
425
    }
426
427
    /**
428
     * get the path (and redeemScript) to specified address
429
     *
430
     * @param string $address
431
     * @return array
432
     */
433
    public function getPathForAddress($address) {
434
        return $this->sdk->getPathForAddress($this->identifier, $address);
435
    }
436
437
    /**
438
     * @param string|BIP32Path  $path
439
     * @return BIP32Key
440
     * @throws \Exception
441
     */
442 View Code Duplication
    public function getBlocktrailPublicKey($path) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
443
        $path = BIP32Path::path($path);
444
445
        $keyIndex = str_replace("'", "", $path[1]);
446
447
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
448
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
449
        }
450
451
        return $this->blocktrailPublicKeys[$keyIndex];
452
    }
453
454
    /**
455
     * generate a new derived key and return the new path and address for it
456
     *
457
     * @return string[]     [path, address]
458
     */
459
    public function getNewAddressPair() {
460
        $path = $this->getNewDerivation();
461
        $address = $this->getAddressByPath($path);
462
463
        return [$path, $address];
0 ignored issues
show
Best Practice introduced by
The expression return array($path, $address); seems to be an array, but some of its elements' types (string[]) are incompatible with the return type declared by the interface Blocktrail\SDK\WalletInterface::getNewAddressPair 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 new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return '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...
464
    }
465
466
    /**
467
     * generate a new derived private key and return the new address for it
468
     *
469
     * @return string
470
     */
471
    public function getNewAddress() {
472
        return $this->getNewAddressPair()[1];
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getNewAddressPair()[1]; of type string[]|string adds the type string[] to the return on line 472 which is incompatible with the return type declared by the interface Blocktrail\SDK\WalletInterface::getNewAddress of type string.
Loading history...
473
    }
474
475
    /**
476
     * get the balance for the wallet
477
     *
478
     * @return int[]            [confirmed, unconfirmed]
479
     */
480
    public function getBalance() {
481
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
482
483
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
484
    }
485
486
    /**
487
     * do wallet discovery (slow)
488
     *
489
     * @param int   $gap        the gap setting to use for discovery
490
     * @return int[]            [confirmed, unconfirmed]
491
     */
492
    public function doDiscovery($gap = 200) {
493
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
494
495
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
496
    }
497
498
    /**
499
     * create, sign and send a transaction
500
     *
501
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
502
     *                                      value should be INT
503
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
504
     * @param bool     $allowZeroConf
505
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
506
     * @param string   $feeStrategy
507
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
508
     * @return string the txid / transaction hash
509
     * @throws \Exception
510
     */
511
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
512
        if ($this->locked) {
513
            throw new \Exception("Wallet needs to be unlocked to pay");
514
        }
515
516
        $outputs = self::normalizeOutputsStruct($outputs);
517
518
        $txBuilder = new TransactionBuilder();
519
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
520
        $txBuilder->setFeeStrategy($feeStrategy);
521
522
        foreach ($outputs as $output) {
523
            $txBuilder->addRecipient($output['address'], $output['value']);
524
        }
525
526
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
527
528
        $apiCheckFee = $forceFee === null;
529
530
        return $this->sendTx($txBuilder, $apiCheckFee);
531
    }
532
533
    /**
534
     * parse outputs into normalized struct
535
     *
536
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
537
     * @return array            [['address' => address, 'value' => value], ]
538
     */
539
    public static function normalizeOutputsStruct(array $outputs) {
540
        $result = [];
541
542
        foreach ($outputs as $k => $v) {
543
            if (is_numeric($k)) {
544
                if (!is_array($v)) {
545
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
546
                }
547
548
                if (isset($v['address']) && isset($v['value'])) {
549
                    $address = $v['address'];
550
                    $value = $v['value'];
551
                } else if (count($v) == 2 && isset($v[0]) && isset($v[1])) {
552
                    $address = $v[0];
553
                    $value = $v[1];
554
                } else {
555
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
556
                }
557
            } else {
558
                $address = $k;
559
                $value = $v;
560
            }
561
562
            $result[] = ['address' => $address, 'value' => $value];
563
        }
564
565
        return $result;
566
    }
567
568
    /**
569
     * 'fund' the txBuilder with UTXOs (modified in place)
570
     *
571
     * @param TransactionBuilder    $txBuilder
572
     * @param bool|true             $lockUTXOs
573
     * @param bool|false            $allowZeroConf
574
     * @param null|int              $forceFee
575
     * @return TransactionBuilder
576
     */
577
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
578
        // get the data we should use for this transaction
579
        $coinSelection = $this->coinSelection($txBuilder->getOutputs(), $lockUTXOs, $allowZeroConf, $txBuilder->getFeeStrategy(), $forceFee);
580
        $utxos = $coinSelection['utxos'];
581
        $fee = $coinSelection['fee'];
582
        $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...
583
584
        if ($forceFee !== null) {
585
            $txBuilder->setFee($forceFee);
586
        } else {
587
            $txBuilder->validateFee($fee);
588
        }
589
590
        foreach ($utxos as $utxo) {
591
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script']);
592
        }
593
594
        return $txBuilder;
595
    }
596
597
    /**
598
     * build inputs and outputs lists for TransactionBuilder
599
     *
600
     * @param TransactionBuilder $txBuilder
601
     * @return array
602
     * @throws \Exception
603
     */
604
    public function buildTx(TransactionBuilder $txBuilder) {
605
        $send = $txBuilder->getOutputs();
606
607
        $utxos = $txBuilder->getUtxos();
608
609
        foreach ($utxos as $utxo) {
610
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKeyHex) {
611
                $tx = $this->sdk->transaction($utxo->hash);
612
613
                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...
614
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
615
                }
616
617
                $output = $tx['outputs'][$utxo->index];
618
619
                if (!$utxo->address) {
620
                    $utxo->address = $output['address'];
621
                }
622
                if (!$utxo->value) {
623
                    $utxo->value = $output['value'];
624
                }
625
                if (!$utxo->scriptPubKeyHex) {
626
                    $utxo->scriptPubKeyHex = $output['script_hex'];
627
                }
628
            }
629
630
            if (!$utxo->path) {
631
                $address = $utxo->address;
632
                if (!BitcoinLib::validate_address($address)) {
633
                    throw new \Exception("Invalid address [{$address}]");
634
                }
635
636
                $utxo->path = $this->getPathForAddress($address);
637
            }
638
639
            if (!$utxo->redeemScript) {
640
                list(, $redeemScript) = $this->getRedeemScriptByPath($utxo->path);
641
                $utxo->redeemScript = $redeemScript;
642
            }
643
        }
644
645
        if (array_sum(array_map(function (UTXO $utxo) { return $utxo->value; }, $utxos)) < array_sum(array_column($send, 'value'))) {
646
            throw new \Exception("Atempting to spend more than sum of UTXOs");
647
        }
648
649
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
650
651
        if ($txBuilder->getValidateFee() !== null) {
652
            if (abs($txBuilder->getValidateFee() - $fee) > Wallet::BASE_FEE) {
653
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
654
            }
655
        }
656
657
        if ($change > 0) {
658
            $send[] = [
659
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewAddress(),
660
                'value' => $change
661
            ];
662
        }
663
664
        // create raw transaction
665
        $inputs = array_map(function (UTXO $utxo) {
666
            return [
667
                'txid' => $utxo->hash,
668
                'vout' => (int)$utxo->index,
669
                'address' => $utxo->address,
670
                'scriptPubKey' => $utxo->scriptPubKeyHex,
671
                'value' => $utxo->value,
672
                'path' => $utxo->path,
673
                'redeemScript' => $utxo->redeemScript
674
            ];
675
        }, $utxos);
676
677
678
        // outputs should be randomized to make the change harder to detect
679
        if ($txBuilder->shouldRandomizeChangeOuput()) {
680
            shuffle($send);
681
        }
682
683
        return [$inputs, $send];
684
    }
685
686
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) {
687
        $send = $txBuilder->getOutputs();
688
        $utxos = $txBuilder->getUtxos();
689
690
        $fee = $txBuilder->getFee();
691
        $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...
692
693
        // if the fee is fixed we just need to calculate the change
694
        if ($fee !== null) {
695
            $change = $this->determineChange($utxos, $send, $fee);
696
697
            // if change is not dust we need to add a change output
698 View Code Duplication
            if ($change > Blocktrail::DUST) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
699
                $send[] = ['address' => 'change', 'value' => $change];
700
            } else {
701
                // if change is dust we do nothing (implicitly it's added to the fee)
702
                $change = 0;
703
            }
704
        } else {
705
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
706
707
            $change = $this->determineChange($utxos, $send, $fee);
708
709
            if ($change > 0) {
710
                $changeIdx = count($send);
711
                // set dummy change output
712
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
713
714
                // recaculate fee now that we know that we have a change output
715
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
716
717
                // unset dummy change output
718
                unset($send[$changeIdx]);
719
720
                // if adding the change output made the fee bump up and the change is smaller than the fee
721
                //  then we're not doing change
722
                if ($fee2 > $fee && $fee2 > $change) {
723
                    $change = 0;
724
                } else {
725
                    $change = $this->determineChange($utxos, $send, $fee2);
726
727
                    // if change is not dust we need to add a change output
728 View Code Duplication
                    if ($change > Blocktrail::DUST) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
729
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
730
                    } else {
731
                        // if change is dust we do nothing (implicitly it's added to the fee)
732
                        $change = 0;
733
                    }
734
                }
735
            }
736
        }
737
738
        $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
739
740
        return [$fee, $change];
741
    }
742
743
    /**
744
     * create, sign and send transction based on TransactionBuilder
745
     *
746
     * @param TransactionBuilder $txBuilder
747
     * @param bool $apiCheckFee     let the API check if the fee is correct
748
     * @return string
749
     * @throws \Exception
750
     */
751
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
752
        list($inputs, $outputs) = $this->buildTx($txBuilder);
753
754
        return $this->_sendTx($inputs, $outputs, $apiCheckFee);
755
    }
756
757
    /**
758
     * !! INTERNAL METHOD, public for testing purposes !!
759
     * create, sign and send transction based on inputs and outputs
760
     *
761
     * @param      $inputs
762
     * @param      $outputs
763
     * @param bool $apiCheckFee     let the API check if the fee is correct
764
     * @return string
765
     * @throws \Exception
766
     * @internal
767
     */
768
    public function _sendTx($inputs, $outputs, $apiCheckFee = true) {
769
        if ($this->locked) {
770
            throw new \Exception("Wallet needs to be unlocked to pay");
771
        }
772
773
        // create raw unsigned TX
774
        $raw_transaction = RawTransaction::create($inputs, $outputs);
775
776
        if (!$raw_transaction) {
777
            throw new \Exception("Failed to create raw transaction");
778
        }
779
780
        // sign the transaction with our keys
781
        $signed = $this->signTransaction($raw_transaction, $inputs);
782
783
        if (!$signed['sign_count']) {
784
            throw new \Exception("Failed to partially sign transaction");
785
        }
786
787
        // send the transaction
788
        $finished = $this->sendTransaction($signed['hex'], array_column($inputs, 'path'), $apiCheckFee);
789
790
        return $finished;
791
    }
792
793
    /**
794
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
795
     *
796
     * @param int $utxoCnt      number of unspent inputs in transaction
797
     * @param int $outputCnt    number of outputs in transaction
798
     * @return float
799
     * @access public           reminder that people might use this!
800
     */
801
    public static function estimateFee($utxoCnt, $outputCnt) {
802
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
803
804
        return self::baseFeeForSize($size);
805
    }
806
807
    /**
808
     * @param int $size     size in bytes
809
     * @return int          fee in satoshi
810
     */
811
    public static function baseFeeForSize($size) {
812
        $sizeKB = (int)ceil($size / 1000);
813
814
        return $sizeKB * self::BASE_FEE;
815
    }
816
817
    /**
818
     * @param int $txinSize
819
     * @param int $txoutSize
820
     * @return float
821
     */
822
    public static function estimateSize($txinSize, $txoutSize) {
823
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
824
    }
825
826
    /**
827
     * only supports estimating size for P2PKH/P2SH outputs
828
     *
829
     * @param int $outputCnt    number of outputs in transaction
830
     * @return float
831
     */
832
    public static function estimateSizeOutputs($outputCnt) {
833
        return ($outputCnt * 34);
834
    }
835
836
    /**
837
     * only supports estimating size for 2of3 multsig UTXOs
838
     *
839
     * @param int $utxoCnt      number of unspent inputs in transaction
840
     * @return float
841
     */
842
    public static function estimateSizeUTXOs($utxoCnt) {
843
        $txinSize = 0;
844
845
        for ($i=0; $i<$utxoCnt; $i++) {
846
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
847
            $multisig = "2of3";
848
849
            if ($multisig) {
850
                $sigCnt = 2;
851
                $msig = explode("of", $multisig);
852
                if (count($msig) == 2 && is_numeric($msig[0])) {
853
                    $sigCnt = $msig[0];
854
                }
855
856
                $txinSize += array_sum([
857
                    32, // txhash
858
                    4, // idx
859
                    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...
860
                    ((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...
861
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
862
                    4, // sequence
863
                ]);
864
            } else {
865
                $txinSize += array_sum([
866
                    32, // txhash
867
                    4, // idx
868
                    73, // sig
869
                    34, // script
870
                    4, // sequence
871
                ]);
872
            }
873
        }
874
875
        return $txinSize;
876
    }
877
878
    /**
879
     * determine how much fee is required based on the inputs and outputs
880
     *  this is an estimation, not a proper 100% correct calculation
881
     *
882
     * @param UTXO[]  $utxos
883
     * @param array[] $outputs
884
     * @param         $feeStrategy
885
     * @param         $optimalFeePerKB
886
     * @param         $lowPriorityFeePerKB
887
     * @return int
888
     * @throws BlocktrailSDKException
889
     */
890
    protected function determineFee($utxos, $outputs, $feeStrategy, $optimalFeePerKB, $lowPriorityFeePerKB) {
891
        $outputSize = 0;
892
        foreach ($outputs as $output) {
893
            if (isset($output['scriptPubKey'])) {
894
                $outputSize += strlen($output['scriptPubKey']) / 2; // asume HEX
895
            } else {
896
                $outputSize += 34;
897
            }
898
        }
899
900
        $size = self::estimateSize(self::estimateSizeUTXOs(count($utxos)), $outputSize);
901
902
        switch ($feeStrategy) {
903
            case self::FEE_STRATEGY_BASE_FEE:
904
                return self::baseFeeForSize($size);
905
906
            case self::FEE_STRATEGY_OPTIMAL:
907
                return (int)round(($size / 1000) * $optimalFeePerKB);
908
909
            case self::FEE_STRATEGY_LOW_PRIORITY:
910
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
911
912
            default:
913
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
914
        }
915
    }
916
917
    /**
918
     * determine how much change is left over based on the inputs and outputs and the fee
919
     *
920
     * @param UTXO[]    $utxos
921
     * @param array[]   $outputs
922
     * @param int       $fee
923
     * @return int
924
     */
925
    protected function determineChange($utxos, $outputs, $fee) {
926
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
927
            return $utxo->value;
928
        }, $utxos));
929
        $outputsTotal = array_sum(array_column($outputs, 'value'));
930
931
        return $inputsTotal - $outputsTotal - $fee;
932
    }
933
934
    /**
935
     * sign a raw transaction with the private keys that we have
936
     *
937
     * @param string    $raw_transaction
938
     * @param array[]   $inputs
939
     * @return array                        response from RawTransaction::sign
940
     * @throws \Exception
941
     */
942
    protected function signTransaction($raw_transaction, array $inputs) {
943
        $wallet = [];
944
        $keys = [];
945
        $redeemScripts = [];
946
947
        foreach ($inputs as $input) {
948
            $redeemScript = null;
0 ignored issues
show
Unused Code introduced by
$redeemScript 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...
949
            $key = null;
0 ignored issues
show
Unused Code introduced by
$key 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...
950
951
            if (isset($input['redeemScript'], $input['path'])) {
952
                $redeemScript = $input['redeemScript'];
953
                $path = BIP32Path::path($input['path'])->privatePath();
954
                $key = $this->primaryPrivateKey->buildKey($path);
955
                $address = $this->getAddressFromKey($key, $path);
956
957
                if ($address != $input['address']) {
958
                    throw new \Exception("Generated address does not match expected address!");
959
                }
960
            } else {
961
                throw new \Exception("No redeemScript/path for input");
962
            }
963
964
            if ($redeemScript && $key) {
965
                $keys[] = $key;
966
                $redeemScripts[] = $redeemScript;
967
            }
968
        }
969
970
        BIP32::bip32_keys_to_wallet($wallet, array_map(function (BIP32Key $key) {
971
            return $key->tuple();
972
        }, $keys));
973
        RawTransaction::redeem_scripts_to_wallet($wallet, $redeemScripts);
974
975
        return RawTransaction::sign($wallet, $raw_transaction, json_encode($inputs));
976
    }
977
978
    /**
979
     * send the transaction using the API
980
     *
981
     * @param string    $signed
982
     * @param string[]  $paths
983
     * @param bool      $checkFee
984
     * @return string           the complete raw transaction
985
     * @throws \Exception
986
     */
987
    protected function sendTransaction($signed, $paths, $checkFee = false) {
988
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
989
    }
990
991
    /**
992
     * use the API to get the best inputs to use based on the outputs
993
     *
994
     * @param array[]  $outputs
995
     * @param bool     $lockUTXO
996
     * @param bool     $allowZeroConf
997
     * @param string   $feeStrategy
998
     * @param null|int $forceFee
999
     * @return array
1000
     */
1001
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1002
        $result = $this->sdk->coinSelection($this->identifier, $outputs, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
1003
1004
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
1005
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
1006
        $this->feePerKBAge = time();
1007
1008
        return $result;
1009
    }
1010
1011
    public function getOptimalFeePerKB() {
1012
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
1013
            $this->updateFeePerKB();
1014
        }
1015
1016
        return $this->optimalFeePerKB;
1017
    }
1018
1019
    public function getLowPriorityFeePerKB() {
1020
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1021
            $this->updateFeePerKB();
1022
        }
1023
1024
        return $this->lowPriorityFeePerKB;
1025
    }
1026
1027
    public function updateFeePerKB() {
1028
        $result = $this->sdk->feePerKB();
1029
1030
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
1031
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
1032
1033
        $this->feePerKBAge = time();
1034
    }
1035
1036
    /**
1037
     * delete the wallet
1038
     *
1039
     * @param bool $force ignore warnings (such as non-zero balance)
1040
     * @return mixed
1041
     * @throws \Exception
1042
     */
1043
    public function deleteWallet($force = false) {
1044
        if ($this->locked) {
1045
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
1046
        }
1047
1048
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
1049
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
1050
    }
1051
1052
    /**
1053
     * create checksum to verify ownership of the master primary key
1054
     *
1055
     * @return string[]     [address, signature]
1056
     */
1057
    protected function createChecksumVerificationSignature() {
1058
        $import = BIP32::import($this->primaryPrivateKey->key());
1059
1060
        $public = $this->primaryPrivateKey->publicKey();
1061
        $address = BitcoinLib::public_key_to_address($public, $import['version']);
0 ignored issues
show
Security Bug introduced by
It seems like $public defined by $this->primaryPrivateKey->publicKey() on line 1060 can also be of type false; however, BitWasp\BitcoinLib\Bitco...public_key_to_address() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1062
1063
        return [$address, BitcoinLib::signMessage($address, $import)];
1064
    }
1065
1066
    /**
1067
     * setup a webhook for our wallet
1068
     *
1069
     * @param string    $url            URL to receive webhook events
1070
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1071
     * @return array
1072
     */
1073
    public function setupWebhook($url, $identifier = null) {
1074
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1075
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1076
    }
1077
1078
    /**
1079
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1080
     * @return mixed
1081
     */
1082
    public function deleteWebhook($identifier = null) {
1083
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1084
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1085
    }
1086
1087
    /**
1088
     * lock a specific unspent output
1089
     *
1090
     * @param     $txHash
1091
     * @param     $txIdx
1092
     * @param int $ttl
1093
     * @return bool
1094
     */
1095
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1096
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1097
    }
1098
1099
    /**
1100
     * unlock a specific unspent output
1101
     *
1102
     * @param     $txHash
1103
     * @param     $txIdx
1104
     * @return bool
1105
     */
1106
    public function unlockUTXO($txHash, $txIdx) {
1107
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1108
    }
1109
1110
    /**
1111
     * get all transactions for the wallet (paginated)
1112
     *
1113
     * @param  integer $page    pagination: page number
1114
     * @param  integer $limit   pagination: records per page (max 500)
1115
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1116
     * @return array            associative array containing the response
1117
     */
1118
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1119
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1120
    }
1121
1122
    /**
1123
     * get all addresses 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 addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1131
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1132
    }
1133
1134
    /**
1135
     * get all UTXOs 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 utxos($page = 1, $limit = 20, $sortDir = 'asc') {
1143
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir);
1144
    }
1145
}
1146