Completed
Branch master (5cb30d)
by
unknown
02:00
created

Wallet::getPathForAddress()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\AddressFactory;
6
use BitWasp\Bitcoin\Bitcoin;
7
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
8
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
9
use BitWasp\Bitcoin\Script\P2shScript;
10
use BitWasp\Bitcoin\Script\ScriptFactory;
11
use BitWasp\Bitcoin\Script\ScriptInterface;
12
use BitWasp\Bitcoin\Script\WitnessScript;
13
use BitWasp\Bitcoin\Transaction\Factory\SignData;
14
use BitWasp\Bitcoin\Transaction\Factory\Signer;
15
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
16
use BitWasp\Bitcoin\Transaction\OutPoint;
17
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
18
use BitWasp\Bitcoin\Transaction\Transaction;
19
use BitWasp\Bitcoin\Transaction\TransactionInterface;
20
use BitWasp\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 CHAIN_BTC_DEFAULT = 0;
35
    const CHAIN_BCC_DEFAULT = 1;
36
    const CHAIN_BTC_SEGWIT = 2;
37
38
    const BASE_FEE = 10000;
39
40
    /**
41
     * development / debug setting
42
     *  when getting a new derivation from the API,
43
     *  will verify address / redeeemScript with the values the API provides
44
     */
45
    const VERIFY_NEW_DERIVATION = true;
46
47
    /**
48
     * @var BlocktrailSDKInterface
49
     */
50
    protected $sdk;
51
52
    /**
53
     * @var string
54
     */
55
    protected $identifier;
56
57
    /**
58
     * BIP32 master primary private key (m/)
59
     *
60
     * @var BIP32Key
61
     */
62
    protected $primaryPrivateKey;
63
64
    /**
65
     * @var BIP32Key[]
66
     */
67
    protected $primaryPublicKeys;
68
69
    /**
70
     * BIP32 master backup public key (M/)
71
72
     * @var BIP32Key
73
     */
74
    protected $backupPublicKey;
75
76
    /**
77
     * map of blocktrail BIP32 public keys
78
     *  keyed by key index
79
     *  path should be `M / key_index'`
80
     *
81
     * @var BIP32Key[]
82
     */
83
    protected $blocktrailPublicKeys;
84
85
    /**
86
     * the 'Blocktrail Key Index' that is used for new addresses
87
     *
88
     * @var int
89
     */
90
    protected $keyIndex;
91
92
    /**
93
     * 'bitcoin'
94
     *
95
     * @var string
96
     */
97
    protected $network;
98
99
    /**
100
     * testnet yes / no
101
     *
102
     * @var bool
103
     */
104
    protected $testnet;
105
106
    /**
107
     * cache of public keys, by path
108
     *
109
     * @var BIP32Key[]
110
     */
111
    protected $pubKeys = [];
112
113
    /**
114
     * cache of address / redeemScript, by path
115
     *
116
     * @var string[][]      [[address, redeemScript)], ]
117
     */
118
    protected $derivations = [];
119
120
    /**
121
     * reverse cache of paths by address
122
     *
123
     * @var string[]
124
     */
125
    protected $derivationsByAddress = [];
126
127
    protected $checksum;
128
129
    /**
130
     * @var bool
131
     */
132
    protected $locked = true;
133
134
    /**
135
     * @var bool
136
     */
137
    protected $isSegwit = false;
138
139
    /**
140
     * @var int
141
     */
142
    protected $chainIndex;
143
144
    /**
145
     * @var int
146
     */
147
    protected $changeIndex;
148
149
    protected $optimalFeePerKB;
150
    protected $lowPriorityFeePerKB;
151
    protected $feePerKBAge;
152
    protected $allowedSignModes = [SignInfo::MODE_DONTSIGN, SignInfo::MODE_SIGN];
153
154
    /**
155
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
156
     * @param string                        $identifier                 identifier of the wallet
157
     * @param BIP32Key[]                    $primaryPublicKeys
158
     * @param BIP32Key                      $backupPublicKey            should be BIP32 master public key M/
159
     * @param BIP32Key[]                    $blocktrailPublicKeys
160
     * @param int                           $keyIndex
161
     * @param string                        $network
162
     * @param bool                          $testnet
163
     * @param bool                          $segwit
164
     * @param string                        $checksum
165
     * @throws BlocktrailSDKException
166
     */
