Completed
Branch master (7ca1f6)
by
unknown
02:09
created

Wallet::getRedeemScriptFromKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 2
dl 0
loc 13
ccs 9
cts 9
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\AddressFactory;
6
use BitWasp\Bitcoin\Bitcoin;
7
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
8
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
9
use BitWasp\Bitcoin\Script\P2shScript;
10
use BitWasp\Bitcoin\Script\ScriptFactory;
11
use BitWasp\Bitcoin\Script\ScriptInterface;
12
use BitWasp\Bitcoin\Script\WitnessScript;
13
use BitWasp\Bitcoin\Transaction\Factory\SignData;
14
use BitWasp\Bitcoin\Transaction\Factory\Signer;
15
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
16
use BitWasp\Bitcoin\Transaction\OutPoint;
17
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
18
use BitWasp\Bitcoin\Transaction\Transaction;
19
use BitWasp\Bitcoin\Transaction\TransactionInterface;
20
use BitWasp\Bitcoin\Transaction\TransactionOutput;
21
use BitWasp\Buffertools\Buffer;
22
use Blocktrail\SDK\Bitcoin\BIP32Key;
23
use Blocktrail\SDK\Bitcoin\BIP32Path;
24
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
25
26
/**
27
 * Class Wallet
28
 */
29
abstract class Wallet implements WalletInterface {
30
31
    const WALLET_VERSION_V1 = 'v1';
32
    const WALLET_VERSION_V2 = 'v2';
33
    const WALLET_VERSION_V3 = 'v3';
34
35
    const CHAIN_BTC_DEFAULT = 0;
36
    const CHAIN_BCC_DEFAULT = 1;
37
    const CHAIN_BTC_SEGWIT = 2;
38
39
    const BASE_FEE = 10000;
40
41
    /**
42
     * development / debug setting
43
     *  when getting a new derivation from the API,
44
     *  will verify address / redeeemScript with the values the API provides
45
     */
46
    const VERIFY_NEW_DERIVATION = true;
47
48
    /**
49
     * @var BlocktrailSDKInterface
50
     */
51
    protected $sdk;
52
53
    /**
54
     * @var string
55
     */
56
    protected $identifier;
57
58
    /**
59
     * BIP32 master primary private key (m/)
60
     *
61
     * @var BIP32Key
62
     */
63
    protected $primaryPrivateKey;
64
65
    /**
66
     * @var BIP32Key[]
67
     */
68
    protected $primaryPublicKeys;
69
70
    /**
71
     * BIP32 master backup public key (M/)
72
73
     * @var BIP32Key
74
     */
75
    protected $backupPublicKey;
76
77
    /**
78
     * map of blocktrail BIP32 public keys
79
     *  keyed by key index
80
     *  path should be `M / key_index'`
81
     *
82
     * @var BIP32Key[]
83
     */
84
    protected $blocktrailPublicKeys;
85
86
    /**
87
     * the 'Blocktrail Key Index' that is used for new addresses
88
     *
89
     * @var int
90
     */
91
    protected $keyIndex;
92
93
    /**
94
     * 'bitcoin'
95
     *
96
     * @var string
97
     */
98
    protected $network;
99
100
    /**
101
     * testnet yes / no
102
     *
103
     * @var bool
104
     */
105
    protected $testnet;
106
107
    /**
108
     * cache of public keys, by path
109
     *
110
     * @var BIP32Key[]
111
     */
112
    protected $pubKeys = [];
113
114
    /**
115
     * cache of address / redeemScript, by path
116
     *
117
     * @var string[][]      [[address, redeemScript)], ]
118
     */
119
    protected $derivations = [];
120
121
    /**
122
     * reverse cache of paths by address
123
     *
124
     * @var string[]
125
     */
126
    protected $derivationsByAddress = [];
127
128
    /**
129
     * @var WalletPath
130
     */
131
    protected $walletPath;
132
133
    protected $checksum;
134
135
    protected $locked = true;
136
137
    protected $optimalFeePerKB;
138
    protected $lowPriorityFeePerKB;
139
    protected $feePerKBAge;
140
    protected $allowedSignModes = [SignInfo::MODE_DONTSIGN, SignInfo::MODE_SIGN];
141
142
    /**
143
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
144
     * @param string                        $identifier                 identifier of the wallet
145
     * @param BIP32Key[]                    $primaryPublicKeys
146
     * @param BIP32Key                      $backupPublicKey            should be BIP32 master public key M/
147
     * @param BIP32Key[]                    $blocktrailPublicKeys
148
     * @param int                           $keyIndex
149
     * @param string                        $network
150
     * @param bool                          $testnet
151
     * @param bool                          $segwit
152
     * @param string                        $checksum
153
     * @throws BlocktrailSDKException
154
     */
155 19
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, array $primaryPublicKeys, $backupPublicKey, array $blocktrailPublicKeys, $keyIndex, $network, $testnet, $segwit, $checksum) {
156 19
        $this->sdk = $sdk;
157
158 19
        $this->identifier = $identifier;
159 19
        $this->backupPublicKey = BlocktrailSDK::normalizeBIP32Key($backupPublicKey);
160 19
        $this->primaryPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($primaryPublicKeys);
161
        ;
162 19
        $this->blocktrailPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($blocktrailPublicKeys);
163
164 19
        $this->network = $network;
165 19
        $this->testnet = $testnet;
166 19
        $this->keyIndex = $keyIndex;
167 19
        $this->checksum = $checksum;
168
169 19
        if ($network === "bitcoin") {
170 19
            if ($segwit) {
171 2
                $chainIdx = self::CHAIN_BTC_SEGWIT;
172
            } else {
173 19
                $chainIdx = self::CHAIN_BTC_DEFAULT;
174
            }
175
        } else {
176
            if ($segwit && $network === "bitcoincash") {
177
                throw new BlocktrailSDKException("Received segwit flag for bitcoincash - abort");
178
            }
179
            $chainIdx = self::CHAIN_BCC_DEFAULT;
180
        }
181
182 19
        $this->walletPath = WalletPath::create($this->keyIndex, $chainIdx);
183 19
    }
