Merge pull request #1 from Swader/master

Added faucet
This commit is contained in:
Bruno Škvorc 2019-07-30 11:35:09 +02:00 committed by GitHub
commit 476c26119a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 16834 additions and 11 deletions

8
.gitignore vendored
View File

@ -3,7 +3,13 @@ node_modules
node_modules/*
deploy/db/*
deploy/keys/*
deploy/faucet/*
deploy/faucet/node_modules
deploy/faucet/tweet-db
deploy/faucet/config.json
deploy/faucet/public/assets/javascripts/application.js
deploy/faucet/public/assets/stylesheets/application.css
!.gitkeep
*.log
.mykeys.json

View File

@ -16,7 +16,7 @@ You will have to pollute your system a little for this to work. Luckily, it work
2. If you want to add some pre-created private keys, add them to the `.mykeys` file.
3. Run `node start.js`. Optionally, pass in a `v` argument to autogenerate that many validators (`v=10`) and/or the `mykeys` argument to make the script read the keys specified in step 2.
The blockchain database will be stored in the `deploy/db` subfolder. The `deploy/keys` subfolder will have keys for relevant accounts generated, including the address to the deposit contract. The `deploy/faucet` folder will contain a simple web UI for a faucet.
The blockchain database will be stored in the `deploy/db` subfolder. The `deploy/keys` subfolder will have keys for relevant accounts generated, including the address to the deposit contract. The `deploy/faucet` folder will contain a simple web UI for a faucet. See hosting below for how to run it.
#### Flags
@ -30,8 +30,8 @@ Augment `start.js` with flags, .e.g. `node start.js v=50 mykeys`:
The generator is deterministic. You always end up with the same addresses, accounts and balances if you use the same mnemonic and `.mykeys` list. Thus, to host it somewhere, simply clone this repo to the server and run it the same way you do locally.
- `node start.js` will run the blockchain and start the server in listen mode with RPC/Web3 allowed
- `yarn run faucet --port 8080` will host the ether faucet at `localhost:8080`
- `yarn run validator-ui --port 8081` will host the validator UI at `localhost:8081` // @TODO
- `yarn faucet` will host the ether faucet at `localhost:5000`
- @TODO `yarn validator-ui` will host the validator UI at `localhost:8081`
### Other commands
@ -39,7 +39,7 @@ The generator is deterministic. You always end up with the same addresses, accou
## Contributing
Please consider contributing PRs, we'd love the help!
Please consider contributing PRs, we'd love the help! There's only one condition: please try to keep the dependencies to a minimum of minimums, and do NOT use something that needs [node-gyp](https://github.com/nodejs/node-gyp/issues/809).
## License

View File

View File

@ -0,0 +1,24 @@
{
"environment": "prod",
"debug": false,
"Captcha": {
"secret": "",
"sitekey": ""
},
"Tweeter": {
"similarityTreshold": 0.75,
"cooldown": 8,
"predefinedTweet": "I'm using the Thundercloud (https://github.com/swader/thundercloud) Faucet to get some Test Ether to launch an Ethereum 2.0 Genesis event",
"predefinedHashTags": "ethereum, eth2, serenity"
},
"Ethereum": {
"milliEtherToTransferWithTweet": 32000,
"milliEtherToTransferWithoutTweet": 1000,
"gasLimit": "21000",
"prod": {
"rpc": "http://127.0.0.1:8545",
"account": "0x471e0575bFC76d7e189ab3354E0ecb70FCbf3E46",
"privateKey": "2960a712dcbcd755be598955b08ee0f697b372aed31005bd55fd02949b6917bb"
}
}
}

44
deploy/faucet/gulpfile.js Normal file
View File

@ -0,0 +1,44 @@
'use strict';
const gulp = require('gulp');
const sass = require('gulp-sass');
const sassGlob = require('gulp-sass-glob');
const autoprefixer = require('gulp-autoprefixer');
const uglifycss = require('gulp-uglifycss');
const include = require('gulp-include');
const addsrc = require('gulp-add-src');
const order = require('gulp-order');
const concat = require('gulp-concat');
const concatCss = require('gulp-concat-css');
const uglify = require('gulp-uglify');
gulp.task('sass', function() {
return gulp.src([
'./public/assets/stylesheets/*.scss',
'./public/assets/stylesheets/sweetalert2.min.css'
])
.pipe(sassGlob())
.pipe(sass().on('error', sass.logError))
.pipe(autoprefixer())
.pipe(concatCss('application.css'))
.pipe(uglifycss())
.pipe(gulp.dest('./public/assets/stylesheets/'));
});
gulp.task('javascript', function() {
return gulp.src('public/assets/javascripts/application/*.js')
.pipe(addsrc('public/assets/javascripts/vendor/index.js'))
.pipe(order([
"public/assets/javascripts/vendor/index.js",
"public/assets/javascripts/application/*.js"
], {base: '.'}))
.pipe(include())
.pipe(concat('application.js'))
// .pipe(uglify())
.pipe(gulp.dest('public/assets/javascripts'));
});
gulp.task('watch', function() {
gulp.watch('./public/assets/stylesheets/**/**/*.scss', ['sass']);
gulp.watch('./public/assets/javascripts/application/*.js', ['javascript']);
});

59
deploy/faucet/index.js Normal file
View File

@ -0,0 +1,59 @@
const express = require('express');
const fs = require('fs');
const bodyParser = require('body-parser');
const path = require('path');
let app = express();
require('./src/helpers/blockchain-helper')(app);
let config;
const configPath = './config.json';
const configExists = fs.existsSync(configPath, fs.F_OK);
if (configExists) {
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
} else {
return console.log('There is no config.json file');
}
app.config = config;
app.configureWeb3(config)
.then(web3 => {
app.web3 = web3;
app.set("view engine", "pug");
app.set("views", path.join(__dirname, "/public/views"));
app.use(express.static(__dirname + '/public'));
app.use(bodyParser.json({
limit: '50mb',
}));
app.use(bodyParser.urlencoded({
limit: '50mb',
extended: true,
}));
require('./src/controllers/index')(app);
app.get('/', function(request, response) {
response.render('index', {
minAmount: app.config.Ethereum.milliEtherToTransferWithoutTweet / 1000,
maxAmount: app.config.Ethereum.milliEtherToTransferWithTweet / 1000,
sitekey: app.config.Captcha.sitekey,
cooldown: app.config.Tweeter.cooldown,
predefinedTweetUrl: encodeURI(
"https://twitter.com/intent/tweet?text=" + app.config.Tweeter.predefinedTweet
+ "&hashtags=" + app.config.Tweeter.predefinedHashTags
)
});
});
app.set('port', (process.env.PORT || 5000));
app.listen(app.get('port'), function () {
console.log('Thundercloud faucet is running on port', app.get('port'));
})
})
.catch(error => {
return console.log(error);
});
module.exports = app;

8710
deploy/faucet/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
{
"name": "thundercloud-faucet",
"version": "1.0.0",
"description": "Thundercloud faucet",
"main": "index.js",
"scripts": {
"start": "yarn run sass && yarn run coffee && node index.js",
"sass": "gulp sass",
"coffee": "gulp javascript",
"watch": "gulp watch"
},
"repository": {
"type": "git",
"url": "https://github.com/swader/thundercloud"
},
"author": "swader",
"license": "MIT",
"dependencies": {
"axios": "^0.18.0",
"body-parser": "1.18.3",
"dateformat": "^3.0.3",
"ethereumjs-tx": "1.3.7",
"express": "4.16.4",
"level": "^4.0.0",
"moment": "2.24.0",
"page-scraper": "^2.0.5",
"pug": "^2.0.3",
"querystring": "0.2.0",
"string-similarity": "^3.0.0",
"typedarray-to-buffer": "^3.1.5",
"web3": "^1.0.0-beta.34"
},
"devDependencies": {
"gulp": "^4.0.0",
"gulp-add-src": "1.0.0",
"gulp-autoprefixer": "6.0.0",
"gulp-cli": "2.0.1",
"gulp-concat": "2.6.1",
"gulp-concat-css": "3.1.0",
"gulp-include": "2.3.1",
"gulp-order": "1.2.0",
"gulp-postcss": "8.0.0",
"gulp-sass": "4.0.2",
"gulp-sass-glob": "1.0.9",
"gulp-uglify": "3.0.1",
"gulp-uglifycss": "1.1.0",
"http-server": "0.11.1"
},
"engines": {
"node": ">8.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -0,0 +1,53 @@
$(function() {
var loader = $(".loading-container");
updateBalance();
// on form submit
$( "#faucetForm" ).submit(function( e ) {
e.preventDefault();
$this = $(this);
loader.removeClass("hidden");
var receiver = $("#receiver").val();
$.ajax({
url:"/",
type:"POST",
data: $this.serialize()
}).done(function(data) {
grecaptcha.reset();
if (!data.success) {
loader.addClass("hidden");
console.log(data)
console.log(data.error)
swal("Error", data.error.message, "error");
return;
}
$("#receiver").val('');
loader.addClass("hidden");
swal("Success",
`0.05 🌩ETH has been successfully transferred to <a href="https://explorer.lisinski.online/tx/${data.success.txHash}" target="blank">${receiver}</a>`,
"success"
);
updateBalance();
}).fail(function(err) {
grecaptcha.reset();
console.log(err);
loader.addClass("hidden");
});
});
});
function updateBalance() {
$.ajax({
url: "/health",
type: "GET"
}).done(function (data) {
if (data.balanceInEth) {
$(".footer-balance").text(data.balanceInEth + " 🌩ETH remaining")
} else {
$(".footer-balance").text("Balance not available");
}
}).fail(function(err) {
$(".footer-balance").text("Balance not available");
console.log(err);
});
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
//=require sweetalert2.min.js
//=require jquery.min.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
@import './index/*';

View File

@ -0,0 +1,9 @@
html,
body,
.content {
color: #333;
line-height: 1;
font-size: 14px;
font-family: 'Poppins', sans-serif;
-webkit-font-smoothing: antialiased;
}

View File

@ -0,0 +1,10 @@
@mixin image-2x($image, $width: 100%, $height: 100%) {
@media (min--moz-device-pixel-ratio: 1.3),
(-o-min-device-pixel-ratio: 2.6/2),
(-webkit-min-device-pixel-ratio: 1.3),
(min-device-pixel-ratio: 1.3),
(min-resolution: 1.3dppx) {
background-image: url($image);
background-size: $width $height;
}
}

View File

@ -0,0 +1,10 @@
html,
body,
main {
height: 100%;
margin: 0;
}
button:focus {
outline: none;
}

View File

@ -0,0 +1,30 @@
%btn {
cursor: pointer;
transition: 0.3s background-color;
border-radius: 3px;
border: 0;
padding: 0 15px 0 15px;
background-color: #08b3f2;
background-repeat: no-repeat;
background-position: left 15px center;
color: #fff;
line-height: 36px;
font-size: 13px;
text-decoration: none;
text-transform: uppercase;
font-weight: bold;
&:hover {
background-color: #20bdf7;
}
&-new {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAFVBMVEX///////////////////////////9nSIHRAAAABnRSTlMASUrk5udXTd49AAAAOUlEQVR42tXQsQEAIAgDQcAn+4+snRZxAK79KokrIcNBwgYdc0Migwxk8Qsd1TJWDf/KQWobqt+9G4coA99W7as5AAAAAElFTkSuQmCC);
background-size: 12px 12px;
}
&-vote {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAASCAMAAAB2Mu6sAAAAgVBMVEX///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+xZKUEAAAAKnRSTlMAAQIDPVBWYmNkZmpxeHp7fIOEhYaHiZeZmp6foKGio6TV2dze5unx9f63Mu9fAAAAgElEQVR4AXXO1w7CMBSD4VM2lFE2lD0I1O//gIBrCUUn/Hf5rCixRI3V695KeLMEcEn4Dp+C9z2+DZ0f6IXz4x8/0Sf63c/P9DEP7fDM5VkpZw8Ao9rX9JnVBWjJNvSlqW7F67GzAZdiS1+QooXNLaqvRe/6ZWquXuVcdW7XPJY3R9MVHwqyyb8AAAAASUVORK5CYII=);
background-size: 12px 9px;
}
}

View File

@ -0,0 +1,24 @@
.hidden {
display:none;
}
.pd-3 {
padding: 3px;
}
.mgl-5 {
margin-left: 5px;
}
.mgl-10 {
margin-left: 10px;
}
.flex-center {
display: flex;
align-items: center;
}
.bring-to-front {
z-index: 200;
}

View File

@ -0,0 +1,36 @@
.row {
margin: 0!important;
}
.ft-img {
width: 100%;
max-height: 60px;
max-width: 100px;
}
.mastfoot {
background-color: #04091e;
color: #545454;
padding: 3px;
}
.center {
position: relative;
float: left;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.social-button {
color: rgb(40, 196, 255);
}
.footer-balance {
color: #3b3c44;
background-color: lightgray;
padding: 10px;
margin: 5px;
text-align: center;
font-weight: bold;
}

View File

@ -0,0 +1,13 @@
.center-logo-img {
display: block;
margin: 5px auto;
max-width: 50%;
max-height: 80px;
height: auto;
}
.masthead {
background-color: rgba(0, 0, 0, 0.8);
padding-bottom: 10px;
padding-top: 5px;
}

View File

@ -0,0 +1,108 @@
button,
input,
textarea {
outline: none;
font-family: 'Poppins', sans-serif;
}
input {
transition: 0.3s border-color;
width: 100%;
border-radius: 3px;
box-sizing: border-box;
border: 1px solid #eee;
&:focus {
border-color: #08b3f2;
}
}
input {
padding: 0 15px;
height: 36px;
margin-bottom: 20px;
}
.request-tokens-button {
@extend %btn;
background-color: #08b3f2;
&:hover {
background-color: #079dd4;
}
}
/* hide the blue outline */
.form-control:focus {
outline: 0 !important;
border-color: initial;
box-shadow: none;
}
// Input card
.card-footer {
font-style: italic;
text-align: justify;
}
.card-header {
font-weight: bold;
font-size: x-large;
}
.card-body {
padding: 1.25rem !important;
}
.card-flip {
perspective: 1000px;
}
.card-flip.flip .flip {
transform: rotateY(180deg);
}
.card-flip,
.front,
.back {
width: 100%;
height: 480px;
}
.flip {
transition: 0.6s;
transform-style: preserve-3d;
position: relative;
}
.front,
.back {
backface-visibility: hidden;
position: absolute;
top: 0;
left: 0;
}
.front {
z-index: 2;
transform: rotateY(0deg);
}
.back {
transform: rotateY(180deg);
}
.card-container {
width: 100%;
}
.gray-button {
background-color: #0d191d;
}
.footer-button {
float: right;
}

