Completed
Branch master (56c2f5)
by
unknown
07:05
created

Wallet::addresses()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

Loading history...
309
     */
310
    public function getRedeemScriptByPath($path) {
311
        $path = BIP32Path::path($path);
312
313
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
314
        if ($pubKey = $this->getParentPublicKey($path)) {
315
            $key = $pubKey->buildKey($path->publicPath());
316
        } else {
317
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
318
        }
319
320
        return $this->getRedeemScriptFromKey($key, $path);
321
    }
322
323
    /**
324
     * @param BIP32Key          $key
325
     * @param string|BIP32Path  $path
326
     * @return string
327
     */
328
    protected function getAddressFromKey(BIP32Key $key, $path) {
329
        return $this->getRedeemScriptFromKey($key, $path)[0];
330
    }
331
332
    /**
333
     * @param BIP32Key          $key
334
     * @param string|BIP32Path  $path
335
     * @return array[string, ScriptInterface]                 [address, redeemScript]
0 ignored issues
show
Documentation introduced by
The doc-type array[string, could not be parsed: Expected "]" at position 2, but found "string". (view supported doc-types)

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

Loading history...
336
     * @throws \Exception
337
     */
338
    protected function getRedeemScriptFromKey(BIP32Key $key, $path) {
339
        $path = BIP32Path::path($path)->publicPath();
340
341
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
342
343
        $redeemScript = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
344
            $key->buildKey($path)->publicKey(),
345
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
346
            $blocktrailPublicKey->buildKey($path)->publicKey()
347
        ]), false);
348
349
        return [(new P2shScript($redeemScript))->getAddress()->getAddress(), $redeemScript];
350
    }
351
352
    /**
353
     * get the path (and redeemScript) to specified address
354
     *
355
     * @param string $address
356
     * @return array
357
     */
358
    public function getPathForAddress($address) {
359
        return $this->sdk->getPathForAddress($this->identifier, $address);
360
    }
361
362
    /**
363
     * @param string|BIP32Path  $path
364
     * @return BIP32Key
365
     * @throws \Exception
366
     */
367 View Code Duplication
    public function getBlocktrailPublicKey($path) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
368
        $path = BIP32Path::path($path);
369
370
        $keyIndex = str_replace("'", "", $path[1]);
371
372
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
373
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
374
        }
375
376
        return $this->blocktrailPublicKeys[$keyIndex];
377
    }
378
379
    /**
380
     * generate a new derived key and return the new path and address for it
381
     *
382
     * @return string[]     [path, address]
383
     */
384
    public function getNewAddressPair() {
385
        $path = $this->getNewDerivation();
386
        $address = $this->getAddressByPath($path);
387
388
        return [$path, $address];
0 ignored issues
show
Best Practice introduced by
The expression return array($path, $address); seems to be an array, but some of its elements' types (string[]) are incompatible with the return type declared by the interface Blocktrail\SDK\WalletInterface::getNewAddressPair of type string[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
389
    }
390
391
    /**
392
     * generate a new derived private key and return the new address for it
393
     *
394
     * @return string
395
     */
396
    public function getNewAddress() {
397
        return $this->getNewAddressPair()[1];
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getNewAddressPair()[1]; of type string[]|string adds the type string[] to the return on line 397 which is incompatible with the return type declared by the interface Blocktrail\SDK\WalletInterface::getNewAddress of type string.
Loading history...
398
    }
399
400
    /**
401
     * get the balance for the wallet
402
     *
403
     * @return int[]            [confirmed, unconfirmed]
404
     */
405
    public function getBalance() {
406
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
407
408
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
409
    }
410
411
    /**
412
     * do wallet discovery (slow)
413
     *
414
     * @param int   $gap        the gap setting to use for discovery
415
     * @return int[]            [confirmed, unconfirmed]
416
     */
417
    public function doDiscovery($gap = 200) {
418
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
419
420
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
421
    }
