Completed
Pull Request — master (#84)
by thomas
22:17
created

Wallet::getLowPriorityFeePerKB()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.1406

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 0
dl 0
loc 7
ccs 3
cts 4
cp 0.75
crap 3.1406
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\Transaction\Factory\SignData;
13
use BitWasp\Bitcoin\Transaction\Factory\Signer;
14
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
15
use BitWasp\Bitcoin\Transaction\OutPoint;
16
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
17
use BitWasp\Bitcoin\Transaction\Transaction;
18
use BitWasp\Bitcoin\Transaction\TransactionInterface;
19
use BitWasp\Bitcoin\Transaction\TransactionOutput;
20
use BitWasp\Buffertools\Buffer;
21
use Blocktrail\SDK\Bitcoin\BIP32Key;
22
use Blocktrail\SDK\Bitcoin\BIP32Path;
23
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
24
25
/**
26
 * Class Wallet
27
 */
28
abstract class Wallet implements WalletInterface {
29
30
    const WALLET_VERSION_V1 = 'v1';
31
    const WALLET_VERSION_V2 = 'v2';
32
    const WALLET_VERSION_V3 = 'v3';
33
34
    const BASE_FEE = 10000;
35
36
    /**
37
     * development / debug setting
38
     *  when getting a new derivation from the API,
39
     *  will verify address / redeeemScript with the values the API provides
40
     */
41
    const VERIFY_NEW_DERIVATION = true;
42
43
    /**
44
     * @var BlocktrailSDKInterface
45
     */
46
    protected $sdk;
47
48
    /**
49
     * @var string
50
     */
51
    protected $identifier;
52
53
    /**
54
     * BIP32 master primary private key (m/)
55
     *
56
     * @var BIP32Key
57
     */
58
    protected $primaryPrivateKey;
59
60
    /**
61
     * @var BIP32Key[]
62
     */
63
    protected $primaryPublicKeys;
64
65
    /**
66
     * BIP32 master backup public key (M/)
67
68
     * @var BIP32Key
69
     */
70
    protected $backupPublicKey;
71
72
    /**
73
     * map of blocktrail BIP32 public keys
74
     *  keyed by key index
75
     *  path should be `M / key_index'`
76
     *
77
     * @var BIP32Key[]
78
     */
79
    protected $blocktrailPublicKeys;
80
81
    /**
82
     * the 'Blocktrail Key Index' that is used for new addresses
83
     *
84
     * @var int
85
     */
86
    protected $keyIndex;
87
88
    /**
89
     * 'bitcoin'
90
     *
91
     * @var string
92
     */
93
    protected $network;
94
95
    /**
96
     * testnet yes / no
97
     *
98
     * @var bool
99
     */
100
    protected $testnet;
101
102
    /**
103
     * cache of public keys, by path
104
     *
105
     * @var BIP32Key[]
106
     */
107
    protected $pubKeys = [];
108
109
    /**
110
     * cache of address / redeemScript, by path
111
     *
112
     * @var string[][]      [[address, redeemScript)], ]
113
     */
114
    protected $derivations = [];
115
116
    /**
117
     * reverse cache of paths by address
118
     *
119
     * @var string[]
120
     */
121
    protected $derivationsByAddress = [];
122
123
    /**
124
     * @var WalletPath
125
     */
126
    protected $walletPath;
127
128
    protected $checksum;
129
130
    protected $locked = true;
131
132
    protected $optimalFeePerKB;
133
    protected $lowPriorityFeePerKB;
134
    protected $feePerKBAge;
135
136
    /**
137
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
138
     * @param string                        $identifier                 identifier of the wallet
139
     * @param BIP32Key[]                    $primaryPublicKeys
140
     * @param BIP32Key                      $backupPublicKey            should be BIP32 master public key M/
141
     * @param BIP32Key[]                    $blocktrailPublicKeys
142
     * @param int                           $keyIndex
143
     * @param string                        $network
144
     * @param bool                          $testnet
145
     * @param string                        $checksum
146
     */
147 15
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, array $primaryPublicKeys, $backupPublicKey, array $blocktrailPublicKeys, $keyIndex, $network, $testnet, $checksum) {
148 15
        $this->sdk = $sdk;
149
150 15
        $this->identifier = $identifier;
151 15
        $this->backupPublicKey = BlocktrailSDK::normalizeBIP32Key($backupPublicKey);
152 15
        $this->primaryPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($primaryPublicKeys);
153 15
        $this->blocktrailPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($blocktrailPublicKeys);
154
155 15
        $this->network = $network;
156 15
        $this->testnet = $testnet;
157 15
        $this->keyIndex = $keyIndex;
158 15
        $this->checksum = $checksum;
159
160 15
        $this->walletPath = WalletPath::create($this->keyIndex);
161 15
    }
162
163
    /**
164
     * return the wallet identifier
165
     *
166
     * @return string
167
     */
168 10
    public function getIdentifier() {
169 10
        return $this->identifier;
170
    }
171
172
    /**
173
     * Returns the wallets backup public key
174
     *
175
     * @return [xpub, path]
0 ignored issues
show
Documentation introduced by
The doc-type xpub,">[xpub, could not be parsed: Unknown type name "[" at position 0. [(view supported doc-types)

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

Loading history...
176
     */
177 1
    public function getBackupKey() {
178 1
        return $this->backupPublicKey->tuple();
179
    }
180
181
    /**
182
     * return list of Blocktrail co-sign extended public keys
183
     *
184
     * @return array[]      [ [xpub, path] ]
185
     */
186 5
    public function getBlocktrailPublicKeys() {
187
        return array_map(function (BIP32Key $key) {
188 5
            return $key->tuple();
189 5
        }, $this->blocktrailPublicKeys);
190
    }
191
192
    /**
193
     * check if wallet is locked
194
     *
195
     * @return bool
196
     */
197 10
    public function isLocked() {
198 10
        return $this->locked;
199
    }
200
201
    /**
202
     * upgrade wallet to different blocktrail cosign key
203
     *
204
     * @param $keyIndex
205
     * @return bool
206
     * @throws \Exception
207
     */
208 5
    public function upgradeKeyIndex($keyIndex) {
209 5
        if ($this->locked) {
210 4
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
211
        }
212
213 5
        $walletPath = WalletPath::create($keyIndex);
214
215
        // do the upgrade to the new 'key_index'
216 5
        $primaryPublicKey = $this->primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath());
217
218
        // $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...
219 5
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
220
221 5
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
222
223 5
        $this->keyIndex = $keyIndex;
224 5
        $this->walletPath = $walletPath;
225
226
        // update the blocktrail public keys
227 5
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
228 5
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
229 5
                $path = $pubKey[1];
230 5
                $pubKey = $pubKey[0];
231 5
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKey), $path);
232
            }
