Initial commit
This commit is contained in:
parent
a34bbd37ba
commit
12e9be66a9
|
@ -0,0 +1,14 @@
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.idea
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
.secret.json
|
|
@ -0,0 +1,154 @@
|
||||||
|
const Events = require("events");
|
||||||
|
const stripHexPrefix = require('strip-hex-prefix');
|
||||||
|
const { isSignatureValid } = require("./utils");
|
||||||
|
const express = require("express");
|
||||||
|
const { check, validationResult } = require("express-validator");
|
||||||
|
const cors = require("cors");
|
||||||
|
const helmet = require("helmet");
|
||||||
|
const rateLimit = require("../middleware/rate-limit");
|
||||||
|
const config = require("../config");
|
||||||
|
const Database = require("../database");
|
||||||
|
const events = new Events();
|
||||||
|
const Subscriber = require("../models/subscriber");
|
||||||
|
const subscriberStatus = require("../models/subscriberStatus");
|
||||||
|
const Mailer = require('../mail/sendgrid');
|
||||||
|
const dappConfig = require('../config/dapps');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const mailer = new Mailer(config);
|
||||||
|
const db = new Database(events, config);
|
||||||
|
|
||||||
|
db.init();
|
||||||
|
|
||||||
|
|
||||||
|
const hexValidator = value => {
|
||||||
|
const regex = /^[0-9A-Fa-f]*$/g;
|
||||||
|
if(regex.test(stripHexPrefix(value))){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new Error('Invalid hex string');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
events.on("db:connected", () => {
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(rateLimit());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(helmet.expectCt({ enforce: true, maxAge: 60 }));
|
||||||
|
app.use(helmet());
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
"/:dappId/subscribe",
|
||||||
|
[
|
||||||
|
check("signature")
|
||||||
|
.exists()
|
||||||
|
.isLength({min: 132, max: 132})
|
||||||
|
.custom(hexValidator),
|
||||||
|
check("address")
|
||||||
|
.exists()
|
||||||
|
.isLength({min: 42, max: 42})
|
||||||
|
.custom(hexValidator),
|
||||||
|
check("email")
|
||||||
|
.exists()
|
||||||
|
.isEmail(),
|
||||||
|
check("dappId").exists()
|
||||||
|
],
|
||||||
|
async (req, res) => {
|
||||||
|
const {
|
||||||
|
params: { dappId },
|
||||||
|
body: { address, email, signature }
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(404).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: dappId should be a token
|
||||||
|
if(!dappConfig[dappId]){
|
||||||
|
return res.status(404).send("Invalid dapp");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSignatureValid(address, email, signature)) {
|
||||||
|
return res.status(404).send("Invalid signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: rate limit the number of times an user can subscribe
|
||||||
|
// TODO: handle subscriptions to particular events
|
||||||
|
|
||||||
|
try {
|
||||||
|
const subscriber = await Subscriber.findOne({
|
||||||
|
dappId,
|
||||||
|
address
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!subscriber) {
|
||||||
|
await Subscriber.create({
|
||||||
|
dappId,
|
||||||
|
email,
|
||||||
|
address,
|
||||||
|
status: subscriberStatus.CONFIRMED // TODO: remove this once email confirmation is done
|
||||||
|
});
|
||||||
|
|
||||||
|
mailer.send(dappId, 'sign-up', { email });
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: add global error handler
|
||||||
|
return res.status(400).send(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send("OK");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
app.post("/:dapp/unsubscribe", async (req, res) => {
|
||||||
|
const {
|
||||||
|
params: { dappId },
|
||||||
|
body: { address, email, signature }
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
// TODO: validate dappId
|
||||||
|
if (dappId !== "status-teller-network") {
|
||||||
|
return res.status(404).send("Invalid dapp");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSignatureValid(address, email, signature)) {
|
||||||
|
return res.status(404).send("Invalid signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle unsubscribe to particular events
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Subscriber.deleteOne({
|
||||||
|
dapp: req.params.dappId,
|
||||||
|
address: req.body.address
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: add global error handler
|
||||||
|
return res.status(400).send(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send("OK");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/confirm/:token", (req, res) => {});
|
||||||
|
|
||||||
|
app.get("/", (req, res) => res.status(200).json({ status: isConnected() }));
|
||||||
|
|
||||||
|
app.listen(config.PORT, () =>
|
||||||
|
console.log(`App listening on port ${config.PORT}!`)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// MVP
|
||||||
|
// ====
|
||||||
|
|
||||||
|
// Folder with DApp information, event ABI, and email templates
|
||||||
|
|
||||||
|
// TODO: register DAPP and content
|
||||||
|
// TODO: handle errors sending email
|
|
@ -0,0 +1,14 @@
|
||||||
|
const web3EthAccounts = require("web3-eth-accounts");
|
||||||
|
const web3Utils = require("web3-utils");
|
||||||
|
|
||||||
|
const isSignatureValid = (address, message, signature) => {
|
||||||
|
const accounts = new web3EthAccounts();
|
||||||
|
address = web3Utils.toChecksumAddress(address);
|
||||||
|
let recoverAddress = accounts.recover(message, signature);
|
||||||
|
recoverAddress = web3Utils.toChecksumAddress(recoverAddress);
|
||||||
|
return address === recoverAddress;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isSignatureValid
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
"teller-network": { }
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
const secret = require('./.secret.json');
|
||||||
|
|
||||||
|
const env = process.env
|
||||||
|
|
||||||
|
/* some defaults cannot be known in advance */
|
||||||
|
const config = {
|
||||||
|
/* Hosting */
|
||||||
|
PORT : env.PORT || 4000,
|
||||||
|
RATE_LIMIT_TIME: env.RATE_LIMIT_TIME || 15,
|
||||||
|
RATE_LIMIT_MAX_REQ: env.RATE_LIMIT_MAX_REQ || 1,
|
||||||
|
/* Database */
|
||||||
|
DB_CONNECTION: env.DB_CONNECTION || secret.DB_CONNECTION || null,
|
||||||
|
/* Blockchain */
|
||||||
|
BLOCKCHAIN_CONNECTION_POINT: env.BLOCKCHAIN_CONNECTION_POINT || secret.BLOCKCHAIN_CONNECTION_POINT || "http://localhost:8545",
|
||||||
|
/* Email */
|
||||||
|
SENDGRID_API_KEY: secret.SENDGRID_API_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config;
|
|
@ -0,0 +1,36 @@
|
||||||
|
module.exports = {
|
||||||
|
from: {
|
||||||
|
email: "noreply@teller.exchange",
|
||||||
|
name: "Teller Network"
|
||||||
|
},
|
||||||
|
templates: {
|
||||||
|
"sign-up": {
|
||||||
|
subject: "Signup email",
|
||||||
|
html: "sign-up.html",
|
||||||
|
text: "sign-up.txt"
|
||||||
|
},
|
||||||
|
contracts: {
|
||||||
|
"0xEE301C6A57e2fBf593F558C1aE52B20485101fC2": {
|
||||||
|
events: {
|
||||||
|
"escrow-creation-seller": {
|
||||||
|
ABI: {
|
||||||
|
name: "Created",
|
||||||
|
type: "event",
|
||||||
|
inputs: [
|
||||||
|
{ indexed: true, name: "offerId", type: "uint256" },
|
||||||
|
{ indexed: true, name: "seller", type: "address" },
|
||||||
|
{ indexed: true, name: "buyer", type: "address" },
|
||||||
|
{ indexed: false, name: "escrowId", type: "uint256" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
index: "seller",
|
||||||
|
template: {
|
||||||
|
html: "escrow-creation-seller.html",
|
||||||
|
text: "escrow-creation-seller.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
<p>A new escrow was created!</p>
|
|
@ -0,0 +1 @@
|
||||||
|
A new escrow was created!
|
|
@ -0,0 +1 @@
|
||||||
|
<p>Signup email</p>
|
|
@ -0,0 +1 @@
|
||||||
|
Signup email
|
|
@ -0,0 +1,33 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const config = require('./config');
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
|
||||||
|
constructor(events, config){
|
||||||
|
this.events = events;
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
this.db = null;
|
||||||
|
this.client = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
init(){
|
||||||
|
if (config.DB_CONNECTION == undefined) {
|
||||||
|
throw Error('Unable to find MongoDB URI in DB_CONNECTION env variable!')
|
||||||
|
}
|
||||||
|
|
||||||
|
mongoose.Promise = global.Promise;
|
||||||
|
|
||||||
|
mongoose.connect(config.DB_CONNECTION, { useNewUrlParser: true, useUnifiedTopology: true }, err => {
|
||||||
|
if(err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
this.db = mongoose;
|
||||||
|
console.log("Connected successfully to db");
|
||||||
|
this.events.emit('db:connected', this.db);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Database;
|
|
@ -0,0 +1,32 @@
|
||||||
|
const sgMail = require("@sendgrid/mail");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
class SendGridMailer {
|
||||||
|
constructor(config) {
|
||||||
|
sgMail.setApiKey(config.SENDGRID_API_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
send(dappId, template, data) {
|
||||||
|
// TODO: extract this logic. Mailer only needs to worry about sending emails
|
||||||
|
const templatePath = path.join("dapps", dappId);
|
||||||
|
const config = require(path.join(path.join('../', templatePath, 'config.js')));
|
||||||
|
const t = config.templates[template];
|
||||||
|
|
||||||
|
// TODO: do not read these files constantly. Keep it on a cache or something. Also, don't use Sync.
|
||||||
|
const text = fs.readFileSync(path.join(templatePath, t.text)).toString();
|
||||||
|
const html = fs.readFileSync(path.join(templatePath, t.html)).toString();
|
||||||
|
|
||||||
|
const msg = {
|
||||||
|
to: data.email,
|
||||||
|
from: config.from,
|
||||||
|
subject: t.subject,
|
||||||
|
text,
|
||||||
|
html
|
||||||
|
};
|
||||||
|
|
||||||
|
sgMail.send(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SendGridMailer;
|
|
@ -0,0 +1,22 @@
|
||||||
|
const rateLimit = require("express-rate-limit");
|
||||||
|
const config = require("./../config");
|
||||||
|
|
||||||
|
class RateLimitMiddleware {
|
||||||
|
static setup() {
|
||||||
|
const windowMs = config.RATE_LIMIT_TIME;
|
||||||
|
const maxReq = config.RATE_LIMIT_MAX_REQ;
|
||||||
|
let limiter = rateLimit({
|
||||||
|
windowMs: windowMs,
|
||||||
|
max: maxReq,
|
||||||
|
handler: function(req, res) {
|
||||||
|
console.warn(this.message);
|
||||||
|
res.status(this.statusCode).send({ error: this.message });
|
||||||
|
},
|
||||||
|
message: `Rate limit was reached, you are able to do ${maxReq} requests per ${windowMs} milliseconds`
|
||||||
|
});
|
||||||
|
|
||||||
|
return limiter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RateLimitMiddleware.setup;
|
|
@ -0,0 +1,50 @@
|
||||||
|
const mongoose = require("mongoose");
|
||||||
|
const subscriberStatus = require('./subscriberStatus');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const validator = require("validator");
|
||||||
|
|
||||||
|
const SubscriberSchema = new Schema({
|
||||||
|
id: Schema.Types.ObjectId,
|
||||||
|
dappId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
validate: {
|
||||||
|
validator: function(value) {
|
||||||
|
// TODO: Validate dapp code
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
message: props => `${props.value} is not a valid dapp token`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
validate: {
|
||||||
|
validator: function(value) {
|
||||||
|
// TODO: validate address
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
message: props => `${props.value} is not a valid address`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
validate: {
|
||||||
|
validator: function(value) {
|
||||||
|
return validator.isEmail(value);
|
||||||
|
},
|
||||||
|
message: props => `${props.value} is not a valid email`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
default: subscriberStatus.UNCONFIRMED,
|
||||||
|
enum: subscriberStatus.ALL
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = mongoose.model("Subscribers", SubscriberSchema);
|
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
UNCONFIRMED: "UNCONFIRMED",
|
||||||
|
CONFIRMED: "CONFIRMED",
|
||||||
|
ALL: ["UNCONFIRMED", "CONFIRMED"]
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "contract-event-notifier",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"api": "node api",
|
||||||
|
"watch": "node watcher"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sendgrid/mail": "^6.4.0",
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"config": "^3.2.4",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"express-rate-limit": "^5.0.0",
|
||||||
|
"express-validator": "^6.2.0",
|
||||||
|
"helmet": "^3.21.2",
|
||||||
|
"mongoose": "5.7.5",
|
||||||
|
"strip-hex-prefix": "^1.0.0",
|
||||||
|
"validator": "11.0.0",
|
||||||
|
"web3": "^1.2.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
pragma solidity >=0.5.0 <0.6.0;
|
||||||
|
|
||||||
|
contract Escrow {
|
||||||
|
event Created(uint indexed offerId, address indexed seller, address indexed buyer, uint escrowId);
|
||||||
|
|
||||||
|
function create() public {
|
||||||
|
emit Created(block.number, msg.sender, msg.sender, block.number);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
const Web3 = require("web3");
|
||||||
|
|
||||||
|
class Ethereum {
|
||||||
|
constructor(events, config) {
|
||||||
|
this.events = events;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.web3 = new Web3(this.config.BLOCKCHAIN_CONNECTION_POINT);
|
||||||
|
|
||||||
|
this.web3.eth.net
|
||||||
|
.isListening()
|
||||||
|
.then(() => {
|
||||||
|
console.log("Connected successfully to web3");
|
||||||
|
this.events.emit("web3:connected");
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(milliseconds) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
async poll(fn) {
|
||||||
|
await fn();
|
||||||
|
await this.sleep(1 * 1000); // TODO: extract to config
|
||||||
|
await this.poll(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEvents(fromBlock, toBlock) {
|
||||||
|
console.log("Queriying ", fromBlock, toBlock);
|
||||||
|
// TODO:
|
||||||
|
let events = await this.contract.getPastEvents("Created", {
|
||||||
|
filter: {},
|
||||||
|
fromBlock: fromBlock,
|
||||||
|
toBlock: toBlock
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let event of events) {
|
||||||
|
// TODO: process each event. See if the return values matches the indexed address field, and send email
|
||||||
|
console.log(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async scan() {
|
||||||
|
// TODO: obtain this for all contracts / events in dapps/ folder
|
||||||
|
const abi = [
|
||||||
|
{
|
||||||
|
name: "Created",
|
||||||
|
type: "event",
|
||||||
|
inputs: [
|
||||||
|
{ indexed: true, name: "offerId", type: "uint256" },
|
||||||
|
{ indexed: true, name: "seller", type: "address" },
|
||||||
|
{ indexed: true, name: "buyer", type: "address" },
|
||||||
|
{ indexed: false, name: "escrowId", type: "uint256" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.contract = new this.web3.eth.Contract(
|
||||||
|
abi,
|
||||||
|
"0xEE301C6A57e2fBf593F558C1aE52B20485101fC2"
|
||||||
|
);
|
||||||
|
|
||||||
|
const MaxBlockRange = 30; // TODO: extract to config
|
||||||
|
|
||||||
|
let latestCachedBlock = 5434202; // TODO: obtain latest block written to database
|
||||||
|
let latestEthBlock = 0; // latest block in blockchain
|
||||||
|
|
||||||
|
await this.poll(async () => {
|
||||||
|
try {
|
||||||
|
latestEthBlock = (await this.web3.eth.getBlockNumber()) - 12; // 12 blocks of delay to avoid reorgs.
|
||||||
|
|
||||||
|
if (latestCachedBlock + MaxBlockRange > latestEthBlock) return; // Wait until more blocks are mined
|
||||||
|
|
||||||
|
latestEthBlock = Math.min(
|
||||||
|
latestEthBlock,
|
||||||
|
latestCachedBlock + MaxBlockRange
|
||||||
|
);
|
||||||
|
if (latestEthBlock > latestCachedBlock) {
|
||||||
|
await this.getEvents(latestCachedBlock, latestEthBlock);
|
||||||
|
latestCachedBlock = latestEthBlock + 1;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Ethereum;
|
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
const Events = require("events");
|
||||||
|
const config = require("../config");
|
||||||
|
const Database = require("../database");
|
||||||
|
const Ethereum = require("./ethereum");
|
||||||
|
const events = new Events();
|
||||||
|
const Mailer = require('../mail/sendgrid');
|
||||||
|
|
||||||
|
const mailer = new Mailer(config);
|
||||||
|
const db = new Database(events, config);
|
||||||
|
const eth = new Ethereum(events, config);
|
||||||
|
|
||||||
|
db.init();
|
||||||
|
eth.init();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
events.on("db:connected", () => {
|
||||||
|
events.on('web3:connected', () => {
|
||||||
|
eth.scan();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: handle errors sending email
|
||||||
|
// TODO: handle web3js disconnects
|
Loading…
Reference in New Issue