UNPKG

45.1 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 = 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 config_1 = require("./config");
13const fetchUtil_1 = require("./fetchUtil");
14const SATOSHIS_PER_BTC = 1e8;
15const TX_BROADCAST_SERVICE_ZONE_FILE_ENDPOINT = 'zone-file';
16const TX_BROADCAST_SERVICE_REGISTRATION_ENDPOINT = 'registration';
17const TX_BROADCAST_SERVICE_TX_ENDPOINT = 'transaction';
18/**
19 * @private
20 * @ignore
21 */
22class 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}
36exports.BitcoinNetwork = BitcoinNetwork;
37/**
38 * @private
39 * @ignore
40 */
41class 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 * @ignore
72 */
73 getDefaultBurnAddress() {
74 return this.coerceAddress('1111111111111111111114oLvT2');
75 }
76 /**
77 * Get the price of a name via the legacy /v1/prices API endpoint.
78 * @param {String} fullyQualifiedName the name to query
79 * @return {Promise} a promise to an Object with { units: String, amount: BigInteger }
80 * @private
81 */
82 getNamePriceV1(fullyQualifiedName) {
83 // legacy code path
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 * Get the price of a namespace via the legacy /v1/prices API endpoint.
109 * @param {String} namespaceID the namespace to query
110 * @return {Promise} a promise to an Object with { units: String, amount: BigInteger }
111 * @private
112 */
113 getNamespacePriceV1(namespaceID) {
114 // legacy code path
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 * Get the price of a name via the /v2/prices API endpoint.
139 * @param {String} fullyQualifiedName the name to query
140 * @return {Promise} a promise to an Object with { units: String, amount: BigInteger }
141 * @private
142 */
143 getNamePriceV2(fullyQualifiedName) {
144 return fetchUtil_1.fetchPrivate(`${this.blockstackAPIUrl}/v2/prices/names/${fullyQualifiedName}`)
145 .then((resp) => {
146 if (resp.status !== 200) {
147 // old core node
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 // must be at least dust-minimum
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 * Get the price of a namespace via the /v2/prices API endpoint.
174 * @param {String} namespaceID the namespace to query
175 * @return {Promise} a promise to an Object with { units: String, amount: BigInteger }
176 * @private
177 */
178 getNamespacePriceV2(namespaceID) {
179 return fetchUtil_1.fetchPrivate(`${this.blockstackAPIUrl}/v2/prices/namespaces/${namespaceID}`)
180 .then((resp) => {
181 if (resp.status !== 200) {
182 // old core node
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 // must be at least dust-minimum
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 * Get the price of a name.
208 * @param {String} fullyQualifiedName the name to query
209 * @return {Promise} a promise to an Object with { units: String, amount: BigInteger }, where
210 * .units encodes the cryptocurrency units to pay (e.g. BTC, STACKS), and
211 * .amount encodes the number of units, in the smallest denominiated amount
212 * (e.g. if .units is BTC, .amount will be satoshis; if .units is STACKS,
213 * .amount will be microStacks)
214 */
215 getNamePrice(fullyQualifiedName) {
216 // handle v1 or v2
217 return Promise.resolve().then(() => this.getNamePriceV2(fullyQualifiedName))
218 .catch(() => this.getNamePriceV1(fullyQualifiedName));
219 }
220 /**
221 * Get the price of a namespace
222 * @param {String} namespaceID the namespace to query
223 * @return {Promise} a promise to an Object with { units: String, amount: BigInteger }, where
224 * .units encodes the cryptocurrency units to pay (e.g. BTC, STACKS), and
225 * .amount encodes the number of units, in the smallest denominiated amount
226 * (e.g. if .units is BTC, .amount will be satoshis; if .units is STACKS,
227 * .amount will be microStacks)
228 */
229 getNamespacePrice(namespaceID) {
230 // handle v1 or v2
231 return Promise.resolve().then(() => this.getNamespacePriceV2(namespaceID))
232 .catch(() => this.getNamespacePriceV1(namespaceID));
233 }
234 /**
235 * How many blocks can pass between a name expiring and the name being able to be
236 * re-registered by a different owner?
237 * @param {string} fullyQualifiedName unused
238 * @return {Promise} a promise to the number of blocks
239 */
240 getGracePeriod(fullyQualifiedName) {
241 return Promise.resolve(5000);
242 }
243 /**
244 * Get the names -- both on-chain and off-chain -- owned by an address.
245 * @param {String} address the blockchain address (the hash of the owner public key)
246 * @return {Promise} a promise that resolves to a list of names (Strings)
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 * Get the blockchain address to which a name's registration fee must be sent
256 * (the address will depend on the namespace in which it is registered.)
257 * @param {String} namespace the namespace ID
258 * @return {Promise} a promise that resolves to an address (String)
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 // pay-to-namespace-creator if this namespace is less than 1 year old
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 * Get WHOIS-like information for a name, including the address that owns it,
287 * the block at which it expires, and the zone file anchored to it (if available).
288 * @param {String} fullyQualifiedName the name to query. Can be on-chain of off-chain.
289 * @return {Promise} a promise that resolves to the WHOIS-like information
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 // the returned address _should_ be in the correct network ---
309 // blockstackd gets into trouble because it tries to coerce back to mainnet
310 // and the regtest transaction generation libraries want to use testnet addresses
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 * Get the pricing parameters and creation history of a namespace.
321 * @param {String} namespaceID the namespace to query
322 * @return {Promise} a promise that resolves to the namespace information.
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 // the returned address _should_ be in the correct network ---
339 // blockstackd gets into trouble because it tries to coerce back to mainnet
340 // and the regtest transaction generation libraries want to use testnet addresses
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 * Get a zone file, given its hash. Throws an exception if the zone file
354 * obtained does not match the hash.
355 * @param {String} zonefileHash the ripemd160(sha256) hash of the zone file
356 * @return {Promise} a promise that resolves to the zone file's text
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 * Get the status of an account for a particular token holding. This includes its total number of
379 * expenditures and credits, lockup times, last txid, and so on.
380 * @param {String} address the account
381 * @param {String} tokenType the token type to query
382 * @return {Promise} a promise that resolves to an object representing the state of the account
383 * for this token
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 // coerce all addresses, and convert credit/debit to biginteger
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 * Get a page of an account's transaction history.
409 * @param {String} address the account's address
410 * @param {number} page the page number. Page 0 is the most recent transactions
411 * @return {Promise} a promise that resolves to an Array of Objects, where each Object encodes
412 * states of the account at various block heights (e.g. prior balances, txids, etc)
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 // coerse all addresses and convert to bigint
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 * Get the state(s) of an account at a particular block height. This includes the state of the
443 * account beginning with this block's transactions, as well as all of the states the account
444 * passed through when this block was processed (if any).
445 * @param {String} address the account's address
446 * @param {Integer} blockHeight the block to query
447 * @return {Promise} a promise that resolves to an Array of Objects, where each Object encodes
448 * states of the account at this block.
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 // coerce all addresses
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 * Get the set of token types that this account owns
479 * @param {String} address the account's address
480 * @return {Promise} a promise that resolves to an Array of Strings, where each item encodes the
481 * type of token this account holds (excluding the underlying blockchain's tokens)
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 * Get the number of tokens owned by an account. If the account does not exist or has no
505 * tokens of this type, then 0 will be returned.
506 * @param {String} address the account's address
507 * @param {String} tokenType the type of token to query.
508 * @return {Promise} a promise that resolves to a BigInteger that encodes the number of tokens
509 * held by this account.
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 // talking to an older blockstack core node without the accounts API
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 * Performs a POST request to the given URL
538 * @param {String} endpoint the name of
539 * @param {String} body [description]
540 * @return {Promise<Object|Error>} Returns a `Promise` that resolves to the object requested.
541 * In the event of an error, it rejects with:
542 * * a `RemoteServiceError` if there is a problem
543 * with the transaction broadcast service
544 * * `MissingParameterError` if you call the function without a required
545 * parameter
546 *
547 * @private
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 * Broadcasts a signed bitcoin transaction to the network optionally waiting to broadcast the
572 * transaction until a second transaction has a certain number of confirmations.
573 *
574 * @param {string} transaction the hex-encoded transaction to broadcast
575 * @param {string} transactionToWatch the hex transaction id of the transaction to watch for
576 * the specified number of confirmations before broadcasting the `transaction`
577 * @param {number} confirmations the number of confirmations `transactionToWatch` must have
578 * before broadcasting `transaction`.
579 * @return {Promise<Object|Error>} Returns a Promise that resolves to an object with a
580 * `transaction_hash` key containing the transaction hash of the broadcasted transaction.
581 *
582 * In the event of an error, it rejects with:
583 * * a `RemoteServiceError` if there is a problem
584 * with the transaction broadcast service
585 * * `MissingParameterError` if you call the function without a required
586 * parameter
587 * @private
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 * POST /v1/broadcast/transaction
604 * Request body:
605 * JSON.stringify({
606 * transaction,
607 * transactionToWatch,
608 * confirmations
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 * Broadcasts a zone file to the Atlas network via the transaction broadcast service.
622 *
623 * @param {String} zoneFile the zone file to be broadcast to the Atlas network
624 * @param {String} transactionToWatch the hex transaction id of the transaction
625 * to watch for confirmation before broadcasting the zone file to the Atlas network
626 * @return {Promise<Object|Error>} Returns a Promise that resolves to an object with a
627 * `transaction_hash` key containing the transaction hash of the broadcasted transaction.
628 *
629 * In the event of an error, it rejects with:
630 * * a `RemoteServiceError` if there is a problem
631 * with the transaction broadcast service
632 * * `MissingParameterError` if you call the function without a required
633 * parameter
634 * @private
635 */
636 broadcastZoneFile(zoneFile, transactionToWatch = null) {
637 if (!zoneFile) {
638 return Promise.reject(new errors_1.MissingParameterError('zoneFile'));
639 }
640 // TODO: validate zonefile
641 if (transactionToWatch) {
642 // broadcast via transaction broadcast service
643 /*
644 * POST /v1/broadcast/zone-file
645 * Request body:
646 * JSON.stringify({
647 * zoneFile,
648 * transactionToWatch
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 // broadcast via core endpoint
660 // zone file is two words but core's api treats it as one word 'zonefile'
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 * Sends the preorder and registration transactions and zone file
683 * for a Blockstack name registration
684 * along with the to the transaction broadcast service.
685 *
686 * The transaction broadcast:
687 *
688 * * immediately broadcasts the preorder transaction
689 * * broadcasts the register transactions after the preorder transaction
690 * has an appropriate number of confirmations
691 * * broadcasts the zone file to the Atlas network after the register transaction
692 * has an appropriate number of confirmations
693 *
694 * @param {String} preorderTransaction the hex-encoded, signed preorder transaction generated
695 * using the `makePreorder` function
696 * @param {String} registerTransaction the hex-encoded, signed register transaction generated
697 * using the `makeRegister` function
698 * @param {String} zoneFile the zone file to be broadcast to the Atlas network
699 * @return {Promise<Object|Error>} Returns a Promise that resolves to an object with a
700 * `transaction_hash` key containing the transaction hash of the broadcasted transaction.
701 *
702 * In the event of an error, it rejects with:
703 * * a `RemoteServiceError` if there is a problem
704 * with the transaction broadcast service
705 * * `MissingParameterError` if you call the function without a required
706 * parameter
707 * @private
708 */
709 broadcastNameRegistration(preorderTransaction, registerTransaction, zoneFile) {
710 /*
711 * POST /v1/broadcast/registration
712 * Request body:
713 * JSON.stringify({
714 * preorderTransaction,
715 * registerTransaction,
716 * zoneFile
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 * @ignore
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 * @ignore
749 */
750 countDustOutputs() {
751 throw new Error('Not implemented.');
752 }
753 /**
754 * @ignore
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 // aaron: I am *well* aware this is O(n)*O(m) runtime
764 // however, clients should clear the exclude set periodically
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 * This will modify the network's utxo set to include UTXOs
776 * from the given transaction and exclude UTXOs *spent* in
777 * that transaction
778 * @param {String} txHex - the hex-encoded transaction to use
779 * @return {void} no return value, this modifies the UTXO config state
780 * @private
781 * @ignore
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 * @ignore
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 * @ignore
840 */
841 getBlockHeight() {
842 return this.btc.getBlockHeight();
843 }
844 getNetworkedUTXOs(address) {
845 return this.btc.getNetworkedUTXOs(address);
846 }
847}
848exports.BlockstackNetwork = BlockstackNetwork;
849/**
850 * @ignore
851 */
852class 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}
860exports.LocalRegtest = LocalRegtest;
861/**
862 * @ignore
863 */
864class 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 // unconfirmed
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}
983exports.BitcoindAPI = BitcoindAPI;
984/**
985 * @ignore
986 */
987class 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}
1029exports.InsightClient = InsightClient;
1030/**
1031 * @ignore
1032 */
1033class 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'); // big_endian
1094 return txHash;
1095 }
1096 else {
1097 throw new errors_1.RemoteServiceError(resp, `Broadcast transaction failed with message: ${respText}`);
1098 }
1099 });
1100 });
1101 }
1102}
1103exports.BlockchainInfoApi = BlockchainInfoApi;
1104/**
1105* @ignore
1106*/
1107const LOCAL_REGTEST = new LocalRegtest('http://localhost:16268', 'http://localhost:16269', new BitcoindAPI('http://localhost:18332/', { username: 'blockstack', password: 'blockstacksystem' }));
1108/**
1109* @ignore
1110*/
1111const MAINNET_DEFAULT = new BlockstackNetwork('https://core.blockstack.org', 'https://broadcast.blockstack.org', new BlockchainInfoApi());
1112/**
1113 * Get WHOIS-like information for a name, including the address that owns it,
1114 * the block at which it expires, and the zone file anchored to it (if available).
1115 * @param {String} fullyQualifiedName the name to query. Can be on-chain of off-chain.
1116 * @return {Promise} a promise that resolves to the WHOIS-like information
1117 */
1118function getNameInfo(fullyQualifiedName) {
1119 return config_1.config.network.getNameInfo(fullyQualifiedName);
1120}
1121exports.getNameInfo = getNameInfo;
1122/**
1123* @ignore
1124*/
1125exports.network = {
1126 BlockstackNetwork,
1127 LocalRegtest,
1128 BlockchainInfoApi,
1129 BitcoindAPI,
1130 InsightClient,
1131 defaults: { LOCAL_REGTEST, MAINNET_DEFAULT }
1132};
1133//# sourceMappingURL=network.js.map
\No newline at end of file