233
        }
234
235 5
        return true;
236
    }
237
238
    /**
239
     * get a new BIP32 derivation for the next (unused) address
240
     *  by requesting it from the API
241
     *
242
     * @return string
243
     * @throws \Exception
244
     */
245 10
    protected function getNewDerivation() {
246 10
        $path = $this->walletPath->path()->last("*");
247
248 10
        if (self::VERIFY_NEW_DERIVATION) {
249 10
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
250
251 10
            $path = $new['path'];
252 10
            $address = $new['address'];
253 10
            $redeemScript = $new['redeem_script'];
254
255
            /** @var ScriptInterface $checkRedeemScript */
256 10
            list($checkAddress, $checkRedeemScript) = $this->getRedeemScriptByPath($path);
257
258 10
            if ($checkAddress != $address) {
259
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
260
            }
261
262 10
            if ($checkRedeemScript->getHex() != $redeemScript) {
263 10
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript->getHex()}]");
264
            }
265
        } else {
266
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
267
        }
268
269 10
        return (string)$path;
270
    }
271
272
    /**
273
     * @param string|BIP32Path  $path
274
     * @return BIP32Key|false
275
     * @throws \Exception
276
     *
277
     * @TODO: hmm?
278
     */
279 10
    protected function getParentPublicKey($path) {
280 10
        $path = BIP32Path::path($path)->parent()->publicPath();
281
282 10
        if ($path->count() <= 2) {
283
            return false;
284
        }
285
286 10
        if ($path->isHardened()) {
287
            return false;
288
        }
289
290 10
        if (!isset($this->pubKeys[(string)$path])) {
291 10
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
292
        }
293
294 10
        return $this->pubKeys[(string)$path];
295
    }
