xcps 7 years ago
commit
5686c01015
  1. 2
      .gitignore
  2. 22
      README.md
  3. 52
      app.js
  4. 51
      bin/initdb.js
  5. 14
      bin/reinitTables.js
  6. 154
      bin/syncBlockchain.babel.js
  7. 2
      bin/syncBlockchain.js
  8. 92
      bin/www
  9. 25
      config/config.json
  10. 21
      models/address.js
  11. 33
      models/block.js
  12. 12
      models/failure.js
  13. 36
      models/index.js
  14. 32
      models/transaction.js
  15. 21
      models/vout.js
  16. 25
      package.json
  17. BIN
      public/favicon.ico
  18. BIN
      public/images/gostcoin-b.png
  19. 57
      public/stylesheets/style.css
  20. 35
      routes/address.js
  21. 28
      routes/block.js
  22. 19
      routes/index.js
  23. 46
      routes/search.js
  24. 48
      routes/transaction.js
  25. 4
      views/404.jade
  26. 11
      views/address.jade
  27. 18
      views/block.jade
  28. 6
      views/error.jade
  29. 11
      views/index.jade
  30. 18
      views/layout.jade
  31. 31
      views/transaction.jade

2
.gitignore vendored

@ -0,0 +1,2 @@
node_modules
package-lock.json

22
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

52
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;

51
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);
});
}
});
}
});
}
});
});

14
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);
});

154
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();

2
bin/syncBlockchain.js

@ -0,0 +1,2 @@
require('babel-register');
require('./syncBlockchain.babel.js');

92
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);
}

25
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
}
}

21
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;
};

33
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;
};

12
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;
};

36
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;

32
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;
};

21
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;
};

25
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"
}
}

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/images/gostcoin-b.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

57
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;
}

35
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;

28
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;

19
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;

46
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;

48
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;

4
views/404.jade

@ -0,0 +1,4 @@
extends layout
block content
h3 Not found

11
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}

18
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}

6
views/error.jade

@ -0,0 +1,6 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}

11
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}

18
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

31
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}
Loading…
Cancel
Save