167 22
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, array $primaryPublicKeys, $backupPublicKey, array $blocktrailPublicKeys, $keyIndex, $network, $testnet, $segwit, $checksum) {
168 22
        $this->sdk = $sdk;
169
170 22
        $this->identifier = $identifier;
171 22
        $this->backupPublicKey = BlocktrailSDK::normalizeBIP32Key($backupPublicKey);
172 22
        $this->primaryPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($primaryPublicKeys);
173 22
        $this->blocktrailPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($blocktrailPublicKeys);
174
175 22
        $this->network = $network;
176 22
        $this->testnet = $testnet;
177 22
        $this->keyIndex = $keyIndex;
178 22
        $this->checksum = $checksum;
179
180 22
        if ($network === "bitcoin") {
181 22
            if ($segwit) {
182 3
                $chainIdx = self::CHAIN_BTC_DEFAULT;
183 3
                $changeIdx = self::CHAIN_BTC_SEGWIT;
184
            } else {
185 20
                $chainIdx = self::CHAIN_BTC_DEFAULT;
186 22
                $changeIdx = self::CHAIN_BTC_DEFAULT;
187
            }
188
        } else {
189
            if ($segwit && $network === "bitcoincash") {
190
                throw new BlocktrailSDKException("Received segwit flag for bitcoincash - abort");
191
            }
192
            $chainIdx = self::CHAIN_BCC_DEFAULT;
193
            $changeIdx = self::CHAIN_BCC_DEFAULT;
194
        }
195
196 22
        $this->isSegwit = (bool) $segwit;
197 22
        $this->chainIndex = $chainIdx;
198 22
        $this->changeIndex = $changeIdx;
199 22
    }
200
201
    /**
202
     * @param int|null $chainIndex
203
     * @return WalletPath
204
     * @throws BlocktrailSDKException
205
     */
206 15
    protected function getWalletPath($chainIndex = null) {
207 15
        if ($chainIndex === null) {
208 12
            return WalletPath::create($this->keyIndex, $this->chainIndex);
209
        } else {
210 6
            if (!is_int($chainIndex)) {
211 1
                throw new BlocktrailSDKException("Chain index is invalid - should be an integer");
212
            }
213 5
            return WalletPath::create($this->keyIndex, $chainIndex);
214
        }
215
    }
216
217
    /**
218
     * @return bool
219
     */
220 3
    public function isSegwit() {
221 3
        return $this->isSegwit;
222
    }
223
224
    /**
225
     * return the wallet identifier
226
     *
227
     * @return string
228
     */
229 10
    public function getIdentifier() {
230 10
        return $this->identifier;
231
    }
232
233
    /**
234
     * Returns the wallets backup public key
235
     *
236
     * @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...
237
     */
238 1
    public function getBackupKey() {
239 1
        return $this->backupPublicKey->tuple();
240
    }
241
242
    /**
243
     * return list of Blocktrail co-sign extended public keys
244
     *
245
     * @return array[]      [ [xpub, path] ]
246
     */
247 5
    public function getBlocktrailPublicKeys() {
248
        return array_map(function (BIP32Key $key) {
249 5
            return $key->tuple();
250 5
        }, $this->blocktrailPublicKeys);
251
    }
252
253
    /**
254
     * check if wallet is locked
255
     *
256
     * @return bool
257
     */
258 10
    public function isLocked() {
259 10
        return $this->locked;
260
    }
261
262
    /**
263
     * upgrade wallet to different blocktrail cosign key
264
     *
265
     * @param $keyIndex
266
     * @return bool
267
     * @throws \Exception
268
     */
269 4
    public function upgradeKeyIndex($keyIndex) {
270 4
        if ($this->locked) {
271 4
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
272
        }
273
274 3
        $walletPath = WalletPath::create($keyIndex);
275
276
        // do the upgrade to the new 'key_index'
277 3
        $primaryPublicKey = $this->primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath());
278
279
        // $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...
280 3
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
281
282 3
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
283 3
        $this->keyIndex = $keyIndex;
284
285
        // update the blocktrail public keys
286 3
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
287 3
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
288 3
                $path = $pubKey[1];
289 3
                $pubKey = $pubKey[0];
290 3
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKey), $path);
291
            }
292
        }
293
294 3
        return true;
295
    }
296
297
    /**
298
     * get a new BIP32 derivation for the next (unused) address
299
     *  by requesting it from the API
300
     *
301
     * @return string
302
     * @param int|null $chainIndex
303
     * @throws \Exception
304
     */
305 15
    protected function getNewDerivation($chainIndex = null) {
306 15
        $path = $this->getWalletPath($chainIndex)->path()->last("*");
307
308 14
        if (self::VERIFY_NEW_DERIVATION) {
309 14
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
310
311 14
            $path = $new['path'];
312 14
            $address = $new['address'];
313 14
            $redeemScript = $new['redeem_script'];
314 14
            $witnessScript = array_key_exists('witness_script', $new) ? $new['witness_script'] : null;
315
316
            /** @var ScriptInterface $checkRedeemScript */
317
            /** @var ScriptInterface $checkWitnessScript */
318 14
            list($checkAddress, $checkRedeemScript, $checkWitnessScript) = $this->getRedeemScriptByPath($path);
319
320 14
            if ($checkAddress != $address) {
321
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
322
            }
323
324 14
            if ($checkRedeemScript && $checkRedeemScript->getHex() != $redeemScript) {
325
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript->getHex()}]");
326
            }