296
297
    /**
298
     * get address for the specified path
299
     *
300
     * @param string|BIP32Path  $path
301
     * @return string
302
     */
303 10
    public function getAddressByPath($path) {
304 10
        $path = (string)BIP32Path::path($path)->privatePath();
305 10
        if (!isset($this->derivations[$path])) {
306 10
            list($address, ) = $this->getRedeemScriptByPath($path);
307
308 10
            $this->derivations[$path] = $address;
309 10
            $this->derivationsByAddress[$address] = $path;
310
        }
311
312 10
        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 312 which is incompatible with the return type declared by the interface Blocktrail\SDK\WalletInterface::getAddressByPath of type string.
Loading history...
313
    }
314
315
    /**
316
     * get address and redeemScript for specified path
317
     *
318
     * @param string    $path
319
     * @return array[string, ScriptInterface]     [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...
320
     */
321 10
    public function getRedeemScriptByPath($path) {
322 10
        $path = BIP32Path::path($path);
323
324
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
325 10
        if ($pubKey = $this->getParentPublicKey($path)) {
326 10
            $key = $pubKey->buildKey($path->publicPath());
327
        } else {
328
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
329
        }
330
331 10
        return $this->getRedeemScriptFromKey($key, $path);
332
    }
333
334
    /**
335
     * @param BIP32Key          $key
336
     * @param string|BIP32Path  $path
337
     * @return string
338
     */
339
    protected function getAddressFromKey(BIP32Key $key, $path) {
340
        return $this->getRedeemScriptFromKey($key, $path)[0];
341
    }
342
343
    /**
344
     * @param BIP32Key          $key
345
     * @param string|BIP32Path  $path
346
     * @return array[string, ScriptInterface]                 [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...
347
     * @throws \Exception
348
     */
349 10
    protected function getRedeemScriptFromKey(BIP32Key $key, $path) {
350 10
        $path = BIP32Path::path($path)->publicPath();
351
352 10
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
353
354 10
        $redeemScript = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
355 10
            $key->buildKey($path)->publicKey(),
356 10
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
357 10
            $blocktrailPublicKey->buildKey($path)->publicKey()
358 10
        ]), false);
359
360 10
        return [(new P2shScript($redeemScript))->getAddress()->getAddress(), $redeemScript];
361
    }
362
363
    /**
364
     * get the path (and redeemScript) to specified address
365
     *
366
     * @param string $address
367
     * @return array
368
     */
369
    public function getPathForAddress($address) {
370
        return $this->sdk->getPathForAddress($this->identifier, $address);
371
    }
372
373
    /**
374
     * @param string|BIP32Path  $path
375
     * @return BIP32Key
376
     * @throws \Exception
377
     */
378 10
    public function getBlocktrailPublicKey($path) {
379 10
        $path = BIP32Path::path($path);
380
381 10
        $keyIndex = str_replace("'", "", $path[1]);
382
383 10
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
384
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
385
        }
386
387 10
        return $this->blocktrailPublicKeys[$keyIndex];
388
    }
389
390
    /**
391
     * generate a new derived key and return the new path and address for it
392
     *
393
     * @return string[]     [path, address]
394
     */
395 10
    public function getNewAddressPair() {
396 10
        $path = $this->getNewDerivation();
397 10
        $address = $this->getAddressByPath($path);
398
399 10
        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...
400
    }
401
402
    /**
403
     * generate a new derived private key and return the new address for it
404
     *
405
     * @return string
406
     */
407 7
    public function getNewAddress() {
408 7
        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 408 which is incompatible with the return type declared by the interface Blocktrail\SDK\WalletInterface::getNewAddress of type string.
Loading history...
409
    }
410
411
    /**
412
     * get the balance for the wallet
413
     *
414
     * @return int[]            [confirmed, unconfirmed]
415
     */
416 9
    public function getBalance() {
417 9
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
418
419 9
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
420
    }
421
422
    /**
423
     * do wallet discovery (slow)
424
     *
425
     * @param int   $gap        the gap setting to use for discovery
426
     * @return int[]            [confirmed, unconfirmed]
427
     */
