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