422
423
    /**
424
     * create, sign and send a transaction
425
     *
426
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
427
     *                                      value should be INT
428
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
429
     * @param bool     $allowZeroConf
430
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
431
     * @param string   $feeStrategy
432
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
433
     * @return string the txid / transaction hash
434
     * @throws \Exception
435
     */
436
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
437
        if ($this->locked) {
438
            throw new \Exception("Wallet needs to be unlocked to pay");
439
        }
440
441
        $outputs = self::normalizeOutputsStruct($outputs);
442
443
        $txBuilder = new TransactionBuilder();
444
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
445
        $txBuilder->setFeeStrategy($feeStrategy);
446
447
        foreach ($outputs as $output) {
448
            $txBuilder->addRecipient($output['address'], $output['value']);
449
        }
450
451
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
452
453
        $apiCheckFee = $forceFee === null;
454
455
        return $this->sendTx($txBuilder, $apiCheckFee);
456
    }
457
458
    /**
459
     * determine max spendable from wallet after fees
460
     *
461
     * @param bool     $allowZeroConf
462
     * @param string   $feeStrategy
463
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
464
     * @param int      $outputCnt
465
     * @return string
466
     * @throws BlocktrailSDKException
467
     */
468
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
469
        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...
470
    }
471
472
    /**
473
     * parse outputs into normalized struct
474
     *
475
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
476
     * @return array            [['address' => address, 'value' => value], ]
477
     */
478
    public static function normalizeOutputsStruct(array $outputs) {
479
        $result = [];
480
481
        foreach ($outputs as $k => $v) {
482
            if (is_numeric($k)) {
483
                if (!is_array($v)) {
484
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
485
                }
486
487
                if (isset($v['address']) && isset($v['value'])) {
488
                    $address = $v['address'];
489
                    $value = $v['value'];
490
                } else if (count($v) == 2 && isset($v[0]) && isset($v[1])) {
491
                    $address = $v[0];
492
                    $value = $v[1];
493
                } else {
494
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
495
                }
496
            } else {
497
                $address = $k;
498
                $value = $v;
499
            }
500
501
            $result[] = ['address' => $address, 'value' => $value];
502
        }
503
504
        return $result;
505
    }
506
507
    /**
508
     * 'fund' the txBuilder with UTXOs (modified in place)
509
     *
510
     * @param TransactionBuilder    $txBuilder
511
     * @param bool|true             $lockUTXOs
512
     * @param bool|false            $allowZeroConf
513
     * @param null|int              $forceFee
514
     * @return TransactionBuilder
515
     */
516
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
517
        // get the data we should use for this transaction
518
        $coinSelection = $this->coinSelection($txBuilder->getOutputs(/* $json = */true), $lockUTXOs, $allowZeroConf, $txBuilder->getFeeStrategy(), $forceFee);
519
        $utxos = $coinSelection['utxos'];
520
        $fee = $coinSelection['fee'];
521
        $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...
522
523
        if ($forceFee !== null) {
524
            $txBuilder->setFee($forceFee);
525
        } else {
526
            $txBuilder->validateFee($fee);
527
        }
528
529
        foreach ($utxos as $utxo) {
530
            $scriptPubKey = ScriptFactory::fromHex($utxo['scriptpubkey_hex']);
531
            $redeemScript = ScriptFactory::fromHex($utxo['redeem_script']);
532
            $address = AddressFactory::fromString($utxo['address']);
533
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $address, $scriptPubKey, $utxo['path'], $redeemScript);
534
        }
535
536
        return $txBuilder;
537
    }
538
539
    /**
540
     * build inputs and outputs lists for TransactionBuilder
541
     *
542
     * @param TransactionBuilder $txBuilder
543
     * @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...
544
     * @throws \Exception
545
     */