428 2
    public function doDiscovery($gap = 200) {
429 2
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
430
431 2
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
432
    }
433
434
    /**
435
     * create, sign and send a transaction
436
     *
437
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
438
     *                                      value should be INT
439
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
440
     * @param bool     $allowZeroConf
441
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
442
     * @param string   $feeStrategy
443
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
444
     * @return string the txid / transaction hash
445
     * @throws \Exception
446
     */
447 8
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
448 8
        if ($this->locked) {
449 4
            throw new \Exception("Wallet needs to be unlocked to pay");
450
        }
451
452 8
        $outputs = self::normalizeOutputsStruct($outputs);
453
454 8
        $txBuilder = new TransactionBuilder();
455 8
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
456 8
        $txBuilder->setFeeStrategy($feeStrategy);
457 8
        $txBuilder->setChangeAddress($changeAddress);
458
459 8
        foreach ($outputs as $output) {
460 8
            $txBuilder->addRecipient($output['address'], $output['value']);
461
        }
462
463 8
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
464
465 2
        $apiCheckFee = $forceFee === null;
466
467 2
        return $this->sendTx($txBuilder, $apiCheckFee);
468
    }
469
470
    /**
471
     * determine max spendable from wallet after fees
472
     *
473
     * @param bool     $allowZeroConf
474
     * @param string   $feeStrategy
475
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
476
     * @param int      $outputCnt
477
     * @return string
478
     * @throws BlocktrailSDKException
479
     */
480
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
481
        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...
482
    }
483
484
    /**
485
     * parse outputs into normalized struct
486
     *
487
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
488
     * @return array            [['address' => address, 'value' => value], ]
489
     */
490 9
    public static function normalizeOutputsStruct(array $outputs) {
491 9
        $result = [];
492
493 9
        foreach ($outputs as $k => $v) {
494 9
            if (is_numeric($k)) {
495 1
                if (!is_array($v)) {
496
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
497
                }
498
499 1
                if (isset($v['address']) && isset($v['value'])) {
500 1
                    $address = $v['address'];
501 1
                    $value = $v['value'];
502 1
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
503 1
                    $address = $v[0];
504 1
                    $value = $v[1];
505
                } else {
506 1
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
507
                }
508
            } else {
509 9
                $address = $k;
510 9
                $value = $v;
511
            }
512
513 9
            $result[] = ['address' => $address, 'value' => $value];
514
        }
515
516 9
        return $result;
517
    }
518
519
    /**
520
     * 'fund' the txBuilder with UTXOs (modified in place)
521
     *
522
     * @param TransactionBuilder    $txBuilder
523
     * @param bool|true             $lockUTXOs
524
     * @param bool|false            $allowZeroConf
525
     * @param null|int              $forceFee
526
     * @return TransactionBuilder
527
     */
528 8
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
529
        // get the data we should use for this transaction
530 8
        $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 528 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...
531 2
        $utxos = $coinSelection['utxos'];
532 2
        $fee = $coinSelection['fee'];
533 2
        $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...
534
535 2
        if ($forceFee !== null) {
536 1
            $txBuilder->setFee($forceFee);
537
        } else {
538 2
            $txBuilder->validateFee($fee);
539
        }
540
541 2
        foreach ($utxos as $utxo) {
542 2
            $scriptPubKey = ScriptFactory::fromHex($utxo['scriptpubkey_hex']);
543 2
            $redeemScript = ScriptFactory::fromHex($utxo['redeem_script']);
544 2
            $address = AddressFactory::fromString($utxo['address']);
545 2
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $address, $scriptPubKey, $utxo['path'], $redeemScript);
546
        }
547
548 2
        return $txBuilder;
549
    }
550
551
    /**
552
     * build inputs and outputs lists for TransactionBuilder
553
     *
554
     * @param TransactionBuilder $txBuilder
555
     * @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...
556
     * @throws \Exception
557
     */