184
185
    /**
186
     * Returns the current chain index, usually
187
     * indicating what type of scripts to derive.
188
     * @return int
189
     */
190 3
    public function getChainIndex() {
191 3
        return $this->walletPath->path()[2];
192
    }
193
194
    /**
195
     * @return bool
196
     */
197 3
    public function isSegwit() {
198 3
        return $this->getChainIndex() == self::CHAIN_BTC_SEGWIT;
199
    }
200
201
    /**
202
     * return the wallet identifier
203
     *
204
     * @return string
205
     */
206 10
    public function getIdentifier() {
207 10
        return $this->identifier;
208
    }
209
210
    /**
211
     * Returns the wallets backup public key
212
     *
213
     * @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...
214
     */
215 1
    public function getBackupKey() {
216 1
        return $this->backupPublicKey->tuple();
217
    }
218
219
    /**
220
     * return list of Blocktrail co-sign extended public keys
221
     *
222
     * @return array[]      [ [xpub, path] ]
223
     */
224 5
    public function getBlocktrailPublicKeys() {
225
        return array_map(function (BIP32Key $key) {
226 5
            return $key->tuple();
227 5
        }, $this->blocktrailPublicKeys);
228
    }
229
230
    /**
231
     * check if wallet is locked
232
     *
233
     * @return bool
234
     */
235 10
    public function isLocked() {
236 10
        return $this->locked;
237
    }
238
239
    /**
240
     * upgrade wallet to different blocktrail cosign key
241
     *
242
     * @param $keyIndex
243
     * @return bool
244
     * @throws \Exception
245
     */
246 5
    public function upgradeKeyIndex($keyIndex) {
247 5
        if ($this->locked) {
248 4
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
249
        }
250
251 5
        $walletPath = WalletPath::create($keyIndex);
252
253
        // do the upgrade to the new 'key_index'
254 5
        $primaryPublicKey = $this->primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath());
255
256
        // $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...
257 5
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
258
259 5
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
260
261 5
        $this->keyIndex = $keyIndex;
262 5
        $this->walletPath = $walletPath;
263
264
        // update the blocktrail public keys
265 5
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
266 5
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
267 5
                $path = $pubKey[1];
268 5
                $pubKey = $pubKey[0];
269 5
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKey), $path);
270
            }
271
        }
272
273 5
        return true;
274
    }
275
276
    /**
277
     * get a new BIP32 derivation for the next (unused) address
278
     *  by requesting it from the API
279
     *
280
     * @return string
281
     * @throws \Exception
282
     */
283 13
    protected function getNewDerivation() {
284 13
        $path = $this->walletPath->path()->last("*");
285 13
        if (self::VERIFY_NEW_DERIVATION) {
286 13
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
287
288 13
            $path = $new['path'];
289 13
            $address = $new['address'];
290 13
            $redeemScript = $new['redeem_script'];
291 13
            $witnessScript = array_key_exists('witness_script', $new) ? $new['witness_script'] : null;
292
293
            /** @var ScriptInterface $checkRedeemScript */
294
            /** @var ScriptInterface $checkWitnessScript */
295 13
            list($checkAddress, $checkRedeemScript, $checkWitnessScript) = $this->getRedeemScriptByPath($path);
296 13
            if ($checkAddress != $address) {
297
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
298
            }
299
300 13
            if ($checkRedeemScript && $checkRedeemScript->getHex() != $redeemScript) {
301
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript->getHex()}]");
302
            }
