Files
stacks-puppet-node/net-test/bin/faucet.sh
2021-02-05 18:16:24 -05:00

1092 lines
30 KiB
Bash
Executable File

#!/usr/bin/env bash
# Yup, it's a faucet HTTP server written in bash. This is what my life has come to.
MAX_BODY_LENGTH=65536
FAUCET_AMOUNT="1.0"
MODE="$1"
BITCOIN_CONF="$2"
STACKS_WORKING_DIR="$3"
REPORT_MODE="http"
CHAIN_MODE="krypton"
# applicable if run with 'serve'
STACKS_WORKING_DIR="$3"
STACKS_BLOCKS_ROOT="$STACKS_WORKING_DIR/$CHAIN_MODE/chainstate/blocks/"
STACKS_STAGING_DB="$STACKS_WORKING_DIR/$CHAIN_MODE/chainstate/vm/index.sqlite"
STACKS_HEADERS_DB="$STACKS_WORKING_DIR/$CHAIN_MODE/chainstate/vm/index.sqlite"
STACKS_SORTITION_DB="$STACKS_WORKING_DIR/$CHAIN_MODE/burnchain/sortition/marf.sqlite"
STACKS_MEMPOOL_DB="$STACKS_WORKING_DIR/$CHAIN_MODE/chainstate/mempool.sqlite"
exit_error() {
printf "$1" >&2
exit 1
}
for cmd in ncat bitcoin-cli egrep grep tr dd sed cut date sqlite3 awk xxd openssl blockstack-cli; do
which $cmd >/dev/null 2>&1 || exit_error "Missing command: $cmd"
done
if [ $(echo ${BASH_VERSION} | cut -d '.' -f 1) -lt 4 ]; then
exit_error "This script requires Bash 4.x or higher"
fi
set -uo pipefail
log() {
printf >&2 "%s\n" "$1"
}
http_200() {
if [[ "$REPORT_MODE" = "http" ]]; then
local CONTENT_LENGTH="$1"
local CONTENT_TYPE="$2"
printf "HTTP/1.1 200 OK\r\nContent-Length: $CONTENT_LENGTH\r\nContent-Type: $CONTENT_TYPE\r\nConnection: close\r\n\r\n"
fi
}
http_401() {
if [[ "$REPORT_MODE" = "http" ]]; then
printf "HTTP/1.1 401 Unsupported Method\r\nConnection: close\r\n"
elif [[ "$REPORT_MODE" = "text" ]]; then
printf "Unsupported method"
fi
}
http_500() {
local ERR="$1"
local ERR_LEN=${#ERR}
if [[ "$REPORT_MODE" = "http" ]]; then
log "500 error: ${ERR}"
printf "HTTP/1.1 500 Internal Server error\r\nContent-Length: $ERR_LEN\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n$ERR"
elif [[ "$REPORT_MODE" = "text" ]]; then
printf "Failed to create report: $ERR\n"
fi
}
http_404() {
local ERR="$1"
local ERR_LEN=${#ERR}
if [[ "$REPORT_MODE" = "http" ]]; then
printf "HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: $ERR_LEN\r\nContent-Type: text/plain\r\n\r\n$ERR"
elif [[ "$REPORT_MODE" = "text" ]]; then
printf "Not found: $ERR\n"
fi
}
http_chunk() {
local CHUNK_DATA="$1"
local CHUNK_DATA_LEN=${#CHUNK_DATA}
if [[ "$REPORT_MODE" = "http" ]]; then
printf "%x\r\n%s\r\n" "$CHUNK_DATA_LEN" "$CHUNK_DATA"
elif [[ "$REPORT_MODE" = "text" ]]; then
printf "$CHUNK_DATA\n"
fi
}
http_stream() {
local LINE
while read LINE; do
http_chunk "$LINE"
done
}
http_stream_end() {
http_chunk ""
}
http_200_stream() {
local CONTENT_TYPE="$1"
if [[ "$REPORT_MODE" = "http" ]]; then
printf "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: close\r\nContent-Type: $CONTENT_TYPE\r\n\r\n"
fi
}
http_page_begin() {
http_200_stream "text/html"
echo "<html><head></head><body>" | http_stream
}
http_page_end() {
echo "</body><html>" | http_stream
http_stream_end
}
http_json_begin() {
http_200_stream "application/json"
}
http_json_end() {
http_stream_end
}
get_ping() {
http_200 5 "text/plain"
printf "alive"
return 0
}
get_bitcoin_ping() {
if ! [ -f "$BITCOIN_CONF" ]; then
http_404 "Bitcoind is not running on this host"
return 2
fi
bitcoin-cli -conf="$BITCOIN_CONF" ping >/dev/null 2>&1
if [ $? -eq 0 ]; then
local MSG="Bitcoind appears to be running"
http_200 ${#MSG} "text/plain"
echo "$MSG"
return 0
else
http_500 "Bitcoind appears to be stopped"
return 1
fi
}
get_balance() {
if ! [ -f "$BITCOIN_CONF" ]; then
http_404 "Bitcoind is not running on this host"
return 2
fi
local BALANCE="$(bitcoin-cli -conf="$BITCOIN_CONF" getbalance 2>&1)"
if [ $? -eq 0 ]; then
http_200 "${#BALANCE}" "text/plain"
echo "$BALANCE"
return 0
else
http_500 "$BALANCE"
return 1
fi
}
get_utxos() {
local ADDR="$1"
if ! [ -f "$BITCOIN_CONF" ]; then
http_404 "Bitcoind is not running on this host"
return 2
fi
local UTXOS="$(bitcoin-cli -conf="$BITCOIN_CONF" listunspent 1 1000000 "[\"$ADDR\"]" 2>&1)"
if [ $? -eq 0 ]; then
http_200 ${#UTXOS} "application/json"
echo "$UTXOS"
return 0
else
http_500 "$UTXOS"
return 1
fi
}
get_confirmations() {
local TXID="$1"
if ! [ -f "$BITCOIN_CONF" ]; then
http_404 "Bitcoind is not running on this host"
return 2
fi
local CONFIRMATIONS="$(bitcoin-cli -conf="$BITCOIN_CONF" gettransaction "$TXID" | jq -r '.confirmations')"
local RC=$?
if [ $RC -eq 0 ]; then
http_200 ${#CONFIRMATIONS} "text/plain"
echo "$CONFIRMATIONS"
return 0
elif [ $RC -eq 1 ]; then
http_500 "$CONFIRMATIONS"
return 1
else
http_404
return 2
fi
}
rows_to_json() {
awk -F '|' '{
print "["
for (i = 1; i <= NF; i++) {
columns[i] = $i
}
if ((getline nextline) == 0) {
exit 1
}
split(nextline, line, "|")
while (1) {
print "{"
for (i = 1; i < NF; i++) {
print "\"" columns[i] "\": \"" line[i] "\","
}
print "\"" columns[NF] "\": \"" line[NF] "\""
if ((getline nextline) == 0) {
print "}"
break;
}
else {
print "},"
split(nextline, line, "|")
}
}
print "]"
}'
}
rows_to_table() {
awk -F '|' '{
print "<table style='"'"'font-family:\"Courier New\", Courier, monospace; font-size:80%'"'"'>"
print "<tr>"
for (i = 1; i <= NF; i++) {
columns[i] = $i
print "<td><b>" columns[i] "</b></td>"
}
if ((getline nextline) == 0) {
exit 1
}
split(nextline, line, "|")
while (1) {
print "<tr>"
for (i = 1; i <= NF; i++) {
print "<td>" line[i] "</td>"
}
print "</tr>"
if ((getline nextline) == 0) {
break;
}
else {
split(nextline, line, "|")
}
}
print "</table>"
}'
}
row_transpose() {
KEY="$1"
VALUE="$2"
printf "$KEY|$VALUE\n"
awk -F '|' '{
for (i = 1; i <= NF; i++) {
columns[i] = $i
}
num_cols = NF
if ((getline nextline) == 0 ) {
exit 1
}
split(nextline, line, "|")
for (i = 1; i <= num_cols; i++) {
print columns[i] "|" line[i]
}
}'
}
make_index_block_hash() {
local CONSENSUS_HASH="$1"
local BLOCK_HASH="$2"
echo "${BLOCK_HASH}${CONSENSUS_HASH}" | xxd -r -p - | openssl dgst -sha512-256 | cut -d ' ' -f 2
}
query_stacks_block_ptrs() {
local PREDICATE="$1"
local COLUMNS="height,index_block_hash,consensus_hash,anchored_block_hash,parent_consensus_hash,parent_anchored_block_hash,processed,attachable,orphaned"
sqlite3 -header "$STACKS_STAGING_DB" "SELECT $COLUMNS FROM staging_blocks $PREDICATE"
}
query_stacks_index_blocks_by_height() {
local PREDICATE="$1"
local COLUMNS="height,index_block_hash,processed,orphaned"
sqlite3 -header "$STACKS_STAGING_DB" "SELECT $COLUMNS FROM staging_blocks $PREDICATE" | ( \
local HEADERS
read HEADERS
printf "height|index_block_hash(processed,orphaned)\n"
local LAST_HEIGHT=0
local HEIGHT=0
local INDEX_BLOCK_HASH=""
local PROCESSED=0
local ORPHANED=0
IFS="|"
while read HEIGHT INDEX_BLOCK_HASH PROCESSED ORPHANED; do
if (( $HEIGHT != $LAST_HEIGHT)); then
if (( $LAST_HEIGHT > 0 )); then
printf "\n"
fi
LAST_HEIGHT="$HEIGHT"
printf "%s|%s(%s,%s)" "$HEIGHT" "$INDEX_BLOCK_HASH" "$PROCESSED" "$ORPHANED"
else
printf ",%s(%s,%s)" "$INDEX_BLOCK_HASH" "$PROCESSED" "$ORPHANED"
fi
done
printf "\n"
)
}
query_sortitions() {
local PREDICATE="$1"
local COLUMNS="block_height,burn_header_hash,consensus_hash,winning_stacks_block_hash"
sqlite3 -header "$STACKS_SORTITION_DB" "SELECT $COLUMNS FROM snapshots $PREDICATE" | ( \
local HEADERS
read HEADERS
printf "height|burn_header_hash|index_block_hash\n"
local BLOCK_HEIGHT
local BURN_HEADER_HASH
local CONSENSUS_HASH
local WINNING_STACKS_BLOCK_HASH
local INDEX_BLOCK_HASH
IFS="|"
while read BLOCK_HEIGHT BURN_HEADER_HASH CONSENSUS_HASH WINNING_STACKS_BLOCK_HASH; do
INDEX_BLOCK_HASH="0000000000000000000000000000000000000000000000000000000000000000"
if [[ "$WINNING_STACKS_BLOCK_HASH" != "0000000000000000000000000000000000000000000000000000000000000000" ]]; then
INDEX_BLOCK_HASH="$(make_index_block_hash "$CONSENSUS_HASH" "$WINNING_STACKS_BLOCK_HASH")"
fi
printf "%d|%s|%s\n" \
"$BLOCK_HEIGHT" "$BURN_HEADER_HASH" "$INDEX_BLOCK_HASH"
done
)
}
query_stacks_miners() {
local PREDICATE="$1"
local COLUMNS="address,block_hash,consensus_hash,parent_block_hash,parent_consensus_hash,coinbase,tx_fees_anchored,tx_fees_streamed,stx_burns,burnchain_commit_burn,burnchain_sortition_burn,stacks_block_height,miner,vtxindex,index_block_hash"
sqlite3 -header "$STACKS_HEADERS_DB" "SELECT $COLUMNS FROM payments $PREDICATE"
}
query_stacks_block_miners() {
local PREDICATE="$1"
local COLUMNS="stacks_block_height as height,address,index_block_hash"
sqlite3 -header "$STACKS_HEADERS_DB" "SELECT $COLUMNS FROM payments $PREDICATE"
}
query_miner_power() {
printf "total_blocks|address|total_btc|total_stx\n"
sqlite3 -noheader "$STACKS_HEADERS_DB" "SELECT DISTINCT address FROM payments" | ( \
local ADDR=""
local COLUMNS="COUNT(index_block_hash) AS total_blocks,address,SUM(burnchain_commit_burn) AS total_btc,(SUM(coinbase) + SUM(tx_fees_anchored) + SUM(tx_fees_streamed)) AS total_stx"
while read ADDR; do
sqlite3 -noheader "$STACKS_HEADERS_DB" "SELECT $COLUMNS FROM payments WHERE address = \"$ADDR\" LIMIT 1"
done
) | sort -rh
}
query_stacks_mempool() {
local PREDICATE="$1"
local COLUMNS="accept_time AS time,txid,origin_address AS origin,origin_nonce AS nonce,sponsor_address AS sponsor,sponsor_nonce,estimated_fee,tx_fee,length"
sqlite3 -header "$STACKS_MEMPOOL_DB" "SELECT $COLUMNS from mempool $PREDICATE"
}
query_stacks_mempool_tx() {
local TXID="$1"
local COLUMNS="lower(hex(tx))"
sqlite3 -noheader "$STACKS_MEMPOOL_DB" "SELECT $COLUMNS FROM mempool WHERE txid = \"$TXID\""
}
make_prev_next_buttons() {
local A_PATH="$1"
local PAGE="$2"
printf "<div style='font-family:\"Courier New\", Courier, monospace; font-size:80%%'>"
if [[ $PAGE =~ ^[0-9]+$ ]]; then
if (( $PAGE > 0 )); then
printf "<a href=\"%s/%d\">[prev]</a> " "$A_PATH" "$((PAGE - 1))"
fi
printf "<a href=\"%s/%d\">[next]</a>" "$A_PATH" "$((PAGE + 1))"
fi
printf "</div><br>\n"
return 0
}
print_table_of_contents() {
IFS="|"
ANCHOR=""
NAME=""
printf "<table style='font-family:\"Courier New\", Courier, monospace; font-size:80%%'>"
printf "<tr><td><b>Table of Contents</b></td></tr>"
while read ANCHOR NAME; do
printf "<tr><td><a href=\"#$ANCHOR\">$NAME</a></td><tr>"
done
printf "</table>\n"
return 0
}
get_page_list_stacks_blocks() {
if ! [ -f "$STACKS_STAGING_DB" ]; then
http_404 "Stacks node not running on this host"
return 2
fi
local FORMAT="$1"
local LIMIT="$2"
local PAGE="$3"
local QUERY="ORDER BY height DESC, processed DESC, orphaned ASC"
if [[ "$LIMIT" != "all" ]]; then
local OFFSET=$((PAGE * LIMIT))
QUERY="$QUERY LIMIT $LIMIT OFFSET $OFFSET"
fi
if [[ "$FORMAT" = "html" ]]; then
echo "<h3 id=\"stacks_history\"><b>Stacks blockchain history</b></h3>" | http_stream
make_prev_next_buttons "/stacks/history" "$PAGE" | http_stream
query_stacks_index_blocks_by_height "$QUERY" | \
sed -r 's/([0-9a-f]{64})/<a href="\/stacks\/blocks\/\1">\1<\/a>/g' | \
rows_to_table | \
http_stream
elif [[ "$FORMAT" = "json" ]]; then
query_stacks_block_ptrs "$QUERY" | rows_to_json | http_stream
fi
return 0
}
get_page_list_sortitions() {
if ! [ -f "$STACKS_SORTITION_DB" ]; then
http_404 "Stacks node not running on this host"
return 2
fi
local FORMAT="$1"
local LIMIT="$2"
local PAGE="$3"
local QUERY="WHERE pox_valid = 1 ORDER BY block_height DESC"
if [[ "$LIMIT" != "all" ]]; then
local OFFSET=$((PAGE * LIMIT))
QUERY="$QUERY LIMIT $LIMIT OFFSET $OFFSET"
fi
if [[ "$FORMAT" = "html" ]]; then
echo "<h3 id=\"stacks_sortitions\"><b>Sortition history</b></h3>" | http_stream
make_prev_next_buttons "/stacks/sortitions" "$PAGE" | http_stream
query_sortitions "$QUERY" | \
sed -r \
-e 's/0{64}/no winner/g' \
-e 's/([0-9a-f]{64})$/<a href="\/stacks\/blocks\/\1">\1<\/a>/g' | \
rows_to_table | \
http_stream
elif [[ "$FORMAT" = "json" ]]; then
query_sortitions "$QUERY" | rows_to_json | http_stream
fi
return 0
}
get_page_list_miners() {
if ! [ -f "$STACKS_HEADERS_DB" ]; then
http_404 "Stacks node not running on this host"
return 2
fi
local FORMAT="$1"
local LIMIT="$2"
local PAGE="$3"
local QUERY="ORDER BY stacks_block_height DESC"
if [[ "$LIMIT" != "all" ]]; then
local OFFSET=$((PAGE * LIMIT))
QUERY="$QUERY LIMIT $LIMIT OFFSET $OFFSET"
fi
if [[ "$FORMAT" = "html" ]]; then
echo "<h3 id=\"stacks_miners\"><b>Stacks Block Miner History</b></h3>" | http_stream
make_prev_next_buttons "/stacks/miners" "$PAGE" | http_stream
query_stacks_block_miners "$QUERY" | \
sed -r \
-e 's/([0-9a-f]{64})$/<a href="\/stacks\/blocks\/\1">\1<\/a>/g' | \
rows_to_table | \
http_stream
elif [[ "$FORMAT" = "json" ]]; then
query_stacks_block_miners "$QUERY" | rows_to_json | http_stream
fi
return 0
}
get_page_list_mempool() {
if ! [ -f "$STACKS_MEMPOOL_DB" ]; then
http_404 "Stacks node not running on this host"
return 2
fi
local FORMAT="html"
local LIMIT="$2"
local PAGE="$3"
local QUERY="ORDER BY time DESC"
if [[ "$LIMIT" != "all" ]]; then
local OFFSET=$((PAGE * LIMIT))
QUERY="$QUERY LIMIT $LIMIT OFFSET $OFFSET"
fi
if [[ "$FORMAT" = "html" ]]; then
echo "<h3 id=\"stacks_mempool\"><b>Node Mempool</b></h3>" | http_stream
make_prev_next_buttons "/stacks/mempool" "$PAGE" | http_stream
query_stacks_mempool "$QUERY" | \
sed -r 's/([0-9a-f]{64})/<a href=\"\/stacks\/mempool_tx\/\1">\1<\/a>/g' | \
rows_to_table | \
http_stream
elif [[ "$FORMAT" = "json" ]]; then
query_stacks_mempool "$QUERY" | rows_to_json | http_stream
fi
return 0
}
get_page_miner_power() {
if ! [ -f "$STACKS_HEADERS_DB" ]; then
http_404 "Stacks node not running on this host"
return 2
fi
local FORMAT="$1"
if [[ "$FORMAT" = "html" ]]; then
echo "<h3 id=\"miner_power\"><b>Miner Power</b></h3>" | http_stream
query_miner_power | rows_to_table | http_stream
elif [[ "$FORMAT" = "json" ]]; then
query_miner_power | rows_to_json | http_stream
fi
return 0
}
get_block_path() {
local INDEX_BLOCK_HASH="$1"
local PATH_SUFFIX="$(echo "$INDEX_BLOCK_HASH" | sed -r 's/^([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]+)$/\1\/\2\/\1\2\3/g')"
echo "$STACKS_BLOCKS_ROOT/$PATH_SUFFIX"
return 0
}
get_page_stacks_block() {
if ! [ -f "$STACKS_STAGING_DB" ]; then
http_404 "Stacks node not running on ths host"
return 2
fi
local FORMAT="$1"
local INDEX_BLOCK_HASH="$2"
local BLOCK_PATH="$(get_block_path "$INDEX_BLOCK_HASH")"
if ! [ -f "$BLOCK_PATH" ]; then
http_404 "No such block: $INDEX_BLOCK_HASH"
return 2
fi
if [[ "$(stat -c "%s" "$BLOCK_PATH")" = "0" ]]; then
http_404 "Invalid block: $INDEX_BLOCK_HASH"
return 2
fi
if [[ "$FORMAT" = "html" ]]; then
http_page_begin
elif [[ "$FORMAT" = "json" ]]; then
http_json_begin
fi
local MINER_QUERY="WHERE index_block_hash = '$INDEX_BLOCK_HASH' AND miner = 1 LIMIT 1"
local PARENT_QUERY="WHERE index_block_hash = '$INDEX_BLOCK_HASH' LIMIT 1"
local HAS_BLOCK_PROCESSED="$(
if [[ "$(query_stacks_miners "$MINER_QUERY" | wc -l)" = "0" ]]; then
echo "0"
else
echo "1"
fi
)"
local PARENT_BLOCK_PTR="$(
query_stacks_block_ptrs "$PARENT_QUERY" | \
rows_to_json | \
jq -r '.[].parent_consensus_hash,.[].parent_anchored_block_hash' | ( \
read PARENT_CONSENSUS_HASH
read PARENT_BLOCK_HASH
echo "$PARENT_CONSENSUS_HASH|$PARENT_BLOCK_HASH"
)
)"
local PARENT_CONSENSUS_HASH="$(echo "$PARENT_BLOCK_PTR" | ( IFS="|" read PARENT_CONSENSUS_HASH UNUSED; echo "$PARENT_CONSENSUS_HASH" ))"
local PARENT_BLOCK_HASH="$(echo "$PARENT_BLOCK_PTR" | ( IFS="|" read UNUSED PARENT_BLOCK_HASH; echo "$PARENT_BLOCK_HASH" ))"
local PARENT_INDEX_BLOCK_HASH="$(
echo "$PARENT_BLOCK_PTR" | ( \
IFS="|" read PARENT_CONSENSUS_HASH PARENT_BLOCK_HASH
make_index_block_hash "$PARENT_CONSENSUS_HASH" "$PARENT_BLOCK_HASH"
))"
if [[ "$FORMAT" = "html" ]]; then
query_stacks_miners "$MINER_QUERY" | ( \
row_transpose "block_id" "$INDEX_BLOCK_HASH"
echo "parent|<a href=\"/stacks/blocks/$PARENT_INDEX_BLOCK_HASH\">$PARENT_INDEX_BLOCK_HASH</a>"
if [[ "$HAS_BLOCK_PROCESSED" = "0" ]]; then
echo "parent_consensus_hash|$PARENT_CONSENSUS_HASH"
echo "parent_block_hash|$PARENT_BLOCK_HASH"
fi
) | \
rows_to_table | \
http_stream
elif [[ "$FORMAT" = "json" ]]; then
echo "{\"metadata\": " | http_stream
query_stacks_miners "$MINER_QUERY" | \
rows_to_json | \
http_stream
echo ", \"parent\": \"$PARENT_INDEX_BLOCK_HASH\", " | http_stream
if [[ "$HAS_BLOCK_PROCESSED" = "0" ]]; then
echo "\"parent_consensus_hash\": \"$PARENT_CONSENSUS_HASH\"," | http_stream
echo "\"parent_block_hash\": \"$PARENT_BLOCK_HASH\"," | http_stream
fi
fi
local BLOCK_JSON="$(/bin/cat "$BLOCK_PATH" | blockstack-cli decode-block - | jq)"
local RAW_BLOCK="$(/bin/cat "$BLOCK_PATH" | xxd -ps -c 65536 | tr -d '\n')"
if [[ "$FORMAT" = "html" ]]; then
echo "<br><div style='font-family:\"Courier New\", Courier, monospace; font-size:80%'><b>Block</b><br><div style=\"white-space: pre-wrap;\">" | http_stream
http_chunk "$BLOCK_JSON"
echo "</div><br>" | http_stream
echo "<div style='font-family:\"Courier New\", Courier, monospace; font-size:80%'><b>Raw block</b><br><div style=\"overflow-wrap: break-word;\"><br>" | http_stream
http_chunk "$RAW_BLOCK"
echo "</div>" | http_stream
http_page_end
elif [[ "$FORMAT" = "json" ]]; then
echo "\"block\": " | http_stream
http_chunk "$BLOCK_JSON"
echo "}" | http_stream
http_json_end
fi
return 0
}
get_page_mempool_tx() {
if ! [ -f "$STACKS_MEMPOOL_DB" ]; then
http_404 "Stacks node not running on this host"
return 2
fi
local FORMAT="$1"
local TXID="$2"
local QUERY="WHERE txid = \"$TXID\" LIMIT 1"
if [[ "$FORMAT" = "html" ]]; then
query_stacks_mempool "$QUERY" | \
row_transpose "txid" "$TXID" | \
rows_to_table | \
http_stream
elif [[ "$FORMAT" = "json" ]]; then
echo "{\"metadata\": " | http_stream
query_stacks_mempool_tx "$QUERY" | \
rows_to_json | \
http_stream
echo "," | http_stream
fi
local TX="$(query_stacks_mempool_tx "$TXID")"
if [ -z "$TX" ]; then
http_404 "No such transaction: $TXID"
return 2
fi
local TXJSON="$(blockstack-cli decode-tx "$TX" | jq)"
if [[ "$FORMAT" = "html" ]]; then
echo "<br><div style='font-family:\"Courier New\", Courier, monospace; font-size:80%'><b>Transaction</b><br><div style=\"white-space: pre-wrap;\">" | http_stream
http_chunk "$TXJSON"
echo "</div><br>" | http_stream
echo "<div style='font-family:\"Courier New\", Courier, monospace; font-size:80%'><b>Raw transaction</b><br><div style=\"overflow-wrap: break-word;\">" | http_stream
http_chunk "$TX"
echo "</div>" | http_stream
elif [[ "$FORMAT" = "json" ]]; then
echo "\"tx\": " | http_stream
http_chunk "$TXJSON"
echo ", \"raw_tx\": " | http_stream
http_chunk "\"$TX\""
echo "}" | http_stream
fi
return 0
}
post_sendbtc() {
local ADDR
if ! [ -f "$BITCOIN_CONF" ]; then
http_404 "Bitcoind is not running on this host"
return 2
fi
# format: address\n
read ADDR
if ! [[ $ADDR =~ ^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{27,35}$ ]]; then
http_401
return 3
fi
TXID="$(bitcoin-cli -conf="$BITCOIN_CONF" sendtoaddress "$ADDR" "$FAUCET_AMOUNT" 2>&1)"
if [ $? -ne 0 ]; then
http_500 "$TXID"
return 1
fi
ERR="$(bitcoin-cli -conf="$BITCOIN_CONF" importaddress "$ADDR" 2>&1)"
if [ $? -ne 0 ]; then
http_500 "$ERR"
return 1
fi
http_200 ${#TXID} "text/plain"
echo "$TXID"
return 0
}
parse_request() {
local REQLINE
local VERB=""
local REQPATH=""
local CONTENT_TYPE=""
local CONTENT_LENGTH=0
while read REQLINE; do
# trim trailing whitespace
REQLINE="${REQLINE%"${REQLINE##*[![:space:]]}"}"
if [ -z "$REQLINE" ]; then
break
fi
# log " reqline = '$REQLINE'"
TOK="$(echo "$REQLINE" | egrep "GET|POST" | sed -r 's/^(GET|POST)[ ]+([^ ]+)[ ]+HTTP\/1.(0|1)$/\1 \2/g')"
if [ -n "$TOK" ] && [ -z "$VERB" ] && [ -z "$REQPATH" ]; then
set -- $TOK
VERB="$1"
REQPATH="$2"
continue
fi
TOK="$(echo "$REQLINE" | grep -i "content-type" | cut -d ' ' -f 2)"
if [ -n "$TOK" ] && [ -z "$CONTENT_TYPE" ]; then
CONTENT_TYPE="${TOK,,}"
continue
fi
TOK="$(echo "$REQLINE" | grep -i "content-length" | cut -d ' ' -f 2)"
if [ -n "$TOK" ] && [ $CONTENT_LENGTH -eq 0 ]; then
if [[ "$TOK" =~ ^[0-9]+$ ]]; then
CONTENT_LENGTH="$TOK"
continue
fi
fi
done
if [ $CONTENT_LENGTH -gt $MAX_BODY_LENGTH ]; then
exit 1
fi
if [ -z "$VERB" ] || [ -z "$REQPATH" ]; then
exit 1
fi
# log " verb = '$VERB', reqpath = '$REQPATH', content-type = '$CONTENT_TYPE', content-length = '$CONTENT_LENGTH'"
printf "$VERB\n$REQPATH\n$CONTENT_TYPE\n$CONTENT_LENGTH\n"
dd bs=$CONTENT_LENGTH 2>/dev/null
return 0
}
handle_request() {
local VERB
local REQPATH
local CONTENT_TYPE
local CONTENT_LENGTH
local STATUS=200
local RC=0
read VERB
read REQPATH
read CONTENT_TYPE
read CONTENT_LENGTH
case "$VERB" in
GET)
case "$REQPATH" in
/ping)
get_ping
if [ $? -ne 0 ]; then
STATUS=500
fi
;;
/bitcoin)
get_bitcoin_ping
RC=$?
;;
/bitcoin/balance)
get_balance
RC=$?
;;
/bitcoin/confirmations/*)
local TXID="${REQPATH#/bitcoin/confirmations/}"
if ! [[ "$TXID" =~ ^[0-9a-f]{64}$ ]]; then
http_401
STATUS=401
else
get_confirmations "$TXID"
RC=$?
fi
;;
/bitcoin/utxos/*)
local ADDR="${REQPATH#/bitcoin/utxos/}"
if ! [[ "$ADDR" =~ ^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{27,35}$ ]]; then
http_401
STATUS=401
else
get_utxos "$ADDR"
RC=$?
fi
;;
/stacks/blocks/*)
local INDEX_BLOCK_HASH="${REQPATH#/stacks/blocks/}"
if ! [[ "$INDEX_BLOCK_HASH" =~ ^[0-9a-f]{64} ]]; then
http_401
STATUS=401
else
get_page_stacks_block "html" "$INDEX_BLOCK_HASH"
RC=$?
fi
;;
/stacks/history/*)
local PAGE="${REQPATH#/stacks/history/}"
if ! [[ $PAGE =~ ^[0-9]+$ ]]; then
http_401
STATUS=401
else
http_page_begin
get_page_list_stacks_blocks "html" 50 "$PAGE"
RC=$?
http_page_end
fi
;;
/stacks/sortitions/*)
local PAGE="${REQPATH#/stacks/sortitions/}"
if ! [[ $PAGE =~ ^[0-9]+$ ]]; then
http_401
STATUS=401
else
http_page_begin
get_page_list_sortitions "html" 50 "$PAGE"
RC=$?
http_page_end
fi
;;
/stacks/miners/*)
local PAGE="${REQPATH#/stacks/miners/}"
if ! [[ $PAGE =~ ^[0-9]+$ ]]; then
http_401
STATUS=401
else
http_page_begin
get_page_list_miners "html" 50 "$PAGE"
RC=$?
http_page_end
fi
;;
/stacks/mempool/*)
local PAGE="${REQPATH#/stacks/mempool/}"
if ! [[ $PAGE =~ ^[0-9]+$ ]]; then
http_401
STATUS=401
else
http_page_begin
get_page_list_mempool "html" 50 "$PAGE"
RC=$?
http_page_end
fi
;;
/stacks/mempool_tx/*)
local TXID="${REQPATH#/stacks/mempool_tx/}"
if ! [[ $TXID =~ ^[0-9a-f]{64}$ ]]; then
http_401
STATUS=401
else
http_page_begin
get_page_mempool_tx "html" "$TXID"
RC=$?
http_page_end
fi
;;
/|/index.html)
http_page_begin
printf "%s\n%s\n%s\n%s\n%s\n" \
"stacks_history|Stacks Blockchain History" \
"stacks_sortitions|Sortition History" \
"stacks_miners|Stacks Block Miner History" \
"miner_power|Stacks Miner Power" \
"stacks_mempool|Node Mempool" | \
print_table_of_contents | http_stream
get_page_list_stacks_blocks "html" 50 0
get_page_list_sortitions "html" 50 0
get_page_list_miners "html" 50 0
get_page_miner_power "html"
get_page_list_mempool "html" 50 0
http_page_end
;;
/api/blocks/*)
local INDEX_BLOCK_HASH="${REQPATH#/api/blocks/}"
if ! [[ "$INDEX_BLOCK_HASH" =~ ^[0-9a-f]{64} ]]; then
http_401
STATUS=401
else
get_page_stacks_block "json" "$INDEX_BLOCK_HASH"
fi
;;
/api/history)
http_json_begin
get_page_list_stacks_blocks "json" "all" "all"
RC=$?
http_json_end
;;
/api/sortitions)
http_json_begin
get_page_list_sortitions "json" "all" "all"
RC=$?
http_json_end
;;
/api/miners)
http_json_begin
get_page_list_miners "json" "all" "all"
RC=$?
http_json_end
;;
/api/miner_power)
http_json_begin
get_page_miner_power "json"
RC=$?
http_json_end
;;
/api/mempool)
http_json_begin
get_page_list_mempool "json" "all" "all"
RC=$?
http_json_end
;;
/api/mempool_tx/*)
local TXID="${REQPATH#/api/mempool_tx/}"
if ! [[ $TXID =~ ^[0-9a-f]{64}$ ]]; then
http_401
STATUS=401
else
http_page_begin
get_page_mempool_tx "json" "$TXID"
RC=$?
http_page_end
fi
;;
*)
http_404 "No such page $REQPATH"
STATUS=404
;;
esac
;;
POST)
case "$REQPATH" in
/bitcoin/fund)
if [ "$CONTENT_TYPE" != "text/plain" ]; then
http_401
STATUS=401
else
post_sendbtc
if [ $? -eq 1 ]; then
STATUS=500
elif [ $? -eq 2 ]; then
STATUS=404
elif [ $? -eq 3 ]; then
STATUS=401
fi
fi
;;
*)
http_404 "No such page $REQPATH"
STATUS=404
;;
esac
;;
*)
http_401
STATUS=404
;;
esac
if [ $STATUS -eq 200 ]; then
if [ $RC -eq 1 ]; then
STATUS=500
elif [ $RC -eq 2 ]; then
STATUS=404
fi
fi
if [[ "$MODE" = "serve" ]]; then
log "[$(date +%s)] $VERB $REQPATH ($CONTENT_LENGTH bytes) - $STATUS"
fi
}
usage() {
exit_error "Usage:\n $0 serve </path/to/bitcoin.conf> </path/to/stacks/chainstate>\n $0 report </path/to/bitcoin.conf> </path/to/stacks/chainstate> <report-name>\n $0 <port> </path/to/bitcoin.conf> </path/to/stacks/chainstate>\n"
}
if [ -z "$MODE" ] || [ -z "$BITCOIN_CONF" ]; then
usage
fi
if [ "$MODE" = "serve" ]; then
parse_request | handle_request
exit 0
elif [ "$MODE" = "report" ]; then
REPORT_PATH="$4"
REPORT_MODE="text"
printf "GET $REPORT_PATH HTTP/1.0\r\n\r\n" | parse_request | handle_request
exit 0
elif [ "$MODE" = "parse" ]; then
# undocumented test mode
parse_request
exit 0
elif [ "$MODE" = "test" ]; then
# undocumented test mode
shift 3
echo "$@"
eval "$@"
exit 0
fi
# $MODE will be the port number in this usage path
if ! [[ $MODE =~ ^[0-9]+$ ]]; then
usage
fi
exec ncat -k -l -p "$MODE" -c "$BASH \"$0\" serve \"$BITCOIN_CONF\" \"$STACKS_WORKING_DIR\""