558 3
    public function buildTx(TransactionBuilder $txBuilder) {
559 3
        $send = $txBuilder->getOutputs();
560 3
        $utxos = $txBuilder->getUtxos();
561 3
        $signInfo = [];
562
563 3
        $txb = new TxBuilder();
564
565 3
        foreach ($utxos as $utxo) {
566 3
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
567
                $tx = $this->sdk->transaction($utxo->hash);
568
569
                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...
570
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
571
                }
572
573
                $output = $tx['outputs'][$utxo->index];
574
575
                if (!$utxo->address) {
576
                    $utxo->address = AddressFactory::fromString($output['address']);
577
                }
578
                if (!$utxo->value) {
579
                    $utxo->value = $output['value'];
580
                }
581
                if (!$utxo->scriptPubKey) {
582
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
583
                }
584
            }
585
586 3
            if (!$utxo->path) {
587
                $utxo->path = $this->getPathForAddress($utxo->address->getAddress());
588
            }
589
590 3
            if (!$utxo->redeemScript) {
591
                list(, $redeemScript) = $this->getRedeemScriptByPath($utxo->path);
592
                $utxo->redeemScript = $redeemScript;
593
            }
594
595 3
            $signInfo[] = new SignInfo($utxo->path, $utxo->redeemScript, new TransactionOutput($utxo->value, $utxo->scriptPubKey));
596
        }
597
598
        $utxoSum = array_sum(array_map(function (UTXO $utxo) {
599 3
            return $utxo->value;
600 3
        }, $utxos));
601 3
        if ($utxoSum < array_sum(array_column($send, 'value'))) {
602 1
            throw new \Exception("Atempting to spend more than sum of UTXOs");
603
        }
604
605 3
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
606
607 3
        if ($txBuilder->getValidateFee() !== null) {
608 2
            if (abs($txBuilder->getValidateFee() - $fee) > Wallet::BASE_FEE) {
609
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
610
            }
611
        }
612
613 3
        if ($change > 0) {
614 3
            $send[] = [
615 3
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewAddress(),
616 3
                'value' => $change
617
            ];
618
        }
619
620 3
        foreach ($utxos as $utxo) {
621 3
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index), $utxo->scriptPubKey);
622
        }
623
624
        // outputs should be randomized to make the change harder to detect
625 3
        if ($txBuilder->shouldRandomizeChangeOuput()) {
626 3
            shuffle($send);
627
        }
628
629 3
        foreach ($send as $out) {
630 3
            assert(isset($out['value']));
631
632 3
            if (isset($out['scriptPubKey'])) {
633 1
                $txb->output($out['value'], $out['scriptPubKey']);
634 3
            } elseif (isset($out['address'])) {
635 3
                $txb->payToAddress($out['value'], AddressFactory::fromString($out['address']));
636
            } else {
637 3
                throw new \Exception();
638
            }
639
        }
640
641 3
        return [$txb->get(), $signInfo];
642
    }
643
644 3
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) {
645 3
        $send = $txBuilder->getOutputs();
646 3
        $utxos = $txBuilder->getUtxos();
647
648 3
        $fee = $txBuilder->getFee();
649 3
        $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...
650
651
        // if the fee is fixed we just need to calculate the change
652 3
        if ($fee !== null) {
653 2
            $change = $this->determineChange($utxos, $send, $fee);
654
655
            // if change is not dust we need to add a change output
656 2
            if ($change > Blocktrail::DUST) {
657 1
                $send[] = ['address' => 'change', 'value' => $change];
658
            } else {
659
                // if change is dust we do nothing (implicitly it's added to the fee)
660 2
                $change = 0;
661
            }
662
        } else {
663 3
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
664
665 3
            $change = $this->determineChange($utxos, $send, $fee);
666
667 3
            if ($change > 0) {
668 3
                $changeIdx = count($send);
669
                // set dummy change output
670 3
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
671
672
                // recaculate fee now that we know that we have a change output
673 3
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
674
675
                // unset dummy change output
676 3
                unset($send[$changeIdx]);
677
678
                // if adding the change output made the fee bump up and the change is smaller than the fee
679
                //  then we're not doing change
680 3
                if ($fee2 > $fee && $fee2 > $change) {
681 1
                    $change = 0;
682
                } else {
683 3
                    $change = $this->determineChange($utxos, $send, $fee2);
684
685
                    // if change is not dust we need to add a change output
686 3
                    if ($change > Blocktrail::DUST) {
687 3
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
688
                    } else {
689
                        // if change is dust we do nothing (implicitly it's added to the fee)
690
                        $change = 0;
691
                    }
692
                }
693
            }
694
        }
695
696 3
        $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
697
698 3
        return [$fee, $change];
699
    }
