From 5686c010153729808f2135729c806993a8ff02b7 Mon Sep 17 00:00:00 2001 From: xcps Date: Sat, 3 Feb 2018 21:14:56 +0500 Subject: [PATCH] init --- .gitignore | 2 + README.md | 22 +++++ app.js | 52 ++++++++++++ bin/initdb.js | 51 ++++++++++++ bin/reinitTables.js | 14 ++++ bin/syncBlockchain.babel.js | 154 +++++++++++++++++++++++++++++++++++ bin/syncBlockchain.js | 2 + bin/www | 92 +++++++++++++++++++++ config/config.json | 25 ++++++ models/address.js | 21 +++++ models/block.js | 33 ++++++++ models/failure.js | 12 +++ models/index.js | 36 ++++++++ models/transaction.js | 32 ++++++++ models/vout.js | 21 +++++ package.json | 25 ++++++ public/favicon.ico | Bin 0 -> 1886 bytes public/images/gostcoin-b.png | Bin 0 -> 22318 bytes public/stylesheets/style.css | 57 +++++++++++++ routes/address.js | 35 ++++++++ routes/block.js | 28 +++++++ routes/index.js | 19 +++++ routes/search.js | 46 +++++++++++ routes/transaction.js | 48 +++++++++++ views/404.jade | 4 + views/address.jade | 11 +++ views/block.jade | 18 ++++ views/error.jade | 6 ++ views/index.jade | 11 +++ views/layout.jade | 18 ++++ views/transaction.jade | 31 +++++++ 31 files changed, 926 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.js create mode 100644 bin/initdb.js create mode 100644 bin/reinitTables.js create mode 100644 bin/syncBlockchain.babel.js create mode 100644 bin/syncBlockchain.js create mode 100755 bin/www create mode 100644 config/config.json create mode 100644 models/address.js create mode 100644 models/block.js create mode 100644 models/failure.js create mode 100644 models/index.js create mode 100644 models/transaction.js create mode 100644 models/vout.js create mode 100644 package.json create mode 100644 public/favicon.ico create mode 100644 public/images/gostcoin-b.png create mode 100644 public/stylesheets/style.css create mode 100644 routes/address.js create mode 100644 routes/block.js create mode 100644 routes/index.js create mode 100644 routes/search.js create mode 100644 routes/transaction.js create mode 100644 views/404.jade create mode 100644 views/address.jade create mode 100644 views/block.jade create mode 100644 views/error.jade create mode 100644 views/index.jade create mode 100644 views/layout.jade create mode 100644 views/transaction.jade diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5f19d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +package-lock.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c8c5a2 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ + +cd gostexplr +npm i +npm run initdb db_root_username db_root_password +npm run syncBlockchain +npm run + + +TODO: + - cut possible -000 from transaction + - datetime zone + - search NOT FOUND message + - pagination on index page + - sync blockchain by db transaction + - block page by index + - move database to server + - setup cron + - ? transaction connect vout to transaction + - address balance info + - lint + - tests + - ES7 \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..34629b6 --- /dev/null +++ b/app.js @@ -0,0 +1,52 @@ +var express = require('express'); +var path = require('path'); +var favicon = require('serve-favicon'); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); +var bodyParser = require('body-parser'); + +var index = require('./routes/index'); +var address = require('./routes/address'); +var transaction = require('./routes/transaction'); +var block = require('./routes/block'); +var search = require('./routes/search'); + +var app = express(); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'jade'); + +// uncomment after placing your favicon in /public +app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); +app.use(logger('dev')); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use('/', index); +app.use('/address', address); +app.use('/transaction', transaction); +app.use('/block', block); +app.use('/search', search); + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + var err = new Error('Not Found'); + err.status = 404; + next(err); +}); + +// error handler +app.use(function(err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render('error'); +}); + +module.exports = app; diff --git a/bin/initdb.js b/bin/initdb.js new file mode 100644 index 0000000..0e0703e --- /dev/null +++ b/bin/initdb.js @@ -0,0 +1,51 @@ +#!/usr/bin/env node +var exec = require('child_process').exec; +var models = require('../models'); +var env = process.env.NODE_ENV || 'development'; +var config = require(__dirname + '/../config/config.json')[env]; + +if (process.argv.length < 4) { + console.log('Provide root user name and password for mysql'); + process.exit(0); +} + +const dropUserDB = `mysql -u${process.argv[2]} -p${process.argv[3]} -e "drop database ${config.database};drop user ${config.username}"` +const createdb = `mysql -u${process.argv[2]} -p${process.argv[3]} -e "create database ${config.database}"`; +const createUser = `mysql -u${process.argv[2]} -p${process.argv[3]} -e "create user ${config.username} identified by '${config.password}'"`; +const grantAccess = `mysql -u${process.argv[2]} -p${process.argv[3]} -e "grant all on ${config.database}.* to ${config.username}"`; + +exec(dropUserDB, function(err,stdout,stderr) { + console.log(stdout); + exec(createdb, function(err,stdout,stderr) { + if (err) { + console.log(err); + process.exit(0); + } else { + console.log(stdout); + exec(createUser, function(err, stdout, stderr) { + if (err) { + console.log(err); + process.exit(0); + } else { + console.log(stdout); + exec(grantAccess, function(err, stdout, stderr) { + if (err) { + console.log(err); + } else { + console.log(stdout); + models.sequelize.sync({force: true}) + .then(() => { + console.log(`\nUSER (${config.username}) AND DATABASE (${config.database}) CREATED SUCCESSFULLY`); + process.exit(0); + }) + .catch((err) => { + console.log(err); + process.exit(0); + }); + } + }); + } + }); + } + }); +}); diff --git a/bin/reinitTables.js b/bin/reinitTables.js new file mode 100644 index 0000000..1245638 --- /dev/null +++ b/bin/reinitTables.js @@ -0,0 +1,14 @@ +#!/usr/bin/env node +var models = require('../models'); + +models.sequelize.sync({force: true}) +.then(() => { + console.log('REINIT SUCCESS') + process.exit(0); +}) +.catch((err) => { + console.log(err); + process.exit(0); +}); + + diff --git a/bin/syncBlockchain.babel.js b/bin/syncBlockchain.babel.js new file mode 100644 index 0000000..6eadf30 --- /dev/null +++ b/bin/syncBlockchain.babel.js @@ -0,0 +1,154 @@ +var http = require('http'); +var models = require('../models'); + +const username = 'gostcoinrpc'; +const password = 'CEQLt9zrNnmyzosSV7pjb3EksAkuY9qeqoUCwDQc2wPc'; +const hostname = '127.0.0.1'; +const port = 9376; + +function MakeRPCRequest(postData) { + return new Promise(function(resolve, reject) { + var post_options = { + host: hostname, + port: port, + auth: `${username}:${password}`, + path: '/', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(postData) + } + }; + var post_req = http.request(post_options, function(res) { + res.setEncoding("utf8"); + let body = ""; + res.on("data", data => { + body += data; + }); + res.on("end", () => { + resolve(body); + }); + }); + post_req.write(postData); + post_req.end(); + }); +} + +async function saveTransaction(txid, blockHeight) { + const res_tx = await MakeRPCRequest(JSON.stringify({ + method: 'getrawtransaction', + params: [txid, 1], + id: 1 + })); + const tx = JSON.parse(res_tx)['result']; + // console.log('tx:', tx); + if (tx === null) { + await models.Failure.create({ + msg: `${txid} fetching failed`, + }); + return; + } + const transaction = await models.Transaction.create({ + txid: tx.txid, + BlockHeight: blockHeight, + }); + for (var i = 0; i < tx.vout.length; i++) { + const vout = tx.vout[i]; + const m_vout = await models.Vout.create({ + value: vout.value, + }); + for (var y = 0; y < vout.scriptPubKey.addresses.length; y++) { + const address = vout.scriptPubKey.addresses[y]; + let m_address = await models.Address.findOne({ + where: { + address, + }, + }); + if (m_address === null) { + m_address = await models.Address.create({ + address, + }); + } + await m_vout.addAddresses(m_address); + } + await transaction.addVouts(m_vout); + } + for (var i = 0; i < tx.vin.length; i++) { + const vin = tx.vin[i]; + if (vin.txid) { + const trans = await models.Transaction.findOne({ + where: { + txid: vin.txid, + }, + }); + if (trans) { + await transaction.addTxtx(trans); + } + } + } +} + +async function syncNextBlock(syncedHeight) { + const height = syncedHeight + 1; + const res_hash = await MakeRPCRequest(JSON.stringify({ + method: 'getblockhash', + params: [height], + id: 1 + })); + const blockHash = JSON.parse(res_hash)['result']; + const res_block = await MakeRPCRequest(JSON.stringify({ + method: 'getblock', + params: [blockHash], + id: 1 + })); + const block = JSON.parse(res_block)['result']; + block.time = new Date(block.time * 1000); + await models.Block.create(block); + // console.log('block:', block); + for (var i = 0; i < block.tx.length; i++) { + await saveTransaction(block.tx[i], block.height); + } + + return height; +} + +async function getCurrentHeight() { + const result = await MakeRPCRequest(JSON.stringify({ + method: 'getblockcount', + params: [], + id: 1 + })); + + return JSON.parse(result)['result']; +} + +async function getSyncedHeight() { + const result = await models.Block.findOne({ + attributes: ['height'], + order: [['height', 'DESC']], + limit: 1 + }); + const height = result ? result.height : -1; + return height; +} + +async function syncBlockchain() { + let syncedHeight = await getSyncedHeight(); + console.log('\x1b[36m%s\x1b[0m', 'syncedHeight is', syncedHeight); + + let currentHeight = await getCurrentHeight(); + console.log('\x1b[36m%s\x1b[0m', 'currentHeight is', currentHeight); + while (syncedHeight < currentHeight) { + syncedHeight = await syncNextBlock(syncedHeight); + console.log('\x1b[36m%s\x1b[0m', 'syncedHeight: ', syncedHeight) + } + process.exit(0); +} + +var postData = JSON.stringify({ + 'method': 'getinfo', + 'params': [], + 'id': 1 + }); + +syncBlockchain(); diff --git a/bin/syncBlockchain.js b/bin/syncBlockchain.js new file mode 100644 index 0000000..09fc16e --- /dev/null +++ b/bin/syncBlockchain.js @@ -0,0 +1,2 @@ +require('babel-register'); +require('./syncBlockchain.babel.js'); diff --git a/bin/www b/bin/www new file mode 100755 index 0000000..176e1b5 --- /dev/null +++ b/bin/www @@ -0,0 +1,92 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('gostexplr:server'); +var http = require('http'); +var models = require('../models'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +models.sequelize.sync().then(function() { + /** + * Listen on provided port, on all network interfaces. + */ + + server.listen(port); + server.on('error', onError); + server.on('listening', onListening); +}); +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..1aab542 --- /dev/null +++ b/config/config.json @@ -0,0 +1,25 @@ +{ + "development": { + "username": "geuser", + "password": "geuser", + "database": "gedb", + "host": "127.0.0.1", + "dialect": "mysql", + "logging": false + }, + "test": { + "username": "root", + "password": null, + "database": "database_test", + "host": "127.0.0.1", + "dialect": "mysql" + }, + "production": { + "username": "geuser", + "password": "geuser", + "database": "gedb", + "host": "127.0.0.1", + "dialect": "mysql", + "logging": false + } +} diff --git a/models/address.js b/models/address.js new file mode 100644 index 0000000..7d41753 --- /dev/null +++ b/models/address.js @@ -0,0 +1,21 @@ +'use strict'; + +module.exports = (sequelize, DataTypes) => { + const Address = sequelize.define('Address', { + address: DataTypes.STRING(34), + }, { + timestamps: false, + indexes: [{ + unique: true, + fields: ['address'] + }], + }); + + const AddressVout = sequelize.define('AddressVout', {}, { timestamps: false }); + + Address.associate = function (models) { + models.Address.belongsToMany(models.Vout, { through: 'AddressVout' }); + }; + + return Address; +}; \ No newline at end of file diff --git a/models/block.js b/models/block.js new file mode 100644 index 0000000..9b9e978 --- /dev/null +++ b/models/block.js @@ -0,0 +1,33 @@ +'use strict'; +module.exports = (sequelize, DataTypes) => { + const Block = sequelize.define('Block', { + height: { + type: DataTypes.INTEGER.UNSIGNED, + primaryKey: true, + }, + hash: DataTypes.STRING(64), + confirmations: DataTypes.MEDIUMINT.UNSIGNED, + size: DataTypes.MEDIUMINT.UNSIGNED, + version: DataTypes.TINYINT.UNSIGNED, + merkleroot: DataTypes.STRING(64), + time: DataTypes.DATE, + nonce: DataTypes.BIGINT, + bits: DataTypes.STRING(8), + difficulty: DataTypes.DECIMAL(16, 8), + previousblockhash: DataTypes.STRING(64), + nextblockhash: DataTypes.STRING(64), + }, { + timestamps: false, + indexes: [{ + unique: true, + fields: ['hash', 'height'] + }], + freezeTableName: true, + }); + + Block.associate = function(models) { + models.Block.hasMany(models.Transaction); + }; + + return Block; +}; \ No newline at end of file diff --git a/models/failure.js b/models/failure.js new file mode 100644 index 0000000..44356e0 --- /dev/null +++ b/models/failure.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = (sequelize, DataTypes) => { + const Failure = sequelize.define('Failure', { + msg: DataTypes.STRING, + }, { + timestamps: true, + updatedAt: false, + }); + + return Failure; +}; \ No newline at end of file diff --git a/models/index.js b/models/index.js new file mode 100644 index 0000000..5662f10 --- /dev/null +++ b/models/index.js @@ -0,0 +1,36 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var Sequelize = require('sequelize'); +var basename = path.basename(__filename); +var env = process.env.NODE_ENV || 'development'; +var config = require(__dirname + '/../config/config.json')[env]; +var db = {}; + +if (config.use_env_variable) { + var sequelize = new Sequelize(process.env[config.use_env_variable], config); +} else { + var sequelize = new Sequelize(config.database, config.username, config.password, config); +} + +fs + .readdirSync(__dirname) + .filter(file => { + return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); + }) + .forEach(file => { + var model = sequelize['import'](path.join(__dirname, file)); + db[model.name] = model; + }); + +Object.keys(db).forEach(modelName => { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; diff --git a/models/transaction.js b/models/transaction.js new file mode 100644 index 0000000..dc52897 --- /dev/null +++ b/models/transaction.js @@ -0,0 +1,32 @@ +'use strict'; + +module.exports = (sequelize, DataTypes) => { + const Transaction = sequelize.define('Transaction', { + txid: DataTypes.STRING(64), + }, { + timestamps: false, + indexes: [{ + unique: true, + fields: ['txid'] + }], + }); + + const TxToTx = sequelize.define('TxToTx', {}, { + timestamps: false, + }); + + Transaction.belongsToMany(Transaction, { through: TxToTx, as: 'txtx' }); + + Transaction.associate = function (models) { + models.Transaction.belongsTo(models.Block, { + onDelete: "CASCADE", + foreignKey: { + allowNull: false + } + }); + + models.Transaction.hasMany(models.Vout); + }; + + return Transaction; +}; \ No newline at end of file diff --git a/models/vout.js b/models/vout.js new file mode 100644 index 0000000..8fc46b5 --- /dev/null +++ b/models/vout.js @@ -0,0 +1,21 @@ +'use strict'; + +module.exports = (sequelize, DataTypes) => { + const Vout = sequelize.define('Vout', { + value: DataTypes.DECIMAL(16, 8), + }, { + timestamps: false, + }); + + Vout.associate = function (models) { + models.Vout.belongsTo(models.Transaction, { + onDelete: "CASCADE", + foreignKey: { + allowNull: false + } + }); + models.Vout.belongsToMany(models.Address, { through: 'AddressVout' }); + }; + + return Vout; +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..086b7be --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "gostexplr", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "node ./bin/www", + "initdb": "node ./bin/initdb.js", + "reinitTables": "node ./bin/reinitTables.js", + "syncBlockchain": "node ./bin/syncBlockchain.js" + }, + "dependencies": { + "babel-register": "^6.26.0", + "body-parser": "~1.18.2", + "cookie-parser": "~1.4.3", + "debug": "~2.6.9", + "express": "~4.15.5", + "jade": "~1.11.0", + "morgan": "~1.9.0", + "mysql": "^2.15.0", + "mysql2": "^1.5.1", + "sequelize": "^4.32.2", + "sequelize-cli": "^3.2.0", + "serve-favicon": "~2.4.5" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e8ade9c3d1501450d4c1ccb7a0a5a578802bf680 GIT binary patch literal 1886 zcmV-k2ch_hP))m0hCKfinL?y@a=0bLI4p5;YR0gcEQN;9d!(y|?<#xm4wM1t`w%`{W_ zhhzCNH5yFVo9uysV${ZpW}Gov6Z2(ElL|FVKz8M21e4p?0{NVricw-yAv6ZR+@7YH3KOC9>l`e)sZ|pLS8 zUhQT6!$IfVYpH7}|Gdl_dp~dk@IB!Bzy|=<)zt&AerhBC4Kn9A;Loqe&$|>CLwWP0 z^0VgNPJj~70Zav^10Mu(z}o<;dbo^>&71&(@SJAebgOrXuha8!p}11twV1iaymHNbfi|P%Vh7fm>V*?TVlucv^>#x)^-fpo`%NV3Tam2F?T8fj;0{-q>;JfV19M znqw#4*b3k_zygiH0$>kNDF+^FEf!9-77KV|X8=op6~L0FntWALO}@XiSU3jIRFm%j zZU!y|eg%9H7y>>Hv^Uk{-*GYEr&w~Se0&P{Fc5$Tfa`z@fnz|6i(x>X->AIrbuld0 zy(Pd>Z|ol6cgppTGSPhJdY)gT|z_5$qZmr3x++GLX z1m0Ct1>kaEAMgcW9I(N~(5HQ0mlNB7Nx%`{3I+9AMutBN%u$~5I=n;d#T#1#{0#Ud zFc+Aj@2i13fe0Ag?*;B{ncPqzWFG|{6EP1e%^kqXG6@r)PSF1YxE8n-*ydvB^TyUn zY#wwmybc@{sV)QRg~F{$)j=1-Bfyp{?aLa_!xwlntc_TTII^@LS*`P`Xf#z7M!K z1JsDcH_PDXy|EQ82E0jdF$@acTv1%XG%I zz{h~`8Q_`Js(J~Kn$&57e*@-wW0O>4Sqa<@+~tj3c2-AOtdyodd3DSwse3PF)XTrE zGJ9L%LCfSuTs%^i*C&;RvTj@n+y{J9#M_;mV-Fe0qPe5s4!As53v>KFG)&^`ma4D{U*yYOaOdH-?X z8<1>=ox;_*MX>n?*y7vK`*)( zN@I`8%`d4{ckEL4dV))O88O{pQL>_MjX6TwNVkQ!!VU^?_wQ{_% z7r7YrX^dMW!tVtdgnVaC8ooxGqJ(CmH?~T1UsfeEMbYgQoC7jAqCKO)#rm#Q$2_R; zjmmOBw#KWQK9Zd7KgmHI_iPUY1(3qqp@*q0p{IXsfh z)=LBCmg`Yf_Nt3vQJK}31Fxu)l~NOP8*q~%TmV$afl6_uOY0isNC7yht6M4gGjKu5 zZb?Xd&c*PDBsG#P)aX>F+>QwL-5SN_>3@~Jr)f={NZ6-zW_9>;s;GJ)JFGQ>z$O>N zc5f_6dPwxf-ZUncydgA->fyV9k1DcHO4lczB{WBY*HnUSnufN^f$^FZ0oq**TgvBV zHDb|PEYMVwKdxLKR&=xFz#Qdxk*;z7X2h=sW{H@!I(PuMQZ2Y%0c>q87EZPn3u7}O zK-mcw!-=Mv{0<%R72wyJ4=wNmwd4W8KH)3}4gpU9EsA`Bc(GeNIsj~TF`Os|eEQX_ z#aNnCv%2bbI&eSm7O*ZgVpWNZH9~7tO4_dxZV+IDv6)f0OX#h3G3+{RzhMRp4dHsb zNLoxI&G85=#?~fdv)ctC+4~jM(<0ndpjEwp6c{P1ogB`}v8&YSigIkOjHh}kc$j8~_-O`Y&9Np_`3JxLtt*1)^N z#c=YBw_|VYOwI2XNl5l9f1MJVSt3`LkeMT40{Ev&aH0?_sIwMRXkH>B?gE~4F&qH+ Y7sf}U`e&4bw*UYD07*qoM6N<$f<7s2NdN!< literal 0 HcmV?d00001 diff --git a/public/images/gostcoin-b.png b/public/images/gostcoin-b.png new file mode 100644 index 0000000000000000000000000000000000000000..4220243ac74c6cb77d871adc63f968c4d53565ba GIT binary patch literal 22318 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 bzsvsuKIee`(pu8R00000NkvXXu0mjf2iBv0 literal 0 HcmV?d00001 diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css new file mode 100644 index 0000000..289dd2c --- /dev/null +++ b/public/stylesheets/style.css @@ -0,0 +1,57 @@ +body { + font: 16px monospace; + width: 1024px; + margin: 0 auto 1em; +} + +a { + color: #a7393d; +} + +a:hover { + color: #bf5c5f; +} + +.header h1 a { + color: black; + text-decoration: none; + align: center; +} + +.header h1 a img { + height: 2em; + position: relative; + margin-right: .5em; +} + +.header form input[type="submit"] { + padding: .2em .7em; +} + +@-moz-document url-prefix() { + .header form input[type="submit"] { + padding: .15em .7em; + } +} + +.header form input[type="text"] { + padding: .2em; +} + +.header h1 a span { + position: relative; + top: -0.5em; +} + +.header h1 a span:hover { + border-bottom: 3px solid; + border-bottom-color: #CF7F7F; +} + +.content { + margin-top: 2em; +} + +.capitalize { + text-transform: capitalize; +} \ No newline at end of file diff --git a/routes/address.js b/routes/address.js new file mode 100644 index 0000000..82c93ea --- /dev/null +++ b/routes/address.js @@ -0,0 +1,35 @@ +var models = require('../models'); +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/:address', function(req, res, next) { + + const address = encodeURI(req.params.address); + + models.Address.findOne({ + where: { + address, + }, + include: { + model: models.Vout, + include: { + model: models.Transaction, + }, + }, + }) + .then((address) => { + if (address === null) { + res.status(404).render('404'); + return; + } + const txes = []; + address.Vouts.forEach((vout) => txes.push(vout.Transaction.txid)); + res.render('address', { + address: address.address, + txes, + }); + }); +}); + +module.exports = router; diff --git a/routes/block.js b/routes/block.js new file mode 100644 index 0000000..900be15 --- /dev/null +++ b/routes/block.js @@ -0,0 +1,28 @@ +var models = require('../models'); +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/:hash', function(req, res, next) { + const hash = encodeURI(req.params.hash); + models.Block.findOne({ + where: { + hash, + }, + include: { + model: models.Transaction, + }, + }) + .then((block) => { + if (block === null) { + res.status(404).render('404'); + return; + } + block.dataValues.time = block.time.toUTCString(); + res.render('block', { + block, + }); + }); +}); + +module.exports = router; diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..9de27f8 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,19 @@ +var models = require('../models'); +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/', function(req, res, next) { + models.Block.findAll({ + order: [['height', 'DESC']], + limit: 30, + }) + .then((blocks) => { + res.render('index', { + blocks, + }); + }); + +}); + +module.exports = router; diff --git a/routes/search.js b/routes/search.js new file mode 100644 index 0000000..2b57aeb --- /dev/null +++ b/routes/search.js @@ -0,0 +1,46 @@ +var models = require('../models'); +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.post('/', function(req, res, next) { + + const search = encodeURI(req.body.search); + + models.Address.findOne({ + where: { + address: search, + }, + }) + .then((address) => { + if (address) { + res.redirect(`/address/${address.address}`); + return; + } + models.Transaction.findOne({ + where: { + txid: search, + }, + }) + .then((transaction) => { + if (transaction) { + res.redirect(`/transaction/${transaction.txid}`); + return; + } + models.Block.findOne({ + where: { + hash: search, + }, + }) + .then((block) => { + if (block) { + res.redirect(`/block/${block.hash}`); + return; + } + res.status(404).render('404'); + }); + }); + }); +}); + +module.exports = router; diff --git a/routes/transaction.js b/routes/transaction.js new file mode 100644 index 0000000..c394532 --- /dev/null +++ b/routes/transaction.js @@ -0,0 +1,48 @@ +var models = require('../models'); +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/:txid', function(req, res, next) { + const txid = encodeURI(req.params.txid); + + models.Transaction.findOne({ + where: { + txid, + }, + include: [{ + attributes: ['hash'], + model: models.Block, + },{ + model: models.Vout, + include: { + model: models.Address, + } + }, { + model: models.Transaction, + as: 'txtx', + }], + }) + .then((transaction) => { + if (transaction === null) { + res.status(404).render('404'); + return; + } + const vouts = []; + transaction.Vouts.forEach((vout) => { + vout.Addresses.forEach((address) => { + vouts.push({ + address: address.address, + value: vout.value, + }); + }); + }); + res.render('transaction', { + transaction, + vouts, + }); + }); + +}); + +module.exports = router; diff --git a/views/404.jade b/views/404.jade new file mode 100644 index 0000000..4082532 --- /dev/null +++ b/views/404.jade @@ -0,0 +1,4 @@ +extends layout + +block content + h3 Not found \ No newline at end of file diff --git a/views/address.jade b/views/address.jade new file mode 100644 index 0000000..b12719b --- /dev/null +++ b/views/address.jade @@ -0,0 +1,11 @@ +extends layout + +block content + h3 Address + div= address + + h3 Transactions + each val in txes + div + a(href='/transaction/#{val}/') #{val} + diff --git a/views/block.jade b/views/block.jade new file mode 100644 index 0000000..6dff623 --- /dev/null +++ b/views/block.jade @@ -0,0 +1,18 @@ +extends layout + +block content + h3 Block + + table + each key in Object.keys(block.dataValues) + if (key === 'Transactions') + -continue + tr + td.capitalize #{key} + td #{block[key]} + + h3 Transactions + each tx in block.Transactions + div + a(href='/transaction/#{tx.txid}/') #{tx.txid} + diff --git a/views/error.jade b/views/error.jade new file mode 100644 index 0000000..51ec12c --- /dev/null +++ b/views/error.jade @@ -0,0 +1,6 @@ +extends layout + +block content + h1= message + h2= error.status + pre #{error.stack} diff --git a/views/index.jade b/views/index.jade new file mode 100644 index 0000000..2d8c05b --- /dev/null +++ b/views/index.jade @@ -0,0 +1,11 @@ +extends layout + +block content + h3 Last 30 blocks + + table + each block in blocks + tr + td #{block.height} + td + a(href='/block/#{block.hash}/') #{block.hash} diff --git a/views/layout.jade b/views/layout.jade new file mode 100644 index 0000000..9b69ea6 --- /dev/null +++ b/views/layout.jade @@ -0,0 +1,18 @@ +doctype html +html + head + title GOSTcoin blockchain explorer + link(rel='stylesheet', href='/stylesheets/style.css') + link(rel='shortcut icon', href='/favicon.ico') + body + div.header + h1 + a(href='/') + img(src='/images/gostcoin-b.png') + span GOSTcoin blockchain explorer + div + form(action="/search/", method="POST") + input(type="text", name="search" placeholder="Search by transaction id, block hash or address", size="68") + input(type="submit", value=">>") + div.content + block content diff --git a/views/transaction.jade b/views/transaction.jade new file mode 100644 index 0000000..063c5a5 --- /dev/null +++ b/views/transaction.jade @@ -0,0 +1,31 @@ +extends layout + +block content + h3 Transaction + + table + tr + td Hash + td #{transaction.txid} + tr + td Block + td + a(href='/block/#{transaction.Block.hash}/') #{transaction.Block.hash} + + h3 In + if transaction.txtx.length + table + each val in transaction.txtx + tr + td + a(href='/transaction/#{val.txid}/') #{val.txid} + else + div Mined + + h3 Out + table + each val in vouts + tr + td + a(href='/address/#{val.address}/') #{val.address} + td #{val.value} \ No newline at end of file