546
    public function buildTx(TransactionBuilder $txBuilder) {
547
        $send = $txBuilder->getOutputs();
548
        $utxos = $txBuilder->getUtxos();
549
        $signInfo = [];
550
551
        $txb = new TxBuilder();
552
553
        foreach ($utxos as $utxo) {
554
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
555
                $tx = $this->sdk->transaction($utxo->hash);
556
557
                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...
558
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
559
                }
560
561
                $output = $tx['outputs'][$utxo->index];
562
563
                if (!$utxo->address) {
564
                    $utxo->address = AddressFactory::fromString($output['address']);
565
                }
566
                if (!$utxo->value) {
567
                    $utxo->value = $output['value'];
568
                }
569
                if (!$utxo->scriptPubKey) {
570
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
571
                }
572
            }
573
574
            if (!$utxo->path) {
575
                $utxo->path = $this->getPathForAddress($utxo->address->getAddress());
576
            }
577
578
            if (!$utxo->redeemScript) {
579
                list(, $redeemScript) = $this->getRedeemScriptByPath($utxo->path);
580
                $utxo->redeemScript = $redeemScript;
581
            }
582
583
            $signInfo[] = new SignInfo($utxo->path, $utxo->redeemScript, new TransactionOutput($utxo->value, $utxo->scriptPubKey));
584
        }
585
586
        if (array_sum(array_map(function (UTXO $utxo) { return $utxo->value; }, $utxos)) < array_sum(array_column($send, 'value'))) {
587
            throw new \Exception("Atempting to spend more than sum of UTXOs");
588
        }
589
590
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
591
592
        if ($txBuilder->getValidateFee() !== null) {
593
            if (abs($txBuilder->getValidateFee() - $fee) > Wallet::BASE_FEE) {
594
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
595
            }
596
        }
597
598
        if ($change > 0) {
599
            $send[] = [
600
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewAddress(),
601
                'value' => $change
602
            ];
603
        }
604
605
        foreach ($utxos as $utxo) {
606
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index), $utxo->scriptPubKey);
607
        }
608
609
        // outputs should be randomized to make the change harder to detect
610
        if ($txBuilder->shouldRandomizeChangeOuput()) {
611
            shuffle($send);
612
        }
613
614
        foreach ($send as $out) {
615
            assert(isset($out['value']));
616
617
            if (isset($out['scriptPubKey'])) {
618
                $txb->output($out['value'], $out['scriptPubKey']);
619
            } else if (isset($out['address'])) {
620
                $txb->payToAddress($out['value'], AddressFactory::fromString($out['address']));
621
            } else {
622
                throw new \Exception();
623
            }
624
        }
625
626
        return [$txb->get(), $signInfo];
627
    }
628
629
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) {
630
        $send = $txBuilder->getOutputs();
631
        $utxos = $txBuilder->getUtxos();
632
633
        $fee = $txBuilder->getFee();
634
        $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...
635
636
        // if the fee is fixed we just need to calculate the change
637
        if ($fee !== null) {
638
            $change = $this->determineChange($utxos, $send, $fee);
639
640
            // if change is not dust we need to add a change output
641 View Code Duplication
            if ($change > Blocktrail::DUST) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
642
                $send[] = ['address' => 'change', 'value' => $change];
643
            } else {
644
                // if change is dust we do nothing (implicitly it's added to the fee)
645
                $change = 0;
646
            }
647
        } else {
648
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
649
650
            $change = $this->determineChange($utxos, $send, $fee);
651
652
            if ($change > 0) {
653
                $changeIdx = count($send);
654
                // set dummy change output
655
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
656
657
                // recaculate fee now that we know that we have a change output
658
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
659
660
                // unset dummy change output
661
                unset($send[$changeIdx]);
662
663
                // if adding the change output made the fee bump up and the change is smaller than the fee
664
                //  then we're not doing change
665
                if ($fee2 > $fee && $fee2 > $change) {
666
                    $change = 0;
667
                } else {
668
                    $change = $this->determineChange($utxos, $send, $fee2);
669
670
                    // if change is not dust we need to add a change output
671 View Code Duplication
                    if ($change > Blocktrail::DUST) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
672
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
673
                    } else {
674
                        // if change is dust we do nothing (implicitly it's added to the fee)
675
                        $change = 0;
676
                    }
677
                }
678
            }
