blockstack
Version:
The Blockstack Javascript library for authentication, identity, and storage.
1,356 lines (1,205 loc) • 49.6 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.network = exports.BlockchainInfoApi = exports.InsightClient = exports.BitcoindAPI = exports.LocalRegtest = exports.BlockstackNetwork = exports.BitcoinNetwork = undefined;
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _bitcoinjsLib = require('bitcoinjs-lib');
var _bitcoinjsLib2 = _interopRequireDefault(_bitcoinjsLib);
var _formData = require('form-data');
var _formData2 = _interopRequireDefault(_formData);
var _bigi = require('bigi');
var _bigi2 = _interopRequireDefault(_bigi);
var _ripemd = require('ripemd160');
var _ripemd2 = _interopRequireDefault(_ripemd);
var _errors = require('./errors');
var _logger = require('./logger');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var SATOSHIS_PER_BTC = 1e8;
var TX_BROADCAST_SERVICE_ZONE_FILE_ENDPOINT = 'zone-file';
var TX_BROADCAST_SERVICE_REGISTRATION_ENDPOINT = 'registration';
var TX_BROADCAST_SERVICE_TX_ENDPOINT = 'transaction';
var BitcoinNetwork = exports.BitcoinNetwork = function () {
function BitcoinNetwork() {
_classCallCheck(this, BitcoinNetwork);
}
_createClass(BitcoinNetwork, [{
key: 'broadcastTransaction',
value: function broadcastTransaction(transaction) {
return Promise.reject(new Error('Not implemented, broadcastTransaction(' + transaction + ')'));
}
}, {
key: 'getBlockHeight',
value: function getBlockHeight() {
return Promise.reject(new Error('Not implemented, getBlockHeight()'));
}
}, {
key: 'getTransactionInfo',
value: function getTransactionInfo(txid) {
return Promise.reject(new Error('Not implemented, getTransactionInfo(' + txid + ')'));
}
}, {
key: 'getNetworkedUTXOs',
value: function getNetworkedUTXOs(address) {
return Promise.reject(new Error('Not implemented, getNetworkedUTXOs(' + address + ')'));
}
}]);
return BitcoinNetwork;
}();
var BlockstackNetwork = exports.BlockstackNetwork = function () {
function BlockstackNetwork(apiUrl, broadcastServiceUrl, bitcoinAPI) {
var network = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : _bitcoinjsLib2.default.networks.bitcoin;
_classCallCheck(this, BlockstackNetwork);
this.blockstackAPIUrl = apiUrl;
this.broadcastServiceUrl = broadcastServiceUrl;
this.layer1 = network;
this.btc = bitcoinAPI;
this.DUST_MINIMUM = 5500;
this.includeUtxoMap = {};
this.excludeUtxoSet = [];
this.MAGIC_BYTES = 'id';
}
_createClass(BlockstackNetwork, [{
key: 'coerceAddress',
value: function coerceAddress(address) {
var _bitcoinjs$address$fr = _bitcoinjsLib2.default.address.fromBase58Check(address),
hash = _bitcoinjs$address$fr.hash,
version = _bitcoinjs$address$fr.version;
var scriptHashes = [_bitcoinjsLib2.default.networks.bitcoin.scriptHash, _bitcoinjsLib2.default.networks.testnet.scriptHash];
var pubKeyHashes = [_bitcoinjsLib2.default.networks.bitcoin.pubKeyHash, _bitcoinjsLib2.default.networks.testnet.pubKeyHash];
var coercedVersion = void 0;
if (scriptHashes.indexOf(version) >= 0) {
coercedVersion = this.layer1.scriptHash;
} else if (pubKeyHashes.indexOf(version) >= 0) {
coercedVersion = this.layer1.pubKeyHash;
} else {
throw new Error('Unrecognized address version number ' + version + ' in ' + address);
}
return _bitcoinjsLib2.default.address.toBase58Check(hash, coercedVersion);
}
}, {
key: 'getDefaultBurnAddress',
value: function getDefaultBurnAddress() {
return this.coerceAddress('1111111111111111111114oLvT2');
}
/**
* Get the price of a name via the legacy /v1/prices API endpoint.
* @param {String} fullyQualifiedName the name to query
* @return {Promise} a promise to an Object with { units: String, amount: BigInteger }
* @private
*/
}, {
key: 'getNamePriceV1',
value: function getNamePriceV1(fullyQualifiedName) {
var _this = this;
// legacy code path
return fetch(this.blockstackAPIUrl + '/v1/prices/names/' + fullyQualifiedName).then(function (resp) {
if (!resp.ok) {
throw new Error('Failed to query name price for ' + fullyQualifiedName);
}
return resp;
}).then(function (resp) {
return resp.json();
}).then(function (resp) {
return resp.name_price;
}).then(function (namePrice) {
if (!namePrice || !namePrice.satoshis) {
throw new Error('Failed to get price for ' + fullyQualifiedName + '. Does the namespace exist?');
}
if (namePrice.satoshis < _this.DUST_MINIMUM) {
namePrice.satoshis = _this.DUST_MINIMUM;
}
var result = {
units: 'BTC',
amount: _bigi2.default.fromByteArrayUnsigned(String(namePrice.satoshis))
};
return result;
});
}
/**
* Get the price of a namespace via the legacy /v1/prices API endpoint.
* @param {String} namespaceID the namespace to query
* @return {Promise} a promise to an Object with { units: String, amount: BigInteger }
* @private
*/
}, {
key: 'getNamespacePriceV1',
value: function getNamespacePriceV1(namespaceID) {
var _this2 = this;
// legacy code path
return fetch(this.blockstackAPIUrl + '/v1/prices/namespaces/' + namespaceID).then(function (resp) {
if (!resp.ok) {
throw new Error('Failed to query name price for ' + namespaceID);
}
return resp;
}).then(function (resp) {
return resp.json();
}).then(function (namespacePrice) {
if (!namespacePrice || !namespacePrice.satoshis) {
throw new Error('Failed to get price for ' + namespaceID);
}
if (namespacePrice.satoshis < _this2.DUST_MINIMUM) {
namespacePrice.satoshis = _this2.DUST_MINIMUM;
}
var result = {
units: 'BTC',
amount: _bigi2.default.fromByteArrayUnsigned(String(namespacePrice.satoshis))
};
return result;
});
}
/**
* Get the price of a name via the /v2/prices API endpoint.
* @param {String} fullyQualifiedName the name to query
* @return {Promise} a promise to an Object with { units: String, amount: BigInteger }
* @private
*/
}, {
key: 'getNamePriceV2',
value: function getNamePriceV2(fullyQualifiedName) {
var _this3 = this;
return fetch(this.blockstackAPIUrl + '/v2/prices/names/' + fullyQualifiedName).then(function (resp) {
if (resp.status !== 200) {
// old core node
throw new Error('The upstream node does not handle the /v2/ price namespace');
}
return resp;
}).then(function (resp) {
return resp.json();
}).then(function (resp) {
return resp.name_price;
}).then(function (namePrice) {
if (!namePrice) {
throw new Error('Failed to get price for ' + fullyQualifiedName + '. Does the namespace exist?');
}
var result = {
units: namePrice.units,
amount: _bigi2.default.fromByteArrayUnsigned(namePrice.amount)
};
if (namePrice.units === 'BTC') {
// must be at least dust-minimum
var dustMin = _bigi2.default.fromByteArrayUnsigned(String(_this3.DUST_MINIMUM));
if (result.amount.compareTo(dustMin) < 0) {
result.amount = dustMin;
}
}
return result;
});
}
/**
* Get the price of a namespace via the /v2/prices API endpoint.
* @param {String} namespaceID the namespace to query
* @return {Promise} a promise to an Object with { units: String, amount: BigInteger }
* @private
*/
}, {
key: 'getNamespacePriceV2',
value: function getNamespacePriceV2(namespaceID) {
var _this4 = this;
return fetch(this.blockstackAPIUrl + '/v2/prices/namespaces/' + namespaceID).then(function (resp) {
if (resp.status !== 200) {
// old core node
throw new Error('The upstream node does not handle the /v2/ price namespace');
}
return resp;
}).then(function (resp) {
return resp.json();
}).then(function (namespacePrice) {
if (!namespacePrice) {
throw new Error('Failed to get price for ' + namespaceID);
}
var result = {
units: namespacePrice.units,
amount: _bigi2.default.fromByteArrayUnsigned(namespacePrice.amount)
};
if (namespacePrice.units === 'BTC') {
// must be at least dust-minimum
var dustMin = _bigi2.default.fromByteArrayUnsigned(String(_this4.DUST_MINIMUM));
if (result.amount.compareTo(dustMin) < 0) {
result.amount = dustMin;
}
}
return result;
});
}
/**
* Get the price of a name.
* @param {String} fullyQualifiedName the name to query
* @return {Promise} a promise to an Object with { units: String, amount: BigInteger }, where
* .units encodes the cryptocurrency units to pay (e.g. BTC, STACKS), and
* .amount encodes the number of units, in the smallest denominiated amount
* (e.g. if .units is BTC, .amount will be satoshis; if .units is STACKS,
* .amount will be microStacks)
*/
}, {
key: 'getNamePrice',
value: function getNamePrice(fullyQualifiedName) {
var _this5 = this;
// handle v1 or v2
return Promise.resolve().then(function () {
return _this5.getNamePriceV2(fullyQualifiedName);
}).catch(function () {
return _this5.getNamePriceV1(fullyQualifiedName);
});
}
/**
* Get the price of a namespace
* @param {String} namespaceID the namespace to query
* @return {Promise} a promise to an Object with { units: String, amount: BigInteger }, where
* .units encodes the cryptocurrency units to pay (e.g. BTC, STACKS), and
* .amount encodes the number of units, in the smallest denominiated amount
* (e.g. if .units is BTC, .amount will be satoshis; if .units is STACKS,
* .amount will be microStacks)
*/
}, {
key: 'getNamespacePrice',
value: function getNamespacePrice(namespaceID) {
var _this6 = this;
// handle v1 or v2
return Promise.resolve().then(function () {
return _this6.getNamespacePriceV2(namespaceID);
}).catch(function () {
return _this6.getNamespacePriceV1(namespaceID);
});
}
/**
* How many blocks can pass between a name expiring and the name being able to be
* re-registered by a different owner?
* @return {Promise} a promise to the number of blocks
*/
}, {
key: 'getGracePeriod',
value: function getGracePeriod() {
return new Promise(function (resolve) {
return resolve(5000);
});
}
/**
* Get the names -- both on-chain and off-chain -- owned by an address.
* @param {String} address the blockchain address (the hash of the owner public key)
* @return {Promise} a promise that resolves to a list of names (Strings)
*/
}, {
key: 'getNamesOwned',
value: function getNamesOwned(address) {
var networkAddress = this.coerceAddress(address);
return fetch(this.blockstackAPIUrl + '/v1/addresses/bitcoin/' + networkAddress).then(function (resp) {
return resp.json();
}).then(function (obj) {
return obj.names;
});
}
/**
* Get the blockchain address to which a name's registration fee must be sent
* (the address will depend on the namespace in which it is registered.)
* @param {String} namespace the namespace ID
* @return {Promise} a promise that resolves to an address (String)
*/
}, {
key: 'getNamespaceBurnAddress',
value: function getNamespaceBurnAddress(namespace) {
var _this7 = this;
return Promise.all([fetch(this.blockstackAPIUrl + '/v1/namespaces/' + namespace), this.getBlockHeight()]).then(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
resp = _ref2[0],
blockHeight = _ref2[1];
if (resp.status === 404) {
throw new Error('No such namespace \'' + namespace + '\'');
} else {
return Promise.all([resp.json(), blockHeight]);
}
}).then(function (_ref3) {
var _ref4 = _slicedToArray(_ref3, 2),
namespaceInfo = _ref4[0],
blockHeight = _ref4[1];
var address = _this7.getDefaultBurnAddress();
if (namespaceInfo.version === 2) {
// pay-to-namespace-creator if this namespace is less than 1 year old
if (namespaceInfo.reveal_block + 52595 >= blockHeight) {
address = namespaceInfo.address;
}
}
return address;
}).then(function (address) {
return _this7.coerceAddress(address);
});
}
/**
* Get WHOIS-like information for a name, including the address that owns it,
* the block at which it expires, and the zone file anchored to it (if available).
* @param {String} fullyQualifiedName the name to query. Can be on-chain of off-chain.
* @return {Promise} a promise that resolves to the WHOIS-like information
*/
}, {
key: 'getNameInfo',
value: function getNameInfo(fullyQualifiedName) {
var _this8 = this;
return fetch(this.blockstackAPIUrl + '/v1/names/' + fullyQualifiedName).then(function (resp) {
if (resp.status === 404) {
throw new Error('Name not found');
} else if (resp.status !== 200) {
throw new Error('Bad response status: ' + resp.status);
} else {
return resp.json();
}
}).then(function (nameInfo) {
// the returned address _should_ be in the correct network ---
// blockstackd gets into trouble because it tries to coerce back to mainnet
// and the regtest transaction generation libraries want to use testnet addresses
if (nameInfo.address) {
return Object.assign({}, nameInfo, { address: _this8.coerceAddress(nameInfo.address) });
} else {
return nameInfo;
}
});
}
/**
* Get the pricing parameters and creation history of a namespace.
* @param {String} namespaceID the namespace to query
* @return {Promise} a promise that resolves to the namespace information.
*/
}, {
key: 'getNamespaceInfo',
value: function getNamespaceInfo(namespaceID) {
var _this9 = this;
return fetch(this.blockstackAPIUrl + '/v1/namespaces/' + namespaceID).then(function (resp) {
if (resp.status === 404) {
throw new Error('Namespace not found');
} else if (resp.status !== 200) {
throw new Error('Bad response status: ' + resp.status);
} else {
return resp.json();
}
}).then(function (namespaceInfo) {
// the returned address _should_ be in the correct network ---
// blockstackd gets into trouble because it tries to coerce back to mainnet
// and the regtest transaction generation libraries want to use testnet addresses
if (namespaceInfo.address && namespaceInfo.recipient_address) {
return Object.assign({}, namespaceInfo, {
address: _this9.coerceAddress(namespaceInfo.address),
recipient_address: _this9.coerceAddress(namespaceInfo.recipient_address)
});
} else {
return namespaceInfo;
}
});
}
/**
* Get a zone file, given its hash. Throws an exception if the zone file
* obtained does not match the hash.
* @param {String} zonefileHash the ripemd160(sha256) hash of the zone file
* @return {Promise} a promise that resolves to the zone file's text
*/
}, {
key: 'getZonefile',
value: function getZonefile(zonefileHash) {
return fetch(this.blockstackAPIUrl + '/v1/zonefiles/' + zonefileHash).then(function (resp) {
if (resp.status === 200) {
return resp.text().then(function (body) {
var sha256 = _bitcoinjsLib2.default.crypto.sha256(body);
var h = new _ripemd2.default().update(sha256).digest('hex');
if (h !== zonefileHash) {
throw new Error('Zone file contents hash to ' + h + ', not ' + zonefileHash);
}
return body;
});
} else {
throw new Error('Bad response status: ' + resp.status);
}
});
}
/**
* Get the status of an account for a particular token holding. This includes its total number of
* expenditures and credits, lockup times, last txid, and so on.
* @param {String} address the account
* @param {String} tokenType the token type to query
* @return {Promise} a promise that resolves to an object representing the state of the account
* for this token
*/
}, {
key: 'getAccountStatus',
value: function getAccountStatus(address, tokenType) {
var _this10 = this;
return fetch(this.blockstackAPIUrl + '/v1/accounts/' + address + '/' + tokenType + '/status').then(function (resp) {
if (resp.status === 404) {
throw new Error('Account not found');
} else if (resp.status !== 200) {
throw new Error('Bad response status: ' + resp.status);
} else {
return resp.json();
}
}).then(function (accountStatus) {
// coerce all addresses, and convert credit/debit to biginteger
var formattedStatus = Object.assign({}, accountStatus, {
address: _this10.coerceAddress(accountStatus.address),
debit_value: _bigi2.default.fromByteArrayUnsigned(String(accountStatus.debit_value)),
credit_value: _bigi2.default.fromByteArrayUnsigned(String(accountStatus.credit_value))
});
return formattedStatus;
});
}
/**
* Get a page of an account's transaction history.
* @param {String} address the account's address
* @param {number} page the page number. Page 0 is the most recent transactions
* @return {Promise} a promise that resolves to an Array of Objects, where each Object encodes
* states of the account at various block heights (e.g. prior balances, txids, etc)
*/
}, {
key: 'getAccountHistoryPage',
value: function getAccountHistoryPage(address, page) {
var _this11 = this;
var url = this.blockstackAPIUrl + '/v1/accounts/' + address + '/history?page=' + page;
return fetch(url).then(function (resp) {
if (resp.status === 404) {
throw new Error('Account not found');
} else if (resp.status !== 200) {
throw new Error('Bad response status: ' + resp.status);
} else {
return resp.json();
}
}).then(function (historyList) {
if (historyList.error) {
throw new Error('Unable to get account history page: ' + historyList.error);
}
// coerse all addresses and convert to bigint
return historyList.map(function (histEntry) {
histEntry.address = _this11.coerceAddress(histEntry.address);
histEntry.debit_value = _bigi2.default.fromByteArrayUnsigned(String(histEntry.debit_value));
histEntry.credit_value = _bigi2.default.fromByteArrayUnsigned(String(histEntry.credit_value));
return histEntry;
});
});
}
/**
* Get the state(s) of an account at a particular block height. This includes the state of the
* account beginning with this block's transactions, as well as all of the states the account
* passed through when this block was processed (if any).
* @param {String} address the account's address
* @param {Integer} blockHeight the block to query
* @return {Promise} a promise that resolves to an Array of Objects, where each Object encodes
* states of the account at this block.
*/
}, {
key: 'getAccountAt',
value: function getAccountAt(address, blockHeight) {
var _this12 = this;
var url = this.blockstackAPIUrl + '/v1/accounts/' + address + '/history/' + blockHeight;
return fetch(url).then(function (resp) {
if (resp.status === 404) {
throw new Error('Account not found');
} else if (resp.status !== 200) {
throw new Error('Bad response status: ' + resp.status);
} else {
return resp.json();
}
}).then(function (historyList) {
if (historyList.error) {
throw new Error('Unable to get historic account state: ' + historyList.error);
}
// coerce all addresses
return historyList.map(function (histEntry) {
histEntry.address = _this12.coerceAddress(histEntry.address);
histEntry.debit_value = _bigi2.default.fromByteArrayUnsigned(String(histEntry.debit_value));
histEntry.credit_value = _bigi2.default.fromByteArrayUnsigned(String(histEntry.credit_value));
return histEntry;
});
});
}
/**
* Get the set of token types that this account owns
* @param {String} address the account's address
* @return {Promise} a promise that resolves to an Array of Strings, where each item encodes the
* type of token this account holds (excluding the underlying blockchain's tokens)
*/
}, {
key: 'getAccountTokens',
value: function getAccountTokens(address) {
return fetch(this.blockstackAPIUrl + '/v1/accounts/' + address + '/tokens').then(function (resp) {
if (resp.status === 404) {
throw new Error('Account not found');
} else if (resp.status !== 200) {
throw new Error('Bad response status: ' + resp.status);
} else {
return resp.json();
}
}).then(function (tokenList) {
if (tokenList.error) {
throw new Error('Unable to get token list: ' + tokenList.error);
}
return tokenList;
});
}
/**
* Get the number of tokens owned by an account. If the account does not exist or has no
* tokens of this type, then 0 will be returned.
* @param {String} address the account's address
* @param {String} tokenType the type of token to query.
* @return {Promise} a promise that resolves to a BigInteger that encodes the number of tokens
* held by this account.
*/
}, {
key: 'getAccountBalance',
value: function getAccountBalance(address, tokenType) {
return fetch(this.blockstackAPIUrl + '/v1/accounts/' + address + '/' + tokenType + '/balance').then(function (resp) {
if (resp.status === 404) {
// talking to an older blockstack core node without the accounts API
return Promise.resolve().then(function () {
return _bigi2.default.fromByteArrayUnsigned('0');
});
} else if (resp.status !== 200) {
throw new Error('Bad response status: ' + resp.status);
} else {
return resp.json();
}
}).then(function (tokenBalance) {
if (tokenBalance.error) {
throw new Error('Unable to get account balance: ' + tokenBalance.error);
}
var balance = '0';
if (tokenBalance && tokenBalance.balance) {
balance = tokenBalance.balance;
}
return _bigi2.default.fromByteArrayUnsigned(balance);
});
}
/**
* Performs a POST request to the given URL
* @param {String} endpoint the name of
* @param {String} body [description]
* @return {Promise<Object|Error>} Returns a `Promise` that resolves to the object requested.
* In the event of an error, it rejects with:
* * a `RemoteServiceError` if there is a problem
* with the transaction broadcast service
* * `MissingParameterError` if you call the function without a required
* parameter
*
* @private
*/
}, {
key: 'broadcastServiceFetchHelper',
value: function broadcastServiceFetchHelper(endpoint, body) {
var requestHeaders = {
Accept: 'application/json',
'Content-Type': 'application/json'
};
var options = {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify(body)
};
var url = this.broadcastServiceUrl + '/v1/broadcast/' + endpoint;
return fetch(url, options).then(function (response) {
if (response.ok) {
return response.json();
} else {
throw new _errors.RemoteServiceError(response);
}
});
}
/**
* Broadcasts a signed bitcoin transaction to the network optionally waiting to broadcast the
* transaction until a second transaction has a certain number of confirmations.
*
* @param {string} transaction the hex-encoded transaction to broadcast
* @param {string} transactionToWatch the hex transaction id of the transaction to watch for
* the specified number of confirmations before broadcasting the `transaction`
* @param {number} confirmations the number of confirmations `transactionToWatch` must have
* before broadcasting `transaction`.
* @return {Promise<Object|Error>} Returns a Promise that resolves to an object with a
* `transaction_hash` key containing the transaction hash of the broadcasted transaction.
*
* In the event of an error, it rejects with:
* * a `RemoteServiceError` if there is a problem
* with the transaction broadcast service
* * `MissingParameterError` if you call the function without a required
* parameter
* @private
*/
}, {
key: 'broadcastTransaction',
value: function broadcastTransaction(transaction) {
var transactionToWatch = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
var confirmations = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 6;
if (!transaction) {
var error = new _errors.MissingParameterError('transaction');
return Promise.reject(error);
}
if (!confirmations && confirmations !== 0) {
var _error = new _errors.MissingParameterError('confirmations');
return Promise.reject(_error);
}
if (transactionToWatch === null) {
return this.btc.broadcastTransaction(transaction);
} else {
/*
* POST /v1/broadcast/transaction
* Request body:
* JSON.stringify({
* transaction,
* transactionToWatch,
* confirmations
* })
*/
var endpoint = TX_BROADCAST_SERVICE_TX_ENDPOINT;
var requestBody = {
transaction: transaction,
transactionToWatch: transactionToWatch,
confirmations: confirmations
};
return this.broadcastServiceFetchHelper(endpoint, requestBody);
}
}
/**
* Broadcasts a zone file to the Atlas network via the transaction broadcast service.
*
* @param {String} zoneFile the zone file to be broadcast to the Atlas network
* @param {String} transactionToWatch the hex transaction id of the transaction
* to watch for confirmation before broadcasting the zone file to the Atlas network
* @return {Promise<Object|Error>} Returns a Promise that resolves to an object with a
* `transaction_hash` key containing the transaction hash of the broadcasted transaction.
*
* In the event of an error, it rejects with:
* * a `RemoteServiceError` if there is a problem
* with the transaction broadcast service
* * `MissingParameterError` if you call the function without a required
* parameter
* @private
*/
}, {
key: 'broadcastZoneFile',
value: function broadcastZoneFile(zoneFile) {
var transactionToWatch = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
if (!zoneFile) {
return Promise.reject(new _errors.MissingParameterError('zoneFile'));
}
// TODO: validate zonefile
if (transactionToWatch) {
// broadcast via transaction broadcast service
/*
* POST /v1/broadcast/zone-file
* Request body:
* JSON.stringify({
* zoneFile,
* transactionToWatch
* })
*/
var requestBody = {
zoneFile: zoneFile,
transactionToWatch: transactionToWatch
};
var endpoint = TX_BROADCAST_SERVICE_ZONE_FILE_ENDPOINT;
return this.broadcastServiceFetchHelper(endpoint, requestBody);
} else {
// broadcast via core endpoint
// zone file is two words but core's api treats it as one word 'zonefile'
var _requestBody = { zonefile: zoneFile };
return fetch(this.blockstackAPIUrl + '/v1/zonefile/', {
method: 'POST',
body: JSON.stringify(_requestBody),
headers: {
'Content-Type': 'application/json'
}
}).then(function (resp) {
var json = resp.json();
return json.then(function (respObj) {
if (respObj.hasOwnProperty('error')) {
throw new _errors.RemoteServiceError(resp);
}
return respObj.servers;
});
});
}
}
/**
* Sends the preorder and registration transactions and zone file
* for a Blockstack name registration
* along with the to the transaction broadcast service.
*
* The transaction broadcast:
*
* * immediately broadcasts the preorder transaction
* * broadcasts the register transactions after the preorder transaction
* has an appropriate number of confirmations
* * broadcasts the zone file to the Atlas network after the register transaction
* has an appropriate number of confirmations
*
* @param {String} preorderTransaction the hex-encoded, signed preorder transaction generated
* using the `makePreorder` function
* @param {String} registerTransaction the hex-encoded, signed register transaction generated
* using the `makeRegister` function
* @param {String} zoneFile the zone file to be broadcast to the Atlas network
* @return {Promise<Object|Error>} Returns a Promise that resolves to an object with a
* `transaction_hash` key containing the transaction hash of the broadcasted transaction.
*
* In the event of an error, it rejects with:
* * a `RemoteServiceError` if there is a problem
* with the transaction broadcast service
* * `MissingParameterError` if you call the function without a required
* parameter
* @private
*/
}, {
key: 'broadcastNameRegistration',
value: function broadcastNameRegistration(preorderTransaction, registerTransaction, zoneFile) {
/*
* POST /v1/broadcast/registration
* Request body:
* JSON.stringify({
* preorderTransaction,
* registerTransaction,
* zoneFile
* })
*/
if (!preorderTransaction) {
var error = new _errors.MissingParameterError('preorderTransaction');
return Promise.reject(error);
}
if (!registerTransaction) {
var _error2 = new _errors.MissingParameterError('registerTransaction');
return Promise.reject(_error2);
}
if (!zoneFile) {
var _error3 = new _errors.MissingParameterError('zoneFile');
return Promise.reject(_error3);
}
var requestBody = {
preorderTransaction: preorderTransaction,
registerTransaction: registerTransaction,
zoneFile: zoneFile
};
var endpoint = TX_BROADCAST_SERVICE_REGISTRATION_ENDPOINT;
return this.broadcastServiceFetchHelper(endpoint, requestBody);
}
}, {
key: 'getFeeRate',
value: function getFeeRate() {
return fetch('https://bitcoinfees.earn.com/api/v1/fees/recommended').then(function (resp) {
return resp.json();
}).then(function (rates) {
return Math.floor(rates.fastestFee);
});
}
}, {
key: 'countDustOutputs',
value: function countDustOutputs() {
throw new Error('Not implemented.');
}
}, {
key: 'getUTXOs',
value: function getUTXOs(address) {
var _this13 = this;
return this.getNetworkedUTXOs(address).then(function (networkedUTXOs) {
var returnSet = networkedUTXOs.concat();
if (_this13.includeUtxoMap.hasOwnProperty(address)) {
returnSet = networkedUTXOs.concat(_this13.includeUtxoMap[address]);
}
// aaron: I am *well* aware this is O(n)*O(m) runtime
// however, clients should clear the exclude set periodically
var excludeSet = _this13.excludeUtxoSet;
returnSet = returnSet.filter(function (utxo) {
var inExcludeSet = excludeSet.reduce(function (inSet, utxoToCheck) {
return inSet || utxoToCheck.tx_hash === utxo.tx_hash && utxoToCheck.tx_output_n === utxo.tx_output_n;
}, false);
return !inExcludeSet;
});
return returnSet;
});
}
/**
* This will modify the network's utxo set to include UTXOs
* from the given transaction and exclude UTXOs *spent* in
* that transaction
* @param {String} txHex - the hex-encoded transaction to use
* @return {void} no return value, this modifies the UTXO config state
* @private
*/
}, {
key: 'modifyUTXOSetFrom',
value: function modifyUTXOSetFrom(txHex) {
var _this14 = this;
var tx = _bitcoinjsLib2.default.Transaction.fromHex(txHex);
var excludeSet = this.excludeUtxoSet.concat();
tx.ins.forEach(function (utxoUsed) {
var reverseHash = Buffer.from(utxoUsed.hash);
reverseHash.reverse();
excludeSet.push({
tx_hash: reverseHash.toString('hex'),
tx_output_n: utxoUsed.index
});
});
this.excludeUtxoSet = excludeSet;
var txHash = tx.getHash().reverse().toString('hex');
tx.outs.forEach(function (utxoCreated, txOutputN) {
var isNullData = function isNullData(script) {
try {
_bitcoinjsLib2.default.payments.embed({ output: script }, { validate: true });
return true;
} catch (_) {
return false;
}
};
if (isNullData(utxoCreated.script)) {
return;
}
var address = _bitcoinjsLib2.default.address.fromOutputScript(utxoCreated.script, _this14.layer1);
var includeSet = [];
if (_this14.includeUtxoMap.hasOwnProperty(address)) {
includeSet = includeSet.concat(_this14.includeUtxoMap[address]);
}
includeSet.push({
tx_hash: txHash,
confirmations: 0,
value: utxoCreated.value,
tx_output_n: txOutputN
});
_this14.includeUtxoMap[address] = includeSet;
});
}
}, {
key: 'resetUTXOs',
value: function resetUTXOs(address) {
delete this.includeUtxoMap[address];
this.excludeUtxoSet = [];
}
}, {
key: 'getConsensusHash',
value: function getConsensusHash() {
return fetch(this.blockstackAPIUrl + '/v1/blockchains/bitcoin/consensus').then(function (resp) {
return resp.json();
}).then(function (x) {
return x.consensus_hash;
});
}
}, {
key: 'getTransactionInfo',
value: function getTransactionInfo(txHash) {
return this.btc.getTransactionInfo(txHash);
}
}, {
key: 'getBlockHeight',
value: function getBlockHeight() {
return this.btc.getBlockHeight();
}
}, {
key: 'getNetworkedUTXOs',
value: function getNetworkedUTXOs(address) {
return this.btc.getNetworkedUTXOs(address);
}
}]);
return BlockstackNetwork;
}();
var LocalRegtest = exports.LocalRegtest = function (_BlockstackNetwork) {
_inherits(LocalRegtest, _BlockstackNetwork);
function LocalRegtest(apiUrl, broadcastServiceUrl, bitcoinAPI) {
_classCallCheck(this, LocalRegtest);
return _possibleConstructorReturn(this, (LocalRegtest.__proto__ || Object.getPrototypeOf(LocalRegtest)).call(this, apiUrl, broadcastServiceUrl, bitcoinAPI, _bitcoinjsLib2.default.networks.testnet));
}
_createClass(LocalRegtest, [{
key: 'getFeeRate',
value: function getFeeRate() {
return Promise.resolve(Math.floor(0.00001000 * SATOSHIS_PER_BTC));
}
}]);
return LocalRegtest;
}(BlockstackNetwork);
var BitcoindAPI = exports.BitcoindAPI = function (_BitcoinNetwork) {
_inherits(BitcoindAPI, _BitcoinNetwork);
function BitcoindAPI(bitcoindUrl, bitcoindCredentials) {
_classCallCheck(this, BitcoindAPI);
var _this16 = _possibleConstructorReturn(this, (BitcoindAPI.__proto__ || Object.getPrototypeOf(BitcoindAPI)).call(this));
_this16.bitcoindUrl = bitcoindUrl;
_this16.bitcoindCredentials = bitcoindCredentials;
_this16.importedBefore = {};
return _this16;
}
_createClass(BitcoindAPI, [{
key: 'broadcastTransaction',
value: function broadcastTransaction(transaction) {
var jsonRPC = {
jsonrpc: '1.0',
method: 'sendrawtransaction',
params: [transaction]
};
var authString = Buffer.from(this.bitcoindCredentials.username + ':' + this.bitcoindCredentials.password).toString('base64');
var headers = { Authorization: 'Basic ' + authString };
return fetch(this.bitcoindUrl, {
method: 'POST',
body: JSON.stringify(jsonRPC),
headers: headers
}).then(function (resp) {
return resp.json();
}).then(function (respObj) {
return respObj.result;
});
}
}, {
key: 'getBlockHeight',
value: function getBlockHeight() {
var jsonRPC = {
jsonrpc: '1.0',
method: 'getblockcount'
};
var authString = Buffer.from(this.bitcoindCredentials.username + ':' + this.bitcoindCredentials.password).toString('base64');
var headers = { Authorization: 'Basic ' + authString };
return fetch(this.bitcoindUrl, {
method: 'POST',
body: JSON.stringify(jsonRPC),
headers: headers
}).then(function (resp) {
return resp.json();
}).then(function (respObj) {
return respObj.result;
});
}
}, {
key: 'getTransactionInfo',
value: function getTransactionInfo(txHash) {
var _this17 = this;
var jsonRPC = {
jsonrpc: '1.0',
method: 'gettransaction',
params: [txHash]
};
var authString = Buffer.from(this.bitcoindCredentials.username + ':' + this.bitcoindCredentials.password).toString('base64');
var headers = { Authorization: 'Basic ' + authString };
return fetch(this.bitcoindUrl, {
method: 'POST',
body: JSON.stringify(jsonRPC),
headers: headers
}).then(function (resp) {
return resp.json();
}).then(function (respObj) {
return respObj.result;
}).then(function (txInfo) {
return txInfo.blockhash;
}).then(function (blockhash) {
var jsonRPCBlock = {
jsonrpc: '1.0',
method: 'getblockheader',
params: [blockhash]
};
headers.Authorization = 'Basic ' + authString;
return fetch(_this17.bitcoindUrl, {
method: 'POST',
body: JSON.stringify(jsonRPCBlock),
headers: headers
});
}).then(function (resp) {
return resp.json();
}).then(function (respObj) {
if (!respObj || !respObj.result) {
// unconfirmed
throw new Error('Unconfirmed transaction');
} else {
return { block_height: respObj.result.height };
}
});
}
}, {
key: 'getNetworkedUTXOs',
value: function getNetworkedUTXOs(address) {
var _this18 = this;
var jsonRPCImport = {
jsonrpc: '1.0',
method: 'importaddress',
params: [address]
};
var jsonRPCUnspent = {
jsonrpc: '1.0',
method: 'listunspent',
params: [0, 9999999, [address]]
};
var authString = Buffer.from(this.bitcoindCredentials.username + ':' + this.bitcoindCredentials.password).toString('base64');
var headers = { Authorization: 'Basic ' + authString };
var importPromise = this.importedBefore[address] ? Promise.resolve() : fetch(this.bitcoindUrl, {
method: 'POST',
body: JSON.stringify(jsonRPCImport),
headers: headers
}).then(function () {
_this18.importedBefore[address] = true;
});
return importPromise.then(function () {
return fetch(_this18.bitcoindUrl, {
method: 'POST',
body: JSON.stringify(jsonRPCUnspent),
headers: headers
});
}).then(function (resp) {
return resp.json();
}).then(function (x) {
return x.result;
}).then(function (utxos) {
return utxos.map(function (x) {
return Object({
value: Math.round(x.amount * SATOSHIS_PER_BTC),
confirmations: x.confirmations,
tx_hash: x.txid,
tx_output_n: x.vout
});
});
});
}
}]);
return BitcoindAPI;
}(BitcoinNetwork);
var InsightClient = exports.InsightClient = function (_BitcoinNetwork2) {
_inherits(InsightClient, _BitcoinNetwork2);
function InsightClient() {
var insightUrl = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'https://utxo.technofractal.com/';
_classCallCheck(this, InsightClient);
var _this19 = _possibleConstructorReturn(this, (InsightClient.__proto__ || Object.getPrototypeOf(InsightClient)).call(this));
_this19.apiUrl = insightUrl;
return _this19;
}
_createClass(InsightClient, [{
key: 'broadcastTransaction',
value: function broadcastTransaction(transaction) {
var jsonData = { tx: transaction };
return fetch(this.apiUrl + '/tx/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(jsonData)
}).then(function (resp) {
return resp.json();
});
}
}, {
key: 'getBlockHeight',
value: function getBlockHeight() {
return fetch(this.apiUrl + '/status').then(function (resp) {
return resp.json();
}).then(function (status) {
return status.blocks;
});
}
}, {
key: 'getTransactionInfo',
value: function getTransactionInfo(txHash) {
var _this20 = this;
return fetch(this.apiUrl + '/tx/' + txHash).then(function (resp) {
return resp.json();
}).then(function (transactionInfo) {
if (transactionInfo.error) {
throw new Error('Error finding transaction: ' + transactionInfo.error);
}
return fetch(_this20.apiUrl + '/block/' + transactionInfo.blockHash);
}).then(function (resp) {
return resp.json();
}).then(function (blockInfo) {
return { block_height: blockInfo.height };
});
}
}, {
key: 'getNetworkedUTXOs',
value: function getNetworkedUTXOs(address) {
return fetch(this.apiUrl + '/addr/' + address + '/utxo').then(function (resp) {
return resp.json();
}).then(function (utxos) {
return utxos.map(function (x) {
return {
value: x.satoshis,
confirmations: x.confirmations,
tx_hash: x.txid,
tx_output_n: x.vout
};
});
});
}
}]);
return InsightClient;
}(BitcoinNetwork);
var BlockchainInfoApi = exports.BlockchainInfoApi = function (_BitcoinNetwork3) {
_inherits(BlockchainInfoApi, _BitcoinNetwork3);
function BlockchainInfoApi() {
var blockchainInfoUrl = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'https://blockchain.info';
_classCallCheck(this, BlockchainInfoApi);
var _this21 = _possibleConstructorReturn(this, (BlockchainInfoApi.__proto__ || Object.getPrototypeOf(BlockchainInfoApi)).call(this));
_this21.utxoProviderUrl = blockchainInfoUrl;
return _this21;
}
_createClass(BlockchainInfoApi, [{
key: 'getBlockHeight',
value: function getBlockHeight() {
return fetch(this.utxoProviderUrl + '/latestblock?cors=true').then(function (resp) {
return resp.json();
}).then(function (blockObj) {
return blockObj.height;
});
}
}, {
key: 'getNetworkedUTXOs',
value: function getNetworkedUTXOs(address) {
return fetch(this.utxoProviderUrl + '/unspent?format=json&active=' + address + '&cors=true').then(function (resp) {
if (resp.status === 500) {
_logger.Logger.debug('UTXO provider 500 usually means no UTXOs: returning []');
return {
unspent_outputs: []
};
} else {
return resp.json();
}
}).then(function (utxoJSON) {
return utxoJSON.unspent_outputs;
}).then(function (utxoList) {
return utxoList.map(function (utxo) {
var utxoOut = {
value: utxo.value,
tx_output_n: utxo.tx_output_n,
confirmations: utxo.confirmations,
tx_hash: utxo.tx_hash_big_endian
};
return utxoOut;
});
});
}
}, {
key: 'getTransactionInfo',
value: function getTransactionInfo(txHash) {
return fetch(this.utxoProviderUrl + '/rawtx/' + txHash + '?cors=true').then(function (resp) {
if (resp.status === 200) {
return resp.json();
} else {
throw new Error('Could not lookup transaction info for \'' + txHash + '\'. Server error.');
}
}).then(function (respObj) {
return { block_height: respObj.block_height };
});
}
}, {
key: 'broadcastTransaction',
value: function broadcastTransaction(transaction) {
var form = new _formData2.default();
form.append('tx', transaction);
return fetch(this.utxoProviderUrl + '/pushtx?cors=true', {
method: 'POST',
body: form
}).then(function (resp) {
var text = resp.text();
return text.then(function (respText) {
if (respText.toLowerCase().indexOf('transaction submitted') >= 0) {
var txHash = _bitcoinjsLib2.default.Transaction.fromHex(transaction).getHash().reverse().toString('hex'); // big_endian
return txHash;
} else {
throw new _errors.RemoteServiceError(resp, 'Broadcast transaction failed with message: ' + respText);
}
});
});
}
}]);
return BlockchainInfoApi;
}(BitcoinNetwork);
var LOCAL_REGTEST = new LocalRegtest('http://localhost:16268', 'http://localhost:16269', new BitcoindAPI('http://localhost:18332/', { username: 'blockstack', password: 'blockstacksystem' }));
var MAINNET_DEFAULT = new BlockstackNetwork('https://core.blockstack.org', 'https://broadcast.blockstack.org', new BlockchainInfoApi());
var network = exports.network = {
BlockstackNetwork: BlockstackNetwork,
LocalRegtest: LocalRegtest,
BlockchainInfoApi: BlockchainInfoApi,
BitcoindAPI: BitcoindAPI,
InsightClient: InsightClient,
defaults: { LOCAL_REGTEST: LOCAL_REGTEST, MAINNET_DEFAULT: MAINNET_DEFAULT }
};