mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-06-05 19:57:35 +08:00
initial testnet website implementation
This commit is contained in:
33
testnet/Makefile
Normal file
33
testnet/Makefile
Normal file
@@ -0,0 +1,33 @@
|
||||
INDEX_HTML := index.html
|
||||
INDEX_CSS := $(wildcard *.css)
|
||||
INDEX_JS = $(wildcard *.js)
|
||||
WWWDIR := www
|
||||
|
||||
INDEX_FILES := $(INDEX_HTML) $(INDEX_CSS) $(INDEX_JS)
|
||||
|
||||
SITE_FILES := $(patsubst %,$(WWWDIR)/%,$(INDEX_FILES))
|
||||
SITE_INDEX_HTML := $(WWWDIR)/$(INDEX_HTML)
|
||||
|
||||
all: $(SITE_FILES)
|
||||
|
||||
$(WWWDIR):
|
||||
mkdir -p "$@"
|
||||
|
||||
$(SITE_INDEX_HTML): $(INDEX_HTML).py $(WWWDIR)
|
||||
./"$<" > $@
|
||||
|
||||
$(WWWDIR)/%.css: %.css $(WWWDIR)
|
||||
cp -a $< $@
|
||||
|
||||
$(WWWDIR)/%.js: %.js $(WWWDIR)
|
||||
cp -a $< $@
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
$(SHELL) -c "cd "$(WWWDIR)" && python ../test/testServer.py"
|
||||
|
||||
clean:
|
||||
rm -rf "$(WWWDIR)"
|
||||
|
||||
print-%: ; @echo $*=$($*)
|
||||
|
||||
6
testnet/bootstrap.min.css
vendored
Normal file
6
testnet/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
testnet/bootstrap.min.js
vendored
Normal file
7
testnet/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
251
testnet/index.html.py
Executable file
251
testnet/index.html.py
Executable file
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env python2
|
||||
|
||||
import virtualchain
|
||||
|
||||
SCRIPTS = [
|
||||
"testnet.js",
|
||||
"jquery.min.js",
|
||||
"bootstrap.min.js",
|
||||
]
|
||||
|
||||
CSS_PATHS = [
|
||||
"bootstrap.min.css",
|
||||
'testnet.css'
|
||||
]
|
||||
|
||||
def attrs(**kw):
|
||||
for k in kw:
|
||||
assert '"' not in kw[k]
|
||||
|
||||
kwstr = " ".join('{}="{}"'.format(k.strip('_'), kw[k]) for k in kw)
|
||||
return kwstr
|
||||
|
||||
def table(body, **kw):
|
||||
kwstr = attrs(**kw)
|
||||
return "<table {}>{}</table>".format(kwstr, body)
|
||||
|
||||
def tr(body, **kw):
|
||||
kwstr = attrs(**kw)
|
||||
return "<tr {}>{}</tr>".format(kwstr, body)
|
||||
|
||||
def td(body, **kw):
|
||||
kwstr = attrs(**kw)
|
||||
return "<td {}>{}</td>".format(kwstr, body)
|
||||
|
||||
def div(body, **kw):
|
||||
kwstr = attrs(**kw)
|
||||
return "<div {}>{}</div>".format(kwstr, body)
|
||||
|
||||
def span(body, **kw):
|
||||
kwstr = attrs(**kw)
|
||||
return "<span {}>{}</span>".format(kwstr, body)
|
||||
|
||||
def ol(body, **kw):
|
||||
kwstr = attrs(**kw)
|
||||
return '<ol {}>{}</ol>'.format(kwstr, body)
|
||||
|
||||
def ul(body, **kw):
|
||||
kwstr = attrs(**kw)
|
||||
return '<ul {}>{}</ul>'.format(kwstr, body)
|
||||
|
||||
def li(body, **kw):
|
||||
kwstr = attrs(**kw)
|
||||
return '<li {}>{}</li>'.format(kwstr, body)
|
||||
|
||||
def p(body, **kw):
|
||||
kwstr = attrs(**kw)
|
||||
return '<p {}>{}</p>'.format(kwstr, body)
|
||||
|
||||
def form(action, method, body, **kw):
|
||||
kwstr = attrs(**kw)
|
||||
return '<form action="{}" method="{}" {}>{}</form>'.format(action, method, kwstr, body)
|
||||
|
||||
def label(name, _for, **kw):
|
||||
kwstr = attrs(**kw)
|
||||
return '<label for="{}" {}>{}</label>'.format(_for, kwstr, name)
|
||||
|
||||
def textinput(name, default, **kw):
|
||||
kwstr = attrs(**kw)
|
||||
return '<input type="text" name="{}" default="{}" {}/>'.format(name, default, kwstr)
|
||||
|
||||
def submit(value, **kw):
|
||||
kwstr = attrs(**kw)
|
||||
assert '"' not in value
|
||||
return '<button type="submit" {}>{}</button>'.format(kwstr, value)
|
||||
|
||||
|
||||
# UI
|
||||
blockheight = "loading..."
|
||||
consensus_hash = "loading..."
|
||||
GAIA_READ_URL = "loading..."
|
||||
GAIA_WRITE_URL = "loading..."
|
||||
SUBDOMAIN_REGISTRAR_URL = "loading..."
|
||||
TRANSACTION_BROADCASTER_URL = "loading..."
|
||||
BITCOIN_JSONRPC_URL = "loading..."
|
||||
BITCOIN_P2P_URL = "loading..."
|
||||
|
||||
|
||||
url_set = div(
|
||||
div(
|
||||
div("Blockchain Height:", _class="col-sm-6", align="right") + div(blockheight, align="left", _class="code col-sm-6", _id="blockHeight"),
|
||||
_class="row") +
|
||||
div(
|
||||
div("Consensus Hash:", _class="col-sm-6", align="right") + div(consensus_hash, align="left", _class="code col-sm-6", _id="consensusHash"),
|
||||
_class="row") +
|
||||
div(
|
||||
div("Gaia read URL:", _class="col-sm-6", align="right") + div(GAIA_READ_URL, align="left", _class="code col-sm-6", _id="gaiaReadURL"),
|
||||
_class="row") +
|
||||
div(
|
||||
div("Gaia write URL:", _class="col-sm-6", align="right") + div(GAIA_WRITE_URL, align="left", _class="code col-sm-6", _id="gaiaWriteURL"),
|
||||
_class="row") +
|
||||
div(
|
||||
div("Transaction broadcaster:", _class="col-sm-6", align="right") + div(TRANSACTION_BROADCASTER_URL, align="left", _class="code col-sm-6", _id="transactionBroadcasterURL"),
|
||||
_class="row") +
|
||||
div(
|
||||
div("Subdomain registrar:", _class="col-sm-6", align="right") + div(SUBDOMAIN_REGISTRAR_URL, align="left", _class="code col-sm-6", _id="subdomainRegistrarURL"),
|
||||
_class="row") +
|
||||
div(
|
||||
div("Bitcoin JSON-RPC:", _class="col-sm-6", align="right") + div(BITCOIN_JSONRPC_URL, align="left", _class="code col-sm-6", _id="bitcoinJSONRPCURL"),
|
||||
_class="row") +
|
||||
div(
|
||||
div("Bitcoin P2P:", _class="col-sm-6", align="right") + div(BITCOIN_P2P_URL, align="left", _class="code col-sm-6", _id="bitcoinP2PURL"),
|
||||
_class="row"),
|
||||
_class="row")
|
||||
|
||||
|
||||
fund_form = div(
|
||||
form("/sendBTC", "POST",
|
||||
div(
|
||||
label('Address:', 'btcAddress', _class='control-label col-sm-4') +
|
||||
div(
|
||||
textinput("addr", "", _class="form-control", _id="btcAddress"),
|
||||
_class="col-sm-4"),
|
||||
_class="form-group") +
|
||||
div(
|
||||
label('Satoshis:', 'btcAmount', _class='control-label col-sm-4') +
|
||||
div(
|
||||
textinput('value', '0', _class='form-control', _id='btcAmount'),
|
||||
_class="col-sm-4"),
|
||||
_class='form-group') +
|
||||
div(
|
||||
div(
|
||||
submit('Fund address with Bitcoin', _class='btn btn-default'),
|
||||
_class='col-sm-offset-3 col-sm-10'),
|
||||
_class='form-group'),
|
||||
_class="form-horizontal"),
|
||||
_class="row") + \
|
||||
div(
|
||||
form("/sendStacks", "POST",
|
||||
div(
|
||||
label('Address:', 'stacksAddress', _class='control-label col-sm-4') +
|
||||
div(
|
||||
textinput("addr", "", _class="form-control", _id="stacksAddress"),
|
||||
_class="col-sm-4"),
|
||||
_class="form-group") +
|
||||
div(
|
||||
label('microStacks:', 'stacksAmount', _class='control-label col-sm-4') +
|
||||
div(
|
||||
textinput('value', '0', _class='form-control', _id='stacksAmount'),
|
||||
_class="col-sm-4"),
|
||||
_class='form-group') +
|
||||
div(
|
||||
div(
|
||||
submit('Fund address with Stacks', _class='btn btn-default'),
|
||||
_class='col-sm-offset-3 col-sm-10'),
|
||||
_class='form-group'),
|
||||
_class="form-horizontal"),
|
||||
_class='row') + "<hr/>" + \
|
||||
div(
|
||||
div(
|
||||
div(
|
||||
label('Address:', 'queryAddress', _class='control-label col-sm-4') +
|
||||
div(
|
||||
textinput('addr', '', _class='form-control', _id='queryAddress'),
|
||||
_class='col-sm-4'),
|
||||
_class='form-group') +
|
||||
div(
|
||||
div(
|
||||
submit('Get Balance', _class='btn btn-default', onclick="getAddressBalance(document.getElementById('queryAddress').value)"),
|
||||
_class='col-sm-offset-3 col-sm-10'),
|
||||
_class='form-group') +
|
||||
div(
|
||||
div(' ', _class='code col-sm-4 col-sm-offset-1', _id='addressBTCBalance') + div(' ', _class='code col-sm-4 col-sm-offset-2', _id='addressStacksBalance'),
|
||||
_class='row'),
|
||||
_class='form-horizontal'),
|
||||
_class='row')
|
||||
|
||||
|
||||
hello_world = div(
|
||||
div(
|
||||
p('Welcome to the Blockstack blockchain testnet. Here\'s how to get started:') + '<br>' +
|
||||
ul(
|
||||
li('Install the <code>feature/stacks-token</code> branch of <a href="https://github.com/blockstack/blockstack.js/tree/feature/stacks-transactions">blockstack.js</a>') +
|
||||
li('Install the new <a href="https://github.com/jcnelson/cli-blockstack">Blockstack CLI</a>') +
|
||||
li('Make a keychain with: ' + p('<code>$ blockstack-cli -t make_keychain</code>')) +
|
||||
li('Use the Faucet below to fund your payment address with some Stacks') +
|
||||
li('Register a name with: ' + p('<code>$ blockstack-cli -t register hello_world.id YOUR_OWNER_KEY YOUR_PAYMENT_KEY GAIA_READ_URL</code>')) +
|
||||
li('Register a subdomain with: ' + p('<code>$ blockstack-cli -t register_subdomain hello_world.personal.id YOUR_OWNER_KEY GAIA_READ_URL SUBDOMAIN_REGISTRAR_URL</code>'))
|
||||
) +
|
||||
p("You can find values for <code>GAIA_READ_URL</code> and <code>SUBDOMAIN_REGISTRAR_URL</code> in the Services panel."),
|
||||
_class='col-sm-offset-1 col-sm-10'),
|
||||
_class='row')
|
||||
|
||||
|
||||
names_namespace_list = div(
|
||||
div(
|
||||
"<b>Names</b>",
|
||||
_class="code col-sm-2 col-sm-offset-1", align='left') +
|
||||
div(
|
||||
"<b>Namespaces</b>",
|
||||
_class="code col-sm-2 col-sm-offset-5", align='left'),
|
||||
_class='row') + \
|
||||
div(
|
||||
div(
|
||||
'loading...', _id='namesList',
|
||||
_class='code col-sm-2 col-sm-offset-1') +
|
||||
div(
|
||||
'loading...', _id='namespaceList',
|
||||
_class='code col-sm-2 col-sm-offset-5'),
|
||||
_class='row')
|
||||
|
||||
main_body = div(
|
||||
div(
|
||||
div(
|
||||
div('<b>About</b>', _class='code text-center panel-heading panel-heading-custom') +
|
||||
div(hello_world, _class='panel-body'),
|
||||
_class='panel panel-default') +
|
||||
div(
|
||||
div("<b>Services</b>", _class="code text-center panel-heading panel-heading-custom") +
|
||||
div(url_set, _class='panel-body'),
|
||||
_class='panel panel-default') +
|
||||
div(
|
||||
div('<b>Faucet</b>', _class='code text-center panel-heading panel-heading-custom') +
|
||||
div(fund_form, _class='panel-body'),
|
||||
_class='panel panel-default') +
|
||||
div(
|
||||
div('<b>Registered Names and Namespaces</b>', _class='code text-center panel-heading panel-heading-custom') +
|
||||
div(names_namespace_list, _class='panel-body'),
|
||||
_class='panel panel-default') +
|
||||
div(
|
||||
div('<b>Latest Blockstack Transactions</b>', _class='code text-center panel-heading panel-heading-custom') +
|
||||
div('loading...', _id='lastOperations', _class='panel-body'),
|
||||
_class='panel panel-default') +
|
||||
div(
|
||||
div('<b>Testnet Peers</b>', _class='code text-center panel-heading panel-heading-custom') +
|
||||
div('loading...', _id='atlasNeighbors', _class='panel-body'),
|
||||
_class='panel panel-default'),
|
||||
_class='panel-group'),
|
||||
_class='container')
|
||||
|
||||
panel = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n'
|
||||
panel += "<html lang=\"en\"><head><title>Stacks Testnet</title>"
|
||||
|
||||
for s in CSS_PATHS:
|
||||
panel += '<link rel="stylesheet" href="{}">'.format(s)
|
||||
|
||||
for s in SCRIPTS:
|
||||
panel += '<script type="text/javascript" src="{}"></script>'.format(s)
|
||||
|
||||
panel += "</head><body>" + main_body + "</body></html>"
|
||||
|
||||
print panel
|
||||
2
testnet/jquery.min.js
vendored
Normal file
2
testnet/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
34
testnet/nginx/testnet
Normal file
34
testnet/nginx/testnet
Normal file
@@ -0,0 +1,34 @@
|
||||
upstream testFramework {
|
||||
server localhost:30001;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name testnet;
|
||||
|
||||
|
||||
location ~ ^/(operations|atlas-neighbors|lastblock)/ {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://testFramework;
|
||||
break;
|
||||
}
|
||||
|
||||
location / {
|
||||
index /home/jude/Desktop/research/git/blockstack-core/testnet/static/index.html;
|
||||
}
|
||||
|
||||
# listen 443 ssl; # managed by Certbot
|
||||
# ssl_certificate /etc/letsencrypt/live/twitter.technofractal.com/fullchain.pem; # managed by Certbot
|
||||
# ssl_certificate_key /etc/letsencrypt/live/twitter.technofractal.com/privkey.pem; # managed by Certbot
|
||||
# include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
# ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
|
||||
# if ($scheme != "https") {
|
||||
# return 301 https://$host$request_uri;
|
||||
# } # managed by Certbot
|
||||
|
||||
}
|
||||
327
testnet/test/testServer.py
Executable file
327
testnet/test/testServer.py
Executable file
@@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env python2
|
||||
|
||||
import os
|
||||
import posixpath
|
||||
import BaseHTTPServer
|
||||
import urllib
|
||||
import cgi
|
||||
import sys
|
||||
import shutil
|
||||
import mimetypes
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
UPSTREAM = "http://localhost:30001"
|
||||
|
||||
UPSTREAM_GET_PATHS = ['/config', '/operations', '/atlas-neighbors', '/blockHeight', '/balance/']
|
||||
UPSTREAM_POST_PATHS = ['/sendBTC', '/sendStacks', '/registerName', '/registerSubdomain']
|
||||
|
||||
MOCK = True
|
||||
|
||||
class TestnetTestServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
|
||||
def get_data(self):
|
||||
contentlen = int(self.headers.getheader('content-length', 0))
|
||||
bits = self.rfile.read(contentlen)
|
||||
return bits
|
||||
|
||||
def do_GET(self):
|
||||
"""Serve a GET request."""
|
||||
if MOCK:
|
||||
if self.path == '/blockHeight':
|
||||
# TODO: return bitcoin block height with cache headers
|
||||
ret = json.dumps({'blockHeight': str(int(time.time())), 'consensusHash': os.urandom(16).encode('hex')})
|
||||
self.send_response(200)
|
||||
self.send_header('content-type', 'application/json')
|
||||
self.send_header('content-length', len(ret))
|
||||
self.send_header('cache-control', 'max-age=60')
|
||||
self.end_headers()
|
||||
self.wfile.write(ret)
|
||||
return
|
||||
|
||||
if self.path == "/operations":
|
||||
# TODO: get operations and return those instead
|
||||
ret = []
|
||||
ret.append({'opcode': 'NAME_PREORDER', 'op_fee': 60000, 'address': '16EMaNw3pkn3v6f2BgnSSs53zAKH4Q8YJg', 'txid': os.urandom(32).encode('hex')})
|
||||
ret.append({'opcode': 'NAME_REGISTER', 'namespace_id': 'id', 'name': 'judecn.id', 'address': '16EMaNw3pkn3v6f2BgnSSs53zAKH4Q8YJg', 'txid': os.urandom(32).encode('hex')})
|
||||
ret.append({'opcode': 'TOKEN_TRANSFER', 'token_fee': 100000000, 'address': '16EMaNw3pkn3v6f2BgnSSs53zAKH4Q8YJg', 'txid': os.urandom(32).encode('hex')})
|
||||
ret = json.dumps(ret)
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('content-type', 'application/json')
|
||||
self.send_header('content-length', len(ret))
|
||||
self.send_header('cache-control', 'max-age=60')
|
||||
self.end_headers()
|
||||
self.wfile.write(ret)
|
||||
return
|
||||
|
||||
if self.path == "/atlas-neighbors":
|
||||
# TODO: get operations and return those instead
|
||||
ret = []
|
||||
ret.append({'host': 'localhost', 'port': 1234})
|
||||
ret.append({'host': 'www.foo.com', 'port': 1234})
|
||||
ret.append({'host': 'www.asdf.com', 'port': 1234})
|
||||
ret = json.dumps(ret)
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('content-type', 'application/json')
|
||||
self.send_header('content-length', len(ret))
|
||||
self.send_header('cache-control', 'max-age=60')
|
||||
self.end_headers()
|
||||
self.wfile.write(ret)
|
||||
return
|
||||
|
||||
if self.path.startswith('/balance/'):
|
||||
ret = {'btc': 123, 'stacks': 1234}
|
||||
ret = json.dumps(ret)
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('content-type', 'application/json')
|
||||
self.send_header('content-length', len(ret))
|
||||
self.send_header('cache-control', 'max-age=60')
|
||||
self.end_headers()
|
||||
self.wfile.write(ret)
|
||||
return
|
||||
|
||||
if self.path.startswith('/names/'):
|
||||
ret = ['larry.id', 'curly.podcast', 'moe.helloworld']
|
||||
ret = json.dumps(ret)
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('content-type', 'application/json')
|
||||
self.send_header('content-length', len(ret))
|
||||
self.send_header('cache-control', 'max-age=60')
|
||||
self.end_headers()
|
||||
self.wfile.write(ret)
|
||||
return
|
||||
|
||||
if self.path.startswith('/namespaces/'):
|
||||
ret = ['id', 'helloworld', 'podcast']
|
||||
ret = json.dumps(ret)
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('content-type', 'application/json')
|
||||
self.send_header('content-length', len(ret))
|
||||
self.send_header('cache-control', 'max-age=60')
|
||||
self.end_headers()
|
||||
self.wfile.write(ret)
|
||||
return
|
||||
|
||||
for upstream_path in UPSTREAM_GET_PATHS:
|
||||
if self.path.startswith(upstream_path):
|
||||
req = requests.get(
|
||||
url=UPSTREAM + self.path,
|
||||
headers={key: self.headers[key] for key in self.headers if key != 'Host'},
|
||||
data=self.get_data(),
|
||||
allow_redirects=False)
|
||||
|
||||
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
|
||||
headers = dict([(name, value) for (name, value) in req.raw.headers.items()
|
||||
if name.lower() not in excluded_headers])
|
||||
|
||||
self.send_response(req.status_code)
|
||||
for h in headers:
|
||||
self.send_header(h, headers[h])
|
||||
|
||||
self.send_header('content-length', len(req.content))
|
||||
|
||||
self.end_headers()
|
||||
self.wfile.write(req.content)
|
||||
return
|
||||
|
||||
f = self.send_head()
|
||||
if f:
|
||||
self.copyfile(f, self.wfile)
|
||||
f.close()
|
||||
|
||||
|
||||
def do_POST(self):
|
||||
"""Serve a POST request."""
|
||||
if self.path in UPSTREAM_POST_PATHS:
|
||||
content_type = self.headers.getheader('content-type')
|
||||
req = requests.post(
|
||||
url=UPSTREAM + self.path,
|
||||
headers={key: self.headers[key] for key in self.headers if key != 'Host'},
|
||||
data=self.get_data(),
|
||||
allow_redirects=False)
|
||||
|
||||
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
|
||||
headers = dict([(name, value) for (name, value) in req.raw.headers.items()
|
||||
if name.lower() not in excluded_headers])
|
||||
|
||||
self.send_response(req.status_code)
|
||||
for h in headers:
|
||||
self.send_header(h, headers[h])
|
||||
|
||||
self.send_header('content-length', len(req.content))
|
||||
|
||||
self.end_headers()
|
||||
self.wfile.write(req.content)
|
||||
return
|
||||
|
||||
return self.send_response(404)
|
||||
|
||||
|
||||
def do_HEAD(self):
|
||||
"""Serve a HEAD request."""
|
||||
f = self.send_head()
|
||||
if f:
|
||||
f.close()
|
||||
|
||||
def send_head(self):
|
||||
"""Common code for GET and HEAD commands.
|
||||
This sends the response code and MIME headers.
|
||||
Return value is either a file object (which has to be copied
|
||||
to the outputfile by the caller unless the command was HEAD,
|
||||
and must be closed by the caller under all circumstances), or
|
||||
None, in which case the caller has nothing further to do.
|
||||
"""
|
||||
path = self.translate_path(self.path)
|
||||
f = None
|
||||
if os.path.isdir(path):
|
||||
if not self.path.endswith('/'):
|
||||
# redirect browser - doing basically what apache does
|
||||
self.send_response(301)
|
||||
self.send_header("Location", self.path + "/")
|
||||
self.end_headers()
|
||||
return None
|
||||
for index in "index.html", "index.htm":
|
||||
index = os.path.join(path, index)
|
||||
if os.path.exists(index):
|
||||
path = index
|
||||
break
|
||||
else:
|
||||
return self.list_directory(path)
|
||||
ctype = self.guess_type(path)
|
||||
try:
|
||||
# Always read in binary mode. Opening files in text mode may cause
|
||||
# newline translations, making the actual size of the content
|
||||
# transmitted *less* than the content-length!
|
||||
f = open(path, 'rb')
|
||||
except IOError:
|
||||
self.send_error(404, "File not found")
|
||||
return None
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", ctype)
|
||||
fs = os.fstat(f.fileno())
|
||||
self.send_header("Content-Length", str(fs[6]))
|
||||
self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
|
||||
self.end_headers()
|
||||
return f
|
||||
|
||||
def list_directory(self, path):
|
||||
"""Helper to produce a directory listing (absent index.html).
|
||||
Return value is either a file object, or None (indicating an
|
||||
error). In either case, the headers are sent, making the
|
||||
interface the same as for send_head().
|
||||
"""
|
||||
try:
|
||||
list = os.listdir(path)
|
||||
except os.error:
|
||||
self.send_error(404, "No permission to list directory")
|
||||
return None
|
||||
list.sort(key=lambda a: a.lower())
|
||||
f = StringIO()
|
||||
displaypath = cgi.escape(urllib.unquote(self.path))
|
||||
f.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">')
|
||||
f.write("<html>\n<title>Directory listing for %s</title>\n" % displaypath)
|
||||
f.write("<body>\n<h2>Directory listing for %s</h2>\n" % displaypath)
|
||||
f.write("<hr>\n<ul>\n")
|
||||
for name in list:
|
||||
fullname = os.path.join(path, name)
|
||||
displayname = linkname = name
|
||||
# Append / for directories or @ for symbolic links
|
||||
if os.path.isdir(fullname):
|
||||
displayname = name + "/"
|
||||
linkname = name + "/"
|
||||
if os.path.islink(fullname):
|
||||
displayname = name + "@"
|
||||
# Note: a link to a directory displays with @ and links with /
|
||||
f.write('<li><a href="%s">%s</a>\n'
|
||||
% (urllib.quote(linkname), cgi.escape(displayname)))
|
||||
f.write("</ul>\n<hr>\n</body>\n</html>\n")
|
||||
length = f.tell()
|
||||
f.seek(0)
|
||||
self.send_response(200)
|
||||
encoding = sys.getfilesystemencoding()
|
||||
self.send_header("Content-type", "text/html; charset=%s" % encoding)
|
||||
self.send_header("Content-Length", str(length))
|
||||
self.end_headers()
|
||||
return f
|
||||
|
||||
def translate_path(self, path):
|
||||
"""Translate a /-separated PATH to the local filename syntax.
|
||||
Components that mean special things to the local file system
|
||||
(e.g. drive or directory names) are ignored. (XXX They should
|
||||
probably be diagnosed.)
|
||||
"""
|
||||
# abandon query parameters
|
||||
path = path.split('?',1)[0]
|
||||
path = path.split('#',1)[0]
|
||||
path = posixpath.normpath(urllib.unquote(path))
|
||||
words = path.split('/')
|
||||
words = filter(None, words)
|
||||
path = os.getcwd()
|
||||
for word in words:
|
||||
drive, word = os.path.splitdrive(word)
|
||||
head, word = os.path.split(word)
|
||||
if word in (os.curdir, os.pardir): continue
|
||||
path = os.path.join(path, word)
|
||||
return path
|
||||
|
||||
def copyfile(self, source, outputfile):
|
||||
"""Copy all data between two file objects.
|
||||
The SOURCE argument is a file object open for reading
|
||||
(or anything with a read() method) and the DESTINATION
|
||||
argument is a file object open for writing (or
|
||||
anything with a write() method).
|
||||
The only reason for overriding this would be to change
|
||||
the block size or perhaps to replace newlines by CRLF
|
||||
-- note however that this the default server uses this
|
||||
to copy binary data as well.
|
||||
"""
|
||||
shutil.copyfileobj(source, outputfile)
|
||||
|
||||
def guess_type(self, path):
|
||||
"""Guess the type of a file.
|
||||
Argument is a PATH (a filename).
|
||||
Return value is a string of the form type/subtype,
|
||||
usable for a MIME Content-type header.
|
||||
The default implementation looks the file's extension
|
||||
up in the table self.extensions_map, using application/octet-stream
|
||||
as a default; however it would be permissible (if
|
||||
slow) to look inside the data to make a better guess.
|
||||
"""
|
||||
|
||||
base, ext = posixpath.splitext(path)
|
||||
if ext in self.extensions_map:
|
||||
return self.extensions_map[ext]
|
||||
ext = ext.lower()
|
||||
if ext in self.extensions_map:
|
||||
return self.extensions_map[ext]
|
||||
else:
|
||||
return self.extensions_map['']
|
||||
|
||||
if not mimetypes.inited:
|
||||
mimetypes.init() # try to read system mime.types
|
||||
extensions_map = mimetypes.types_map.copy()
|
||||
extensions_map.update({
|
||||
'': 'application/octet-stream', # Default
|
||||
'.py': 'text/plain',
|
||||
'.c': 'text/plain',
|
||||
'.h': 'text/plain',
|
||||
})
|
||||
|
||||
|
||||
def test(HandlerClass = TestnetTestServerHandler,
|
||||
ServerClass = BaseHTTPServer.HTTPServer):
|
||||
BaseHTTPServer.test(HandlerClass, ServerClass)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
7
testnet/testnet.css
Normal file
7
testnet/testnet.css
Normal file
@@ -0,0 +1,7 @@
|
||||
table { border-collapse: separate; border-spacing: 6pt; border-style: hidden hidden; }
|
||||
.code { font-family: monospace; line-height: 100%; }
|
||||
.not-given { font-style: italic; color: rgb(128,128,128); }
|
||||
.panel-default > .panel-heading-custom {
|
||||
background: #41294e;
|
||||
color: #fff;
|
||||
}
|
||||
263
testnet/testnet.js
Normal file
263
testnet/testnet.js
Normal file
@@ -0,0 +1,263 @@
|
||||
function makeHttpObject() {
|
||||
try {return new XMLHttpRequest();}
|
||||
catch (error) {}
|
||||
try {return new ActiveXObject("Msxml2.XMLHTTP");}
|
||||
catch (error) {}
|
||||
try {return new ActiveXObject("Microsoft.XMLHTTP");}
|
||||
catch (error) {}
|
||||
|
||||
throw new Error("Could not create HTTP request object.");
|
||||
}
|
||||
|
||||
function formatCode(body) {
|
||||
return "<div class=\"code\" align=\"left\">" + body + "</div>";
|
||||
}
|
||||
|
||||
function formatNotGiven(body) {
|
||||
return "<div class=\"not-given\" align=\"left\">" + body + "</div>";
|
||||
}
|
||||
|
||||
function makeOperationsTable(operations) {
|
||||
var tableData = "<table width=\"100%\">"
|
||||
for (var i = 0; i < operations.length; i++) {
|
||||
var txid = operations[i].txid;
|
||||
var opcode = operations[i].opcode;
|
||||
var address = operations[i].address;
|
||||
var name = operations[i].name;
|
||||
var op_fee = operations[i].op_fee;
|
||||
var token_fee = operations[i].token_fee;
|
||||
var namespace_id = operations[i].namespace_id;
|
||||
|
||||
if (!token_fee) {
|
||||
token_fee = formatNotGiven("(no Stacks fee)");
|
||||
}
|
||||
else {
|
||||
token_fee = formatCode("uStacks: " + token_fee);
|
||||
}
|
||||
|
||||
if (!op_fee) {
|
||||
op_fee = formatNotGiven("(no BTC fee)");
|
||||
}
|
||||
else {
|
||||
op_fee = formatCode("satoshis: " + op_fee);
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
name = formatNotGiven("(no name)");
|
||||
}
|
||||
else {
|
||||
name = formatCode(name);
|
||||
}
|
||||
|
||||
if (!namespace_id) {
|
||||
namespace_id = formatNotGiven("(no namespace)");
|
||||
}
|
||||
else {
|
||||
namespace_id = formatCode(namespace_id);
|
||||
}
|
||||
|
||||
tableData += "<tr>";
|
||||
tableData += "<td>" + name + "</td>";
|
||||
tableData += "<td>" + namespace_id + "</td>";
|
||||
tableData += "<td>" + op_fee + "</td>";
|
||||
tableData += "<td>" + token_fee + "</td>";
|
||||
tableData += "</tr><tr>"
|
||||
tableData += "<td>" + formatCode(opcode) + "</td>";
|
||||
tableData += "<td>" + formatCode(address) + "</td>";
|
||||
tableData += "<td>" + formatCode(txid) + "</td>";
|
||||
tableData += "</tr>";
|
||||
tableData += "<tr><td colspan=\"4\"><hr/></td></tr>";
|
||||
}
|
||||
if (operations.length == 0) {
|
||||
tableData += "<tr><td colspan=\"4\">" + formatNotGiven("(no Blockstack transactions)") + "</tr>";
|
||||
}
|
||||
|
||||
tableData += "</table>"
|
||||
return tableData;
|
||||
}
|
||||
|
||||
function makeAtlasNeighborsTable(neighbors) {
|
||||
var tableData = "<table width=\"100%\">"
|
||||
for (var i = 0; i < neighbors.length; i++) {
|
||||
var atlasHost = neighbors[i].host;
|
||||
var atlasPort = neighbors[i].port;
|
||||
|
||||
tableData += "<tr>";
|
||||
tableData += "<td>" + formatCode(atlasHost + ":" + atlasPort) + "<td>";
|
||||
tableData += "</tr>";
|
||||
}
|
||||
if (neighbors.length == 0) {
|
||||
tableData += "<tr><td colspan=\"2\">" + formatNotGiven("(no neighbor peers)") + "</tr>";
|
||||
}
|
||||
tableData += "</table>";
|
||||
return tableData;
|
||||
}
|
||||
|
||||
function getBlockHeight() {
|
||||
var blockHeightRequest = makeHttpObject();
|
||||
blockHeightRequest.open("GET", "/blockHeight", true);
|
||||
blockHeightRequest.send(null);
|
||||
blockHeightRequest.onreadystatechange = function() {
|
||||
if (blockHeightRequest.readyState == 4) {
|
||||
var blockInfo = JSON.parse(blockHeightRequest.responseText);
|
||||
var blockHeight = blockInfo.blockHeight;
|
||||
var consensusHash = blockInfo.consensusHash;
|
||||
|
||||
var blockHeightElem = document.getElementById("blockHeight");
|
||||
blockHeightElem.innerHTML = blockHeight;
|
||||
|
||||
var chElem = document.getElementById("consensusHash");
|
||||
chElem.innerHTML = consensusHash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getBlockchainOperations() {
|
||||
var operationsRequest = makeHttpObject();
|
||||
operationsRequest.open("GET", "/operations");
|
||||
operationsRequest.send(null);
|
||||
operationsRequest.onreadystatechange = function() {
|
||||
if (operationsRequest.readyState == 4) {
|
||||
var operations = JSON.parse(operationsRequest.responseText);
|
||||
var operationsElem = document.getElementById("lastOperations");
|
||||
operationsElem.innerHTML = makeOperationsTable(operations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAtlasNeighbors() {
|
||||
var atlasNeighborsRequest = makeHttpObject();
|
||||
atlasNeighborsRequest.open("GET", "/atlas-neighbors");
|
||||
atlasNeighborsRequest.send(null);
|
||||
atlasNeighborsRequest.onreadystatechange = function() {
|
||||
if (atlasNeighborsRequest.readyState == 4) {
|
||||
var neighbors = JSON.parse(atlasNeighborsRequest.responseText);
|
||||
var neighborsElem = document.getElementById("atlasNeighbors");
|
||||
neighborsElem.innerHTML = makeAtlasNeighborsTable(neighbors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getTestnetConfig() {
|
||||
var configRequest = makeHttpObject();
|
||||
configRequest.open("GET", "/config");
|
||||
configRequest.send(null);
|
||||
configRequest.onreadystatechange = function() {
|
||||
if (configRequest.readyState == 4) {
|
||||
var configData = JSON.parse(configRequest.responseText);
|
||||
for (var configItem of Object.keys(configData)) {
|
||||
var configElem = document.getElementById(configItem);
|
||||
configElem.innerHTML = configData[configItem];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAddressBalance(addr) {
|
||||
var balanceRequest = makeHttpObject();
|
||||
balanceRequest.open('GET', '/balance/' + addr)
|
||||
balanceRequest.send(null);
|
||||
balanceRequest.onreadystatechange = function() {
|
||||
if (balanceRequest.readyState == 4) {
|
||||
var balanceData = JSON.parse(balanceRequest.responseText);
|
||||
var btcBalance = balanceData.btc;
|
||||
var stacksBalance = balanceData.stacks;
|
||||
|
||||
document.getElementById('addressBTCBalance').innerHTML = 'BTC (satoshis): ' + btcBalance;
|
||||
document.getElementById('addressStacksBalance').innerHTML = 'Stacks (microStacks): ' + stacksBalance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var namesPage = 0;
|
||||
var namespacePage = 0;
|
||||
|
||||
function getNames(page) {
|
||||
var namesRequest = makeHttpObject();
|
||||
namesRequest.open('GET', '/names/' + page)
|
||||
namesRequest.send(null);
|
||||
namesRequest.onreadystatechange = function() {
|
||||
if (namesRequest.readyState == 4) {
|
||||
var namesList = JSON.parse(namesRequest.responseText);
|
||||
var namesHtml = '';
|
||||
for (var i = 0; i < namesList.length; i++) {
|
||||
namesHtml += '<div class="row">' + namesList[i] + '</div>';
|
||||
}
|
||||
|
||||
// little hacky, but prevent overruns
|
||||
if (namesList.length > 0) {
|
||||
document.getElementById('namesList').innerHTML = namesHtml;
|
||||
}
|
||||
else {
|
||||
namesPage = namesPage > 0 ? namesPage - 1: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNamespaces(page) {
|
||||
var namespaceRequest = makeHttpObject();
|
||||
namespaceRequest.open('GET', '/namespaces/' + page)
|
||||
namespaceRequest.send(null);
|
||||
namespaceRequest.onreadystatechange = function() {
|
||||
if (namespaceRequest.readyState == 4) {
|
||||
var namespaceList = JSON.parse(namespaceRequest.responseText);
|
||||
var namespaceHtml = '';
|
||||
for (var i = 0; i < namespaceList.length; i++) {
|
||||
namespaceHtml += '<div class="row">' + namespaceList[i] + '</div>';
|
||||
}
|
||||
|
||||
// little hacky, but prevent overruns
|
||||
if (namespaceList.length > 0) {
|
||||
document.getElementById('namespaceList').innerHTML = namespaceHtml;
|
||||
}
|
||||
else {
|
||||
namespacePage = namespacePage > 0 ? namespacePage - 1: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function nextNamespacePage() {
|
||||
namespacePage += 1;
|
||||
getNamespaces(namespacePage);
|
||||
}
|
||||
|
||||
function prevNamespacePage() {
|
||||
namespacePage -= 1;
|
||||
if (namespacePage < 0) {
|
||||
namespacePage = 0;
|
||||
}
|
||||
else {
|
||||
getNamespaces(namespacePage);
|
||||
}
|
||||
}
|
||||
|
||||
function nextNamesPage() {
|
||||
namesPage += 1;
|
||||
getNames(namesPage);
|
||||
}
|
||||
|
||||
function prevNamesPage() {
|
||||
namesPage -= 1;
|
||||
if (namesPage < 0) {
|
||||
namesPage = 0;
|
||||
}
|
||||
else {
|
||||
getNames(namesPage);
|
||||
}
|
||||
}
|
||||
|
||||
function loadStats() {
|
||||
getTestnetConfig();
|
||||
getBlockHeight();
|
||||
getBlockchainOperations();
|
||||
getAtlasNeighbors();
|
||||
getNames(0);
|
||||
getNamespaces(0);
|
||||
}
|
||||
|
||||
// window.setInterval(loadStats, 15000);
|
||||
window.setInterval(loadStats, 1000);
|
||||
|
||||
Reference in New Issue
Block a user