700
701
    /**
702
     * create, sign and send transction based on TransactionBuilder
703
     *
704
     * @param TransactionBuilder $txBuilder
705
     * @param bool $apiCheckFee     let the API check if the fee is correct
706
     * @return string
707
     * @throws \Exception
708
     */
709 2
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
710 2
        list($tx, $signInfo) = $this->buildTx($txBuilder);
711
712 2
        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...
713
    }
714
715
    /**
716
     * !! INTERNAL METHOD, public for testing purposes !!
717
     * create, sign and send transction based on inputs and outputs
718
     *
719
     * @param Transaction $tx
720
     * @param SignInfo[]  $signInfo
721
     * @param bool $apiCheckFee     let the API check if the fee is correct
722
     * @return string
723
     * @throws \Exception
724
     * @internal
725
     */
726 2
    public function _sendTx(Transaction $tx, array $signInfo, $apiCheckFee = true) {
727 2
        if ($this->locked) {
728
            throw new \Exception("Wallet needs to be unlocked to pay");
729
        }
730
731
        assert(Util::all(function ($signInfo) {
732 2
            return $signInfo instanceof SignInfo;
733 2
        }, $signInfo), '$signInfo should be SignInfo[]');
734
735
        // sign the transaction with our keys
736 2
        $signed = $this->signTransaction($tx, $signInfo);
737
738
        // send the transaction
739
        $finished = $this->sendTransaction($signed->getHex(), array_map(function (SignInfo $r) {
740 2
            return $r->path;
741 2
        }, $signInfo), $apiCheckFee);
742
743 2
        return $finished;
744
    }
745
746
    /**
747
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
748
     *
749
     * @param int $utxoCnt      number of unspent inputs in transaction
750
     * @param int $outputCnt    number of outputs in transaction
751
     * @return float
752
     * @access public           reminder that people might use this!
753
     */
754 1
    public static function estimateFee($utxoCnt, $outputCnt) {
755 1
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
756
757 1
        return self::baseFeeForSize($size);
758
    }
759
760
    /**
761
     * @param int $size     size in bytes
762
     * @return int          fee in satoshi
763
     */
764 3
    public static function baseFeeForSize($size) {
765 3
        $sizeKB = (int)ceil($size / 1000);
766
767 3
        return $sizeKB * self::BASE_FEE;
768
    }
769
770
    /**
771
     * @param int $txinSize
772
     * @param int $txoutSize
773
     * @return float
774
     */
775 5
    public static function estimateSize($txinSize, $txoutSize) {
776 5
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
777
    }
778
779
    /**
780
     * only supports estimating size for P2PKH/P2SH outputs
781
     *
782
     * @param int $outputCnt    number of outputs in transaction
783
     * @return float
784
     */
785 2
    public static function estimateSizeOutputs($outputCnt) {
786 2
        return ($outputCnt * 34);
787
    }
788
789
    /**
790
     * only supports estimating size for 2of3 multsig UTXOs
791
     *
792
     * @param int $utxoCnt      number of unspent inputs in transaction
793
     * @return float
794
     */
