|
| 1 | +import sys |
| 2 | +import time |
| 3 | +import sqlite3 |
| 4 | +import base64 |
| 5 | + |
| 6 | +#------------------------------------------------------------------------------ |
| 7 | + |
| 8 | +if __name__ == '__main__': |
| 9 | + import os.path as _p |
| 10 | + sys.path.insert(0, _p.abspath(_p.join(_p.dirname(_p.abspath(sys.argv[0])), '..'))) |
| 11 | + src_dir_path = _p.dirname(_p.dirname(_p.dirname(_p.abspath(sys.argv[0])))) |
| 12 | + sys.path.insert(0, src_dir_path) |
| 13 | + sys.path.insert(0, _p.join(src_dir_path, 'bitdust_forks', 'Bismuth')) |
| 14 | + |
| 15 | +#------------------------------------------------------------------------------ |
| 16 | + |
| 17 | +from twisted.internet import reactor |
| 18 | +from twisted.web import server, resource |
| 19 | + |
| 20 | +#------------------------------------------------------------------------------ |
| 21 | + |
| 22 | +from bitdust.logs import lg |
| 23 | + |
| 24 | +from bitdust.lib import strng |
| 25 | + |
| 26 | +from bitdust.main import settings |
| 27 | +from bitdust.main import config |
| 28 | + |
| 29 | +from bitdust.interface import web_html_template |
| 30 | + |
| 31 | +from bitdust.blockchain import bismuth_node |
| 32 | + |
| 33 | +#------------------------------------------------------------------------------ |
| 34 | + |
| 35 | +_Debug = True |
| 36 | +_DebugLevel = 10 |
| 37 | + |
| 38 | +#------------------------------------------------------------------------------ |
| 39 | + |
| 40 | +_DataDirPath = None |
| 41 | +_ExplorerHost = None |
| 42 | +_ExplorerPort = None |
| 43 | +_WebListener = None |
| 44 | + |
| 45 | +#------------------------------------------------------------------------------ |
| 46 | + |
| 47 | + |
| 48 | +def init(): |
| 49 | + global _DataDirPath |
| 50 | + global _ExplorerHost |
| 51 | + global _ExplorerPort |
| 52 | + global _WebListener |
| 53 | + |
| 54 | + _DataDirPath = settings.ServiceDir('bismuth_blockchain') |
| 55 | + _ExplorerHost = config.conf().getString('services/blockchain-explorer/host', '127.0.0.1') |
| 56 | + _ExplorerPort = config.conf().getInt('services/blockchain-explorer/web-port', 19080) |
| 57 | + |
| 58 | + root = BlockchainRootPage() |
| 59 | + root.putChild(b'', BlockchainMainPage()) |
| 60 | + try: |
| 61 | + _WebListener = reactor.listenTCP(_ExplorerPort, server.Site(root)) # @UndefinedVariable |
| 62 | + if _Debug: |
| 63 | + lg.out(_DebugLevel, ' have started web server at port %d hostname=%s' % (_ExplorerPort, strng.to_text(_ExplorerHost))) |
| 64 | + except: |
| 65 | + if _Debug: |
| 66 | + lg.err('exception while trying to listen port ' + str(self.web_port)) |
| 67 | + lg.exc() |
| 68 | + if _Debug: |
| 69 | + lg.args(_DebugLevel, data_dir_path=_DataDirPath) |
| 70 | + return True |
| 71 | + |
| 72 | + |
| 73 | +def shutdown(): |
| 74 | + global _WebListener |
| 75 | + if _Debug: |
| 76 | + lg.dbg(_DebugLevel, '') |
| 77 | + if _WebListener: |
| 78 | + _WebListener.stopListening() |
| 79 | + if _Debug: |
| 80 | + lg.out(_DebugLevel, ' stopped web listener') |
| 81 | + _WebListener = None |
| 82 | + return True |
| 83 | + |
| 84 | + |
| 85 | +#------------------------------------------------------------------------------ |
| 86 | + |
| 87 | + |
| 88 | +def execute(cursor, query, param): |
| 89 | + while True: |
| 90 | + try: |
| 91 | + cursor.execute(query, param) |
| 92 | + break |
| 93 | + except Exception as e: |
| 94 | + print('Database query: {} {}'.format(cursor, query)) |
| 95 | + print('Database retry reason: {}'.format(e)) |
| 96 | + time.sleep(0.2) |
| 97 | + return cursor |
| 98 | + |
| 99 | + |
| 100 | +#------------------------------------------------------------------------------ |
| 101 | + |
| 102 | + |
| 103 | +class BlockchainMainPage(resource.Resource): |
| 104 | + def render_GET(self, request): |
| 105 | + global _ExplorerHost |
| 106 | + |
| 107 | + page_size = 500 |
| 108 | + |
| 109 | + page_num = request.args.get(b'page', []) |
| 110 | + if page_num: |
| 111 | + try: |
| 112 | + page_num = int(strng.to_text(page_num[0])) |
| 113 | + except: |
| 114 | + lg.exc() |
| 115 | + page_num = 0 |
| 116 | + else: |
| 117 | + page_num = 0 |
| 118 | + |
| 119 | + page_max = -1 |
| 120 | + |
| 121 | + src = '' |
| 122 | + |
| 123 | + conn = sqlite3.connect(bismuth_node.nod().ledger_path, timeout=60.0) |
| 124 | + c = conn.cursor() |
| 125 | + execute(c, 'SELECT * FROM transactions ORDER BY block_height DESC, timestamp DESC LIMIT ? OFFSET ?;', ( |
| 126 | + page_size, |
| 127 | + page_size*page_num, |
| 128 | + )) |
| 129 | + _all = c.fetchall() |
| 130 | + c.close() |
| 131 | + conn.close() |
| 132 | + conn = None |
| 133 | + c = None |
| 134 | + |
| 135 | + view = [] |
| 136 | + b = -1 |
| 137 | + x_old = 'init' |
| 138 | + |
| 139 | + for x in _all: |
| 140 | + if int(x[0]) == 1: |
| 141 | + page_max = page_num |
| 142 | + |
| 143 | + if x[0] != x_old: |
| 144 | + color_cell = '#F8F8F8' |
| 145 | + view.append('<tr><td> </td></tr>') #block separator |
| 146 | + else: |
| 147 | + color_cell = 'white' |
| 148 | + |
| 149 | + view.append('<tr bgcolor ={}>'.format(color_cell)) |
| 150 | + |
| 151 | + if x[0] != x_old: |
| 152 | + b = b + 1 |
| 153 | + |
| 154 | + if x_old != x[0]: |
| 155 | + view.append('<td>{}</td>'.format(x[0])) #block height |
| 156 | + else: |
| 157 | + view.append('<td>{}</td>'.format(x[0])) #block height |
| 158 | + |
| 159 | + view.append('<td>{}'.format(time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(float(x[1]))))) |
| 160 | + view.append('<td>{}…</td>'.format(x[2][:6])) #from |
| 161 | + view.append('<td>{}…</td>'.format(x[3][:6])) #to |
| 162 | + view.append('<td>{0:g}</td>'.format(float(x[4]))) #amount |
| 163 | + |
| 164 | + if x_old != x[0]: |
| 165 | + view.append('<td>{}…</td>'.format(x[7][:6])) #block hash |
| 166 | + else: |
| 167 | + view.append('<td> </td>') #block hash |
| 168 | + |
| 169 | + view.append('<td>{0:g}</td>'.format(float(x[8]))) #fee |
| 170 | + view.append('<td>{0:g}</td>'.format(float(x[9]))) #reward |
| 171 | + |
| 172 | + view.append('<td>{}{}</td>'.format(x[10][:16], '…' if len(x[10]) > 16 else '')) #operation |
| 173 | + view.append('<td>{}{}</td>'.format(x[11][:24], '…' if len(x[11]) > 24 else '')) #openfield |
| 174 | + |
| 175 | + view.append('<td><a href="/{}">{}…</a></td>'.format( |
| 176 | + strng.to_text(base64.b64encode(strng.to_bin(x[5]), altchars=b'-_')), |
| 177 | + x[5][:6], |
| 178 | + )) #TXID |
| 179 | + |
| 180 | + view.append('</tr>') |
| 181 | + |
| 182 | + x_old = x[0] |
| 183 | + |
| 184 | + src += '<div id="ui-blockchain-explorer" class="section bg-light">' |
| 185 | + src += '<div class="container">\n' |
| 186 | + src += '<div class="ui-card">\n' |
| 187 | + src += '<div class="card-body">\n' |
| 188 | + |
| 189 | + src += '<div class="row justify-content-center">\n' |
| 190 | + src += '<h1 align=center>BitDust blockchain explorer</h1>\n' |
| 191 | + src += '</div>\n' |
| 192 | + |
| 193 | + src += '<div class="row justify-content-center">\n' |
| 194 | + src += '<ul class="pagination pagination-sm">\n' |
| 195 | + src += '<li class="page-item">\n' |
| 196 | + src += '<a class="page-link" href="/?page={}">\n'.format(max(0, page_num - 1)) |
| 197 | + src += '<span aria-hidden="true">«</span>\n' |
| 198 | + src += '</a>\n' |
| 199 | + src += '</li>\n' |
| 200 | + src += '<li class="page-item">\n' |
| 201 | + src += '<a class="page-link" href="/?page={}">\n'.format(min(page_max, page_num + 1)) |
| 202 | + src += '<span aria-hidden="true">»</span>\n' |
| 203 | + src += '</a>\n' |
| 204 | + src += '</li>\n' |
| 205 | + src += '</ul>\n' |
| 206 | + src += '</div>\n' |
| 207 | + |
| 208 | + src += '<div class="row justify-content-center">\n' |
| 209 | + |
| 210 | + src += '<style type="text/css">#blockchain-table td {padding: 0px 5px; font-size: 0.9em; font-family: monospace, monospace; }</style>' |
| 211 | + |
| 212 | + src += '<table id="blockchain-table" class="table table-responsive">\n' |
| 213 | + |
| 214 | + src += '<tr bgcolor=white>\n' |
| 215 | + src += '<td><b>Block</b></td>\n' |
| 216 | + src += '<td><b>Timestamp</b></td>\n' |
| 217 | + src += '<td><b>From</b></td>\n' |
| 218 | + src += '<td><b>To</b></td>\n' |
| 219 | + src += '<td><b>Amount</b></td>\n' |
| 220 | + src += '<td><b>Hash</b></td>\n' |
| 221 | + src += '<td><b>Fee</b></td>\n' |
| 222 | + src += '<td><b>Reward</b></td>\n' |
| 223 | + src += '<td><b>Operation</b></td>\n' |
| 224 | + src += '<td><b>Openfield</b></td>\n' |
| 225 | + src += '<td><b>TXID</b></td>\n' |
| 226 | + src += '</tr>\n' |
| 227 | + |
| 228 | + src += ''.join(view) |
| 229 | + |
| 230 | + src += '</table>\n' |
| 231 | + src += '</div>\n' |
| 232 | + |
| 233 | + src += '<br>\n' |
| 234 | + src += '<div class="row justify-content-center">\n' |
| 235 | + src += '<ul class="pagination pagination-sm">\n' |
| 236 | + src += '<li class="page-item">\n' |
| 237 | + src += '<a class="page-link" href="/?page={}">\n'.format(max(0, page_num - 1)) |
| 238 | + src += '<span aria-hidden="true">«</span>\n' |
| 239 | + src += '</a>\n' |
| 240 | + src += '</li>\n' |
| 241 | + src += '<li class="page-item">\n' |
| 242 | + src += '<a class="page-link" href="/?page={}">\n'.format(min(page_max, page_num + 1)) |
| 243 | + src += '<span aria-hidden="true">»</span>\n' |
| 244 | + src += '</a>\n' |
| 245 | + src += '</li>\n' |
| 246 | + src += '</ul>\n' |
| 247 | + src += '</div>\n' |
| 248 | + |
| 249 | + src += '</div>\n' |
| 250 | + src += '</div>\n' |
| 251 | + src += '</div>\n' |
| 252 | + src += '</div>\n' |
| 253 | + |
| 254 | + html_src = web_html_template.WEB_ROOT_TEMPLATE % dict( |
| 255 | + title='BitDust blockchain explorer', |
| 256 | + site_url='https://bitdust.io', |
| 257 | + basepath='https://bitdust.io/', |
| 258 | + wikipath='https://bitdust.io/wiki/', |
| 259 | + idserverspath='https://identities.bitdust.io/', |
| 260 | + blockchainpath='https://blockchain.bitdust.io/', |
| 261 | + div_main_class='main blockchain', |
| 262 | + div_main_body=src, |
| 263 | + google_analytics='', |
| 264 | + ) |
| 265 | + return strng.to_bin(html_src) |
| 266 | + |
| 267 | + |
| 268 | +#------------------------------------------------------------------------------ |
| 269 | + |
| 270 | + |
| 271 | +class BlockchainTransactionPage(resource.Resource): |
| 272 | + def __init__(self, tx_id): |
| 273 | + resource.Resource.__init__(self) |
| 274 | + self.tx_id = tx_id |
| 275 | + |
| 276 | + def render_GET(self, request): |
| 277 | + src = '' |
| 278 | + src += '<div id="ui-blockchain-transaction" class="section bg-light">' |
| 279 | + src += '<div class="container">\n' |
| 280 | + src += '<div class="ui-card">\n' |
| 281 | + src += '<div class="card-body">\n' |
| 282 | + |
| 283 | + try: |
| 284 | + _t = strng.to_text(base64.b64decode(self.tx_id, altchars='-_')) |
| 285 | + except: |
| 286 | + src += '<p align=center>invalid transaction ID</p>\n' |
| 287 | + src += '</div>\n' |
| 288 | + src += '</div>\n' |
| 289 | + src += '</div>\n' |
| 290 | + src += '</div>\n' |
| 291 | + html_src = web_html_template.WEB_ROOT_TEMPLATE % dict( |
| 292 | + title='BitDust blockchain explorer', |
| 293 | + site_url='https://bitdust.io', |
| 294 | + basepath='https://bitdust.io/', |
| 295 | + wikipath='https://bitdust.io/wiki/', |
| 296 | + idserverspath='https://identities.bitdust.io/', |
| 297 | + blockchainpath='https://blockchain.bitdust.io/', |
| 298 | + div_main_class='main blockchain-transaction', |
| 299 | + div_main_body=src, |
| 300 | + google_analytics='', |
| 301 | + ) |
| 302 | + return strng.to_bin(html_src) |
| 303 | + |
| 304 | + try: |
| 305 | + conn = sqlite3.connect(bismuth_node.nod().ledger_path, timeout=60.0) |
| 306 | + c = conn.cursor() |
| 307 | + execute(c, 'SELECT * FROM transactions WHERE substr(signature,1,4)=substr(?1,1,4) and signature like ?1;', (_t + '%', )) |
| 308 | + raw = c.fetchone() |
| 309 | + c.close() |
| 310 | + conn.close() |
| 311 | + conn = None |
| 312 | + c = None |
| 313 | + except: |
| 314 | + src += '<p align=center>reading failed</p>\n' |
| 315 | + src += '</div>\n' |
| 316 | + src += '</div>\n' |
| 317 | + src += '</div>\n' |
| 318 | + src += '</div>\n' |
| 319 | + html_src = web_html_template.WEB_ROOT_TEMPLATE % dict( |
| 320 | + title='BitDust blockchain explorer', |
| 321 | + site_url='https://bitdust.io', |
| 322 | + basepath='https://bitdust.io/', |
| 323 | + wikipath='https://bitdust.io/wiki/', |
| 324 | + idserverspath='https://identities.bitdust.io/', |
| 325 | + blockchainpath='https://blockchain.bitdust.io/', |
| 326 | + div_main_class='main blockchain-transaction', |
| 327 | + div_main_body=src, |
| 328 | + google_analytics='', |
| 329 | + ) |
| 330 | + return strng.to_bin(html_src) |
| 331 | + |
| 332 | + src += '<div>block: <b>{}</b></div><br>\n'.format(raw[0]) |
| 333 | + src += '<div>block hash: <b><code>{}</code></b></div><br>\n'.format(raw[7]) |
| 334 | + src += '<div>timestamp: <b>{}</b></div><br>\n'.format(time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(float(raw[1])))) |
| 335 | + src += '<div>sender: <b><code>{}</code></b></div><br>\n'.format(raw[2]) |
| 336 | + src += '<div>recipient: <b><code>{}</code></b></div><br>\n'.format(raw[3]) |
| 337 | + src += '<div>amount: <b>{0:g}</b></div><br>\n'.format(raw[4]) |
| 338 | + src += '<div>fee: <b>{0:g}</b></div><br>\n'.format(raw[8]) |
| 339 | + src += '<div>reward: <b>{0:g}</b></div><br>\n'.format(raw[9]) |
| 340 | + src += '<div>operation: <b>{}</b></div><br>\n'.format(raw[10]) |
| 341 | + src += '<div style="margin: 0 auto; overflow-wrap: break-word; word-wrap: break-word;">openfield: <b><code>{}</code></b></div>\n'.format(raw[11]) |
| 342 | + src += '<div style="margin: 0 auto; overflow-wrap: break-word; word-wrap: break-word;">signature:\n<b><code>{}</code></b></div><br>\n'.format(self.tx_id) |
| 343 | + src += '</div>\n' |
| 344 | + src += '</div>\n' |
| 345 | + src += '</div>\n' |
| 346 | + src += '</div>\n' |
| 347 | + |
| 348 | + html_src = web_html_template.WEB_ROOT_TEMPLATE % dict( |
| 349 | + title='BitDust blockchain explorer', |
| 350 | + site_url='https://bitdust.io', |
| 351 | + basepath='https://bitdust.io/', |
| 352 | + wikipath='https://bitdust.io/wiki/', |
| 353 | + idserverspath='https://identities.bitdust.io/', |
| 354 | + blockchainpath='https://blockchain.bitdust.io/', |
| 355 | + div_main_class='main blockchain-transaction', |
| 356 | + div_main_body=src, |
| 357 | + google_analytics='', |
| 358 | + ) |
| 359 | + return strng.to_bin(html_src) |
| 360 | + |
| 361 | + |
| 362 | +#------------------------------------------------------------------------------ |
| 363 | + |
| 364 | + |
| 365 | +class BlockchainRootPage(resource.Resource): |
| 366 | + def getChild(self, path, request): |
| 367 | + if not path: |
| 368 | + return self |
| 369 | + try: |
| 370 | + path = strng.to_text(path) |
| 371 | + except: |
| 372 | + return resource.NoResource('Not found') |
| 373 | + if path: |
| 374 | + return BlockchainTransactionPage(path) |
| 375 | + return resource.NoResource('Not found') |
| 376 | + |
| 377 | + |
| 378 | +#------------------------------------------------------------------------------ |
| 379 | + |
| 380 | + |
| 381 | +def main(): |
| 382 | + bismuth_node.init() |
| 383 | + settings.init() |
| 384 | + reactor.addSystemEventTrigger('before', 'shutdown', shutdown) # @UndefinedVariable |
| 385 | + reactor.callWhenRunning(init) # @UndefinedVariable |
| 386 | + reactor.run() # @UndefinedVariable |
| 387 | + settings.shutdown() |
| 388 | + |
| 389 | + |
| 390 | +#------------------------------------------------------------------------------ |
| 391 | + |
| 392 | +if __name__ == '__main__': |
| 393 | + main() |
0 commit comments