327
328 14
            if ($checkWitnessScript && $checkWitnessScript->getHex() != $witnessScript) {
329 14
                throw new \Exception("Failed to verify that witnessScript from API [{$witnessScript}] matches address locally [{$checkWitnessScript->getHex()}]");
330
            }
331
        } else {
332
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
333
        }
334
335 14
        return (string)$path;
336
    }
337
338
    /**
339
     * @param string|BIP32Path  $path
340
     * @return BIP32Key|false
341
     * @throws \Exception
342
     *
343
     * @TODO: hmm?
344
     */
345 16
    protected function getParentPublicKey($path) {
346 16
        $path = BIP32Path::path($path)->parent()->publicPath();
347
348 16
        if ($path->count() <= 2) {
349
            return false;
350
        }
351
352 16
        if ($path->isHardened()) {
353
            return false;
354
        }
355
356 16
        if (!isset($this->pubKeys[(string)$path])) {
357 16
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
358
        }
359
360 16
        return $this->pubKeys[(string)$path];
361
    }
362
363
    /**
364
     * get address for the specified path
365
     *
366
     * @param string|BIP32Path  $path
367
     * @return string
368
     */
369 14
    public function getAddressByPath($path) {
370 14
        $path = (string)BIP32Path::path($path)->privatePath();
371 14
        if (!isset($this->derivations[$path])) {
372 14
            list($address, ) = $this->getRedeemScriptByPath($path);
373
374 14
            $this->derivations[$path] = $address;
375 14
            $this->derivationsByAddress[$address] = $path;
376
        }
377
378 14
        return $this->derivations[$path];
379
    }
380
381
    /**
382
     * @param string $path
383
     * @return WalletScript
384
     */
385 16
    public function getWalletScriptByPath($path) {
386 16
        $path = BIP32Path::path($path);
387
388
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
389 16
        if ($pubKey = $this->getParentPublicKey($path)) {
390 16
            $key = $pubKey->buildKey($path->publicPath());
391
        } else {
392
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
393
        }
394
395 16
        return $this->getWalletScriptFromKey($key, $path);
396
    }
397
398
    /**
399
     * get address and redeemScript for specified path
400
     *
401
     * @param string    $path
402
     * @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...
403
     */
404 15
    public function getRedeemScriptByPath($path) {
405 15
        $walletScript = $this->getWalletScriptByPath($path);
406
407 15
        $redeemScript = $walletScript->isP2SH() ? $walletScript->getRedeemScript() : null;
408 15
        $witnessScript = $walletScript->isP2WSH() ? $walletScript->getWitnessScript() : null;
409 15
        return [$walletScript->getAddress()->getAddress(), $redeemScript, $witnessScript];
410
    }
411
412
    /**
413
     * @param BIP32Key          $key
414
     * @param string|BIP32Path  $path
415
     * @return string
416
     */
417
    protected function getAddressFromKey(BIP32Key $key, $path) {
418
        return $this->getWalletScriptFromKey($key, $path)->getAddress()->getAddress();
419
    }
420
421
    /**
422
     * @param BIP32Key          $key
423
     * @param string|BIP32Path  $path
424
     * @return WalletScript
425
     * @throws \Exception
426
     */
427 16
    protected function getWalletScriptFromKey(BIP32Key $key, $path) {
428 16
        $path = BIP32Path::path($path)->publicPath();
429
430 16
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
431
432 16
        $multisig = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
433 16
            $key->buildKey($path)->publicKey(),
434 16
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
435 16
            $blocktrailPublicKey->buildKey($path)->publicKey()
436 16
        ]), false);
437
438 16
        $type = (int)$key->path()[2];
439 16
        if ($this->isSegwit && $type === Wallet::CHAIN_BTC_SEGWIT) {
440 3
            $witnessScript = new WitnessScript($multisig);
441 3
            $redeemScript = new P2shScript($witnessScript);
442 3
            $scriptPubKey = $redeemScript->getOutputScript();
443 16
        } else if ($type === Wallet::CHAIN_BTC_DEFAULT || $type === Wallet::CHAIN_BCC_DEFAULT) {
444 15
            $witnessScript = null;
445 15
            $redeemScript = new P2shScript($multisig);
446 15
            $scriptPubKey = $redeemScript->getOutputScript();
447
        } else {
448 1
            throw new BlocktrailSDKException("Unsupported chain in path");
449
        }