303
304 13
            if ($checkWitnessScript && $checkWitnessScript->getHex() != $witnessScript) {
305 13
                throw new \Exception("Failed to verify that witnessScript from API [{$witnessScript}] matches address locally [{$checkWitnessScript->getHex()}]");
306
            }
307
        } else {
308
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
309
        }
310
311 13
        return (string)$path;
312
    }
313
314
    /**
315
     * @param string|BIP32Path  $path
316
     * @return BIP32Key|false
317
     * @throws \Exception
318
     *
319
     * @TODO: hmm?
320
     */
321 14
    protected function getParentPublicKey($path) {
322 14
        $path = BIP32Path::path($path)->parent()->publicPath();
323
324 14
        if ($path->count() <= 2) {
325
            return false;
326
        }
327
328 14
        if ($path->isHardened()) {
329
            return false;
330
        }
331
332 14
        if (!isset($this->pubKeys[(string)$path])) {
333 14
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
334
        }
335
336 14
        return $this->pubKeys[(string)$path];
337
    }
338
339
    /**
340
     * get address for the specified path
341
     *
342
     * @param string|BIP32Path  $path
343
     * @return string
344
     */
345 13
    public function getAddressByPath($path) {
346 13
        $path = (string)BIP32Path::path($path)->privatePath();
347 13
        if (!isset($this->derivations[$path])) {
348 13
            list($address, ) = $this->getRedeemScriptByPath($path);
349
350 13
            $this->derivations[$path] = $address;
351 13
            $this->derivationsByAddress[$address] = $path;
352
        }
353
354 13
        return $this->derivations[$path];
355
    }
356
357
    /**
358
     * @param string $path
359
     * @return WalletScript
360
     */
361 14
    public function getWalletScriptByPath($path) {
362 14
        $path = BIP32Path::path($path);
363
364
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
365 14
        if ($pubKey = $this->getParentPublicKey($path)) {
366 14
            $key = $pubKey->buildKey($path->publicPath());
367
        } else {
368
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
369
        }
370
371 14
        return $this->getWalletScriptFromKey($key, $path);
372
    }
373
374
    /**
375
     * get address and redeemScript for specified path
376
     *
377
     * @param string    $path
378
     * @return array[string, ScriptInterface, ScriptInterface|null]     [address, redeemScript, witnessScript]
0 ignored issues
show
Documentation introduced by
The doc-type array[string, could not be parsed: Expected "]" at position 2, but found "string". (view supported doc-types)

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

Loading history...
379
     */
380 14
    public function getRedeemScriptByPath($path) {
381 14
        $walletScript = $this->getWalletScriptByPath($path);
382
383 14
        $redeemScript = $walletScript->isP2SH() ? $walletScript->getRedeemScript() : null;
384 14
        $witnessScript = $walletScript->isP2WSH() ? $walletScript->getWitnessScript() : null;
385 14
        return [$walletScript->getAddress()->getAddress(), $redeemScript, $witnessScript];
386
    }
387
388
    /**
389
     * @param BIP32Key          $key
390
     * @param string|BIP32Path  $path
391
     * @return string
392
     */
393
    protected function getAddressFromKey(BIP32Key $key, $path) {
394
        return $this->getWalletScriptFromKey($key, $path)->getAddress()->getAddress();
395
    }
396
397
    /**
398
     * @param BIP32Key          $key
399
     * @param string|BIP32Path  $path
400
     * @return WalletScript
401
     * @throws \Exception
402
     */
403 14
    protected function getWalletScriptFromKey(BIP32Key $key, $path) {
404 14
        $path = BIP32Path::path($path)->publicPath();
405
406 14
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
407
408 14
        $multisig = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
409 14
            $key->buildKey($path)->publicKey(),
410 14
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
411 14
            $blocktrailPublicKey->buildKey($path)->publicKey()
412 14
        ]), false);
413
414 14
        $type = (int)$key->path()[2];
415 14
        if ($this->network !== "bitcoincash" && $type === 2) {
416 3
            $witnessScript = new WitnessScript($multisig);
417 3
            $redeemScript = new P2shScript($witnessScript);
418 3
            $scriptPubKey = $redeemScript->getOutputScript();
419
        } else {
420 14
            $witnessScript = null;
421 14
            $redeemScript = new P2shScript($multisig);
422 14
            $scriptPubKey = $redeemScript->getOutputScript();
423
        }
424
425 14
        return new WalletScript($path, $scriptPubKey, $redeemScript, $witnessScript);
426
    }
427
428
    /**
429
     * get the path (and redeemScript) to specified address
430
     *
431
     * @param string $address
432
     * @return array
433
     */
434
    public function getPathForAddress($address) {
435
        return $this->sdk->getPathForAddress($this->identifier, $address);
436
    }
437
438
    /**
439
     * @param string|BIP32Path  $path
440
     * @return BIP32Key
441
     * @throws \Exception
442
     */