795 5
    public static function estimateSizeUTXOs($utxoCnt) {
796 5
        $txinSize = 0;
797
798 5
        for ($i=0; $i<$utxoCnt; $i++) {
799
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
800 5
            $multisig = "2of3";
801
802 5
            if ($multisig) {
803 5
                $sigCnt = 2;
804 5
                $msig = explode("of", $multisig);
805 5
                if (count($msig) == 2 && is_numeric($msig[0])) {
806 5
                    $sigCnt = $msig[0];
807
                }
808
809 5
                $txinSize += array_sum([
810 5
                    32, // txhash
811 5
                    4, // idx
812 5
                    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...
813 5
                    ((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...
814
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
815
                    1, // OP_0
816 5
                    4, // sequence
817
                ]);
818
            } else {
819
                $txinSize += array_sum([
820
                    32, // txhash
821
                    4, // idx
822
                    73, // sig
823
                    34, // script
824
                    4, // sequence
825
                ]);
826
            }
827
        }
828
829 5
        return $txinSize;
830
    }
831
832
    /**
833
     * determine how much fee is required based on the inputs and outputs
834
     *  this is an estimation, not a proper 100% correct calculation
835
     *
836
     * @param UTXO[]  $utxos
837
     * @param array[] $outputs
838
     * @param         $feeStrategy
839
     * @param         $optimalFeePerKB
840
     * @param         $lowPriorityFeePerKB
841
     * @return int
842
     * @throws BlocktrailSDKException
843
     */
844 3
    protected function determineFee($utxos, $outputs, $feeStrategy, $optimalFeePerKB, $lowPriorityFeePerKB) {
845 3
        $outputSize = 0;
846 3
        foreach ($outputs as $output) {
847 3
            if (isset($output['scriptPubKey'])) {
848 1
                if ($output['scriptPubKey'] instanceof ScriptInterface) {
849 1
                    $outputSize += $output['scriptPubKey']->getBuffer()->getSize();
850
                } else {
851 1
                    $outputSize += strlen($output['scriptPubKey']) / 2; // asume HEX
852
                }
853
            } else {
854 3
                $outputSize += 34;
855
            }
856
        }
857
858 3
        $size = self::estimateSize(self::estimateSizeUTXOs(count($utxos)), $outputSize);
859
860
        switch ($feeStrategy) {
861 3
            case self::FEE_STRATEGY_BASE_FEE:
862 2
                return self::baseFeeForSize($size);
863
864 2
            case self::FEE_STRATEGY_OPTIMAL:
865 2
                return (int)round(($size / 1000) * $optimalFeePerKB);
866
867 1
            case self::FEE_STRATEGY_LOW_PRIORITY:
868 1
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
869
870
            default:
871
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
872
        }
873
    }
874
875
    /**
876
     * determine how much change is left over based on the inputs and outputs and the fee
877
     *
878
     * @param UTXO[]    $utxos
879
     * @param array[]   $outputs
880
     * @param int       $fee
881
     * @return int
882
     */
883 3
    protected function determineChange($utxos, $outputs, $fee) {
884
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
885 3
            return $utxo->value;
886 3
        }, $utxos));
887 3
        $outputsTotal = array_sum(array_column($outputs, 'value'));
888
889 3
        return $inputsTotal - $outputsTotal - $fee;
890
    }
891
892
    /**
893
     * sign a raw transaction with the private keys that we have
894
     *
895
     * @param Transaction $tx
896
     * @param SignInfo[]  $signInfo
897
     * @return TransactionInterface
898
     * @throws \Exception
899
     */
900 2
    protected function signTransaction(Transaction $tx, array $signInfo) {
901 2
        $signer = new Signer($tx, Bitcoin::getEcAdapter());
902
903 2
        assert(Util::all(function ($signInfo) {
904 2
            return $signInfo instanceof SignInfo;
905 2
        }, $signInfo), '$signInfo should be SignInfo[]');
906
907 2
        $sigHash = SigHash::ALL;
908 2
        if ($this->network === "bitcoincash") {
909
            $sigHash |= SigHash::BITCOINCASH;
910
            $signer->redeemBitcoinCash(true);
911
        }
912
913 2
        foreach ($signInfo as $idx => $info) {
914 2
            $path = BIP32Path::path($info->path)->privatePath();
915 2
            $redeemScript = $info->redeemScript;
916 2
            $output = $info->output;
917
918 2
            $key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey();
919
920 2
            $input = $signer->input($idx, $output, (new SignData())->p2sh($redeemScript));
921 2
            $input->sign($key, $sigHash);
922
        }
923
924 2
        return $signer->get();
925
    }
926
927
    /**
928
     * send the transaction using the API
929
     *
930
     * @param string    $signed
931
     * @param string[]  $paths
932
     * @param bool      $checkFee
933
     * @return string           the complete raw transaction
934
     * @throws \Exception
935
     */
936 2
    protected function sendTransaction($signed, $paths, $checkFee = false) {
937 2
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
938
    }
939
940
    /**
941
     * @param \array[] $outputs
942
     * @param bool $lockUTXO
943
     * @param bool $allowZeroConf
944
     * @param int|null|string $feeStrategy
945
     * @param null $forceFee
946
     * @return array
947
     */
