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