450
451 15
        return new WalletScript($path, $scriptPubKey, $redeemScript, $witnessScript);
452
    }
453
454
    /**
455
     * get the path (and redeemScript) to specified address
456
     *
457
     * @param string $address
458
     * @return array
459
     */
460 1
    public function getPathForAddress($address) {
461 1
        return $this->sdk->getPathForAddress($this->identifier, $address);
462
    }
463
464
    /**
465
     * @param string|BIP32Path  $path
466
     * @return BIP32Key
467
     * @throws \Exception
468
     */
469 16
    public function getBlocktrailPublicKey($path) {
470 16
        $path = BIP32Path::path($path);
471
472 16
        $keyIndex = str_replace("'", "", $path[1]);
473
474 16
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
475
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
476
        }
477
478 16
        return $this->blocktrailPublicKeys[$keyIndex];
479
    }
480
481
    /**
482
     * generate a new derived key and return the new path and address for it
483
     *
484
     * @param int|null $chainIndex
485
     * @return string[]     [path, address]
486
     */
487 15
    public function getNewAddressPair($chainIndex = null) {
488 15
        $path = $this->getNewDerivation($chainIndex);
489 14
        $address = $this->getAddressByPath($path);
490
491 14
        return [$path, $address];
492
    }
493
494
    /**
495
     * generate a new derived private key and return the new address for it
496
     *
497
     * @param int|null $chainIndex
498
     * @return string
499
     */
500 7
    public function getNewAddress($chainIndex = null) {
501 7
        return $this->getNewAddressPair($chainIndex)[1];
502
    }
503
504
    /**
505
     * generate a new derived private key and return the new address for it
506
     *
507
     * @return string
508
     */
509 4
    public function getNewChangeAddress() {
510 4
        return $this->getNewAddressPair($this->changeIndex)[1];
511
    }
512
513
    /**
514
     * get the balance for the wallet
515
     *
516
     * @return int[]            [confirmed, unconfirmed]
517
     */
518 9
    public function getBalance() {
519 9
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
520
521 9
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
522
    }
523
524
    /**
525
     * do wallet discovery (slow)
526
     *
527
     * @param int   $gap        the gap setting to use for discovery
528
     * @return int[]            [confirmed, unconfirmed]
529
     */
530 2
    public function doDiscovery($gap = 200) {
531 2
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
532
533 1
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
534
    }
535
536
    /**
537
     * create, sign and send a transaction
538
     *
539
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
540
     *                                      value should be INT
541
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
542
     * @param bool     $allowZeroConf
543
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
544
     * @param string   $feeStrategy
545
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
546
     * @return string the txid / transaction hash
547
     * @throws \Exception
548
     */
549 9
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
550 9
        if ($this->locked) {
551 4
            throw new \Exception("Wallet needs to be unlocked to pay");
552
        }
553
554 8
        $outputs = self::normalizeOutputsStruct($outputs);
555
556 8
        $txBuilder = new TransactionBuilder();
557 8
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
558 8
        $txBuilder->setFeeStrategy($feeStrategy);
559 8
        $txBuilder->setChangeAddress($changeAddress);
560
561 8
        foreach ($outputs as $output) {
562 8
            $txBuilder->addRecipient($output['address'], $output['value']);
563
        }
564
565 8
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
566
567 3
        $apiCheckFee = $forceFee === null;
568
569 3
        return $this->sendTx($txBuilder, $apiCheckFee);
570
    }
571
572
    /**
573
     * determine max spendable from wallet after fees
574
     *
575
     * @param bool     $allowZeroConf
576
     * @param string   $feeStrategy
577
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
578
     * @param int      $outputCnt
579
     * @return string
580
     * @throws BlocktrailSDKException
581
     */
582
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
583
        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...
584
    }
585
586
    /**
587
     * parse outputs into normalized struct
588
     *
589
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
590
     * @return array            [['address' => address, 'value' => value], ]
591
     */
592 9
    public static function normalizeOutputsStruct(array $outputs) {
593 9
        $result = [];
594
595 9
        foreach ($outputs as $k => $v) {
596 9
            if (is_numeric($k)) {
597 1
                if (!is_array($v)) {
598
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
599
                }
600
601 1
                if (isset($v['address']) && isset($v['value'])) {
602 1
                    $address = $v['address'];
603 1
                    $value = $v['value'];
604 1
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
605 1
                    $address = $v[0];
606 1
                    $value = $v[1];
607
                } else {
608 1
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
609
                }
610
            } else {
611 9
                $address = $k;
612 9
                $value = $v;
613
            }
614
615 9
            $result[] = ['address' => $address, 'value' => $value];
616
        }
617
618 9
        return $result;
619
    }