View File

@ -0,0 +1,80 @@
@keyframes fadeOut {
0% {
opacity: .2;
}
20% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: .2;
transform: scale(0.3);
}
}
.loading {
display: flex;
justify-content: space-between;
position: absolute;
left: 50%;
top: 50%;
width: 206px;
margin: -30px 0 0 -111.5px;
padding-top: 50px;
&:before {
@include image-2x('../images/cloud.png', 206px);
content: '';
position: absolute;
left: 0;
top: 0;
width: 206px;
height: 50px;
background-image: url("../images/cloud.png");
background-position: 0 0;
background-repeat: no-repeat;
}
&-container {
position: fixed;
z-index: 1000000;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: fade-out(#1c1c21, 0.2);
}
&-i {
animation-duration: 2s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: fadeOut;
animation-timing-function: linear;
opacity:.2;
width: 9px;
height: 9px;
border-radius: 50%;
background-color: #fff;
&:nth-child(2) {
animation-delay: .1s;
}
&:nth-child(3) {
animation-delay: .2s;
}
&:nth-child(4) {
animation-delay: .3s;
}
&:nth-child(5) {
animation-delay: .4s;
}
&:nth-child(6) {
animation-delay: .5s;
}
}
}

View File

@ -0,0 +1,31 @@
main {
padding-top: 2%;
padding-bottom: 5%;
background-image: url('../images/cloud.png');
background-size: cover;
overflow: auto;
}
// OVERLAY
main::after {
content: "";
position: absolute;
top: 0;
left: 0;
overflow: hidden;
width: 100%;
height: 100%;
background-color: rgba(148, 146, 129, 0.9);
}
.flex-container {
display: flex;
flex-wrap: wrap;
text-align: center;
justify-content: center;
width: 50%;
}
.row {
width: auto;
}

View File

@ -0,0 +1,7 @@
.text-xs-center {
text-align: center;
}
.g-recaptcha {
display: inline-block;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,133 @@
html
head
<!------- META ------->
meta(charset='utf-8')
meta(name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no')
// Meta Description & Keywords
meta(name='description' content='The Thundercloud faucet lets you get 32 ether to deposit and become a test validator for Ethereum 2.0.')
meta(name='keywords' content='blockchain, ethereum')
// Favicon
link(rel='shortcut icon' href='assets/images/cloud.png')
// Site Title
title Thundercloud faucet
<!------- CSS ------->
link(rel='stylesheet' type='text/css' href='./assets/stylesheets/application.css')
link(rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css' integrity='sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T' crossorigin='anonymous')
link(href='https://fonts.googleapis.com/css?family=Poppins:300,400,500,700' rel='stylesheet')
// Fontawesome icons
link(rel='stylesheet' href='https://use.fontawesome.com/releases/v5.7.2/css/all.css' integrity='sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr' crossorigin='anonymous')
body
.d-flex.w-100.h-100.mx-auto.flex-column
header.masthead.mb-auto.bring-to-front
.inner
h1.hidden Thundercloud Testnet Faucet
.allign-middle
a(href='https://github.com/swader/thundercloud')
img.center-logo-img(src='assets/images/cloud.png' alt='Thundercloud Logo')
main.inner.content(role='main')
.container
.row.justify-content-center
.flex-container.bring-to-front
.card-container
.card-flip.noflip
.flip
.front
.card.text-center
.card-header
| Thundercloud faucet
.card-body
form#faucetForm(action='/' method='POST')
.form-group
.input-group
span.input-group-prepend
span.input-group-text.bg-transparent.border-right-0
i.fas.fa-wallet.fa-2x
input#receiver.form-control.form-control-lg.py-2.border-left-0.border(name='receiver' type='text' placeholder='Enter wallet address...')
span.input-group-append
button.btn.btn-outline-secondary.border-left-0.border(type='button' data-toggle='popover' data-trigger='focus' title='Wallet Address' data-content='This address will get the Ether')
i.fas.fa-info
.form-group
.input-group
span.input-group-prepend
span.input-group-text.bg-transparent.border-right-0
i.fab.fa-twitter.fa-2x
input#tweetUrl.form-control.form-control-lg.py-2.border-left-0.border(name='tweetUrl' type='text' placeholder='Enter tweet url...')
span.input-group-append
button.btn.btn-outline-secondary.border-left-0.border(type='button' data-toggle='popover' data-trigger='focus' title='Tweet Url' data-content='Tweet about Thundercloud, then paste tweet URL here.')
i.fas.fa-info
.form-group
a.twitter-share-button(href=predefinedTweetUrl data-size='large')
| Tweet about Thundercloud
.form-group
button#requestTokens.request-tokens-button(type='submit') Request #{maxAmount} Thundercloud Ether (🌩ETH)
.card-footer.text-muted
.footer-balance
.footer-note
| There's a cooldown of #{cooldown} hours in place.
.footer-button
button.request-tokens-button.gray-button.toggle I want #{minAmount} 🌩ETH
.back
.card.text-center
.card-header
| Thundercloud Faucet
.card-body
form#faucetFormMin(action='/' method='POST')
.form-group
.input-group
span.input-group-prepend
span.input-group-text.bg-transparent.border-right-0
i.fas.fa-wallet.fa-2x
input#receiverMin.form-control.form-control-lg.py-2.border-left-0.border(name='receiver' type='text' placeholder='Enter wallet address...')
span.input-group-append
button.btn.btn-outline-secondary.border-left-0.border(type='button' data-toggle='popover' data-trigger='focus' title='Wallet Address' data-content='This address will get the Ether.')
i.fas.fa-info
.form-group
button.request-tokens-button(type='submit') Request #{minAmount} 🌩ETH
.card-footer.text-muted
.footer-balance
.footer-button
button.request-tokens-button.gray-button.toggle I want #{maxAmount} 🌩ETH
.loading-container.hidden
.loading
.loading-i
.loading-i
.loading-i
.loading-i
.loading-i
.loading-i
script(src='./assets/javascripts/application.js?v=1.4' type='text/javascript')
// jQuery, Popper.js, Bootstrap JS
script(src='assets/javascripts/vendor/jquery-3.3.1.min.js' integrity='sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=' crossorigin='anonymous')
script(src='assets/javascripts/vendor/popper.min.js' integrity='sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1' crossorigin='anonymous')
script(src='assets/javascripts/vendor/bootstrap.min.js' integrity='sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM' crossorigin='anonymous')
script.
// Activate info popovers
$(document).ready(function () {
$('[data-toggle="popover"]').popover();
});
$('.popover-dismiss').popover({
trigger: 'focus'
});
// Flip input card
$('.toggle').on('click', function () {
$('.card-flip').toggleClass("flip");
});
script.
// Activate tweet button
window.twttr = (function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0], t = window.twttr || {};
if (d.getElementById(id)) return t;
js = d.createElement(s);
js.id = id;
js.src = "https://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
t._e = [];
t.ready = function (f) {
t._e.push(f);
};
return t;
}(document, "script", "twitter-wjs"));

View File

@ -0,0 +1,151 @@
const EthereumTx = require('ethereumjs-tx');
const { generateErrorResponse } = require('../helpers/generate-response');
const { validateCaptcha } = require('../helpers/captcha-helper');
const { debug } = require('../helpers/debug');
const { checkIfValidTweet } = require('../helpers/tweet-helper');
module.exports = function (app) {
const config = app.config;
const web3 = app.web3;
const messages = {
INVALID_ADDRESS: 'Invalid address',
TX_HAS_BEEN_MINED_WITH_FALSE_STATUS: 'Transaction has been mined, but status is false',
TX_HAS_BEEN_MINED: 'Tx has been mined',
};
app.post('/', async function(request, response) {
const isDebug = app.config.debug;
debug(isDebug, "REQUEST:");
debug(isDebug, request.body);
// const recaptureResponse = request.body["g-recaptcha-response"];
// if (!recaptureResponse) {
// const error = {
// message: messages.INVALID_CAPTCHA,
// };
// return generateErrorResponse(response, error);
// }
let captchaResponse;
// Temp disable
// try {
// captchaResponse = await validateCaptcha(app, recaptureResponse);
// } catch(e) {
// return generateErrorResponse(response, e);
// }
const receiver = request.body.receiver;
if (await validateCaptchaResponse(captchaResponse, receiver, response)) {
if (!web3.utils.isAddress(receiver)) {
return generateErrorResponse(response, {message: messages.INVALID_ADDRESS});
}
const noTweet = !request.body.tweetUrl;
if (noTweet || await validateTweet(request.body.tweetUrl, response)) {
await checkBalanceStatus(response);
await sendPOAToRecipient(web3, receiver, response, isDebug, noTweet);
}
}
});
app.get('/health', async function(request, response) {
const resp = await checkBalanceStatus(response);
response.send(resp);
});
async function checkBalanceStatus(response) {
let balanceInWei;
let balanceInEth;
const address = config.Ethereum[config.environment].account;
// get balance
try {
balanceInWei = await web3.eth.getBalance(address);
balanceInEth = await web3.utils.fromWei(balanceInWei, "ether");
} catch (error) {
return generateErrorResponse(response, error);
}
return {
address,
balanceInWei: balanceInWei,
balanceInEth: Math.round(balanceInEth)
};
}
async function validateCaptchaResponse(captchaResponse, receiver, response) {
// hack to disable captcha for now
return true;
if (!captchaResponse || !captchaResponse.success) {
generateErrorResponse(response, {message: messages.INVALID_CAPTCHA});
return false;
}
return true;
}
async function validateTweet(tweetUrl, response) {
const resp = await checkIfValidTweet(tweetUrl);
if (!resp.valid) {
generateErrorResponse(response, {message: resp.message});
}
return resp.valid;
}
async function sendPOAToRecipient(web3, receiver, response, isDebug, isWithoutTweet) {
let senderPrivateKey = config.Ethereum[config.environment].privateKey;
const privateKeyHex = Buffer.from(senderPrivateKey, 'hex');
const gasPrice = web3.utils.toWei('1', 'gwei');
const gasPriceHex = web3.utils.toHex(gasPrice);
const gasLimitHex = web3.utils.toHex(config.Ethereum.gasLimit);
const nonce = await web3.eth.getTransactionCount(config.Ethereum[config.environment].account);
const nonceHex = web3.utils.toHex(nonce);
const BN = web3.utils.BN;
const miliEthToSend = (isWithoutTweet) ?
config.Ethereum.milliEtherToTransferWithoutTweet : config.Ethereum.milliEtherToTransferWithTweet;
const ethToSend = web3.utils.toWei(new BN(miliEthToSend), "milliether");
const rawTx = {
nonce: nonceHex,
gasPrice: gasPriceHex,
gasLimit: gasLimitHex,
to: receiver,
value: ethToSend,
data: '0x00'
};
const tx = new EthereumTx(rawTx);
tx.sign(privateKeyHex);
const serializedTx = tx.serialize();
let txHash;
web3.eth.sendSignedTransaction("0x" + serializedTx.toString('hex'))
.on('transactionHash', (_txHash) => {
txHash = _txHash
})
.on('receipt', (receipt) => {
debug(isDebug, receipt);
if (receipt.status == '0x1') {
return sendRawTransactionResponse(txHash, response);
} else {
const error = {
message: messages.TX_HAS_BEEN_MINED_WITH_FALSE_STATUS,
};
return generateErrorResponse(response, error);
}
})
.on('error', (error) => {
return generateErrorResponse(response, error);
});
}
function sendRawTransactionResponse(txHash, response) {
const successResponse = {
code: 200,
title: 'Success',
message: messages.TX_HAS_BEEN_MINED,
txHash: txHash
};
response.send({
success: successResponse
});
}
};

View File

@ -0,0 +1,27 @@
const Web3 = require('web3')
module.exports = function (app) {
app.configureWeb3 = configureWeb3
function configureWeb3 (config) {
return new Promise((resolve, reject) => {
let web3
if (typeof web3 !== 'undefined') {
web3 = new Web3(web3.currentProvider)
}
else {
web3 = new Web3(new Web3.providers.HttpProvider(config.Ethereum[config.environment].rpc))
}
if (typeof web3 !== 'undefined') {
return resolve(web3)
}
reject({
code: 500,
title: "Error",
message: "check RPC"
})
});
}
}

View File

@ -0,0 +1,64 @@
const querystring = require('querystring')
const https = require('https')
const { debug } = require('../helpers/debug')
function validateCaptcha (app, captchaResponse) {
const config = app.config
return new Promise((resolve, reject) => {
const isDebug = app.config.debug
const secret = config.Captcha.secret
const post_data_json = {
secret,
"response": captchaResponse
}
const post_data = querystring.stringify(post_data_json)
debug(isDebug, post_data_json)
debug(isDebug, post_data)
const post_options = {
host: 'www.google.com',
port: '443',
path: '/recaptcha/api/siteverify',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
debug(isDebug, post_options)
const post_req = https.request(post_options, function (res) {
res.setEncoding('utf8')
let output = ''
res.on('data', function (chunk) {
output += chunk
})
res.on('end', function () {
debug(isDebug, "##############")
debug(isDebug, 'Output from validateCaptcha: ')
debug(isDebug, output)
debug(isDebug, "##############")
if (output) {
debug(isDebug, JSON.parse(output))
resolve(JSON.parse(output))
} else {
resolve()
}
})
})
post_req.on('error', function (error) {
debug(isDebug, error)
reject(error)
})
post_req.write(post_data, 'binary', function(error) {
if (error) debug(isDebug, error)
})
post_req.end()
})
}
module.exports = { validateCaptcha }

View File

@ -0,0 +1,6 @@
function debug (isDebug, text) {
if (isDebug) {
console.log(text)
}
}
module.exports = { debug }

View File

@ -0,0 +1,13 @@
function generateErrorResponse (response, err) {
const out = {
error: {
code: err.code || 500,
title: err.title || 'Error',
message: err.message || 'Internal server error'
}
};
console.log(err);
response.send(out);
}
module.exports = { generateErrorResponse };

View File

@ -0,0 +1,93 @@
const scrape = require('page-scraper');
const level = require('level');
const stringSimilarity = require('string-similarity');
const app = require('../../index');
// similarity constants
const referenceString = app.config.Tweeter.predefinedTweet + app.config.Tweeter.predefinedHashTags;
const similarityThreshold = app.config.Tweeter.similarityTreshold || 0.8;
// defined as number of hours
const timeoutThresholdInHours = app.config.Tweeter.cooldown || 6;
const db = level('tweet-db');
async function checkIfValidTweet (tweetUrl) {
const response = {valid: false, message: ""};
try {
// Check if user claimed reward in last 8h
const tweetUser = getTweetUsername(tweetUrl);
await checkIfTimeoutExpired(tweetUser);
// Check if tweet already used for reward
await checkIfNewTweet(tweetUrl);
// Check if tweet content is about Lisinski Testnet
const tweetContent = await scrapeTweetContent(tweetUrl);
checkIfValidTweetContent(tweetContent);
// Tweet is valid, save record
await saveTweetData(tweetUrl, tweetUser);
response.valid = true;
} catch (err) {
response.message = err.message;
}
return response;
}
async function saveTweetData(tweetUrl, tweetUser) {
await db.put("tweet::" + tweetUrl, tweetUser);
await db.put("user::" + tweetUser, Date.now());
console.log(`${tweetUser} claimed reward for ${tweetUrl}.`)
}
async function checkIfNewTweet(tweetUrl) {
try {
await db.get("tweet::" + tweetUrl);
} catch (error) {
if (error.type === 'NotFoundError') return;
console.log(error.message);
throw new Error(error.message);
}
throw new Error("This tweet already used for claiming LETH reward!");
}
async function scrapeTweetContent(tweetUrl) {
let content = '';
try {
const $ = await scrape(tweetUrl);
content = $('.tweet-text').text();
} catch (error) {
console.error(error.message);
}
if (content.length === 0) {
throw new Error('Tweet url is not valid!');
}
return content;
}
function checkIfValidTweetContent(tweetContent) {
const similarity = stringSimilarity.compareTwoStrings(tweetContent, referenceString);
if (similarity < similarityThreshold) {
throw new Error('Tweet content is not valid, Lisinski Testnet must be mentioned in Tweet!');
}
}
function getTweetUsername(tweetUrl) {
return tweetUrl.split("/")[3];
}
async function checkIfTimeoutExpired(tweetUser) {
let timestamp;
try {
timestamp = await db.get("user::" + tweetUser);
} catch (error) {
if (error.type !== 'NotFoundError') {
throw new Error(error.message);
}
}
// check if timeout expired
const hoursFromLastTweet = Math.abs(timestamp - Date.now()) / 36e5;
if (hoursFromLastTweet <= timeoutThresholdInHours) {
throw new Error(`Reward already claimed in last ${timeoutThresholdInHours} hours!`)
}
}
module.exports = { checkIfValidTweet };

7005
deploy/faucet/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@
"ganache-cli": "^6.5.0"
},
"scripts": {
"clean": "rm -rf deploy/db/*"
"clean": "rm -rf deploy/db/*",
"faucet": "cd deploy/faucet && yarn start"
}
}

View File

@ -96,7 +96,7 @@ provider.listAccounts().then(function(result){
let mnemonic = process.env.mnemonic;
let mnemonicWallet = ethers.Wallet.fromMnemonic(mnemonic);
fs.writeFile("deploy/keys/faucetkey.txt", mnemonicWallet.privateKey, function(err) {
fs.writeFile("deploy/keys/faucetkey.txt", mnemonicWallet.privateKey + ":" + mnemonicWallet.address, function(err) {
if(err) {
return console.log(err);
}
@ -189,11 +189,12 @@ async function makeValidatorDeposits() {
let withdraw_pubkey = new keypair.Keypair(privateKey.PrivateKey.fromHexString(item.bls_key_withdraw)).publicKey.toHexString();
// Withdrawal credentials is the sha256 hash of the withdrawal pubkey (32 bytes), but the first byte of the hash is replaced with the prefix (currently 0 for version 0)
//let withdrawal_credentials = "00" + sha256.sha256(withdraw_pubkey).slice(2); // 32 byte output
let withdrawal_credentials = Buffer.from(sha256.arrayBuffer(withdraw_pubkey))
let withdrawal_credentials_hex = "0x00" + sha256.sha256(withdraw_pubkey).slice(2); // 32 byte output
let withdrawal_credentials = Buffer.from(sha256.arrayBuffer(withdraw_pubkey));
withdrawal_credentials[0] = 0;
// Signature is technically bls_sign(signing_privkey, signing_root(deposit_data)) but due to the circular dependency the signature here is actually ignored (!!) and can be nothing, null, or random data.
let signature_dd = Buffer.alloc(5);
let signature_dd = Buffer.alloc(0);
// Put it together somehow
let depositData = {
@ -219,7 +220,7 @@ async function makeValidatorDeposits() {
// A signer is needed to sign a transaction from a given account
let wallet = new ethers.Wallet(item.pk, provider);
contract = contract.connect(wallet);
let tx = contract.deposit(sign_pubkey, withdrawal_credentials, signature_d).then(console.log);
let tx = contract.deposit(signkeys.publicKey.toHexString(), withdrawal_credentials_hex, signature_d).then(console.log);
//console.log("Validator " + item.address + " is depositing 32 ether to the deposit contract at " + contractAddress + " via TX " + tx.hash);
});