Completed
Pull Request — master (#91)
by thomas
24:38
created

Wallet::getWalletScriptFromKey()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 26
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 5

Importance

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