|
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\Network\NetworkFactory; |
|
9
|
|
|
use BitWasp\Bitcoin\Script\P2shScript; |
|
10
|
|
|
use BitWasp\Bitcoin\Script\ScriptFactory; |
|
11
|
|
|
use BitWasp\Bitcoin\Transaction\Factory\SignData; |
|
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\TransactionInterface; |
|
16
|
|
|
use BitWasp\Bitcoin\Transaction\TransactionOutput; |
|
17
|
|
|
use BitWasp\Buffertools\Buffer; |
|
18
|
|
|
use BitWasp\Buffertools\BufferInterface; |
|
19
|
|
|
use Blocktrail\SDK\Bitcoin\BIP32Key; |
|
20
|
|
|
use Blocktrail\SDK\Bitcoin\BIP32Path; |
|
21
|
|
|
use Blocktrail\SDK\Exceptions\BlocktrailSDKException; |
|
22
|
|
|
|
|
23
|
|
|
abstract class WalletSweeper { |
|
24
|
|
|
|
|
25
|
|
|
/** |
|
26
|
|
|
* network to use - currently only supporting 'bitcoin' |
|
27
|
|
|
* @var string |
|
28
|
|
|
*/ |
|
29
|
|
|
protected $network; |
|
30
|
|
|
|
|
31
|
|
|
/** |
|
32
|
|
|
* using testnet or not |
|
33
|
|
|
* @var bool |
|
34
|
|
|
*/ |
|
35
|
|
|
protected $testnet; |
|
36
|
|
|
|
|
37
|
|
|
/** |
|
38
|
|
|
* backup private key |
|
39
|
|
|
* @var BIP32Key |
|
40
|
|
|
*/ |
|
41
|
|
|
protected $backupPrivateKey; |
|
42
|
|
|
|
|
43
|
|
|
/** |
|
44
|
|
|
* primary private key |
|
45
|
|
|
* @var BIP32Key |
|
46
|
|
|
*/ |
|
47
|
|
|
protected $primaryPrivateKey; |
|
48
|
|
|
|
|
49
|
|
|
/** |
|
50
|
|
|
* blocktrail public keys, mapped to their relevant keyIndex |
|
51
|
|
|
* @var |
|
52
|
|
|
*/ |
|
53
|
|
|
protected $blocktrailPublicKeys; |
|
54
|
|
|
|
|
55
|
|
|
/** |
|
56
|
|
|
* gets unspent outputs for addresses |
|
57
|
|
|
* @var UnspentOutputFinder |
|
58
|
|
|
*/ |
|
59
|
|
|
protected $utxoFinder; |
|
60
|
|
|
|
|
61
|
|
|
/** |
|
62
|
|
|
* holds wallet addresses, along with path, redeem script, and discovered unspent outputs |
|
63
|
|
|
* @var array |
|
64
|
|
|
*/ |
|
65
|
|
|
protected $sweepData; |
|
66
|
|
|
|
|
67
|
|
|
/** |
|
68
|
|
|
* process logging for debugging |
|
69
|
|
|
* @var bool |
|
70
|
|
|
*/ |
|
71
|
|
|
protected $debug = false; |
|
72
|
|
|
|
|
73
|
|
|
/** |
|
74
|
|
|
* @param BufferInterface $primarySeed |
|
75
|
|
|
* @param BufferInterface $backupSeed |
|
76
|
|
|
* @param array $blocktrailPublicKeys = |
|
77
|
|
|
* @param UnspentOutputFinder $utxoFinder |
|
78
|
|
|
* @param string $network |
|
79
|
|
|
* @param bool $testnet |
|
80
|
|
|
* @throws \Exception |
|
81
|
|
|
*/ |
|
82
|
|
|
public function __construct(BufferInterface $primarySeed, BufferInterface $backupSeed, array $blocktrailPublicKeys, UnspentOutputFinder $utxoFinder, $network = 'btc', $testnet = false) { |
|
83
|
|
|
// normalize network and set bitcoinlib to the right magic-bytes |
|
84
|
|
|
list($this->network, $this->testnet) = $this->normalizeNetwork($network, $testnet); |
|
85
|
|
|
|
|
86
|
|
|
assert($this->network == "bitcoin"); |
|
87
|
|
|
Bitcoin::setNetwork($this->testnet ? NetworkFactory::bitcoinTestnet() : NetworkFactory::bitcoin()); |
|
88
|
|
|
|
|
89
|
|
|
//create BIP32 keys for the Blocktrail public keys |
|
90
|
|
|
foreach ($blocktrailPublicKeys as $blocktrailKey) { |
|
91
|
|
|
$this->blocktrailPublicKeys[$blocktrailKey['keyIndex']] = BlocktrailSDK::normalizeBIP32Key([$blocktrailKey['pubkey'], $blocktrailKey['path']]); |
|
92
|
|
|
} |
|
93
|
|
|
|
|
94
|
|
|
$this->utxoFinder = $utxoFinder; |
|
95
|
|
|
|
|
96
|
|
|
$this->primaryPrivateKey = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m"); |
|
97
|
|
|
$this->backupPrivateKey = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m"); |
|
98
|
|
|
} |
|
99
|
|
|
|
|
100
|
|
|
/** |
|
101
|
|
|
* enable debug info logging (just to console) |
|
102
|
|
|
*/ |
|
103
|
|
|
public function enableLogging() { |
|
104
|
|
|
$this->debug = true; |
|
105
|
|
|
$this->utxoFinder->enableLogging(); |
|
106
|
|
|
} |
|
107
|
|
|
|
|
108
|
|
|
/** |
|
109
|
|
|
* disable debug info logging |
|
110
|
|
|
*/ |
|
111
|
|
|
public function disableLogging() { |
|
112
|
|
|
$this->debug = false; |
|
113
|
|
|
$this->utxoFinder->disableLogging(); |
|
114
|
|
|
} |
|
115
|
|
|
|
|
116
|
|
|
|
|
117
|
|
|
/** |
|
118
|
|
|
* normalize network string |
|
119
|
|
|
* |
|
120
|
|
|
* @param $network |
|
121
|
|
|
* @param $testnet |
|
122
|
|
|
* @return array |
|
123
|
|
|
* @throws \Exception |
|
124
|
|
|
*/ |
|
125
|
|
View Code Duplication |
protected function normalizeNetwork($network, $testnet) { |
|
|
|
|
|
|
126
|
|
|
switch (strtolower($network)) { |
|
127
|
|
|
case 'btc': |
|
128
|
|
|
case 'bitcoin': |
|
129
|
|
|
$network = 'bitcoin'; |
|
130
|
|
|
|
|
131
|
|
|
break; |
|
132
|
|
|
|
|
133
|
|
|
case 'tbtc': |
|
134
|
|
|
case 'bitcoin-testnet': |
|
135
|
|
|
$network = 'bitcoin'; |
|
136
|
|
|
$testnet = true; |
|
137
|
|
|
|
|
138
|
|
|
break; |
|
139
|
|
|
|
|
140
|
|
|
default: |
|
141
|
|
|
throw new \Exception("Unknown network [{$network}]"); |
|
142
|
|
|
} |
|
143
|
|
|
|
|
144
|
|
|
return [$network, $testnet]; |
|
145
|
|
|
} |
|
146
|
|
|
|
|
147
|
|
|
|
|
148
|
|
|
/** |
|
149
|
|
|
* generate multisig address for given path |
|
150
|
|
|
* |
|
151
|
|
|
* @param string|BIP32Path $path |
|
152
|
|
|
* @return array[string, ScriptInterface] [address, redeemScript] |
|
|
|
|
|
|
153
|
|
|
* @throws \Exception |
|
154
|
|
|
*/ |
|
155
|
|
|
protected function createAddress($path) { |
|
156
|
|
|
$path = BIP32Path::path($path)->publicPath(); |
|
157
|
|
|
|
|
158
|
|
|
$redeemScript = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([ |
|
159
|
|
|
$this->primaryPrivateKey->buildKey($path)->publicKey(), |
|
160
|
|
|
$this->backupPrivateKey->buildKey($path->unhardenedPath())->publicKey(), |
|
161
|
|
|
$this->getBlocktrailPublicKey($path)->buildKey($path)->publicKey() |
|
162
|
|
|
]), false); |
|
163
|
|
|
|
|
164
|
|
|
return [(new P2shScript($redeemScript))->getAddress()->getAddress(), $redeemScript]; |
|
165
|
|
|
} |
|
166
|
|
|
|
|
167
|
|
|
/** |
|
168
|
|
|
* create a batch of multisig addresses |
|
169
|
|
|
* |
|
170
|
|
|
* @param $start |
|
171
|
|
|
* @param $count |
|
172
|
|
|
* @param $keyIndex |
|
173
|
|
|
* @return array |
|
174
|
|
|
*/ |
|
175
|
|
|
protected function createBatchAddresses($start, $count, $keyIndex) { |
|
176
|
|
|
$addresses = array(); |
|
177
|
|
|
|
|
178
|
|
|
for ($i = 0; $i < $count; $i++) { |
|
179
|
|
|
//create a path subsequent address |
|
180
|
|
|
$path = (string)WalletPath::create($keyIndex, $_chain = 0, $start+$i)->path()->publicPath(); |
|
181
|
|
|
list($address, $redeem) = $this->createAddress($path); |
|
182
|
|
|
$addresses[$address] = array( |
|
183
|
|
|
//'address' => $address, |
|
|
|
|
|
|
184
|
|
|
'redeem' => $redeem, |
|
185
|
|
|
'path' => $path |
|
186
|
|
|
); |
|
187
|
|
|
|
|
188
|
|
|
if ($this->debug) { |
|
189
|
|
|
echo "."; |
|
190
|
|
|
} |
|
191
|
|
|
} |
|
192
|
|
|
|
|
193
|
|
|
return $addresses; |
|
194
|
|
|
} |
|
195
|
|
|
|
|
196
|
|
|
|
|
197
|
|
|
/** |
|
198
|
|
|
* gets the blocktrail pub key for the given path from the stored array of pub keys |
|
199
|
|
|
* |
|
200
|
|
|
* @param string|BIP32Path $path |
|
201
|
|
|
* @return BIP32Key |
|
202
|
|
|
* @throws \Exception |
|
203
|
|
|
*/ |
|
204
|
|
View Code Duplication |
protected function getBlocktrailPublicKey($path) { |
|
|
|
|
|
|
205
|
|
|
$path = BIP32Path::path($path); |
|
206
|
|
|
|
|
207
|
|
|
$keyIndex = str_replace("'", "", $path[1]); |
|
208
|
|
|
|
|
209
|
|
|
if (!isset($this->blocktrailPublicKeys[$keyIndex])) { |
|
210
|
|
|
throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]"); |
|
211
|
|
|
} |
|
212
|
|
|
|
|
213
|
|
|
return $this->blocktrailPublicKeys[$keyIndex]; |
|
214
|
|
|
} |
|
215
|
|
|
|
|
216
|
|
|
/** |
|
217
|
|
|
* discover funds in the wallet |
|
218
|
|
|
* |
|
219
|
|
|
* @param int $increment how many addresses to scan at a time |
|
220
|
|
|
* @return array |
|
221
|
|
|
* @throws BlocktrailSDKException |
|
222
|
|
|
*/ |
|
223
|
|
|
public function discoverWalletFunds($increment = 200) { |
|
224
|
|
|
$totalBalance = 0; |
|
225
|
|
|
$totalUTXOs = 0; |
|
226
|
|
|
$totalAddressesGenerated = 0; |
|
227
|
|
|
|
|
228
|
|
|
$addressUTXOs = array(); //addresses and their utxos, paths and redeem scripts |
|
229
|
|
|
|
|
230
|
|
|
//for each blocktrail pub key, do fund discovery on batches of addresses |
|
231
|
|
|
foreach ($this->blocktrailPublicKeys as $keyIndex => $blocktrailPubKey) { |
|
232
|
|
|
$i = 0; |
|
233
|
|
|
do { |
|
234
|
|
|
if ($this->debug) { |
|
235
|
|
|
echo "\ngenerating $increment addresses using blocktrail key index $keyIndex\n"; |
|
236
|
|
|
} |
|
237
|
|
|
$addresses = $this->createBatchAddresses($i, $increment, $keyIndex); |
|
238
|
|
|
$totalAddressesGenerated += count($addresses); |
|
239
|
|
|
|
|
240
|
|
|
if ($this->debug) { |
|
241
|
|
|
echo "\nstarting fund discovery for $increment addresses"; |
|
242
|
|
|
} |
|
243
|
|
|
|
|
244
|
|
|
//get the unspent outputs for this batch of addresses |
|
245
|
|
|
$utxos = $this->utxoFinder->getUTXOs(array_keys($addresses)); |
|
246
|
|
|
//save the address utxos, along with relevant path and redeem script |
|
247
|
|
|
foreach ($utxos as $utxo) { |
|
248
|
|
|
if (!isset($utxo['address'], $utxo['value'])) { |
|
249
|
|
|
throw new BlocktrailSDKException("Missing data"); |
|
250
|
|
|
} |
|
251
|
|
|
|
|
252
|
|
|
$address = $utxo['address']; |
|
253
|
|
|
|
|
254
|
|
|
if (!isset($addressUTXOs[$address])) { |
|
255
|
|
|
$addressUTXOs[$address] = [ |
|
256
|
|
|
'path' => $addresses[$address]['path'], |
|
257
|
|
|
'redeem' => $addresses[$address]['redeem'], |
|
258
|
|
|
'utxos' => [], |
|
259
|
|
|
]; |
|
260
|
|
|
} |
|
261
|
|
|
|
|
262
|
|
|
$addressUTXOs[$address]['utxos'][] = $utxo; |
|
263
|
|
|
$totalUTXOs ++; |
|
264
|
|
|
|
|
265
|
|
|
//add up the total utxo value for all addresses |
|
266
|
|
|
$totalBalance += $utxo['value']; |
|
267
|
|
|
} |
|
268
|
|
|
|
|
269
|
|
|
if ($this->debug) { |
|
270
|
|
|
echo "\nfound ".count($utxos)." unspent outputs"; |
|
271
|
|
|
} |
|
272
|
|
|
|
|
273
|
|
|
//increment for next batch |
|
274
|
|
|
$i += $increment; |
|
275
|
|
|
} while (count($utxos) > 0); |
|
276
|
|
|
} |
|
277
|
|
|
|
|
278
|
|
|
if ($this->debug) { |
|
279
|
|
|
echo "\nfinished fund discovery: $totalBalance Satoshi (in $totalUTXOs outputs) found when searching $totalAddressesGenerated addresses"; |
|
280
|
|
|
} |
|
281
|
|
|
|
|
282
|
|
|
$this->sweepData = ['utxos' => $addressUTXOs, 'count' => $totalUTXOs, 'balance' => $totalBalance, 'addressesSearched' => $totalAddressesGenerated]; |
|
283
|
|
|
return $this->sweepData; |
|
284
|
|
|
} |
|
285
|
|
|
|
|
286
|
|
|
/** |
|
287
|
|
|
* sweep the wallet of all funds and send to a single address |
|
288
|
|
|
* |
|
289
|
|
|
* @param string $destinationAddress address to receive found funds |
|
290
|
|
|
* @param int $sweepBatchSize number of addresses to search at a time |
|
291
|
|
|
* @return string HEX string of signed transaction |
|
292
|
|
|
* @throws \Exception |
|
293
|
|
|
*/ |
|
294
|
|
|
public function sweepWallet($destinationAddress, $sweepBatchSize = 200) { |
|
295
|
|
|
if ($this->debug) { |
|
296
|
|
|
echo "\nstarting wallet sweeping to address $destinationAddress"; |
|
297
|
|
|
} |
|
298
|
|
|
|
|
299
|
|
|
//do wallet fund discovery |
|
300
|
|
|
if (!isset($this->sweepData)) { |
|
301
|
|
|
$this->discoverWalletFunds($sweepBatchSize); |
|
302
|
|
|
} |
|
303
|
|
|
|
|
304
|
|
|
if ($this->sweepData['balance'] === 0) { |
|
305
|
|
|
//no funds found |
|
306
|
|
|
throw new \Exception("No funds found after searching through {$this->sweepData['addressesSearched']} addresses"); |
|
307
|
|
|
} |
|
308
|
|
|
|
|
309
|
|
|
//create and sign the transaction |
|
310
|
|
|
$transaction = $this->createTransaction($destinationAddress); |
|
311
|
|
|
|
|
312
|
|
|
//return or send the transaction |
|
313
|
|
|
return $transaction->getHex(); |
|
314
|
|
|
} |
|
315
|
|
|
|
|
316
|
|
|
/** |
|
317
|
|
|
* @param $destinationAddress |
|
318
|
|
|
* @return TransactionInterface |
|
319
|
|
|
*/ |
|
320
|
|
|
protected function createTransaction($destinationAddress) { |
|
321
|
|
|
$txb = new TxBuilder(); |
|
322
|
|
|
|
|
323
|
|
|
$signInfo = []; |
|
324
|
|
|
$utxos = []; |
|
325
|
|
|
foreach ($this->sweepData['utxos'] as $address => $data) { |
|
326
|
|
|
foreach ($data['utxos'] as $utxo) { |
|
327
|
|
|
$utxo = new UTXO( |
|
328
|
|
|
$utxo['hash'], |
|
329
|
|
|
$utxo['index'], |
|
330
|
|
|
$utxo['value'], |
|
331
|
|
|
AddressFactory::fromString($address), |
|
332
|
|
|
ScriptFactory::fromHex($utxo['script_hex']), |
|
333
|
|
|
$data['path'], |
|
334
|
|
|
$data['redeem'] |
|
335
|
|
|
); |
|
336
|
|
|
|
|
337
|
|
|
$utxos[] = $utxo; |
|
338
|
|
|
$signInfo[] = new SignInfo($utxo->path, $utxo->redeemScript, new TransactionOutput($utxo->value, $utxo->scriptPubKey)); |
|
339
|
|
|
} |
|
340
|
|
|
} |
|
341
|
|
|
|
|
342
|
|
|
foreach ($utxos as $utxo) { |
|
343
|
|
|
$txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index), $utxo->scriptPubKey); |
|
344
|
|
|
} |
|
345
|
|
|
|
|
346
|
|
|
$fee = Wallet::estimateFee($this->sweepData['count'], 1); |
|
347
|
|
|
|
|
348
|
|
|
$txb->payToAddress($this->sweepData['balance'] - $fee, AddressFactory::fromString($destinationAddress)); |
|
349
|
|
|
|
|
350
|
|
|
|
|
351
|
|
|
if ($this->debug) { |
|
352
|
|
|
echo "\nSigning transaction"; |
|
353
|
|
|
} |
|
354
|
|
|
|
|
355
|
|
|
$tx = $this->signTransaction($txb->get(), $signInfo); |
|
356
|
|
|
|
|
357
|
|
|
return $tx; |
|
358
|
|
|
} |
|
359
|
|
|
|
|
360
|
|
|
/** |
|
361
|
|
|
* sign a raw transaction with the private keys that we have |
|
362
|
|
|
* |
|
363
|
|
|
* @param TransactionInterface $tx |
|
364
|
|
|
* @param SignInfo[] $signInfo |
|
365
|
|
|
* @return TransactionInterface |
|
366
|
|
|
* @throws \Exception |
|
367
|
|
|
*/ |
|
368
|
|
|
protected function signTransaction(TransactionInterface $tx, array $signInfo) { |
|
369
|
|
|
$signer = new Signer($tx, Bitcoin::getEcAdapter()); |
|
370
|
|
|
|
|
371
|
|
|
assert(Util::all(function ($signInfo) { |
|
372
|
|
|
return $signInfo instanceof SignInfo; |
|
373
|
|
|
}, $signInfo), '$signInfo should be SignInfo[]'); |
|
374
|
|
|
|
|
375
|
|
|
foreach ($signInfo as $idx => $info) { |
|
376
|
|
|
$path = BIP32Path::path($info->path)->privatePath(); |
|
377
|
|
|
$redeemScript = $info->redeemScript; |
|
378
|
|
|
$output = $info->output; |
|
379
|
|
|
|
|
380
|
|
|
$key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey(); |
|
381
|
|
|
$backupKey = $this->backupPrivateKey->buildKey($path->unhardenedPath())->key()->getPrivateKey(); |
|
382
|
|
|
|
|
383
|
|
|
$signData = new SignData(); |
|
384
|
|
|
$signData->p2sh($redeemScript); |
|
385
|
|
|
$input = $signer->input($idx, $output, $signData); |
|
386
|
|
|
|
|
387
|
|
|
$input->sign($key); |
|
388
|
|
|
$input->sign($backupKey); |
|
389
|
|
|
} |
|
390
|
|
|
|
|
391
|
|
|
return $signer->get(); |
|
392
|
|
|
} |
|
393
|
|
|
} |
|
394
|
|
|
|
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.