620
621
    /**
622
     * 'fund' the txBuilder with UTXOs (modified in place)
623
     *
624
     * @param TransactionBuilder    $txBuilder
625
     * @param bool|true             $lockUTXOs
626
     * @param bool|false            $allowZeroConf
627
     * @param null|int              $forceFee
628
     * @return TransactionBuilder
629
     */
630 10
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
631
        // get the data we should use for this transaction
632 10
        $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 630 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...
633
        
634 5
        $utxos = $coinSelection['utxos'];
635 5
        $fee = $coinSelection['fee'];
636 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...
637
638 5
        if ($forceFee !== null) {
639
            $txBuilder->setFee($forceFee);
640
        } else {
641 5
            $txBuilder->validateFee($fee);
642
        }
643
644 5
        foreach ($utxos as $utxo) {
645 5
            $signMode = SignInfo::MODE_SIGN;
646 5
            if (isset($utxo['sign_mode'])) {
647
                $signMode = $utxo['sign_mode'];
648
                if (!in_array($signMode, $this->allowedSignModes)) {
649
                    throw new \Exception("Sign mode disallowed by wallet");
650
                }
651
            }
652
653 5
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script'], $utxo['witness_script'], $signMode);
654
        }
655
656 5
        return $txBuilder;
657
    }
658
659
    /**
660
     * build inputs and outputs lists for TransactionBuilder
661
     *
662
     * @param TransactionBuilder $txBuilder
663
     * @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...
664
     * @throws \Exception
665
     */
666 7
    public function buildTx(TransactionBuilder $txBuilder) {
667 7
        $send = $txBuilder->getOutputs();
668 7
        $utxos = $txBuilder->getUtxos();
669 7
        $signInfo = [];
670
671 7
        $txb = new TxBuilder();
672
673 7
        foreach ($utxos as $utxo) {
674 7
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
675 1
                $tx = $this->sdk->transaction($utxo->hash);
676
677 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...
678
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
679
                }
680
681 1
                $output = $tx['outputs'][$utxo->index];
682
683 1
                if (!$utxo->address) {
684
                    $utxo->address = AddressFactory::fromString($output['address']);
685
                }
686 1
                if (!$utxo->value) {
687
                    $utxo->value = $output['value'];
688
                }
689 1
                if (!$utxo->scriptPubKey) {
690 1
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
691
                }
692
            }
693
694 7
            if (SignInfo::MODE_SIGN === $utxo->signMode) {
695 7
                if (!$utxo->path) {
696
                    $utxo->path = $this->getPathForAddress($utxo->address->getAddress());
697
                }
698
699 7
                if (!$utxo->redeemScript || !$utxo->witnessScript) {
700 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...
701 6
                    $utxo->redeemScript = $redeemScript;
702 6
                    $utxo->witnessScript = $witnessScript;
703
                }
704
            }
705
706 7
            $signInfo[] = $utxo->getSignInfo();
707
        }
708
709
        $utxoSum = array_sum(array_map(function (UTXO $utxo) {
710 7
            return $utxo->value;
711 7
        }, $utxos));
712 7
        if ($utxoSum < array_sum(array_column($send, 'value'))) {
713 1
            throw new \Exception("Atempting to spend more than sum of UTXOs");
714
        }
715
716 7
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
717
718 7
        if ($txBuilder->getValidateFee() !== null) {
719
            // sanity check to make sure the API isn't giving us crappy data
720 5
            if (abs($txBuilder->getValidateFee() - $fee) > (Wallet::BASE_FEE * 5)) {
721
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
722
            }
723
        }
724
725 7
        if ($change > 0) {
726 5
            $send[] = [
727 5
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewChangeAddress(),
728 5
                'value' => $change
729
            ];
730
        }
731
732 7
        foreach ($utxos as $utxo) {
733 7
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index));
734
        }
735
736
        // outputs should be randomized to make the change harder to detect
737 7
        if ($txBuilder->shouldRandomizeChangeOuput()) {
738 7
            shuffle($send);
739
        }
740
741 7
        foreach ($send as $out) {
742 7
            assert(isset($out['value']));
743
744 7
            if (isset($out['scriptPubKey'])) {
745 1
                $txb->output($out['value'], $out['scriptPubKey']);
746 7
            } elseif (isset($out['address'])) {
747 7
                $txb->payToAddress($out['value'], AddressFactory::fromString($out['address']));
748
            } else {
749 7
                throw new \Exception();
750
            }
751
        }
752
753 7
        return [$txb->get(), $signInfo];
754
    }
