aladinnetwork-blockstack
Version:
The Aladin Javascript library for authentication, identity, and storage.
1,133 lines • 45 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const bitcoinjs_lib_1 = require("bitcoinjs-lib");
const form_data_1 = __importDefault(require("form-data"));
const bn_js_1 = __importDefault(require("bn.js"));
const ripemd160_1 = __importDefault(require("ripemd160"));
const errors_1 = require("./errors");
const logger_1 = require("./logger");
const config_1 = require("./config");
const fetchUtil_1 = require("./fetchUtil");
const SATOSHIS_PER_BTC = 1e8;
const TX_BROADCAST_SERVICE_ZONE_FILE_ENDPOINT = 'zone-file';
const TX_BROADCAST_SERVICE_REGISTRATION_ENDPOINT = 'registration';
const TX_BROADCAST_SERVICE_TX_ENDPOINT = 'transaction';
/**
* @private
* @ignore
*/
class BitcoinNetwork {
broadcastTransaction(transaction) {
return Promise.reject(new Error(`Not implemented, broadcastTransaction(${transaction})`));
}
getBlockHeight() {
return Promise.reject(new Error('Not implemented, getBlockHeight()'));
}
getTransactionInfo(txid) {
return Promise.reject(new Error(`Not implemented, getTransactionInfo(${txid})`));
}
getNetworkedUTXOs(address) {
return Promise.reject(new Error(`Not implemented, getNetworkedUTXOs(${address})`));
}
}
exports.BitcoinNetwork = BitcoinNetwork;
/**
* @private
* @ignore
*/
class AladinNetwork {
constructor(apiUrl, broadcastServiceUrl, bitcoinAPI, network = bitcoinjs_lib_1.networks.bitcoin) {
this.aladinAPIUrl = apiUrl;
this.broadcastServiceUrl = broadcastServiceUrl;
this.layer1 = network;
this.btc = bitcoinAPI;
this.DUST_MINIMUM = 5500;
this.includeUtxoMap = {};
this.excludeUtxoSet = [];
this.MAGIC_BYTES = 'id';
}
coerceAddress(address) {
const { hash, version } = bitcoinjs_lib_1.address.fromBase58Check(address);
const scriptHashes = [bitcoinjs_lib_1.networks.bitcoin.scriptHash,
bitcoinjs_lib_1.networks.testnet.scriptHash];
const pubKeyHashes = [bitcoinjs_lib_1.networks.bitcoin.pubKeyHash,
bitcoinjs_lib_1.networks.testnet.pubKeyHash];
let coercedVersion;
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 bitcoinjs_lib_1.address.toBase58Check(hash, coercedVersion);
}
/**
* @ignore
*/
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
*/
getNamePriceV1(fullyQualifiedName) {
// legacy code path
return fetchUtil_1.fetchPrivate(`${this.aladinAPIUrl}/v1/prices/names/${fullyQualifiedName}`)
.then((resp) => {
if (!resp.ok) {
throw new Error(`Failed to query name price for ${fullyQualifiedName}`);
}
return resp;
})
.then(resp => resp.json())
.then(resp => resp.name_price)
.then((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;
}
const result = {
units: 'BTC',
amount: new bn_js_1.default(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
*/
getNamespacePriceV1(namespaceID) {
// legacy code path
return fetchUtil_1.fetchPrivate(`${this.aladinAPIUrl}/v1/prices/namespaces/${namespaceID}`)
.then((resp) => {
if (!resp.ok) {
throw new Error(`Failed to query name price for ${namespaceID}`);
}
return resp;
})
.then(resp => resp.json())
.then((namespacePrice) => {
if (!namespacePrice || !namespacePrice.satoshis) {
throw new Error(`Failed to get price for ${namespaceID}`);
}
if (namespacePrice.satoshis < this.DUST_MINIMUM) {
namespacePrice.satoshis = this.DUST_MINIMUM;
}
const result = {
units: 'BTC',
amount: new bn_js_1.default(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
*/
getNamePriceV2(fullyQualifiedName) {
return fetchUtil_1.fetchPrivate(`${this.aladinAPIUrl}/v2/prices/names/${fullyQualifiedName}`)
.then((resp) => {
if (resp.status !== 200) {
// old core node
throw new Error('The upstream node does not handle the /v2/ price namespace');
}
return resp;
})
.then(resp => resp.json())
.then(resp => resp.name_price)
.then((namePrice) => {
if (!namePrice) {
throw new Error(`Failed to get price for ${fullyQualifiedName}. Does the namespace exist?`);
}
const result = {
units: namePrice.units,
amount: new bn_js_1.default(namePrice.amount)
};
if (namePrice.units === 'BTC') {
// must be at least dust-minimum
const dustMin = new bn_js_1.default(String(this.DUST_MINIMUM));
if (result.amount.ucmp(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
*/
getNamespacePriceV2(namespaceID) {
return fetchUtil_1.fetchPrivate(`${this.aladinAPIUrl}/v2/prices/namespaces/${namespaceID}`)
.then((resp) => {
if (resp.status !== 200) {
// old core node
throw new Error('The upstream node does not handle the /v2/ price namespace');
}
return resp;
})
.then(resp => resp.json())
.then((namespacePrice) => {
if (!namespacePrice) {
throw new Error(`Failed to get price for ${namespaceID}`);
}
const result = {
units: namespacePrice.units,
amount: new bn_js_1.default(namespacePrice.amount)
};
if (namespacePrice.units === 'BTC') {
// must be at least dust-minimum
const dustMin = new bn_js_1.default(String(this.DUST_MINIMUM));
if (result.amount.ucmp(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)
*/
getNamePrice(fullyQualifiedName) {
// handle v1 or v2
return Promise.resolve().then(() => this.getNamePriceV2(fullyQualifiedName))
.catch(() => this.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)
*/
getNamespacePrice(namespaceID) {
// handle v1 or v2
return Promise.resolve().then(() => this.getNamespacePriceV2(namespaceID))
.catch(() => this.getNamespacePriceV1(namespaceID));
}
/**
* How many blocks can pass between a name expiring and the name being able to be
* re-registered by a different owner?
* @param {string} fullyQualifiedName unused
* @return {Promise} a promise to the number of blocks
*/
getGracePeriod(fullyQualifiedName) {
return Promise.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)
*/
getNamesOwned(address) {
const networkAddress = this.coerceAddress(address);
return fetchUtil_1.fetchPrivate(`${this.aladinAPIUrl}/v1/addresses/bitcoin/${networkAddress}`)
.then(resp => resp.json())
.then(obj => 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)
*/
getNamespaceBurnAddress(namespace) {
return Promise.all([
fetchUtil_1.fetchPrivate(`${this.aladinAPIUrl}/v1/namespaces/${namespace}`),
this.getBlockHeight()
])
.then(([resp, blockHeight]) => {
if (resp.status === 404) {
throw new Error(`No such namespace '${namespace}'`);
}
else {
return Promise.all([resp.json(), blockHeight]);
}
})
.then(([namespaceInfo, blockHeight]) => {
let address = this.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(address => this.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
*/
getNameInfo(fullyQualifiedName) {
logger_1.Logger.debug(this.aladinAPIUrl);
const nameLookupURL = `${this.aladinAPIUrl}/v1/names/${fullyQualifiedName}`;
return fetchUtil_1.fetchPrivate(nameLookupURL)
.then((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((nameInfo) => {
logger_1.Logger.debug(`nameInfo: ${JSON.stringify(nameInfo)}`);
// the returned address _should_ be in the correct network ---
// aladind 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: this.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.
*/
getNamespaceInfo(namespaceID) {
return fetchUtil_1.fetchPrivate(`${this.aladinAPIUrl}/v1/namespaces/${namespaceID}`)
.then((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((namespaceInfo) => {
// the returned address _should_ be in the correct network ---
// aladind 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: this.coerceAddress(namespaceInfo.address),
recipient_address: this.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
*/
getZonefile(zonefileHash) {
return fetchUtil_1.fetchPrivate(`${this.aladinAPIUrl}/v1/zonefiles/${zonefileHash}`)
.then((resp) => {
if (resp.status === 200) {
return resp.text()
.then((body) => {
const sha256 = bitcoinjs_lib_1.crypto.sha256(Buffer.from(body));
const h = (new ripemd160_1.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
*/
getAccountStatus(address, tokenType) {
return fetchUtil_1.fetchPrivate(`${this.aladinAPIUrl}/v1/accounts/${address}/${tokenType}/status`)
.then((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((accountStatus) => {
// coerce all addresses, and convert credit/debit to biginteger
const formattedStatus = Object.assign({}, accountStatus, {
address: this.coerceAddress(accountStatus.address),
debit_value: new bn_js_1.default(String(accountStatus.debit_value)),
credit_value: new bn_js_1.default(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)
*/
getAccountHistoryPage(address, page) {
const url = `${this.aladinAPIUrl}/v1/accounts/${address}/history?page=${page}`;
return fetchUtil_1.fetchPrivate(url)
.then((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((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((histEntry) => {
histEntry.address = this.coerceAddress(histEntry.address);
histEntry.debit_value = new bn_js_1.default(String(histEntry.debit_value));
histEntry.credit_value = new bn_js_1.default(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.
*/
getAccountAt(address, blockHeight) {
const url = `${this.aladinAPIUrl}/v1/accounts/${address}/history/${blockHeight}`;
return fetchUtil_1.fetchPrivate(url)
.then((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((historyList) => {
if (historyList.error) {
throw new Error(`Unable to get historic account state: ${historyList.error}`);
}
// coerce all addresses
return historyList.map((histEntry) => {
histEntry.address = this.coerceAddress(histEntry.address);
histEntry.debit_value = new bn_js_1.default(String(histEntry.debit_value));
histEntry.credit_value = new bn_js_1.default(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)
*/
getAccountTokens(address) {
return fetchUtil_1.fetchPrivate(`${this.aladinAPIUrl}/v1/accounts/${address}/tokens`)
.then((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((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.
*/
getAccountBalance(address, tokenType) {
return fetchUtil_1.fetchPrivate(`${this.aladinAPIUrl}/v1/accounts/${address}/${tokenType}/balance`)
.then((resp) => {
if (resp.status === 404) {
// talking to an older aladin core node without the accounts API
return Promise.resolve().then(() => new bn_js_1.default('0'));
}
else if (resp.status !== 200) {
throw new Error(`Bad response status: ${resp.status}`);
}
else {
return resp.json();
}
})
.then((tokenBalance) => {
if (tokenBalance.error) {
throw new Error(`Unable to get account balance: ${tokenBalance.error}`);
}
let balance = '0';
if (tokenBalance && tokenBalance.balance) {
balance = tokenBalance.balance;
}
return new bn_js_1.default(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
*/
broadcastServiceFetchHelper(endpoint, body) {
const requestHeaders = {
Accept: 'application/json',
'Content-Type': 'application/json'
};
const options = {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify(body)
};
const url = `${this.broadcastServiceUrl}/v1/broadcast/${endpoint}`;
return fetchUtil_1.fetchPrivate(url, options)
.then((response) => {
if (response.ok) {
return response.json();
}
else {
throw new errors_1.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
*/
broadcastTransaction(transaction, transactionToWatch = null, confirmations = 6) {
if (!transaction) {
const error = new errors_1.MissingParameterError('transaction');
return Promise.reject(error);
}
if (!confirmations && confirmations !== 0) {
const error = new errors_1.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
* })
*/
const endpoint = TX_BROADCAST_SERVICE_TX_ENDPOINT;
const requestBody = {
transaction,
transactionToWatch,
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
*/
broadcastZoneFile(zoneFile, transactionToWatch = null) {
if (!zoneFile) {
return Promise.reject(new errors_1.MissingParameterError('zoneFile'));
}
// TODO: validate zonefile
if (transactionToWatch) {
// broadcast via transaction broadcast service
/*
* POST /v1/broadcast/zone-file
* Request body:
* JSON.stringify({
* zoneFile,
* transactionToWatch
* })
*/
const requestBody = {
zoneFile,
transactionToWatch
};
const 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'
const requestBody = { zonefile: zoneFile };
return fetchUtil_1.fetchPrivate(`${this.aladinAPIUrl}/v1/zonefile/`, {
method: 'POST',
body: JSON.stringify(requestBody),
headers: {
'Content-Type': 'application/json'
}
})
.then((resp) => {
const json = resp.json();
return json
.then((respObj) => {
if (respObj.hasOwnProperty('error')) {
throw new errors_1.RemoteServiceError(resp);
}
return respObj.servers;
});
});
}
}
/**
* Sends the preorder and registration transactions and zone file
* for a Aladin 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
*/
broadcastNameRegistration(preorderTransaction, registerTransaction, zoneFile) {
/*
* POST /v1/broadcast/registration
* Request body:
* JSON.stringify({
* preorderTransaction,
* registerTransaction,
* zoneFile
* })
*/
if (!preorderTransaction) {
const error = new errors_1.MissingParameterError('preorderTransaction');
return Promise.reject(error);
}
if (!registerTransaction) {
const error = new errors_1.MissingParameterError('registerTransaction');
return Promise.reject(error);
}
if (!zoneFile) {
const error = new errors_1.MissingParameterError('zoneFile');
return Promise.reject(error);
}
const requestBody = {
preorderTransaction,
registerTransaction,
zoneFile
};
const endpoint = TX_BROADCAST_SERVICE_REGISTRATION_ENDPOINT;
return this.broadcastServiceFetchHelper(endpoint, requestBody);
}
/**
* @ignore
*/
getFeeRate() {
return fetchUtil_1.fetchPrivate('https://bitcoinfees.earn.com/api/v1/fees/recommended')
.then(resp => resp.json())
.then(rates => Math.floor(rates.fastestFee));
}
/**
* @ignore
*/
countDustOutputs() {
throw new Error('Not implemented.');
}
/**
* @ignore
*/
getUTXOs(address) {
return this.getNetworkedUTXOs(address)
.then((networkedUTXOs) => {
let returnSet = networkedUTXOs.concat();
if (this.includeUtxoMap.hasOwnProperty(address)) {
returnSet = networkedUTXOs.concat(this.includeUtxoMap[address]);
}
// aaron: I am *well* aware this is O(n)*O(m) runtime
// however, clients should clear the exclude set periodically
const excludeSet = this.excludeUtxoSet;
returnSet = returnSet.filter((utxo) => {
const inExcludeSet = excludeSet.reduce((inSet, utxoToCheck) => 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
* @ignore
*/
modifyUTXOSetFrom(txHex) {
const tx = bitcoinjs_lib_1.Transaction.fromHex(txHex);
const excludeSet = this.excludeUtxoSet.concat();
tx.ins.forEach((utxoUsed) => {
const reverseHash = Buffer.from(utxoUsed.hash);
reverseHash.reverse();
excludeSet.push({
tx_hash: reverseHash.toString('hex'),
tx_output_n: utxoUsed.index
});
});
this.excludeUtxoSet = excludeSet;
const txHash = Buffer.from(tx.getHash().reverse()).toString('hex');
tx.outs.forEach((utxoCreated, txOutputN) => {
const isNullData = function isNullData(script) {
try {
bitcoinjs_lib_1.payments.embed({ output: script }, { validate: true });
return true;
}
catch (_) {
return false;
}
};
if (isNullData(utxoCreated.script)) {
return;
}
const address = bitcoinjs_lib_1.address.fromOutputScript(utxoCreated.script, this.layer1);
let includeSet = [];
if (this.includeUtxoMap.hasOwnProperty(address)) {
includeSet = includeSet.concat(this.includeUtxoMap[address]);
}
includeSet.push({
tx_hash: txHash,
confirmations: 0,
value: utxoCreated.value,
tx_output_n: txOutputN
});
this.includeUtxoMap[address] = includeSet;
});
}
resetUTXOs(address) {
delete this.includeUtxoMap[address];
this.excludeUtxoSet = [];
}
/**
* @ignore
*/
getConsensusHash() {
return fetchUtil_1.fetchPrivate(`${this.aladinAPIUrl}/v1/blockchains/bitcoin/consensus`)
.then(resp => resp.json())
.then(x => x.consensus_hash);
}
getTransactionInfo(txHash) {
return this.btc.getTransactionInfo(txHash);
}
/**
* @ignore
*/
getBlockHeight() {
return this.btc.getBlockHeight();
}
getNetworkedUTXOs(address) {
return this.btc.getNetworkedUTXOs(address);
}
}
exports.AladinNetwork = AladinNetwork;
/**
* @ignore
*/
class LocalRegtest extends AladinNetwork {
constructor(apiUrl, broadcastServiceUrl, bitcoinAPI) {
super(apiUrl, broadcastServiceUrl, bitcoinAPI, bitcoinjs_lib_1.networks.testnet);
}
getFeeRate() {
return Promise.resolve(Math.floor(0.00001000 * SATOSHIS_PER_BTC));
}
}
exports.LocalRegtest = LocalRegtest;
/**
* @ignore
*/
class BitcoindAPI extends BitcoinNetwork {
constructor(bitcoindUrl, bitcoindCredentials) {
super();
this.bitcoindUrl = bitcoindUrl;
this.bitcoindCredentials = bitcoindCredentials;
this.importedBefore = {};
}
broadcastTransaction(transaction) {
const jsonRPC = {
jsonrpc: '1.0',
method: 'sendrawtransaction',
params: [transaction]
};
const authString = Buffer.from(`${this.bitcoindCredentials.username}:${this.bitcoindCredentials.password}`)
.toString('base64');
const headers = { Authorization: `Basic ${authString}` };
return fetchUtil_1.fetchPrivate(this.bitcoindUrl, {
method: 'POST',
body: JSON.stringify(jsonRPC),
headers
})
.then(resp => resp.json())
.then(respObj => respObj.result);
}
getBlockHeight() {
const jsonRPC = {
jsonrpc: '1.0',
method: 'getblockcount'
};
const authString = Buffer.from(`${this.bitcoindCredentials.username}:${this.bitcoindCredentials.password}`)
.toString('base64');
const headers = { Authorization: `Basic ${authString}` };
return fetchUtil_1.fetchPrivate(this.bitcoindUrl, {
method: 'POST',
body: JSON.stringify(jsonRPC),
headers
})
.then(resp => resp.json())
.then(respObj => respObj.result);
}
getTransactionInfo(txHash) {
const jsonRPC = {
jsonrpc: '1.0',
method: 'gettransaction',
params: [txHash]
};
const authString = Buffer.from(`${this.bitcoindCredentials.username}:${this.bitcoindCredentials.password}`)
.toString('base64');
const headers = { Authorization: `Basic ${authString}` };
return fetchUtil_1.fetchPrivate(this.bitcoindUrl, {
method: 'POST',
body: JSON.stringify(jsonRPC),
headers
})
.then(resp => resp.json())
.then(respObj => respObj.result)
.then(txInfo => txInfo.blockhash)
.then((blockhash) => {
const jsonRPCBlock = {
jsonrpc: '1.0',
method: 'getblockheader',
params: [blockhash]
};
headers.Authorization = `Basic ${authString}`;
return fetchUtil_1.fetchPrivate(this.bitcoindUrl, {
method: 'POST',
body: JSON.stringify(jsonRPCBlock),
headers
});
})
.then(resp => resp.json())
.then((respObj) => {
if (!respObj || !respObj.result) {
// unconfirmed
throw new Error('Unconfirmed transaction');
}
else {
return { block_height: respObj.result.height };
}
});
}
getNetworkedUTXOs(address) {
const jsonRPCImport = {
jsonrpc: '1.0',
method: 'importaddress',
params: [address]
};
const jsonRPCUnspent = {
jsonrpc: '1.0',
method: 'listunspent',
params: [0, 9999999, [address]]
};
const authString = Buffer.from(`${this.bitcoindCredentials.username}:${this.bitcoindCredentials.password}`)
.toString('base64');
const headers = { Authorization: `Basic ${authString}` };
const importPromise = (this.importedBefore[address])
? Promise.resolve()
: fetchUtil_1.fetchPrivate(this.bitcoindUrl, {
method: 'POST',
body: JSON.stringify(jsonRPCImport),
headers
})
.then(() => { this.importedBefore[address] = true; });
return importPromise
.then(() => fetchUtil_1.fetchPrivate(this.bitcoindUrl, {
method: 'POST',
body: JSON.stringify(jsonRPCUnspent),
headers
}))
.then(resp => resp.json())
.then(x => x.result)
.then(utxos => utxos.map((x) => ({
value: Math.round(x.amount * SATOSHIS_PER_BTC),
confirmations: x.confirmations,
tx_hash: x.txid,
tx_output_n: x.vout
})));
}
}
exports.BitcoindAPI = BitcoindAPI;
/**
* @ignore
*/
class InsightClient extends BitcoinNetwork {
constructor(insightUrl = 'https://utxo.technofractal.com/') {
super();
this.apiUrl = insightUrl;
}
broadcastTransaction(transaction) {
const jsonData = { rawtx: transaction };
return fetchUtil_1.fetchPrivate(`${this.apiUrl}/tx/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(jsonData)
})
.then(resp => resp.json());
}
getBlockHeight() {
return fetchUtil_1.fetchPrivate(`${this.apiUrl}/status`)
.then(resp => resp.json())
.then(status => status.blocks);
}
getTransactionInfo(txHash) {
return fetchUtil_1.fetchPrivate(`${this.apiUrl}/tx/${txHash}`)
.then(resp => resp.json())
.then((transactionInfo) => {
if (transactionInfo.error) {
throw new Error(`Error finding transaction: ${transactionInfo.error}`);
}
return fetchUtil_1.fetchPrivate(`${this.apiUrl}/block/${transactionInfo.blockHash}`);
})
.then(resp => resp.json())
.then(blockInfo => ({ block_height: blockInfo.height }));
}
getNetworkedUTXOs(address) {
return fetchUtil_1.fetchPrivate(`${this.apiUrl}/addr/${address}/utxo`)
.then(resp => resp.json())
.then(utxos => utxos.map((x) => ({
value: x.satoshis,
confirmations: x.confirmations,
tx_hash: x.txid,
tx_output_n: x.vout
})));
}
}
exports.InsightClient = InsightClient;
/**
* @ignore
*/
class BlockchainInfoApi extends BitcoinNetwork {
constructor(blockchainInfoUrl = 'https://blockchain.info') {
super();
this.utxoProviderUrl = blockchainInfoUrl;
}
getBlockHeight() {
return fetchUtil_1.fetchPrivate(`${this.utxoProviderUrl}/latestblock?cors=true`)
.then(resp => resp.json())
.then(blockObj => blockObj.height);
}
getNetworkedUTXOs(address) {
return fetchUtil_1.fetchPrivate(`${this.utxoProviderUrl}/unspent?format=json&active=${address}&cors=true`)
.then((resp) => {
if (resp.status === 500) {
logger_1.Logger.debug('UTXO provider 500 usually means no UTXOs: returning []');
return {
unspent_outputs: []
};
}
else {
return resp.json();
}
})
.then(utxoJSON => utxoJSON.unspent_outputs)
.then(utxoList => utxoList.map((utxo) => {
const utxoOut = {
value: utxo.value,
tx_output_n: utxo.tx_output_n,
confirmations: utxo.confirmations,
tx_hash: utxo.tx_hash_big_endian
};
return utxoOut;
}));
}
getTransactionInfo(txHash) {
return fetchUtil_1.fetchPrivate(`${this.utxoProviderUrl}/rawtx/${txHash}?cors=true`)
.then((resp) => {
if (resp.status === 200) {
return resp.json();
}
else {
throw new Error(`Could not lookup transaction info for '${txHash}'. Server error.`);
}
})
.then(respObj => ({ block_height: respObj.block_height }));
}
broadcastTransaction(transaction) {
const form = new form_data_1.default();
form.append('tx', transaction);
return fetchUtil_1.fetchPrivate(`${this.utxoProviderUrl}/pushtx?cors=true`, {
method: 'POST',
body: form
})
.then((resp) => {
const text = resp.text();
return text
.then((respText) => {
if (respText.toLowerCase().indexOf('transaction submitted') >= 0) {
const txHash = Buffer.from(bitcoinjs_lib_1.Transaction.fromHex(transaction)
.getHash()
.reverse()).toString('hex'); // big_endian
return txHash;
}
else {
throw new errors_1.RemoteServiceError(resp, `Broadcast transaction failed with message: ${respText}`);
}
});
});
}
}
exports.BlockchainInfoApi = BlockchainInfoApi;
/**
* @ignore
*/
const LOCAL_REGTEST = new LocalRegtest('http://localhost:16268', 'http://localhost:16269', new BitcoindAPI('http://localhost:18332/', { username: 'aladin', password: 'aladinsystem' }));
/**
* @ignore
*/
const MAINNET_DEFAULT = new AladinNetwork('https://core.blockstack.org', 'https://broadcast.blockstack.org', new BlockchainInfoApi());
/**
* 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
*/
function getNameInfo(fullyQualifiedName) {
return config_1.config.network.getNameInfo(fullyQualifiedName);
}
exports.getNameInfo = getNameInfo;
/**
* @ignore
*/
exports.network = {
AladinNetwork,
LocalRegtest,
BlockchainInfoApi,
BitcoindAPI,
InsightClient,
defaults: { LOCAL_REGTEST, MAINNET_DEFAULT }
};
//# sourceMappingURL=network.js.map
;