443 14
    public function getBlocktrailPublicKey($path) {
444 14
        $path = BIP32Path::path($path);
445
446 14
        $keyIndex = str_replace("'", "", $path[1]);
447
448 14
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
449
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
450
        }
451
452 14
        return $this->blocktrailPublicKeys[$keyIndex];
453
    }
454
455
    /**
456
     * generate a new derived key and return the new path and address for it
457
     *
458
     * @return string[]     [path, address]
459
     */
460 13
    public function getNewAddressPair() {
461 13
        $path = $this->getNewDerivation();
462 13
        $address = $this->getAddressByPath($path);
463
464 13
        return [$path, $address];
465
    }
466
467
    /**
468
     * generate a new derived private key and return the new address for it
469
     *
470
     * @return string
471
     */
472 9
    public function getNewAddress() {
473 9
        return $this->getNewAddressPair()[1];
474
    }
475
476
    /**
477
     * get the balance for the wallet
478
     *
479
     * @return int[]            [confirmed, unconfirmed]
480
     */
481 9
    public function getBalance() {
482 9
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
483
484 9
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
485
    }
486
487
    /**
488
     * do wallet discovery (slow)
489
     *
490
     * @param int   $gap        the gap setting to use for discovery
491
     * @return int[]            [confirmed, unconfirmed]
492
     */
493 2
    public function doDiscovery($gap = 200) {
494 2
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
495
496 2
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
497
    }
498
499
    /**
500
     * create, sign and send a transaction
501
     *
502
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
503
     *                                      value should be INT
504
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
505
     * @param bool     $allowZeroConf
506
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
507
     * @param string   $feeStrategy
508
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
509
     * @return string the txid / transaction hash
510
     * @throws \Exception
511
     */
512 9
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
513 9
        if ($this->locked) {
514 4
            throw new \Exception("Wallet needs to be unlocked to pay");
515
        }
516
517 9
        $outputs = self::normalizeOutputsStruct($outputs);
518
519 9
        $txBuilder = new TransactionBuilder();
520 9
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
521 9
        $txBuilder->setFeeStrategy($feeStrategy);
522 9
        $txBuilder->setChangeAddress($changeAddress);
523
524 9
        foreach ($outputs as $output) {
525 9
            $txBuilder->addRecipient($output['address'], $output['value']);
526
        }
527
528 9
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
529
530 3
        $apiCheckFee = $forceFee === null;
531
532 3
        return $this->sendTx($txBuilder, $apiCheckFee);
533
    }
534
535
    /**
536
     * determine max spendable from wallet after fees
537
     *
538
     * @param bool     $allowZeroConf
539
     * @param string   $feeStrategy
540
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
541
     * @param int      $outputCnt
542
     * @return string
543
     * @throws BlocktrailSDKException
544
     */
545
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
546
        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...
547
    }
548
549
    /**
550
     * parse outputs into normalized struct
551
     *
552
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
553
     * @return array            [['address' => address, 'value' => value], ]
554
     */
555 10
    public static function normalizeOutputsStruct(array $outputs) {
556 10
        $result = [];
557
558 10
        foreach ($outputs as $k => $v) {
559 10
            if (is_numeric($k)) {
560 1
                if (!is_array($v)) {
561
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
562
                }
563
564 1
                if (isset($v['address']) && isset($v['value'])) {
565 1
                    $address = $v['address'];
566 1
                    $value = $v['value'];
567 1
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
568 1
                    $address = $v[0];
569 1
                    $value = $v[1];
570
                } else {
571 1
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
572
                }
573
            } else {
574 10
                $address = $k;
575 10
                $value = $v;
576
            }
577
578 10
            $result[] = ['address' => $address, 'value' => $value];
579
        }
580
581 10
        return $result;
582
    }
583
584
    /**
585
     * 'fund' the txBuilder with UTXOs (modified in place)
586
     *
587
     * @param TransactionBuilder    $txBuilder
588
     * @param bool|true             $lockUTXOs
589
     * @param bool|false            $allowZeroConf
590
     * @param null|int              $forceFee
591
     * @return TransactionBuilder
592
     */
593 11
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
594
        // get the data we should use for this transaction
595 11
        $coinSelection = $this->coinSelection($txBuilder->getOutputs(/* $json = */true), $lockUTXOs, $allowZeroConf, $txBuilder->getFeeStrategy(), $forceFee);
0 ignored issues
show
Bug introduced by
It seems like $forceFee defined by parameter $forceFee on line 593 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...
596
        
597 5
        $utxos = $coinSelection['utxos'];
598 5
        $fee = $coinSelection['fee'];
599 5
        $change = $coinSelection['change'];
0 ignored issues
show
Unused Code introduced by
$change is not used, you could remove the assignment.

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

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

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

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

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

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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