948 8
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
949 8
        $result = $this->sdk->coinSelection($this->identifier, $outputs, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
950
951 2
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
952 2
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
953 2
        $this->feePerKBAge = time();
954
955 2
        return $result;
956
    }
957
958 3
    public function getOptimalFeePerKB() {
959 3
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
960 1
            $this->updateFeePerKB();
961
        }
962
963 3
        return $this->optimalFeePerKB;
964
    }
965
966 3
    public function getLowPriorityFeePerKB() {
967 3
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
968
            $this->updateFeePerKB();
969
        }
970
971 3
        return $this->lowPriorityFeePerKB;
972
    }
973
974 1
    public function updateFeePerKB() {
975 1
        $result = $this->sdk->feePerKB();
976
977 1
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
978 1
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
979
980 1
        $this->feePerKBAge = time();
981 1
    }
982
983
    /**
984
     * delete the wallet
985
     *
986
     * @param bool $force ignore warnings (such as non-zero balance)
987
     * @return mixed
988
     * @throws \Exception
989
     */
990 10
    public function deleteWallet($force = false) {
991 10
        if ($this->locked) {
992
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
993
        }
994
995 10
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
996 10
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
997
    }
998
999
    /**
1000
     * create checksum to verify ownership of the master primary key
1001
     *
1002
     * @return string[]     [address, signature]
1003
     */
1004 10
    protected function createChecksumVerificationSignature() {
1005 10
        $privKey = $this->primaryPrivateKey->key();
1006
1007 10
        $pubKey = $this->primaryPrivateKey->publicKey();
1008 10
        $address = $pubKey->getAddress()->getAddress();
1009
1010 10
        $signer = new MessageSigner(Bitcoin::getEcAdapter());
1011 10
        $signed = $signer->sign($address, $privKey->getPrivateKey());
1012
1013 10
        return [$address, base64_encode($signed->getCompactSignature()->getBuffer()->getBinary())];
1014
    }
1015
1016
    /**
1017
     * setup a webhook for our wallet
1018
     *
1019
     * @param string    $url            URL to receive webhook events
1020
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1021
     * @return array
1022
     */
1023 1
    public function setupWebhook($url, $identifier = null) {
1024 1
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1025 1
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1026
    }
1027
1028
    /**
1029
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1030
     * @return mixed
1031
     */
1032 1
    public function deleteWebhook($identifier = null) {
1033 1
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1034 1
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1035
    }
1036
1037
    /**
1038
     * lock a specific unspent output
1039
     *
1040
     * @param     $txHash
1041
     * @param     $txIdx
1042
     * @param int $ttl
1043
     * @return bool
1044
     */
1045
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1046
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1047
    }
1048
1049
    /**
1050
     * unlock a specific unspent output
1051
     *
1052
     * @param     $txHash
1053
     * @param     $txIdx
1054
     * @return bool
1055
     */
1056
    public function unlockUTXO($txHash, $txIdx) {
1057
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1058
    }
1059
1060
    /**
1061
     * get all transactions for the wallet (paginated)
1062
     *
1063
     * @param  integer $page    pagination: page number
1064
     * @param  integer $limit   pagination: records per page (max 500)
1065
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1066
     * @return array            associative array containing the response
1067
     */
1068 1
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1069 1
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1070
    }
1071
1072
    /**
1073
     * get all addresses for the wallet (paginated)
1074
     *
1075
     * @param  integer $page    pagination: page number
1076
     * @param  integer $limit   pagination: records per page (max 500)
1077
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1078
     * @return array            associative array containing the response
1079
     */
1080 1
    public function addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1081 1
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1082
    }
1083
1084
    /**
1085
     * get all UTXOs for the wallet (paginated)
1086
     *
1087
     * @param  integer $page        pagination: page number
1088
     * @param  integer $limit       pagination: records per page (max 500)
1089
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1090
     * @param  boolean $zeroconf    include zero confirmation transactions
1091
     * @return array                associative array containing the response
1092
     */
1093 1
    public function utxos($page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1094 1
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir, $zeroconf);
1095
    }
1096
}
1097