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