679
        }
680
681
        $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
682
683
        return [$fee, $change];
684
    }
685
686
    /**
687
     * create, sign and send transction based on TransactionBuilder
688
     *
689
     * @param TransactionBuilder $txBuilder
690
     * @param bool $apiCheckFee     let the API check if the fee is correct
691
     * @return string
692
     * @throws \Exception
693
     */
694
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
695
        list($tx, $signInfo) = $this->buildTx($txBuilder);
696
697
        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...
698
    }
699
700
    /**
701
     * !! INTERNAL METHOD, public for testing purposes !!
702
     * create, sign and send transction based on inputs and outputs
703
     *
704
     * @param Transaction $tx
705
     * @param SignInfo[]  $signInfo
706
     * @param bool $apiCheckFee     let the API check if the fee is correct
707
     * @return string
708
     * @throws \Exception
709
     * @internal
710
     */
711
    public function _sendTx(Transaction $tx, array $signInfo, $apiCheckFee = true) {
712
        if ($this->locked) {
713
            throw new \Exception("Wallet needs to be unlocked to pay");
714
        }
715
716
        assert(Util::all(function ($signInfo) {
717
            return $signInfo instanceof SignInfo;
718
        }, $signInfo), '$signInfo should be SignInfo[]');
719
720
        // sign the transaction with our keys
721
        $signed = $this->signTransaction($tx, $signInfo);
722
723
        // send the transaction
724
        $finished = $this->sendTransaction($signed->getHex(), array_map(function (SignInfo $r) {
725
            return $r->path;
726
        }, $signInfo), $apiCheckFee);
727
728
        return $finished;
729
    }
730
731
    /**
732
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
733
     *
734
     * @param int $utxoCnt      number of unspent inputs in transaction
735
     * @param int $outputCnt    number of outputs in transaction
736
     * @return float
737
     * @access public           reminder that people might use this!
738
     */
739
    public static function estimateFee($utxoCnt, $outputCnt) {
740
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
741
742
        return self::baseFeeForSize($size);
743
    }
744
745
    /**
746
     * @param int $size     size in bytes
747
     * @return int          fee in satoshi
748
     */
749
    public static function baseFeeForSize($size) {
750
        $sizeKB = (int)ceil($size / 1000);
751
752
        return $sizeKB * self::BASE_FEE;
753
    }
754
755
    /**
756
     * @param int $txinSize
757
     * @param int $txoutSize
758
     * @return float
759
     */
760
    public static function estimateSize($txinSize, $txoutSize) {
761
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
762
    }
763
764
    /**
765
     * only supports estimating size for P2PKH/P2SH outputs
766
     *
767
     * @param int $outputCnt    number of outputs in transaction
768
     * @return float
769
     */
770
    public static function estimateSizeOutputs($outputCnt) {
771
        return ($outputCnt * 34);
772
    }
773
774
    /**
775
     * only supports estimating size for 2of3 multsig UTXOs
776
     *
777
     * @param int $utxoCnt      number of unspent inputs in transaction
778
     * @return float
779
     */