755
756 7
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) {
757 7
        $send = $txBuilder->getOutputs();
758 7
        $utxos = $txBuilder->getUtxos();
759
760 7
        $fee = $txBuilder->getFee();
761 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...
762
763
        // if the fee is fixed we just need to calculate the change
764 7
        if ($fee !== null) {
765 1
            $change = $this->determineChange($utxos, $send, $fee);
766
767
            // if change is not dust we need to add a change output
768 1
            if ($change > Blocktrail::DUST) {
769
                $send[] = ['address' => 'change', 'value' => $change];
770
            } else {
771
                // if change is dust we do nothing (implicitly it's added to the fee)
772 1
                $change = 0;
773
            }
774
        } else {
775 7
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
776
777 7
            $change = $this->determineChange($utxos, $send, $fee);
778
779 7
            if ($change > 0) {
780 6
                $changeIdx = count($send);
781
                // set dummy change output
782 6
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
783
784
                // recaculate fee now that we know that we have a change output
785 6
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
786
787
                // unset dummy change output
788 6
                unset($send[$changeIdx]);
789
790
                // if adding the change output made the fee bump up and the change is smaller than the fee
791
                //  then we're not doing change
792 6
                if ($fee2 > $fee && $fee2 > $change) {
793 2
                    $change = 0;
794
                } else {
795 5
                    $change = $this->determineChange($utxos, $send, $fee2);
796
797
                    // if change is not dust we need to add a change output
798 5
                    if ($change > Blocktrail::DUST) {
799 5
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
800
                    } else {
801
                        // if change is dust we do nothing (implicitly it's added to the fee)
802
                        $change = 0;
803
                    }
804
                }
805
            }
806
        }
807
808 7
        $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
809
810 7
        return [$fee, $change];
811
    }
812
813
    /**
814
     * create, sign and send transction based on TransactionBuilder
815
     *
816
     * @param TransactionBuilder $txBuilder
817
     * @param bool $apiCheckFee     let the API check if the fee is correct
818
     * @return string
819
     * @throws \Exception
820
     */
821 4
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
822 4
        list($tx, $signInfo) = $this->buildTx($txBuilder);
823
824 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...
825
    }
826
827
    /**
828
     * !! INTERNAL METHOD, public for testing purposes !!
829
     * create, sign and send transction based on inputs and outputs
830
     *
831
     * @param Transaction $tx
832
     * @param SignInfo[]  $signInfo
833
     * @param bool $apiCheckFee     let the API check if the fee is correct
834
     * @return string
835
     * @throws \Exception
836
     * @internal
837
     */
838 4
    public function _sendTx(Transaction $tx, array $signInfo, $apiCheckFee = true) {
839 4
        if ($this->locked) {
840
            throw new \Exception("Wallet needs to be unlocked to pay");
841
        }
842
843
        assert(Util::all(function ($signInfo) {
844 4
            return $signInfo instanceof SignInfo;
845 4
        }, $signInfo), '$signInfo should be SignInfo[]');
846
847
        // sign the transaction with our keys
848 4
        $signed = $this->signTransaction($tx, $signInfo);
849
850
        $txs = [
851 4
            'signed_transaction' => $signed->getHex(),
852 4
            'base_transaction' => $signed->getBaseSerialization()->getHex(),
853
        ];
854
855
        // send the transaction
856
        return $this->sendTransaction($txs, array_map(function (SignInfo $r) {
857 4
            return (string)$r->path;
858 4
        }, $signInfo), $apiCheckFee);
859
    }
860
861
    /**
862
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
863
     *
864
     * @todo: mark this as deprecated, insist on the utxo's or qualified scripts.
865
     * @param int $utxoCnt      number of unspent inputs in transaction
866
     * @param int $outputCnt    number of outputs in transaction
867
     * @return float
868
     * @access public           reminder that people might use this!
869
     */
870 1
    public static function estimateFee($utxoCnt, $outputCnt) {
871 1
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
872
873 1
        return self::baseFeeForSize($size);
874
    }
875
876
    /**
877
     * @param int $size     size in bytes
878
     * @return int          fee in satoshi
879
     */
880 5
    public static function baseFeeForSize($size) {
881 5
        $sizeKB = (int)ceil($size / 1000);
882
883 5
        return $sizeKB * self::BASE_FEE;
884
    }
885
886
    /**
887
     * @todo: variable varint
888
     * @todo: deprecate
889
     * @param int $txinSize
890
     * @param int $txoutSize
891
     * @return float
892
     */
893 2
    public static function estimateSize($txinSize, $txoutSize) {
894 2
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
895
    }
896
897
    /**
898
     * only supports estimating size for P2PKH/P2SH outputs
899
     *
900
     * @param int $outputCnt    number of outputs in transaction
901
     * @return float
902
     */
903 2
    public static function estimateSizeOutputs($outputCnt) {
904 2
        return ($outputCnt * 34);
905
    }
