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