780
    public static function estimateSizeUTXOs($utxoCnt) {
781
        $txinSize = 0;
782
783
        for ($i=0; $i<$utxoCnt; $i++) {
784
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
785
            $multisig = "2of3";
786
787
            if ($multisig) {
788
                $sigCnt = 2;
789
                $msig = explode("of", $multisig);
790
                if (count($msig) == 2 && is_numeric($msig[0])) {
791
                    $sigCnt = $msig[0];
792
                }
793
794
                $txinSize += array_sum([
795
                    32, // txhash
796
                    4, // idx
797
                    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...
798
                    ((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...
799
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
800
                    4, // sequence
801
                ]);
802
            } else {
803
                $txinSize += array_sum([
804
                    32, // txhash
805
                    4, // idx
806
                    73, // sig
807
                    34, // script
808
                    4, // sequence
809
                ]);
810
            }
811
        }
812
813
        return $txinSize;
814
    }
815
816
    /**
817
     * determine how much fee is required based on the inputs and outputs
818
     *  this is an estimation, not a proper 100% correct calculation
819
     *
820
     * @param UTXO[]  $utxos
821
     * @param array[] $outputs
822
     * @param         $feeStrategy
823
     * @param         $optimalFeePerKB
824
     * @param         $lowPriorityFeePerKB
825
     * @return int
826
     * @throws BlocktrailSDKException
827
     */
828
    protected function determineFee($utxos, $outputs, $feeStrategy, $optimalFeePerKB, $lowPriorityFeePerKB) {
829
        $outputSize = 0;
830
        foreach ($outputs as $output) {
831
            if (isset($output['scriptPubKey'])) {
832
                if ($output['scriptPubKey'] instanceof ScriptInterface) {
833
                    $outputSize += $output['scriptPubKey']->getBuffer()->getSize();
834
                } else {
835
                    $outputSize += strlen($output['scriptPubKey']) / 2; // asume HEX
836
                }
837
            } else {
838
                $outputSize += 34;
839
            }
840
        }
841
842
        $size = self::estimateSize(self::estimateSizeUTXOs(count($utxos)), $outputSize);
843
844
        switch ($feeStrategy) {
845
            case self::FEE_STRATEGY_BASE_FEE:
846
                return self::baseFeeForSize($size);
847
848
            case self::FEE_STRATEGY_OPTIMAL:
849
                return (int)round(($size / 1000) * $optimalFeePerKB);
850
851
            case self::FEE_STRATEGY_LOW_PRIORITY:
852
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
853
854
            default:
855
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
856
        }
857
    }
858
859
    /**
860
     * determine how much change is left over based on the inputs and outputs and the fee
861
     *
862
     * @param UTXO[]    $utxos
863
     * @param array[]   $outputs
864
     * @param int       $fee
865
     * @return int
866
     */
867
    protected function determineChange($utxos, $outputs, $fee) {
868
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
869
            return $utxo->value;
870
        }, $utxos));
871
        $outputsTotal = array_sum(array_column($outputs, 'value'));
872
873
        return $inputsTotal - $outputsTotal - $fee;
874
    }
875
876
    /**
877
     * sign a raw transaction with the private keys that we have
878
     *
879
     * @param Transaction $tx
880
     * @param SignInfo[]  $signInfo
881
     * @return TransactionInterface
882
     * @throws \Exception
883
     */
884
    protected function signTransaction(Transaction $tx, array $signInfo) {
885
        $signer = new Signer($tx, Bitcoin::getEcAdapter());
886
887
        assert(Util::all(function ($signInfo) {
888
            return $signInfo instanceof SignInfo;
889
        }, $signInfo), '$signInfo should be SignInfo[]');
890
891
        foreach ($signInfo as $idx => $info) {
892
            $path = BIP32Path::path($info->path)->privatePath();
893
            $redeemScript = $info->redeemScript;
894
            $output = $info->output;
895
896
            $key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey();
897
898
            $signer->sign($idx, $key, $output, $redeemScript);
899
        }
900
901
        return $signer->get();
902
    }
903
904
    /**
905
     * send the transaction using the API
906
     *
907
     * @param string    $signed
908
     * @param string[]  $paths
909
     * @param bool      $checkFee
910
     * @return string           the complete raw transaction
911
     * @throws \Exception
912
     */
913
    protected function sendTransaction($signed, $paths, $checkFee = false) {
914
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
915
    }
916
917
    /**
918
     * use the API to get the best inputs to use based on the outputs
919
     *
920
     * @param array[]  $outputs
921
     * @param bool     $lockUTXO
922
     * @param bool     $allowZeroConf
923
     * @param string   $feeStrategy
924
     * @param null|int $forceFee
925
     * @return array
926
     */
