UNPKG

43.6 kBJavaScriptView Raw
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3 return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5Object.defineProperty(exports, "__esModule", { value: true });
6const bitcoinjs_lib_1 = __importDefault(require("bitcoinjs-lib"));
7const form_data_1 = __importDefault(require("form-data"));
8const bn_js_1 = __importDefault(require("bn.js"));
9const ripemd160_1 = __importDefault(require("ripemd160"));
10const errors_1 = require("./errors");
11const logger_1 = require("./logger");
12const SATOSHIS_PER_BTC = 1e8;
13const TX_BROADCAST_SERVICE_ZONE_FILE_ENDPOINT = 'zone-file';
14const TX_BROADCAST_SERVICE_REGISTRATION_ENDPOINT = 'registration';
15const TX_BROADCAST_SERVICE_TX_ENDPOINT = 'transaction';
16class 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}
30exports.BitcoinNetwork = BitcoinNetwork;
31class 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 * Get the price of a name via the legacy /v1/prices API endpoint.
65 * @param {String} fullyQualifiedName the name to query
66 * @return {Promise} a promise to an Object with { units: String, amount: BigInteger }
67 * @private
68 */
69 getNamePriceV1(fullyQualifiedName) {
70 // legacy code path
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 * Get the price of a namespace via the legacy /v1/prices API endpoint.
96 * @param {String} namespaceID the namespace to query
97 * @return {Promise} a promise to an Object with { units: String, amount: BigInteger }
98 * @private
99 */
100 getNamespacePriceV1(namespaceID) {
101 // legacy code path
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 * Get the price of a name via the /v2/prices API endpoint.
126 * @param {String} fullyQualifiedName the name to query
127 * @return {Promise} a promise to an Object with { units: String, amount: BigInteger }
128 * @private
129 */
130 getNamePriceV2(fullyQualifiedName) {
131 return fetch(`${this.blockstackAPIUrl}/v2/prices/names/${fullyQualifiedName}`)
132 .then((resp) => {
133 if (resp.status !== 200) {
134 // old core node
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 // must be at least dust-minimum
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 * Get the price of a namespace via the /v2/prices API endpoint.
161 * @param {String} namespaceID the namespace to query
162 * @return {Promise} a promise to an Object with { units: String, amount: BigInteger }
163 * @private
164 */
165 getNamespacePriceV2(namespaceID) {
166 return fetch(`${this.blockstackAPIUrl}/v2/prices/namespaces/${namespaceID}`)
167 .then((resp) => {
168 if (resp.status !== 200) {
169 // old core node
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 // must be at least dust-minimum
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 * Get the price of a name.
195 * @param {String} fullyQualifiedName the name to query
196 * @return {Promise} a promise to an Object with { units: String, amount: BigInteger }, where
197 * .units encodes the cryptocurrency units to pay (e.g. BTC, STACKS), and
198 * .amount encodes the number of units, in the smallest denominiated amount
199 * (e.g. if .units is BTC, .amount will be satoshis; if .units is STACKS,
200 * .amount will be microStacks)
201 */
202 getNamePrice(fullyQualifiedName) {
203 // handle v1 or v2
204 return Promise.resolve().then(() => this.getNamePriceV2(fullyQualifiedName))
205 .catch(() => this.getNamePriceV1(fullyQualifiedName));
206 }
207 /**
208 * Get the price of a namespace
209 * @param {String} namespaceID the namespace to query
210 * @return {Promise} a promise to an Object with { units: String, amount: BigInteger }, where
211 * .units encodes the cryptocurrency units to pay (e.g. BTC, STACKS), and
212 * .amount encodes the number of units, in the smallest denominiated amount
213 * (e.g. if .units is BTC, .amount will be satoshis; if .units is STACKS,
214 * .amount will be microStacks)
215 */
216 getNamespacePrice(namespaceID) {
217 // handle v1 or v2
218 return Promise.resolve().then(() => this.getNamespacePriceV2(namespaceID))
219 .catch(() => this.getNamespacePriceV1(namespaceID));
220 }
221 /**
222 * How many blocks can pass between a name expiring and the name being able to be
223 * re-registered by a different owner?
224 * @param {string} fullyQualifiedName unused
225 * @return {Promise} a promise to the number of blocks
226 */
227 getGracePeriod(fullyQualifiedName) {
228 return Promise.resolve(5000);
229 }
230 /**
231 * Get the names -- both on-chain and off-chain -- owned by an address.
232 * @param {String} address the blockchain address (the hash of the owner public key)
233 * @return {Promise} a promise that resolves to a list of names (Strings)
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 * Get the blockchain address to which a name's registration fee must be sent
243 * (the address will depend on the namespace in which it is registered.)
244 * @param {String} namespace the namespace ID
245 * @return {Promise} a promise that resolves to an address (String)
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 // pay-to-namespace-creator if this namespace is less than 1 year old
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 * Get WHOIS-like information for a name, including the address that owns it,
274 * the block at which it expires, and the zone file anchored to it (if available).
275 * @param {String} fullyQualifiedName the name to query. Can be on-chain of off-chain.
276 * @return {Promise} a promise that resolves to the WHOIS-like information
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 // the returned address _should_ be in the correct network ---
296 // blockstackd gets into trouble because it tries to coerce back to mainnet
297 // and the regtest transaction generation libraries want to use testnet addresses
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 * Get the pricing parameters and creation history of a namespace.
308 * @param {String} namespaceID the namespace to query
309 * @return {Promise} a promise that resolves to the namespace information.
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 // the returned address _should_ be in the correct network ---
326 // blockstackd gets into trouble because it tries to coerce back to mainnet
327 // and the regtest transaction generation libraries want to use testnet addresses
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 * Get a zone file, given its hash. Throws an exception if the zone file
341 * obtained does not match the hash.
342 * @param {String} zonefileHash the ripemd160(sha256) hash of the zone file
343 * @return {Promise} a promise that resolves to the zone file's text
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 * Get the status of an account for a particular token holding. This includes its total number of
366 * expenditures and credits, lockup times, last txid, and so on.
367 * @param {String} address the account
368 * @param {String} tokenType the token type to query
369 * @return {Promise} a promise that resolves to an object representing the state of the account
370 * for this token
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 // coerce all addresses, and convert credit/debit to biginteger
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 * Get a page of an account's transaction history.
396 * @param {String} address the account's address
397 * @param {number} page the page number. Page 0 is the most recent transactions
398 * @return {Promise} a promise that resolves to an Array of Objects, where each Object encodes
399 * states of the account at various block heights (e.g. prior balances, txids, etc)
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 // coerse all addresses and convert to bigint
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 * Get the state(s) of an account at a particular block height. This includes the state of the
430 * account beginning with this block's transactions, as well as all of the states the account
431 * passed through when this block was processed (if any).
432 * @param {String} address the account's address
433 * @param {Integer} blockHeight the block to query
434 * @return {Promise} a promise that resolves to an Array of Objects, where each Object encodes
435 * states of the account at this block.
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 // coerce all addresses
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 * Get the set of token types that this account owns
466 * @param {String} address the account's address
467 * @return {Promise} a promise that resolves to an Array of Strings, where each item encodes the
468 * type of token this account holds (excluding the underlying blockchain's tokens)
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 * Get the number of tokens owned by an account. If the account does not exist or has no
492 * tokens of this type, then 0 will be returned.
493 * @param {String} address the account's address
494 * @param {String} tokenType the type of token to query.
495 * @return {Promise} a promise that resolves to a BigInteger that encodes the number of tokens
496 * held by this account.
497 */
498 getAccountBalance(address, tokenType) {
499 return fetch(`${this.blockstackAPIUrl}/v1/accounts/${address}/${tokenType}/balance`)
500 .then((resp) => {
501 if (resp.status === 404) {
502 // talking to an older blockstack core node without the accounts API
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 * Performs a POST request to the given URL
525 * @param {String} endpoint the name of
526 * @param {String} body [description]
527 * @return {Promise<Object|Error>} Returns a `Promise` that resolves to the object requested.
528 * In the event of an error, it rejects with:
529 * * a `RemoteServiceError` if there is a problem
530 * with the transaction broadcast service
531 * * `MissingParameterError` if you call the function without a required
532 * parameter
533 *
534 * @private
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 * Broadcasts a signed bitcoin transaction to the network optionally waiting to broadcast the
559 * transaction until a second transaction has a certain number of confirmations.
560 *
561 * @param {string} transaction the hex-encoded transaction to broadcast
562 * @param {string} transactionToWatch the hex transaction id of the transaction to watch for
563 * the specified number of confirmations before broadcasting the `transaction`
564 * @param {number} confirmations the number of confirmations `transactionToWatch` must have
565 * before broadcasting `transaction`.
566 * @return {Promise<Object|Error>} Returns a Promise that resolves to an object with a
567 * `transaction_hash` key containing the transaction hash of the broadcasted transaction.
568 *
569 * In the event of an error, it rejects with:
570 * * a `RemoteServiceError` if there is a problem
571 * with the transaction broadcast service
572 * * `MissingParameterError` if you call the function without a required
573 * parameter
574 * @private
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 * POST /v1/broadcast/transaction
591 * Request body:
592 * JSON.stringify({
593 * transaction,
594 * transactionToWatch,
595 * confirmations
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 * Broadcasts a zone file to the Atlas network via the transaction broadcast service.
609 *
610 * @param {String} zoneFile the zone file to be broadcast to the Atlas network
611 * @param {String} transactionToWatch the hex transaction id of the transaction
612 * to watch for confirmation before broadcasting the zone file to the Atlas network
613 * @return {Promise<Object|Error>} Returns a Promise that resolves to an object with a
614 * `transaction_hash` key containing the transaction hash of the broadcasted transaction.
615 *
616 * In the event of an error, it rejects with:
617 * * a `RemoteServiceError` if there is a problem
618 * with the transaction broadcast service
619 * * `MissingParameterError` if you call the function without a required
620 * parameter
621 * @private
622 */
623 broadcastZoneFile(zoneFile, transactionToWatch = null) {
624 if (!zoneFile) {
625 return Promise.reject(new errors_1.MissingParameterError('zoneFile'));
626 }
627 // TODO: validate zonefile
628 if (transactionToWatch) {
629 // broadcast via transaction broadcast service
630 /*
631 * POST /v1/broadcast/zone-file
632 * Request body:
633 * JSON.stringify({
634 * zoneFile,
635 * transactionToWatch
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 // broadcast via core endpoint
647 // zone file is two words but core's api treats it as one word 'zonefile'
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 * Sends the preorder and registration transactions and zone file
670 * for a Blockstack name registration
671 * along with the to the transaction broadcast service.
672 *
673 * The transaction broadcast:
674 *
675 * * immediately broadcasts the preorder transaction
676 * * broadcasts the register transactions after the preorder transaction
677 * has an appropriate number of confirmations
678 * * broadcasts the zone file to the Atlas network after the register transaction
679 * has an appropriate number of confirmations
680 *
681 * @param {String} preorderTransaction the hex-encoded, signed preorder transaction generated
682 * using the `makePreorder` function
683 * @param {String} registerTransaction the hex-encoded, signed register transaction generated
684 * using the `makeRegister` function
685 * @param {String} zoneFile the zone file to be broadcast to the Atlas network
686 * @return {Promise<Object|Error>} Returns a Promise that resolves to an object with a
687 * `transaction_hash` key containing the transaction hash of the broadcasted transaction.
688 *
689 * In the event of an error, it rejects with:
690 * * a `RemoteServiceError` if there is a problem
691 * with the transaction broadcast service
692 * * `MissingParameterError` if you call the function without a required
693 * parameter
694 * @private
695 */
696 broadcastNameRegistration(preorderTransaction, registerTransaction, zoneFile) {
697 /*
698 * POST /v1/broadcast/registration
699 * Request body:
700 * JSON.stringify({
701 * preorderTransaction,
702 * registerTransaction,
703 * zoneFile
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 // aaron: I am *well* aware this is O(n)*O(m) runtime
742 // however, clients should clear the exclude set periodically
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 * This will modify the network's utxo set to include UTXOs
754 * from the given transaction and exclude UTXOs *spent* in
755 * that transaction
756 * @param {String} txHex - the hex-encoded transaction to use
757 * @return {void} no return value, this modifies the UTXO config state
758 * @private
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}
819exports.BlockstackNetwork = BlockstackNetwork;
820class 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}
828exports.LocalRegtest = LocalRegtest;
829class 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 // unconfirmed
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}
948exports.BitcoindAPI = BitcoindAPI;
949class 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}
991exports.InsightClient = InsightClient;
992class 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'); // big_endian
1053 return txHash;
1054 }
1055 else {
1056 throw new errors_1.RemoteServiceError(resp, `Broadcast transaction failed with message: ${respText}`);
1057 }
1058 });
1059 });
1060 }
1061}
1062exports.BlockchainInfoApi = BlockchainInfoApi;
1063const LOCAL_REGTEST = new LocalRegtest('http://localhost:16268', 'http://localhost:16269', new BitcoindAPI('http://localhost:18332/', { username: 'blockstack', password: 'blockstacksystem' }));
1064const MAINNET_DEFAULT = new BlockstackNetwork('https://core.blockstack.org', 'https://broadcast.blockstack.org', new BlockchainInfoApi());
1065exports.network = {
1066 BlockstackNetwork,
1067 LocalRegtest,
1068 BlockchainInfoApi,
1069 BitcoindAPI,
1070 InsightClient,
1071 defaults: { LOCAL_REGTEST, MAINNET_DEFAULT }
1072};
1073//# sourceMappingURL=network.js.map
\No newline at end of file