Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
lsaether committed Sep 4, 2019
0 parents commit bc984f2
Show file tree
Hide file tree
Showing 8 changed files with 4,119 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.env
*storage.db
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM node:10.15.3-alpine

WORKDIR /bot

COPY . /bot

RUN npm run bot
27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "faucet",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"backend": "node src/server/index.js",
"bot": "node src/bot/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@polkadot/api": "^0.90.1",
"@polkadot/keyring": "^1.1.1",
"@polkadot/util": "^1.1.1",
"@polkadot/wasm-crypto": "^0.13.1",
"axios": "^0.19.0",
"body-parser": "^1.19.0",
"bs58": "^4.0.1",
"crypto": "^1.0.1",
"dotenv": "^8.1.0",
"express": "^4.17.1",
"matrix-js-sdk": "^2.3.0",
"nedb": "^1.8.0"
}
}
93 changes: 93 additions & 0 deletions src/bot/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const mSDK = require('matrix-js-sdk');
const axios = require('axios');
const pdKeyring = require('@polkadot/keyring');
require('dotenv').config()

const bot = mSDK.createClient({
baseUrl: 'https://matrix.org',
accessToken: process.env.MATRIX_ACCESS_TOKEN,
userId: process.env.MATRIX_USER_ID,
localTimeoutMs: 10000,
});

let ax = axios.create({
baseURL: process.env.BACKEND_URL,
timeout: 10000,

});

const sendMessage = (roomId, msg) => {
bot.sendEvent(
roomId,
'm.room.message',
{ 'body': msg, 'msgtype': 'm.text' },
'',
console.error,
);
}

bot.on('RoomMember.membership', (_, member) => {
if (member.membership === 'invite' && member.userId === '@multichain:matrix.org') {
bot.joinRoom(member.roomId).done(() => {
console.log(`Auto-joined ${member.roomId}.`);
});
}
});

bot.on('Room.timeline', async (event) => {
if (event.getType() !== 'm.room.message') {
return; // Only act on messages (for now).
}

const { content: { body }, event_id: eventId, room_id: roomId, sender } = event.event;

let [action, arg0, arg1] = body.split(' ');

if (action === '!balance') {
const res = await ax.get('/balance');
const balance = res.data;

bot.sendHtmlMessage(roomId, `The faucet has ${balance/10**15} DOTs remaining.`, `The faucet has ${balance/10**15} DOTs remaining.`)
}

if (action === '!drip') {
try {
pdKeyring.decodeAddress(arg0);
} catch (e) {
sendMessage(roomId, `${sender} provided an incompatible address.`);
return;
}

let amount = 150;
if (sender.endsWith(':web3.foundation') && arg1) {
amount = arg1;
}

const res = await ax.post('/bot-endpoint', {
sender,
address: arg0,
amount,
});

if (res.data === 'LIMIT') {
sendMessage(roomId, `${sender} has reached their daily quota. Only request twice per 24 hours.`);
return;
}

bot.sendHtmlMessage(
roomId,
`Sent ${sender} ${amount} mDOTs. Extrinsic hash: ${res.data}.`,
`Sent ${sender} ${amount} mDOTs. <a href="https://polkascan.io/pre/alexander/transaction/${res.data}">View on Polkascan.</a>`
);
}

if (action === '!faucet') {
sendMessage(roomId, `
Usage:
!balance - Get the faucet's balance.
!drip <Address> - Send Alexander DOTs to <Address>.
!faucet - Prints usage information.`);
}
});

bot.startClient(0);
26 changes: 26 additions & 0 deletions src/server/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { WsProvider, ApiPromise } = require('@polkadot/api');
const pdKeyring = require('@polkadot/keyring');

class Actions {
async create(mnemonic, url = 'wss://poc3-rpc.polkadot.io/') {
const provider = new WsProvider(url);
this.api = await ApiPromise.create({ provider });
const keyring = new pdKeyring.Keyring({ type: 'sr25519' });
this.account = keyring.addFromMnemonic(mnemonic);
}

async sendDOTs(address, amount = 150) {
amount = amount * 10**12;

const transfer = this.api.tx.balances.transfer(address, amount);
const hash = await transfer.signAndSend(this.account);

return hash.toHex();
}

async checkBalance() {
return this.api.query.balances.freeBalance(this.account.address);
}
}

module.exports = Actions;
57 changes: 57 additions & 0 deletions src/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const express = require('express');
const bodyParser = require('body-parser');
require('dotenv').config()

const Actions = require('./actions.js');

const Storage = require('./storage.js');
const storage = new Storage();

const app = express();
app.use(bodyParser.json());
const port = 5555;

const mnemonic = process.env.MNEMONIC;

app.get('/health', (_, res) => {
res.send('Faucet backend is healthy.');
});

const createAndApplyActions = async () => {
const actions = new Actions();
await actions.create(mnemonic);

app.get('/balance', async (_, res) => {
const balance = await actions.checkBalance();
res.send(balance.toString());
});

app.post('/bot-endpoint', async (req, res) => {
const { address, amount, sender } = req.body;

if (!(await storage.isValid(sender, address)) && !sender.endsWith(':web3.foundation')) {
res.send('LIMIT');
}

await storage.saveData(sender, address);

const hash = await actions.sendDOTs(address, amount);
res.send(hash);
});


app.post('/web-endpoint', (req, res) => {

});
}

const main = async () => {
await createAndApplyActions();

app.listen(port, () => console.log(`Faucet backend listening on port ${port}.`));
}

try {
main();
} catch (e) { console.error(e); }

91 changes: 91 additions & 0 deletions src/server/storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const Datastore = require('nedb');
const crypto = require('crypto');

const SECOND = 1000;
const HOUR = 60 * SECOND;
const DAY = 24 * HOUR;

const CompactionTimeout = 10 * SECOND;

const sha256 = x =>
crypto
.createHash('sha256')
.update(x, 'utf8')
.digest('hex');

const now = () => new Date().getTime();

class Storage {
constructor(filename = './storage.db', autoload = true) {
this._db = new Datastore({ filename, autoload });
}

async close() {
this._db.persistence.compactDatafile();

return new Promise((resolve, reject) => {
this._db.on('compaction.done', () => {
this._db.removeAllListeners('compaction.done');
resolve();
});

setTimeout(() => {
resolve();
}, CompactionTimeout);
});
}

async isValid(username, addr, limit = 2, span = DAY) {
username = sha256(username);
addr = sha256(addr);

const totalUsername = await this._query(username, span);
const totalAddr = await this._query(addr, span);

if (totalUsername < limit && totalAddr < limit) {
return true;
}

return false;
}

async saveData(username, addr) {
username = sha256(username);
addr = sha256(addr);

await this._insert(username);
await this._insert(addr);
return true;
}

async _insert(item) {
const timestamp = now();

return new Promise((resolve, reject) => {
this._db.insert({ item, timestamp }, (err) => {
if (err) reject(err);
resolve();
});
});
}

async _query(item, span) {
const timestamp = now();

const query = {
$and: [
{item},
{timestamp: { $gt: timestamp - span }},
],
};

return new Promise((resolve, reject) => {
this._db.find(query, (err, docs) => {
if (err) reject();
resolve(docs.length);
});
});
}
}

module.exports = Storage;
Loading

0 comments on commit bc984f2

Please sign in to comment.