927
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
928
        $result = $this->sdk->coinSelection($this->identifier, $outputs, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
929
930
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
931
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
932
        $this->feePerKBAge = time();
933
934
        return $result;
935
    }
936
937
    public function getOptimalFeePerKB() {
938
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
939
            $this->updateFeePerKB();
940
        }
941
942
        return $this->optimalFeePerKB;
943
    }
944
945
    public function getLowPriorityFeePerKB() {
946
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
947
            $this->updateFeePerKB();
948
        }
949
950
        return $this->lowPriorityFeePerKB;
951
    }
952
953
    public function updateFeePerKB() {
954
        $result = $this->sdk->feePerKB();
955
956
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
957
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
958
959
        $this->feePerKBAge = time();
960
    }
961
962
    /**
963
     * delete the wallet
964
     *
965
     * @param bool $force ignore warnings (such as non-zero balance)
966
     * @return mixed
967
     * @throws \Exception
968
     */
969
    public function deleteWallet($force = false) {
970
        if ($this->locked) {
971
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
972
        }
973
974
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
975
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
976
    }
977
978
    /**
979
     * create checksum to verify ownership of the master primary key
980
     *
981
     * @return string[]     [address, signature]
982
     */
983
    protected function createChecksumVerificationSignature() {
984
        $privKey = $this->primaryPrivateKey->key();
985
986
        $pubKey = $this->primaryPrivateKey->publicKey();
987
        $address = $pubKey->getAddress()->getAddress();
988
989
        $signer = new MessageSigner(Bitcoin::getEcAdapter());
990
        $signed = $signer->sign($address, $privKey->getPrivateKey());
991
992
        return [$address, base64_encode($signed->getCompactSignature()->getBuffer()->getBinary())];
993
    }
994
995
    /**
996
     * setup a webhook for our wallet
997
     *
998
     * @param string    $url            URL to receive webhook events
999
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1000
     * @return array
1001
     */
1002
    public function setupWebhook($url, $identifier = null) {
1003
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1004
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1005
    }
1006
1007
    /**
1008
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1009
     * @return mixed
1010
     */
1011
    public function deleteWebhook($identifier = null) {
1012
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1013
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1014
    }
1015
1016
    /**
1017
     * lock a specific unspent output
1018
     *
1019
     * @param     $txHash
1020
     * @param     $txIdx
1021
     * @param int $ttl
1022
     * @return bool
1023
     */
1024
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1025
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1026
    }
1027
1028
    /**
1029
     * unlock a specific unspent output
1030
     *
1031
     * @param     $txHash
1032
     * @param     $txIdx
1033
     * @return bool
1034
     */
1035
    public function unlockUTXO($txHash, $txIdx) {
1036
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1037
    }
1038
1039
    /**
1040
     * get all transactions for the wallet (paginated)
1041
     *
1042
     * @param  integer $page    pagination: page number
1043
     * @param  integer $limit   pagination: records per page (max 500)
1044
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1045
     * @return array            associative array containing the response
1046
     */
1047
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1048
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1049
    }
1050
1051
    /**
1052
     * get all addresses for the wallet (paginated)
1053
     *
1054
     * @param  integer $page    pagination: page number
1055
     * @param  integer $limit   pagination: records per page (max 500)
1056
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1057
     * @return array            associative array containing the response
1058
     */
1059
    public function addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1060
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1061
    }
1062
1063
    /**
1064
     * get all UTXOs for the wallet (paginated)
1065
     *
1066
     * @param  integer $page    pagination: page number
1067
     * @param  integer $limit   pagination: records per page (max 500)
1068
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1069
     * @return array            associative array containing the response
1070
     */
1071
    public function utxos($page = 1, $limit = 20, $sortDir = 'asc') {
1072
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir);
1073
    }
1074
}
1075