UNPKG

aladinnetwork-blockstack

Version:

The Aladin Javascript library for authentication, identity, and storage.

1,133 lines 45 kB
"use strict"; 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