906
907
    /**
908
     * only supports estimating size for 2of3 multsig UTXOs
909
     *
910
     * @param int $utxoCnt      number of unspent inputs in transaction
911
     * @return float
912
     */
913 3
    public static function estimateSizeUTXOs($utxoCnt) {
914 3
        $txinSize = 0;
915
916 3
        for ($i=0; $i<$utxoCnt; $i++) {
917
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
918 3
            $multisig = "2of3";
919
920 3
            if ($multisig) {
921 3
                $sigCnt = 2;
922 3
                $msig = explode("of", $multisig);
923 3
                if (count($msig) == 2 && is_numeric($msig[0])) {
924 3
                    $sigCnt = $msig[0];
925
                }
926
927 3
                $txinSize += array_sum([
928 3
                    32, // txhash
929 3
                    4, // idx
930 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...
931 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...
932
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
933
                    1, // OP_0
934 3
                    4, // sequence
935
                ]);
936
            } else {
937
                $txinSize += array_sum([
938
                    32, // txhash
939
                    4, // idx
940
                    73, // sig
941
                    34, // script
942
                    4, // sequence
943
                ]);
944
            }
945
        }
946
947 3
        return $txinSize;
948
    }
949
950
    /**
951
     * determine how much fee is required based on the inputs and outputs
952
     *  this is an estimation, not a proper 100% correct calculation
953
     *
954
     * @param UTXO[]  $utxos
955
     * @param array[] $outputs
956
     * @param         $feeStrategy
957
     * @param         $optimalFeePerKB
958
     * @param         $lowPriorityFeePerKB
959
     * @return int
960
     * @throws BlocktrailSDKException
961
     */
962 7
    protected function determineFee($utxos, $outputs, $feeStrategy, $optimalFeePerKB, $lowPriorityFeePerKB) {
963
964 7
        $size = SizeEstimation::estimateVsize($utxos, $outputs);
965
966
        switch ($feeStrategy) {
967 7
            case self::FEE_STRATEGY_BASE_FEE:
968 4
                return self::baseFeeForSize($size);
969
970 4
            case self::FEE_STRATEGY_OPTIMAL:
971 3
                return (int)round(($size / 1000) * $optimalFeePerKB);
972
973 1
            case self::FEE_STRATEGY_LOW_PRIORITY:
974 1
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
975
976
            default:
977
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
978
        }
979
    }
980
981
    /**
982
     * determine how much change is left over based on the inputs and outputs and the fee
983
     *
984
     * @param UTXO[]    $utxos
985
     * @param array[]   $outputs
986
     * @param int       $fee
987
     * @return int
988
     */
989 7
    protected function determineChange($utxos, $outputs, $fee) {
990
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
991 7
            return $utxo->value;
992 7
        }, $utxos));
993 7
        $outputsTotal = array_sum(array_column($outputs, 'value'));
994
995 7
        return $inputsTotal - $outputsTotal - $fee;
996
    }
997
998
    /**
999
     * sign a raw transaction with the private keys that we have
1000
     *
1001
     * @param Transaction $tx
1002
     * @param SignInfo[]  $signInfo
1003
     * @return TransactionInterface
1004
     * @throws \Exception
1005
     */
1006 4
    protected function signTransaction(Transaction $tx, array $signInfo) {
1007 4
        $signer = new Signer($tx, Bitcoin::getEcAdapter());
1008
1009 4
        assert(Util::all(function ($signInfo) {
1010 4
            return $signInfo instanceof SignInfo;
1011 4
        }, $signInfo), '$signInfo should be SignInfo[]');
1012
1013 4
        $sigHash = SigHash::ALL;
1014 4
        if ($this->network === "bitcoincash") {
1015
            $sigHash |= SigHash::BITCOINCASH;
1016
            $signer->redeemBitcoinCash(true);
1017
        }
1018
1019 4
        foreach ($signInfo as $idx => $info) {
1020 4
            if ($info->mode === SignInfo::MODE_SIGN) {
1021
                // required SignInfo: path, redeemScript|witnessScript, output
1022 4
                $path = BIP32Path::path($info->path)->privatePath();
1023 4
                $key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey();
1024 4
                $signData = new SignData();
1025 4
                if ($info->redeemScript) {
1026 4
                    $signData->p2sh($info->redeemScript);
1027
                }
1028 4
                if ($info->witnessScript) {
1029 1
                    $signData->p2wsh($info->witnessScript);
1030
                }
1031 4
                $input = $signer->input($idx, $info->output, $signData);
1032 4
                $input->sign($key, $sigHash);
1033
            }
1034
        }
1035
1036 4
        return $signer->get();
1037
    }
1038
1039
    /**
1040
     * send the transaction using the API
1041
     *
1042
     * @param string|array  $signed
1043
     * @param string[]      $paths
1044
     * @param bool          $checkFee
1045
     * @return string           the complete raw transaction
1046
     * @throws \Exception
1047
     */
