-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit bc984f2
Showing
8 changed files
with
4,119 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules | ||
.env | ||
*storage.db |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); } | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.