mirror of
https://github.com/status-im/thundercloud.git
synced 2025-02-28 19:00:32 +00:00
commit
476c26119a
8
.gitignore
vendored
8
.gitignore
vendored
@ -3,7 +3,13 @@ node_modules
|
|||||||
node_modules/*
|
node_modules/*
|
||||||
deploy/db/*
|
deploy/db/*
|
||||||
deploy/keys/*
|
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
|
!.gitkeep
|
||||||
*.log
|
*.log
|
||||||
.mykeys.json
|
.mykeys.json
|
@ -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.
|
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.
|
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
|
#### 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.
|
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
|
- `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 faucet` will host the ether faucet at `localhost:5000`
|
||||||
- `yarn run validator-ui --port 8081` will host the validator UI at `localhost:8081` // @TODO
|
- @TODO `yarn validator-ui` will host the validator UI at `localhost:8081`
|
||||||
|
|
||||||
### Other commands
|
### Other commands
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ The generator is deterministic. You always end up with the same addresses, accou
|
|||||||
|
|
||||||
## Contributing
|
## 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
|
## License
|
||||||
|
|
||||||
|
24
deploy/faucet/config.json.example
Normal file
24
deploy/faucet/config.json.example
Normal 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
44
deploy/faucet/gulpfile.js
Normal 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
59
deploy/faucet/index.js
Normal 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
8710
deploy/faucet/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
deploy/faucet/package.json
Normal file
52
deploy/faucet/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
BIN
deploy/faucet/public/assets/images/cloud.png
Normal file
BIN
deploy/faucet/public/assets/images/cloud.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
deploy/faucet/public/assets/images/loading.png
Normal file
BIN
deploy/faucet/public/assets/images/loading.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
BIN
deploy/faucet/public/assets/images/loading@2x.png
Normal file
BIN
deploy/faucet/public/assets/images/loading@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
53
deploy/faucet/public/assets/javascripts/application/index.js
Normal file
53
deploy/faucet/public/assets/javascripts/application/index.js
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
7
deploy/faucet/public/assets/javascripts/vendor/bootstrap.min.js
vendored
Normal file
7
deploy/faucet/public/assets/javascripts/vendor/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
deploy/faucet/public/assets/javascripts/vendor/index.js
vendored
Normal file
2
deploy/faucet/public/assets/javascripts/vendor/index.js
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
//=require sweetalert2.min.js
|
||||||
|
//=require jquery.min.js
|
2
deploy/faucet/public/assets/javascripts/vendor/jquery-3.3.1.min.js
vendored
Normal file
2
deploy/faucet/public/assets/javascripts/vendor/jquery-3.3.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
deploy/faucet/public/assets/javascripts/vendor/jquery.min.js
vendored
Normal file
4
deploy/faucet/public/assets/javascripts/vendor/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
deploy/faucet/public/assets/javascripts/vendor/popper.min.js
vendored
Normal file
5
deploy/faucet/public/assets/javascripts/vendor/popper.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
deploy/faucet/public/assets/javascripts/vendor/sweetalert2.min.js
vendored
Normal file
1
deploy/faucet/public/assets/javascripts/vendor/sweetalert2.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
deploy/faucet/public/assets/stylesheets/index.scss
Normal file
1
deploy/faucet/public/assets/stylesheets/index.scss
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import './index/*';
|
@ -0,0 +1,9 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
.content {
|
||||||
|
color: #333;
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
10
deploy/faucet/public/assets/stylesheets/index/_1_mixins.scss
Normal file
10
deploy/faucet/public/assets/stylesheets/index/_1_mixins.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
10
deploy/faucet/public/assets/stylesheets/index/_2_base.scss
Normal file
10
deploy/faucet/public/assets/stylesheets/index/_2_base.scss
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
main {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
24
deploy/faucet/public/assets/stylesheets/index/addition.scss
Normal file
24
deploy/faucet/public/assets/stylesheets/index/addition.scss
Normal 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;
|
||||||
|
}
|
36
deploy/faucet/public/assets/stylesheets/index/footer.scss
Normal file
36
deploy/faucet/public/assets/stylesheets/index/footer.scss
Normal 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;
|
||||||
|
}
|
13
deploy/faucet/public/assets/stylesheets/index/header.scss
Normal file
13
deploy/faucet/public/assets/stylesheets/index/header.scss
Normal 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;
|
||||||
|
}
|
108
deploy/faucet/public/assets/stylesheets/index/inputs.scss
Normal file
108
deploy/faucet/public/assets/stylesheets/index/inputs.scss
Normal 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;
|
||||||
|
}
|
80
deploy/faucet/public/assets/stylesheets/index/loading.scss
Normal file
80
deploy/faucet/public/assets/stylesheets/index/loading.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
deploy/faucet/public/assets/stylesheets/index/main.scss
Normal file
31
deploy/faucet/public/assets/stylesheets/index/main.scss
Normal 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;
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
.text-xs-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-recaptcha {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
1
deploy/faucet/public/assets/stylesheets/sweetalert2.min.css
vendored
Normal file
1
deploy/faucet/public/assets/stylesheets/sweetalert2.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
133
deploy/faucet/public/views/index.pug
Normal file
133
deploy/faucet/public/views/index.pug
Normal 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"));
|
151
deploy/faucet/src/controllers/index.js
Normal file
151
deploy/faucet/src/controllers/index.js
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
27
deploy/faucet/src/helpers/blockchain-helper.js
Normal file
27
deploy/faucet/src/helpers/blockchain-helper.js
Normal 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"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
64
deploy/faucet/src/helpers/captcha-helper.js
Normal file
64
deploy/faucet/src/helpers/captcha-helper.js
Normal 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 }
|
6
deploy/faucet/src/helpers/debug.js
Normal file
6
deploy/faucet/src/helpers/debug.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
function debug (isDebug, text) {
|
||||||
|
if (isDebug) {
|
||||||
|
console.log(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = { debug }
|
13
deploy/faucet/src/helpers/generate-response.js
Normal file
13
deploy/faucet/src/helpers/generate-response.js
Normal 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 };
|
93
deploy/faucet/src/helpers/tweet-helper.js
Normal file
93
deploy/faucet/src/helpers/tweet-helper.js
Normal 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
7005
deploy/faucet/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -13,6 +13,7 @@
|
|||||||
"ganache-cli": "^6.5.0"
|
"ganache-cli": "^6.5.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf deploy/db/*"
|
"clean": "rm -rf deploy/db/*",
|
||||||
|
"faucet": "cd deploy/faucet && yarn start"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
start.js
11
start.js
@ -96,7 +96,7 @@ provider.listAccounts().then(function(result){
|
|||||||
let mnemonic = process.env.mnemonic;
|
let mnemonic = process.env.mnemonic;
|
||||||
let mnemonicWallet = ethers.Wallet.fromMnemonic(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) {
|
if(err) {
|
||||||
return console.log(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();
|
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)
|
// 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_hex = "0x00" + sha256.sha256(withdraw_pubkey).slice(2); // 32 byte output
|
||||||
let withdrawal_credentials = Buffer.from(sha256.arrayBuffer(withdraw_pubkey))
|
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.
|
// 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
|
// Put it together somehow
|
||||||
let depositData = {
|
let depositData = {
|
||||||
@ -219,7 +220,7 @@ async function makeValidatorDeposits() {
|
|||||||
// A signer is needed to sign a transaction from a given account
|
// A signer is needed to sign a transaction from a given account
|
||||||
let wallet = new ethers.Wallet(item.pk, provider);
|
let wallet = new ethers.Wallet(item.pk, provider);
|
||||||
contract = contract.connect(wallet);
|
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);
|
//console.log("Validator " + item.address + " is depositing 32 ether to the deposit contract at " + contractAddress + " via TX " + tx.hash);
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user