1048 4
    protected function sendTransaction($signed, $paths, $checkFee = false) {
1049 4
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
1050
    }
1051
1052
    /**
1053
     * @param \array[] $outputs
1054
     * @param bool $lockUTXO
1055
     * @param bool $allowZeroConf
1056
     * @param int|null|string $feeStrategy
1057
     * @param null $forceFee
1058
     * @return array
1059
     */
1060 10
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1061 10
        $result = $this->sdk->coinSelection($this->identifier, $outputs, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
1062
1063 5
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
1064 5
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
1065 5
        $this->feePerKBAge = time();
1066
1067 5
        return $result;
1068
    }
1069
1070 7
    public function getOptimalFeePerKB() {
1071 7
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
1072 3
            $this->updateFeePerKB();
1073
        }
1074
1075 7
        return $this->optimalFeePerKB;
1076
    }
1077
1078 7
    public function getLowPriorityFeePerKB() {
1079 7
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1080
            $this->updateFeePerKB();
1081
        }
1082
1083 7
        return $this->lowPriorityFeePerKB;
1084
    }
1085
1086 3
    public function updateFeePerKB() {
1087 3
        $result = $this->sdk->feePerKB();
1088
1089 3
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
1090 3
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
1091
1092 3
        $this->feePerKBAge = time();
1093 3
    }
1094
1095
    /**
1096
     * delete the wallet
1097
     *
1098
     * @param bool $force ignore warnings (such as non-zero balance)
1099
     * @return mixed
1100
     * @throws \Exception
1101
     */
1102 10
    public function deleteWallet($force = false) {
1103 10
        if ($this->locked) {
1104
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
1105
        }
1106
1107 10
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
1108 10
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
1109
    }
1110
1111
    /**
1112
     * create checksum to verify ownership of the master primary key
1113
     *
1114
     * @return string[]     [address, signature]
1115
     */
1116 10
    protected function createChecksumVerificationSignature() {
1117 10
        $privKey = $this->primaryPrivateKey->key();
1118
1119 10
        $pubKey = $this->primaryPrivateKey->publicKey();
1120 10
        $address = $pubKey->getAddress()->getAddress();
1121
1122 10
        $signer = new MessageSigner(Bitcoin::getEcAdapter());
1123 10
        $signed = $signer->sign($address, $privKey->getPrivateKey());
1124
1125 10
        return [$address, base64_encode($signed->getCompactSignature()->getBuffer()->getBinary())];
1126
    }
1127
1128
    /**
1129
     * setup a webhook for our wallet
1130
     *
1131
     * @param string    $url            URL to receive webhook events
1132
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1133
     * @return array
1134
     */
1135 1
    public function setupWebhook($url, $identifier = null) {
1136 1
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1137 1
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1138
    }
1139
1140
    /**
1141
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1142
     * @return mixed
1143
     */
1144 1
    public function deleteWebhook($identifier = null) {
1145 1
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1146 1
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1147
    }
1148
1149
    /**
1150
     * lock a specific unspent output
1151
     *
1152
     * @param     $txHash
1153
     * @param     $txIdx
1154
     * @param int $ttl
1155
     * @return bool
1156
     */
1157
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1158
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1159
    }
1160
1161
    /**
1162
     * unlock a specific unspent output
1163
     *
1164
     * @param     $txHash
1165
     * @param     $txIdx
1166
     * @return bool
1167
     */
1168
    public function unlockUTXO($txHash, $txIdx) {
1169
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1170
    }
1171
1172
    /**
1173
     * get all transactions for the wallet (paginated)
1174
     *
1175
     * @param  integer $page    pagination: page number
1176
     * @param  integer $limit   pagination: records per page (max 500)
1177
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1178
     * @return array            associative array containing the response
1179
     */
1180 1
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1181 1
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1182
    }
1183
1184
    /**
1185
     * get all addresses for the wallet (paginated)
1186
     *
1187
     * @param  integer $page    pagination: page number
1188
     * @param  integer $limit   pagination: records per page (max 500)
1189
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1190
     * @return array            associative array containing the response
1191
     */
1192 1
    public function addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1193 1
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1194
    }
1195
1196
    /**
1197
     * get all UTXOs for the wallet (paginated)
1198
     *
1199
     * @param  integer $page        pagination: page number
1200
     * @param  integer $limit       pagination: records per page (max 500)
1201
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1202
     * @param  boolean $zeroconf    include zero confirmation transactions
1203
     * @return array                associative array containing the response
1204
     */
1205 1
    public function utxos($page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1206 1
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir, $zeroconf);
1207
    }
1208
}
1209