1 | "use strict";
|
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
|
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
|
4 | };
|
5 | Object.defineProperty(exports, "__esModule", { value: true });
|
6 | const bitcoinjs_lib_1 = __importDefault(require("bitcoinjs-lib"));
|
7 | const form_data_1 = __importDefault(require("form-data"));
|
8 | const bn_js_1 = __importDefault(require("bn.js"));
|
9 | const ripemd160_1 = __importDefault(require("ripemd160"));
|
10 | const errors_1 = require("./errors");
|
11 | const logger_1 = require("./logger");
|
12 | const SATOSHIS_PER_BTC = 1e8;
|
13 | const TX_BROADCAST_SERVICE_ZONE_FILE_ENDPOINT = 'zone-file';
|
14 | const TX_BROADCAST_SERVICE_REGISTRATION_ENDPOINT = 'registration';
|
15 | const TX_BROADCAST_SERVICE_TX_ENDPOINT = 'transaction';
|
16 | class BitcoinNetwork {
|
17 | broadcastTransaction(transaction) {
|
18 | return Promise.reject(new Error(`Not implemented, broadcastTransaction(${transaction})`));
|
19 | }
|
20 | getBlockHeight() {
|
21 | return Promise.reject(new Error('Not implemented, getBlockHeight()'));
|
22 | }
|
23 | getTransactionInfo(txid) {
|
24 | return Promise.reject(new Error(`Not implemented, getTransactionInfo(${txid})`));
|
25 | }
|
26 | getNetworkedUTXOs(address) {
|
27 | return Promise.reject(new Error(`Not implemented, getNetworkedUTXOs(${address})`));
|
28 | }
|
29 | }
|
30 | exports.BitcoinNetwork = BitcoinNetwork;
|
31 | class BlockstackNetwork {
|
32 | constructor(apiUrl, broadcastServiceUrl, bitcoinAPI, network = bitcoinjs_lib_1.default.networks.bitcoin) {
|
33 | this.blockstackAPIUrl = apiUrl;
|
34 | this.broadcastServiceUrl = broadcastServiceUrl;
|
35 | this.layer1 = network;
|
36 | this.btc = bitcoinAPI;
|
37 | this.DUST_MINIMUM = 5500;
|
38 | this.includeUtxoMap = {};
|
39 | this.excludeUtxoSet = [];
|
40 | this.MAGIC_BYTES = 'id';
|
41 | }
|
42 | coerceAddress(address) {
|
43 | const { hash, version } = bitcoinjs_lib_1.default.address.fromBase58Check(address);
|
44 | const scriptHashes = [bitcoinjs_lib_1.default.networks.bitcoin.scriptHash,
|
45 | bitcoinjs_lib_1.default.networks.testnet.scriptHash];
|
46 | const pubKeyHashes = [bitcoinjs_lib_1.default.networks.bitcoin.pubKeyHash,
|
47 | bitcoinjs_lib_1.default.networks.testnet.pubKeyHash];
|
48 | let coercedVersion;
|
49 | if (scriptHashes.indexOf(version) >= 0) {
|
50 | coercedVersion = this.layer1.scriptHash;
|
51 | }
|
52 | else if (pubKeyHashes.indexOf(version) >= 0) {
|
53 | coercedVersion = this.layer1.pubKeyHash;
|
54 | }
|
55 | else {
|
56 | throw new Error(`Unrecognized address version number ${version} in ${address}`);
|
57 | }
|
58 | return bitcoinjs_lib_1.default.address.toBase58Check(hash, coercedVersion);
|
59 | }
|
60 | getDefaultBurnAddress() {
|
61 | return this.coerceAddress('1111111111111111111114oLvT2');
|
62 | }
|
63 | |
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 | getNamePriceV1(fullyQualifiedName) {
|
70 |
|
71 | return fetch(`${this.blockstackAPIUrl}/v1/prices/names/${fullyQualifiedName}`)
|
72 | .then((resp) => {
|
73 | if (!resp.ok) {
|
74 | throw new Error(`Failed to query name price for ${fullyQualifiedName}`);
|
75 | }
|
76 | return resp;
|
77 | })
|
78 | .then(resp => resp.json())
|
79 | .then(resp => resp.name_price)
|
80 | .then((namePrice) => {
|
81 | if (!namePrice || !namePrice.satoshis) {
|
82 | throw new Error(`Failed to get price for ${fullyQualifiedName}. Does the namespace exist?`);
|
83 | }
|
84 | if (namePrice.satoshis < this.DUST_MINIMUM) {
|
85 | namePrice.satoshis = this.DUST_MINIMUM;
|
86 | }
|
87 | const result = {
|
88 | units: 'BTC',
|
89 | amount: new bn_js_1.default(String(namePrice.satoshis))
|
90 | };
|
91 | return result;
|
92 | });
|
93 | }
|
94 | |
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 | getNamespacePriceV1(namespaceID) {
|
101 |
|
102 | return fetch(`${this.blockstackAPIUrl}/v1/prices/namespaces/${namespaceID}`)
|
103 | .then((resp) => {
|
104 | if (!resp.ok) {
|
105 | throw new Error(`Failed to query name price for ${namespaceID}`);
|
106 | }
|
107 | return resp;
|
108 | })
|
109 | .then(resp => resp.json())
|
110 | .then((namespacePrice) => {
|
111 | if (!namespacePrice || !namespacePrice.satoshis) {
|
112 | throw new Error(`Failed to get price for ${namespaceID}`);
|
113 | }
|
114 | if (namespacePrice.satoshis < this.DUST_MINIMUM) {
|
115 | namespacePrice.satoshis = this.DUST_MINIMUM;
|
116 | }
|
117 | const result = {
|
118 | units: 'BTC',
|
119 | amount: new bn_js_1.default(String(namespacePrice.satoshis))
|
120 | };
|
121 | return result;
|
122 | });
|
123 | }
|
124 | |
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 | getNamePriceV2(fullyQualifiedName) {
|
131 | return fetch(`${this.blockstackAPIUrl}/v2/prices/names/${fullyQualifiedName}`)
|
132 | .then((resp) => {
|
133 | if (resp.status !== 200) {
|
134 |
|
135 | throw new Error('The upstream node does not handle the /v2/ price namespace');
|
136 | }
|
137 | return resp;
|
138 | })
|
139 | .then(resp => resp.json())
|
140 | .then(resp => resp.name_price)
|
141 | .then((namePrice) => {
|
142 | if (!namePrice) {
|
143 | throw new Error(`Failed to get price for ${fullyQualifiedName}. Does the namespace exist?`);
|
144 | }
|
145 | const result = {
|
146 | units: namePrice.units,
|
147 | amount: new bn_js_1.default(namePrice.amount)
|
148 | };
|
149 | if (namePrice.units === 'BTC') {
|
150 |
|
151 | const dustMin = new bn_js_1.default(String(this.DUST_MINIMUM));
|
152 | if (result.amount.ucmp(dustMin) < 0) {
|
153 | result.amount = dustMin;
|
154 | }
|
155 | }
|
156 | return result;
|
157 | });
|
158 | }
|
159 | |
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 | getNamespacePriceV2(namespaceID) {
|
166 | return fetch(`${this.blockstackAPIUrl}/v2/prices/namespaces/${namespaceID}`)
|
167 | .then((resp) => {
|
168 | if (resp.status !== 200) {
|
169 |
|
170 | throw new Error('The upstream node does not handle the /v2/ price namespace');
|
171 | }
|
172 | return resp;
|
173 | })
|
174 | .then(resp => resp.json())
|
175 | .then((namespacePrice) => {
|
176 | if (!namespacePrice) {
|
177 | throw new Error(`Failed to get price for ${namespaceID}`);
|
178 | }
|
179 | const result = {
|
180 | units: namespacePrice.units,
|
181 | amount: new bn_js_1.default(namespacePrice.amount)
|
182 | };
|
183 | if (namespacePrice.units === 'BTC') {
|
184 |
|
185 | const dustMin = new bn_js_1.default(String(this.DUST_MINIMUM));
|
186 | if (result.amount.ucmp(dustMin) < 0) {
|
187 | result.amount = dustMin;
|
188 | }
|
189 | }
|
190 | return result;
|
191 | });
|
192 | }
|
193 | |
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 | getNamePrice(fullyQualifiedName) {
|
203 |
|
204 | return Promise.resolve().then(() => this.getNamePriceV2(fullyQualifiedName))
|
205 | .catch(() => this.getNamePriceV1(fullyQualifiedName));
|
206 | }
|
207 | |
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 | getNamespacePrice(namespaceID) {
|
217 |
|
218 | return Promise.resolve().then(() => this.getNamespacePriceV2(namespaceID))
|
219 | .catch(() => this.getNamespacePriceV1(namespaceID));
|
220 | }
|
221 | |
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 | getGracePeriod(fullyQualifiedName) {
|
228 | return Promise.resolve(5000);
|
229 | }
|
230 | |
231 |
|
232 |
|
233 |
|
234 |
|
235 | getNamesOwned(address) {
|
236 | const networkAddress = this.coerceAddress(address);
|
237 | return fetch(`${this.blockstackAPIUrl}/v1/addresses/bitcoin/${networkAddress}`)
|
238 | .then(resp => resp.json())
|
239 | .then(obj => obj.names);
|
240 | }
|
241 | |
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 | getNamespaceBurnAddress(namespace) {
|
248 | return Promise.all([
|
249 | fetch(`${this.blockstackAPIUrl}/v1/namespaces/${namespace}`),
|
250 | this.getBlockHeight()
|
251 | ])
|
252 | .then(([resp, blockHeight]) => {
|
253 | if (resp.status === 404) {
|
254 | throw new Error(`No such namespace '${namespace}'`);
|
255 | }
|
256 | else {
|
257 | return Promise.all([resp.json(), blockHeight]);
|
258 | }
|
259 | })
|
260 | .then(([namespaceInfo, blockHeight]) => {
|
261 | let address = this.getDefaultBurnAddress();
|
262 | if (namespaceInfo.version === 2) {
|
263 |
|
264 | if (namespaceInfo.reveal_block + 52595 >= blockHeight) {
|
265 | address = namespaceInfo.address;
|
266 | }
|
267 | }
|
268 | return address;
|
269 | })
|
270 | .then(address => this.coerceAddress(address));
|
271 | }
|
272 | |
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 | getNameInfo(fullyQualifiedName) {
|
279 | logger_1.Logger.debug(this.blockstackAPIUrl);
|
280 | const nameLookupURL = `${this.blockstackAPIUrl}/v1/names/${fullyQualifiedName}`;
|
281 | return fetch(nameLookupURL)
|
282 | .then((resp) => {
|
283 | if (resp.status === 404) {
|
284 | throw new Error('Name not found');
|
285 | }
|
286 | else if (resp.status !== 200) {
|
287 | throw new Error(`Bad response status: ${resp.status}`);
|
288 | }
|
289 | else {
|
290 | return resp.json();
|
291 | }
|
292 | })
|
293 | .then((nameInfo) => {
|
294 | logger_1.Logger.debug(`nameInfo: ${JSON.stringify(nameInfo)}`);
|
295 |
|
296 |
|
297 |
|
298 | if (nameInfo.address) {
|
299 | return Object.assign({}, nameInfo, { address: this.coerceAddress(nameInfo.address) });
|
300 | }
|
301 | else {
|
302 | return nameInfo;
|
303 | }
|
304 | });
|
305 | }
|
306 | |
307 |
|
308 |
|
309 |
|
310 |
|
311 | getNamespaceInfo(namespaceID) {
|
312 | return fetch(`${this.blockstackAPIUrl}/v1/namespaces/${namespaceID}`)
|
313 | .then((resp) => {
|
314 | if (resp.status === 404) {
|
315 | throw new Error('Namespace not found');
|
316 | }
|
317 | else if (resp.status !== 200) {
|
318 | throw new Error(`Bad response status: ${resp.status}`);
|
319 | }
|
320 | else {
|
321 | return resp.json();
|
322 | }
|
323 | })
|
324 | .then((namespaceInfo) => {
|
325 |
|
326 |
|
327 |
|
328 | if (namespaceInfo.address && namespaceInfo.recipient_address) {
|
329 | return Object.assign({}, namespaceInfo, {
|
330 | address: this.coerceAddress(namespaceInfo.address),
|
331 | recipient_address: this.coerceAddress(namespaceInfo.recipient_address)
|
332 | });
|
333 | }
|
334 | else {
|
335 | return namespaceInfo;
|
336 | }
|
337 | });
|
338 | }
|
339 | |
340 |
|
341 |
|
342 |
|
343 |
|
344 |
|
345 | getZonefile(zonefileHash) {
|
346 | return fetch(`${this.blockstackAPIUrl}/v1/zonefiles/${zonefileHash}`)
|
347 | .then((resp) => {
|
348 | if (resp.status === 200) {
|
349 | return resp.text()
|
350 | .then((body) => {
|
351 | const sha256 = bitcoinjs_lib_1.default.crypto.sha256(Buffer.from(body));
|
352 | const h = (new ripemd160_1.default()).update(sha256).digest('hex');
|
353 | if (h !== zonefileHash) {
|
354 | throw new Error(`Zone file contents hash to ${h}, not ${zonefileHash}`);
|
355 | }
|
356 | return body;
|
357 | });
|
358 | }
|
359 | else {
|
360 | throw new Error(`Bad response status: ${resp.status}`);
|
361 | }
|
362 | });
|
363 | }
|
364 | |
365 |
|
366 |
|
367 |
|
368 |
|
369 |
|
370 |
|
371 |
|
372 | getAccountStatus(address, tokenType) {
|
373 | return fetch(`${this.blockstackAPIUrl}/v1/accounts/${address}/${tokenType}/status`)
|
374 | .then((resp) => {
|
375 | if (resp.status === 404) {
|
376 | throw new Error('Account not found');
|
377 | }
|
378 | else if (resp.status !== 200) {
|
379 | throw new Error(`Bad response status: ${resp.status}`);
|
380 | }
|
381 | else {
|
382 | return resp.json();
|
383 | }
|
384 | }).then((accountStatus) => {
|
385 |
|
386 | const formattedStatus = Object.assign({}, accountStatus, {
|
387 | address: this.coerceAddress(accountStatus.address),
|
388 | debit_value: new bn_js_1.default(String(accountStatus.debit_value)),
|
389 | credit_value: new bn_js_1.default(String(accountStatus.credit_value))
|
390 | });
|
391 | return formattedStatus;
|
392 | });
|
393 | }
|
394 | |
395 |
|
396 |
|
397 |
|
398 |
|
399 |
|
400 |
|
401 | getAccountHistoryPage(address, page) {
|
402 | const url = `${this.blockstackAPIUrl}/v1/accounts/${address}/history?page=${page}`;
|
403 | return fetch(url)
|
404 | .then((resp) => {
|
405 | if (resp.status === 404) {
|
406 | throw new Error('Account not found');
|
407 | }
|
408 | else if (resp.status !== 200) {
|
409 | throw new Error(`Bad response status: ${resp.status}`);
|
410 | }
|
411 | else {
|
412 | return resp.json();
|
413 | }
|
414 | })
|
415 | .then((historyList) => {
|
416 | if (historyList.error) {
|
417 | throw new Error(`Unable to get account history page: ${historyList.error}`);
|
418 | }
|
419 |
|
420 | return historyList.map((histEntry) => {
|
421 | histEntry.address = this.coerceAddress(histEntry.address);
|
422 | histEntry.debit_value = new bn_js_1.default(String(histEntry.debit_value));
|
423 | histEntry.credit_value = new bn_js_1.default(String(histEntry.credit_value));
|
424 | return histEntry;
|
425 | });
|
426 | });
|
427 | }
|
428 | |
429 |
|
430 |
|
431 |
|
432 |
|
433 |
|
434 |
|
435 |
|
436 |
|
437 | getAccountAt(address, blockHeight) {
|
438 | const url = `${this.blockstackAPIUrl}/v1/accounts/${address}/history/${blockHeight}`;
|
439 | return fetch(url)
|
440 | .then((resp) => {
|
441 | if (resp.status === 404) {
|
442 | throw new Error('Account not found');
|
443 | }
|
444 | else if (resp.status !== 200) {
|
445 | throw new Error(`Bad response status: ${resp.status}`);
|
446 | }
|
447 | else {
|
448 | return resp.json();
|
449 | }
|
450 | })
|
451 | .then((historyList) => {
|
452 | if (historyList.error) {
|
453 | throw new Error(`Unable to get historic account state: ${historyList.error}`);
|
454 | }
|
455 |
|
456 | return historyList.map((histEntry) => {
|
457 | histEntry.address = this.coerceAddress(histEntry.address);
|
458 | histEntry.debit_value = new bn_js_1.default(String(histEntry.debit_value));
|
459 | histEntry.credit_value = new bn_js_1.default(String(histEntry.credit_value));
|
460 | return histEntry;
|
461 | });
|
462 | });
|
463 | }
|
464 | |
465 |
|
466 |
|
467 |
|
468 |
|
469 |
|
470 | getAccountTokens(address) {
|
471 | return fetch(`${this.blockstackAPIUrl}/v1/accounts/${address}/tokens`)
|
472 | .then((resp) => {
|
473 | if (resp.status === 404) {
|
474 | throw new Error('Account not found');
|
475 | }
|
476 | else if (resp.status !== 200) {
|
477 | throw new Error(`Bad response status: ${resp.status}`);
|
478 | }
|
479 | else {
|
480 | return resp.json();
|
481 | }
|
482 | })
|
483 | .then((tokenList) => {
|
484 | if (tokenList.error) {
|
485 | throw new Error(`Unable to get token list: ${tokenList.error}`);
|
486 | }
|
487 | return tokenList;
|
488 | });
|
489 | }
|
490 | |
491 |
|
492 |
|
493 |
|
494 |
|
495 |
|
496 |
|
497 |
|
498 | getAccountBalance(address, tokenType) {
|
499 | return fetch(`${this.blockstackAPIUrl}/v1/accounts/${address}/${tokenType}/balance`)
|
500 | .then((resp) => {
|
501 | if (resp.status === 404) {
|
502 |
|
503 | return Promise.resolve().then(() => new bn_js_1.default('0'));
|
504 | }
|
505 | else if (resp.status !== 200) {
|
506 | throw new Error(`Bad response status: ${resp.status}`);
|
507 | }
|
508 | else {
|
509 | return resp.json();
|
510 | }
|
511 | })
|
512 | .then((tokenBalance) => {
|
513 | if (tokenBalance.error) {
|
514 | throw new Error(`Unable to get account balance: ${tokenBalance.error}`);
|
515 | }
|
516 | let balance = '0';
|
517 | if (tokenBalance && tokenBalance.balance) {
|
518 | balance = tokenBalance.balance;
|
519 | }
|
520 | return new bn_js_1.default(balance);
|
521 | });
|
522 | }
|
523 | |
524 |
|
525 |
|
526 |
|
527 |
|
528 |
|
529 |
|
530 |
|
531 |
|
532 |
|
533 |
|
534 |
|
535 |
|
536 | broadcastServiceFetchHelper(endpoint, body) {
|
537 | const requestHeaders = {
|
538 | Accept: 'application/json',
|
539 | 'Content-Type': 'application/json'
|
540 | };
|
541 | const options = {
|
542 | method: 'POST',
|
543 | headers: requestHeaders,
|
544 | body: JSON.stringify(body)
|
545 | };
|
546 | const url = `${this.broadcastServiceUrl}/v1/broadcast/${endpoint}`;
|
547 | return fetch(url, options)
|
548 | .then((response) => {
|
549 | if (response.ok) {
|
550 | return response.json();
|
551 | }
|
552 | else {
|
553 | throw new errors_1.RemoteServiceError(response);
|
554 | }
|
555 | });
|
556 | }
|
557 | |
558 |
|
559 |
|
560 |
|
561 |
|
562 |
|
563 |
|
564 |
|
565 |
|
566 |
|
567 |
|
568 |
|
569 |
|
570 |
|
571 |
|
572 |
|
573 |
|
574 |
|
575 |
|
576 | broadcastTransaction(transaction, transactionToWatch = null, confirmations = 6) {
|
577 | if (!transaction) {
|
578 | const error = new errors_1.MissingParameterError('transaction');
|
579 | return Promise.reject(error);
|
580 | }
|
581 | if (!confirmations && confirmations !== 0) {
|
582 | const error = new errors_1.MissingParameterError('confirmations');
|
583 | return Promise.reject(error);
|
584 | }
|
585 | if (transactionToWatch === null) {
|
586 | return this.btc.broadcastTransaction(transaction);
|
587 | }
|
588 | else {
|
589 | |
590 |
|
591 |
|
592 |
|
593 |
|
594 |
|
595 |
|
596 |
|
597 |
|
598 | const endpoint = TX_BROADCAST_SERVICE_TX_ENDPOINT;
|
599 | const requestBody = {
|
600 | transaction,
|
601 | transactionToWatch,
|
602 | confirmations
|
603 | };
|
604 | return this.broadcastServiceFetchHelper(endpoint, requestBody);
|
605 | }
|
606 | }
|
607 | |
608 |
|
609 |
|
610 |
|
611 |
|
612 |
|
613 |
|
614 |
|
615 |
|
616 |
|
617 |
|
618 |
|
619 |
|
620 |
|
621 |
|
622 |
|
623 | broadcastZoneFile(zoneFile, transactionToWatch = null) {
|
624 | if (!zoneFile) {
|
625 | return Promise.reject(new errors_1.MissingParameterError('zoneFile'));
|
626 | }
|
627 |
|
628 | if (transactionToWatch) {
|
629 |
|
630 | |
631 |
|
632 |
|
633 |
|
634 |
|
635 |
|
636 |
|
637 |
|
638 | const requestBody = {
|
639 | zoneFile,
|
640 | transactionToWatch
|
641 | };
|
642 | const endpoint = TX_BROADCAST_SERVICE_ZONE_FILE_ENDPOINT;
|
643 | return this.broadcastServiceFetchHelper(endpoint, requestBody);
|
644 | }
|
645 | else {
|
646 |
|
647 |
|
648 | const requestBody = { zonefile: zoneFile };
|
649 | return fetch(`${this.blockstackAPIUrl}/v1/zonefile/`, {
|
650 | method: 'POST',
|
651 | body: JSON.stringify(requestBody),
|
652 | headers: {
|
653 | 'Content-Type': 'application/json'
|
654 | }
|
655 | })
|
656 | .then((resp) => {
|
657 | const json = resp.json();
|
658 | return json
|
659 | .then((respObj) => {
|
660 | if (respObj.hasOwnProperty('error')) {
|
661 | throw new errors_1.RemoteServiceError(resp);
|
662 | }
|
663 | return respObj.servers;
|
664 | });
|
665 | });
|
666 | }
|
667 | }
|
668 | |
669 |
|
670 |
|
671 |
|
672 |
|
673 |
|
674 |
|
675 |
|
676 |
|
677 |
|
678 |
|
679 |
|
680 |
|
681 |
|
682 |
|
683 |
|
684 |
|
685 |
|
686 |
|
687 |
|
688 |
|
689 |
|
690 |
|
691 |
|
692 |
|
693 |
|
694 |
|
695 |
|
696 | broadcastNameRegistration(preorderTransaction, registerTransaction, zoneFile) {
|
697 | |
698 |
|
699 |
|
700 |
|
701 |
|
702 |
|
703 |
|
704 |
|
705 |
|
706 | if (!preorderTransaction) {
|
707 | const error = new errors_1.MissingParameterError('preorderTransaction');
|
708 | return Promise.reject(error);
|
709 | }
|
710 | if (!registerTransaction) {
|
711 | const error = new errors_1.MissingParameterError('registerTransaction');
|
712 | return Promise.reject(error);
|
713 | }
|
714 | if (!zoneFile) {
|
715 | const error = new errors_1.MissingParameterError('zoneFile');
|
716 | return Promise.reject(error);
|
717 | }
|
718 | const requestBody = {
|
719 | preorderTransaction,
|
720 | registerTransaction,
|
721 | zoneFile
|
722 | };
|
723 | const endpoint = TX_BROADCAST_SERVICE_REGISTRATION_ENDPOINT;
|
724 | return this.broadcastServiceFetchHelper(endpoint, requestBody);
|
725 | }
|
726 | getFeeRate() {
|
727 | return fetch('https://bitcoinfees.earn.com/api/v1/fees/recommended')
|
728 | .then(resp => resp.json())
|
729 | .then(rates => Math.floor(rates.fastestFee));
|
730 | }
|
731 | countDustOutputs() {
|
732 | throw new Error('Not implemented.');
|
733 | }
|
734 | getUTXOs(address) {
|
735 | return this.getNetworkedUTXOs(address)
|
736 | .then((networkedUTXOs) => {
|
737 | let returnSet = networkedUTXOs.concat();
|
738 | if (this.includeUtxoMap.hasOwnProperty(address)) {
|
739 | returnSet = networkedUTXOs.concat(this.includeUtxoMap[address]);
|
740 | }
|
741 |
|
742 |
|
743 | const excludeSet = this.excludeUtxoSet;
|
744 | returnSet = returnSet.filter((utxo) => {
|
745 | const inExcludeSet = excludeSet.reduce((inSet, utxoToCheck) => inSet || (utxoToCheck.tx_hash === utxo.tx_hash
|
746 | && utxoToCheck.tx_output_n === utxo.tx_output_n), false);
|
747 | return !inExcludeSet;
|
748 | });
|
749 | return returnSet;
|
750 | });
|
751 | }
|
752 | |
753 |
|
754 |
|
755 |
|
756 |
|
757 |
|
758 |
|
759 |
|
760 | modifyUTXOSetFrom(txHex) {
|
761 | const tx = bitcoinjs_lib_1.default.Transaction.fromHex(txHex);
|
762 | const excludeSet = this.excludeUtxoSet.concat();
|
763 | tx.ins.forEach((utxoUsed) => {
|
764 | const reverseHash = Buffer.from(utxoUsed.hash);
|
765 | reverseHash.reverse();
|
766 | excludeSet.push({
|
767 | tx_hash: reverseHash.toString('hex'),
|
768 | tx_output_n: utxoUsed.index
|
769 | });
|
770 | });
|
771 | this.excludeUtxoSet = excludeSet;
|
772 | const txHash = Buffer.from(tx.getHash().reverse()).toString('hex');
|
773 | tx.outs.forEach((utxoCreated, txOutputN) => {
|
774 | const isNullData = function isNullData(script) {
|
775 | try {
|
776 | bitcoinjs_lib_1.default.payments.embed({ output: script }, { validate: true });
|
777 | return true;
|
778 | }
|
779 | catch (_) {
|
780 | return false;
|
781 | }
|
782 | };
|
783 | if (isNullData(utxoCreated.script)) {
|
784 | return;
|
785 | }
|
786 | const address = bitcoinjs_lib_1.default.address.fromOutputScript(utxoCreated.script, this.layer1);
|
787 | let includeSet = [];
|
788 | if (this.includeUtxoMap.hasOwnProperty(address)) {
|
789 | includeSet = includeSet.concat(this.includeUtxoMap[address]);
|
790 | }
|
791 | includeSet.push({
|
792 | tx_hash: txHash,
|
793 | confirmations: 0,
|
794 | value: utxoCreated.value,
|
795 | tx_output_n: txOutputN
|
796 | });
|
797 | this.includeUtxoMap[address] = includeSet;
|
798 | });
|
799 | }
|
800 | resetUTXOs(address) {
|
801 | delete this.includeUtxoMap[address];
|
802 | this.excludeUtxoSet = [];
|
803 | }
|
804 | getConsensusHash() {
|
805 | return fetch(`${this.blockstackAPIUrl}/v1/blockchains/bitcoin/consensus`)
|
806 | .then(resp => resp.json())
|
807 | .then(x => x.consensus_hash);
|
808 | }
|
809 | getTransactionInfo(txHash) {
|
810 | return this.btc.getTransactionInfo(txHash);
|
811 | }
|
812 | getBlockHeight() {
|
813 | return this.btc.getBlockHeight();
|
814 | }
|
815 | getNetworkedUTXOs(address) {
|
816 | return this.btc.getNetworkedUTXOs(address);
|
817 | }
|
818 | }
|
819 | exports.BlockstackNetwork = BlockstackNetwork;
|
820 | class LocalRegtest extends BlockstackNetwork {
|
821 | constructor(apiUrl, broadcastServiceUrl, bitcoinAPI) {
|
822 | super(apiUrl, broadcastServiceUrl, bitcoinAPI, bitcoinjs_lib_1.default.networks.testnet);
|
823 | }
|
824 | getFeeRate() {
|
825 | return Promise.resolve(Math.floor(0.00001000 * SATOSHIS_PER_BTC));
|
826 | }
|
827 | }
|
828 | exports.LocalRegtest = LocalRegtest;
|
829 | class BitcoindAPI extends BitcoinNetwork {
|
830 | constructor(bitcoindUrl, bitcoindCredentials) {
|
831 | super();
|
832 | this.bitcoindUrl = bitcoindUrl;
|
833 | this.bitcoindCredentials = bitcoindCredentials;
|
834 | this.importedBefore = {};
|
835 | }
|
836 | broadcastTransaction(transaction) {
|
837 | const jsonRPC = {
|
838 | jsonrpc: '1.0',
|
839 | method: 'sendrawtransaction',
|
840 | params: [transaction]
|
841 | };
|
842 | const authString = Buffer.from(`${this.bitcoindCredentials.username}:${this.bitcoindCredentials.password}`)
|
843 | .toString('base64');
|
844 | const headers = { Authorization: `Basic ${authString}` };
|
845 | return fetch(this.bitcoindUrl, {
|
846 | method: 'POST',
|
847 | body: JSON.stringify(jsonRPC),
|
848 | headers
|
849 | })
|
850 | .then(resp => resp.json())
|
851 | .then(respObj => respObj.result);
|
852 | }
|
853 | getBlockHeight() {
|
854 | const jsonRPC = {
|
855 | jsonrpc: '1.0',
|
856 | method: 'getblockcount'
|
857 | };
|
858 | const authString = Buffer.from(`${this.bitcoindCredentials.username}:${this.bitcoindCredentials.password}`)
|
859 | .toString('base64');
|
860 | const headers = { Authorization: `Basic ${authString}` };
|
861 | return fetch(this.bitcoindUrl, {
|
862 | method: 'POST',
|
863 | body: JSON.stringify(jsonRPC),
|
864 | headers
|
865 | })
|
866 | .then(resp => resp.json())
|
867 | .then(respObj => respObj.result);
|
868 | }
|
869 | getTransactionInfo(txHash) {
|
870 | const jsonRPC = {
|
871 | jsonrpc: '1.0',
|
872 | method: 'gettransaction',
|
873 | params: [txHash]
|
874 | };
|
875 | const authString = Buffer.from(`${this.bitcoindCredentials.username}:${this.bitcoindCredentials.password}`)
|
876 | .toString('base64');
|
877 | const headers = { Authorization: `Basic ${authString}` };
|
878 | return fetch(this.bitcoindUrl, {
|
879 | method: 'POST',
|
880 | body: JSON.stringify(jsonRPC),
|
881 | headers
|
882 | })
|
883 | .then(resp => resp.json())
|
884 | .then(respObj => respObj.result)
|
885 | .then(txInfo => txInfo.blockhash)
|
886 | .then((blockhash) => {
|
887 | const jsonRPCBlock = {
|
888 | jsonrpc: '1.0',
|
889 | method: 'getblockheader',
|
890 | params: [blockhash]
|
891 | };
|
892 | headers.Authorization = `Basic ${authString}`;
|
893 | return fetch(this.bitcoindUrl, {
|
894 | method: 'POST',
|
895 | body: JSON.stringify(jsonRPCBlock),
|
896 | headers
|
897 | });
|
898 | })
|
899 | .then(resp => resp.json())
|
900 | .then((respObj) => {
|
901 | if (!respObj || !respObj.result) {
|
902 |
|
903 | throw new Error('Unconfirmed transaction');
|
904 | }
|
905 | else {
|
906 | return { block_height: respObj.result.height };
|
907 | }
|
908 | });
|
909 | }
|
910 | getNetworkedUTXOs(address) {
|
911 | const jsonRPCImport = {
|
912 | jsonrpc: '1.0',
|
913 | method: 'importaddress',
|
914 | params: [address]
|
915 | };
|
916 | const jsonRPCUnspent = {
|
917 | jsonrpc: '1.0',
|
918 | method: 'listunspent',
|
919 | params: [0, 9999999, [address]]
|
920 | };
|
921 | const authString = Buffer.from(`${this.bitcoindCredentials.username}:${this.bitcoindCredentials.password}`)
|
922 | .toString('base64');
|
923 | const headers = { Authorization: `Basic ${authString}` };
|
924 | const importPromise = (this.importedBefore[address])
|
925 | ? Promise.resolve()
|
926 | : fetch(this.bitcoindUrl, {
|
927 | method: 'POST',
|
928 | body: JSON.stringify(jsonRPCImport),
|
929 | headers
|
930 | })
|
931 | .then(() => { this.importedBefore[address] = true; });
|
932 | return importPromise
|
933 | .then(() => fetch(this.bitcoindUrl, {
|
934 | method: 'POST',
|
935 | body: JSON.stringify(jsonRPCUnspent),
|
936 | headers
|
937 | }))
|
938 | .then(resp => resp.json())
|
939 | .then(x => x.result)
|
940 | .then(utxos => utxos.map((x) => ({
|
941 | value: Math.round(x.amount * SATOSHIS_PER_BTC),
|
942 | confirmations: x.confirmations,
|
943 | tx_hash: x.txid,
|
944 | tx_output_n: x.vout
|
945 | })));
|
946 | }
|
947 | }
|
948 | exports.BitcoindAPI = BitcoindAPI;
|
949 | class InsightClient extends BitcoinNetwork {
|
950 | constructor(insightUrl = 'https://utxo.technofractal.com/') {
|
951 | super();
|
952 | this.apiUrl = insightUrl;
|
953 | }
|
954 | broadcastTransaction(transaction) {
|
955 | const jsonData = { rawtx: transaction };
|
956 | return fetch(`${this.apiUrl}/tx/send`, {
|
957 | method: 'POST',
|
958 | headers: { 'Content-Type': 'application/json' },
|
959 | body: JSON.stringify(jsonData)
|
960 | })
|
961 | .then(resp => resp.json());
|
962 | }
|
963 | getBlockHeight() {
|
964 | return fetch(`${this.apiUrl}/status`)
|
965 | .then(resp => resp.json())
|
966 | .then(status => status.blocks);
|
967 | }
|
968 | getTransactionInfo(txHash) {
|
969 | return fetch(`${this.apiUrl}/tx/${txHash}`)
|
970 | .then(resp => resp.json())
|
971 | .then((transactionInfo) => {
|
972 | if (transactionInfo.error) {
|
973 | throw new Error(`Error finding transaction: ${transactionInfo.error}`);
|
974 | }
|
975 | return fetch(`${this.apiUrl}/block/${transactionInfo.blockHash}`);
|
976 | })
|
977 | .then(resp => resp.json())
|
978 | .then(blockInfo => ({ block_height: blockInfo.height }));
|
979 | }
|
980 | getNetworkedUTXOs(address) {
|
981 | return fetch(`${this.apiUrl}/addr/${address}/utxo`)
|
982 | .then(resp => resp.json())
|
983 | .then(utxos => utxos.map((x) => ({
|
984 | value: x.satoshis,
|
985 | confirmations: x.confirmations,
|
986 | tx_hash: x.txid,
|
987 | tx_output_n: x.vout
|
988 | })));
|
989 | }
|
990 | }
|
991 | exports.InsightClient = InsightClient;
|
992 | class BlockchainInfoApi extends BitcoinNetwork {
|
993 | constructor(blockchainInfoUrl = 'https://blockchain.info') {
|
994 | super();
|
995 | this.utxoProviderUrl = blockchainInfoUrl;
|
996 | }
|
997 | getBlockHeight() {
|
998 | return fetch(`${this.utxoProviderUrl}/latestblock?cors=true`)
|
999 | .then(resp => resp.json())
|
1000 | .then(blockObj => blockObj.height);
|
1001 | }
|
1002 | getNetworkedUTXOs(address) {
|
1003 | return fetch(`${this.utxoProviderUrl}/unspent?format=json&active=${address}&cors=true`)
|
1004 | .then((resp) => {
|
1005 | if (resp.status === 500) {
|
1006 | logger_1.Logger.debug('UTXO provider 500 usually means no UTXOs: returning []');
|
1007 | return {
|
1008 | unspent_outputs: []
|
1009 | };
|
1010 | }
|
1011 | else {
|
1012 | return resp.json();
|
1013 | }
|
1014 | })
|
1015 | .then(utxoJSON => utxoJSON.unspent_outputs)
|
1016 | .then(utxoList => utxoList.map((utxo) => {
|
1017 | const utxoOut = {
|
1018 | value: utxo.value,
|
1019 | tx_output_n: utxo.tx_output_n,
|
1020 | confirmations: utxo.confirmations,
|
1021 | tx_hash: utxo.tx_hash_big_endian
|
1022 | };
|
1023 | return utxoOut;
|
1024 | }));
|
1025 | }
|
1026 | getTransactionInfo(txHash) {
|
1027 | return fetch(`${this.utxoProviderUrl}/rawtx/${txHash}?cors=true`)
|
1028 | .then((resp) => {
|
1029 | if (resp.status === 200) {
|
1030 | return resp.json();
|
1031 | }
|
1032 | else {
|
1033 | throw new Error(`Could not lookup transaction info for '${txHash}'. Server error.`);
|
1034 | }
|
1035 | })
|
1036 | .then(respObj => ({ block_height: respObj.block_height }));
|
1037 | }
|
1038 | broadcastTransaction(transaction) {
|
1039 | const form = new form_data_1.default();
|
1040 | form.append('tx', transaction);
|
1041 | return fetch(`${this.utxoProviderUrl}/pushtx?cors=true`, {
|
1042 | method: 'POST',
|
1043 | body: form
|
1044 | })
|
1045 | .then((resp) => {
|
1046 | const text = resp.text();
|
1047 | return text
|
1048 | .then((respText) => {
|
1049 | if (respText.toLowerCase().indexOf('transaction submitted') >= 0) {
|
1050 | const txHash = Buffer.from(bitcoinjs_lib_1.default.Transaction.fromHex(transaction)
|
1051 | .getHash()
|
1052 | .reverse()).toString('hex');
|
1053 | return txHash;
|
1054 | }
|
1055 | else {
|
1056 | throw new errors_1.RemoteServiceError(resp, `Broadcast transaction failed with message: ${respText}`);
|
1057 | }
|
1058 | });
|
1059 | });
|
1060 | }
|
1061 | }
|
1062 | exports.BlockchainInfoApi = BlockchainInfoApi;
|
1063 | const LOCAL_REGTEST = new LocalRegtest('http://localhost:16268', 'http://localhost:16269', new BitcoindAPI('http://localhost:18332/', { username: 'blockstack', password: 'blockstacksystem' }));
|
1064 | const MAINNET_DEFAULT = new BlockstackNetwork('https://core.blockstack.org', 'https://broadcast.blockstack.org', new BlockchainInfoApi());
|
1065 | exports.network = {
|
1066 | BlockstackNetwork,
|
1067 | LocalRegtest,
|
1068 | BlockchainInfoApi,
|
1069 | BitcoindAPI,
|
1070 | InsightClient,
|
1071 | defaults: { LOCAL_REGTEST, MAINNET_DEFAULT }
|
1072 | };
|
1073 |
|
\ | No newline at end of file |