Initial commit
This commit is contained in:
commit
a408b4e9d3
18 changed files with 1676 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
config/config.js
|
||||
node_modules
|
||||
token
|
0
README.md
Normal file
0
README.md
Normal file
809
app.js
Normal file
809
app.js
Normal file
|
@ -0,0 +1,809 @@
|
|||
const async = require('async');
|
||||
const base58 = require('bs58check');
|
||||
const config = require('./config/config');
|
||||
const fs = require('fs');
|
||||
const moment = require('moment');
|
||||
const mysql = require('mysql');
|
||||
const request = require('request');
|
||||
const util = require('util');
|
||||
if (config.debug) {
|
||||
require('request-debug')(request);
|
||||
}
|
||||
|
||||
// URLS
|
||||
const howToUseUrl = 'https://reddit.com/r/lbry';
|
||||
const baseUrl = 'https://oauth.reddit.com';
|
||||
const rateUrl = 'https://api.lbry.io/lbc/exchange_rate';
|
||||
const tokenUrlFormat = 'https://%s:%s@www.reddit.com/api/v1/access_token';
|
||||
const txBaseUrl = 'https://explorer.lbry.io/tx';
|
||||
|
||||
// Other globals
|
||||
const userAgent = 'lbryian/1.0.0 Node.js (by /u/lbryian)';
|
||||
const commentKind = 't1';
|
||||
const privateMessageKind = 't4';
|
||||
let globalAccessToken;
|
||||
let accessTokenTime;
|
||||
|
||||
// Load message templates
|
||||
const messageTemplates = {};
|
||||
const templateNames = [
|
||||
'onbalance',
|
||||
'ondeposit',
|
||||
'onsendtip',
|
||||
'onsendtip.insufficientfunds',
|
||||
'onsendtip.invalidamount',
|
||||
'onwithdraw',
|
||||
'onwithdraw.amountltefee',
|
||||
'onwithdraw.insufficientfunds',
|
||||
'onwithdraw.invalidaddress',
|
||||
'onwithdraw.invalidamount'
|
||||
];
|
||||
for (let i = 0; i < templateNames.length; i++) {
|
||||
const name = templateNames[i];
|
||||
messageTemplates[name] = fs.readFileSync(`templates/${name}.txt`, { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
// Connect to the database
|
||||
let db;
|
||||
const initSqlConnection = () => {
|
||||
const _db = mysql.createConnection({
|
||||
host: config.mariadb.host,
|
||||
user: config.mariadb.username,
|
||||
password: config.mariadb.password,
|
||||
database: config.mariadb.database,
|
||||
charset: 'utf8mb4',
|
||||
timezone: 'Z'
|
||||
});
|
||||
|
||||
_db.on('error', (err) => {
|
||||
if (err.code === 2006 || ['PROTOCOL_CONNECTION_LOST', 'PROTOCOL_PACKETS_OUT_OF_ORDER', 'PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR'].indexOf(err.code) > -1) {
|
||||
_db.destroy();
|
||||
db = initSqlConnection();
|
||||
}
|
||||
});
|
||||
|
||||
return _db;
|
||||
};
|
||||
db = initSqlConnection();
|
||||
|
||||
const loadAccessToken = (callback) => {
|
||||
if (fs.existsSync(config.accessTokenPath)) {
|
||||
const token = fs.readFileSync(config.accessTokenPath, { encoding: 'utf8' });
|
||||
return callback(null, String(token));
|
||||
}
|
||||
|
||||
return callback(null, null);
|
||||
};
|
||||
|
||||
const oauth = (callback) => {
|
||||
const url = util.format(tokenUrlFormat, config.clientId, config.clientSecret);
|
||||
request.post(url, { form: { grant_type: 'password', username: config.username, password: config.password} }, (err, res, body) => {
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
let accessToken = null;
|
||||
try {
|
||||
const response = JSON.parse(body);
|
||||
accessToken = response.access_token;
|
||||
accessTokenTime = moment();
|
||||
if (accessToken && accessToken.trim().length > 0) {
|
||||
fs.writeFileSync(config.accessTokenPath, accessToken);
|
||||
}
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
}
|
||||
|
||||
return callback(null, accessToken);
|
||||
});
|
||||
};
|
||||
|
||||
const retrieveUnreadMessages = (accessToken, callback) => {
|
||||
const url = util.format('%s/message/unread?limit=100', baseUrl);
|
||||
request.get({ url: url, headers: { 'User-Agent': 'lbryian/1.0.0 Node.js (by /u/lbryian)', 'Authorization': 'Bearer ' + accessToken } }, (err, res, body) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(body);
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
}
|
||||
|
||||
return callback(null, response.data.children);
|
||||
});
|
||||
};
|
||||
|
||||
const createOrGetUserId = (username, callback) => {
|
||||
async.waterfall([
|
||||
(cb) => {
|
||||
db.query('SELECT Id FROM Users WHERE LOWER(Username) = ?', [username.toLowerCase()], cb);
|
||||
},
|
||||
(res, fields, cb) => {
|
||||
if (res.length === 0) {
|
||||
// user does not exist, create the user
|
||||
return cb(null, 0);
|
||||
}
|
||||
|
||||
return cb(null, res[0].Id);
|
||||
},
|
||||
(userId, cb) => {
|
||||
if (userId === 0) {
|
||||
return db.query('INSERT INTO Users (Username, Created) VALUES (?, UTC_TIMESTAMP())', [username], (err, res) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return cb(err, null);
|
||||
}
|
||||
|
||||
return cb(null, res.insertId);
|
||||
});
|
||||
}
|
||||
|
||||
return cb(null, userId);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
const getBalance = (userId, callback) => {
|
||||
db.query('SELECT Balance FROM Users WHERE Id = ?', [userId], (err, res) => {
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
return callback(0, res.length === 0 ? 0 : res[0].Balance);
|
||||
});
|
||||
};
|
||||
|
||||
const generateDepositAddress = (callback) => {
|
||||
request.post({ url: config.lbrycrd.rpcurl, json: { method: 'getnewaddress', params: [config.lbrycrd.account] } }, (err, resp, body) => {
|
||||
if (err || body.error) {
|
||||
return callback(err || body.error, null);
|
||||
}
|
||||
|
||||
return callback(null, body.result);
|
||||
});
|
||||
};
|
||||
|
||||
const getDepositAddress = (userId, callback) => {
|
||||
let newAddress = false;
|
||||
async.waterfall([
|
||||
(cb) => {
|
||||
db.query('SELECT DepositAddress FROM Users WHERE Id = ?', [userId], cb);
|
||||
},
|
||||
(res, fields, cb) => {
|
||||
const address = res.length > 0 ? res[0].DepositAddress : null;
|
||||
if (!address || address.trim().length === 0) {
|
||||
newAddress = true;
|
||||
return generateDepositAddress(cb);
|
||||
}
|
||||
return cb(null, address);
|
||||
},
|
||||
(address, cb) => {
|
||||
if (newAddress) {
|
||||
return db.query('UPDATE Users SET DepositAddress = ? WHERE Id = ?', [address, userId], (err) => {
|
||||
if (err) {
|
||||
return cb(err, null);
|
||||
}
|
||||
|
||||
return cb(null, address);
|
||||
});
|
||||
}
|
||||
|
||||
return cb(null, address);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
const sendTip = (sender, recipient, amount, tipdata, callback) => {
|
||||
console.log(`sending ${amount} LBC from ${sender} to ${recipient}`);
|
||||
|
||||
const data = {};
|
||||
async.waterfall([
|
||||
(cb) => {
|
||||
// Start DB transaction
|
||||
db.beginTransaction((err) => {
|
||||
if (err) {
|
||||
return cb(err, null);
|
||||
}
|
||||
return cb(null, true);
|
||||
});
|
||||
},
|
||||
(started, cb) => {
|
||||
// start a transaction
|
||||
// check the sender's balance
|
||||
createOrGetUserId(sender, cb);
|
||||
},
|
||||
(senderId, cb) => {
|
||||
data.senderId = senderId;
|
||||
getBalance(senderId, cb);
|
||||
},
|
||||
(senderBalance, cb) => {
|
||||
// balance is less than amount to tip, or the difference after sending the tip is negative
|
||||
if (senderBalance < amount || (senderBalance - amount) < 0) {
|
||||
return sendPMUsingTemplate('onsendtip.insufficientfunds', { how_to_use_url: howToUseUrl }, message.data.author, () => {
|
||||
cb(new Error('Insufficient funds'), null);
|
||||
});
|
||||
}
|
||||
|
||||
return db.query('UPDATE Users SET Balance = Balance - ? WHERE Id = ?', [amount, data.senderId], cb);
|
||||
},
|
||||
(res, fields, cb) => {
|
||||
// Update the recipient's balance
|
||||
createOrGetUserId(recipient, cb);
|
||||
},
|
||||
(recipientId, cb) => {
|
||||
data.recipientId = recipientId;
|
||||
db.query('UPDATE Users SET Balance = Balance + ? WHERE Id = ?', [amount, recipientId], cb);
|
||||
},
|
||||
(res, fields, cb) => {
|
||||
// save the message
|
||||
const msgdata = tipdata.message.data;
|
||||
db.query( ['INSERT INTO Messages (AuthorId, Type, FullId, RedditId, ParentRedditId, Subreddit, Body, Context, RedditCreated, Created) ',
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP())'].join(''),
|
||||
[data.senderId,
|
||||
tipdata.message.kind === privateMessageKind ? 1 : 2,
|
||||
msgdata.name,
|
||||
msgdata.id,
|
||||
msgdata.parent_id,
|
||||
msgdata.subreddit,
|
||||
msgdata.body,
|
||||
msgdata.context,
|
||||
moment.utc(msgdata.created_utc * 1000).format('YYYY-MM-DD HH:mm:ss')
|
||||
], cb);
|
||||
},
|
||||
(res, fields, cb) => {
|
||||
console.log('Inserting tip.');
|
||||
// save the tip information
|
||||
db.query( ['INSERT INTO Tips (MessageId, SenderId, RecipientId, Amount, AmountUsd, ParsedAmount, Created) ',
|
||||
'VALUES (?, ?, ?, ?, ?, ?, UTC_TIMESTAMP())'].join(''),
|
||||
[res.insertId,
|
||||
data.senderId,
|
||||
data.recipientId,
|
||||
amount,
|
||||
tipdata.amountUsd,
|
||||
tipdata.parsedAmount,
|
||||
], cb);
|
||||
},
|
||||
(res, fields, cb) => {
|
||||
// reply to the source message with message template after successful commit
|
||||
replyMessageUsingTemplate('onsendtip', { recipient: `u/${recipient}`, tip: `${amount} LBC ($${tipdata.amountUsd})`, how_to_use_url: howToUseUrl},
|
||||
tipdata.message.data.name, cb);
|
||||
},
|
||||
(success, cb) => {
|
||||
// Mark the message as read
|
||||
markMessageRead(tipdata.message.data.name, cb);
|
||||
},
|
||||
(success, cb) => {
|
||||
// commit the transaction
|
||||
db.commit((err) => {
|
||||
if (err) {
|
||||
return cb(err, null);
|
||||
}
|
||||
|
||||
return cb(null, true);
|
||||
});
|
||||
}
|
||||
], (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return db.rollback(() => {
|
||||
callback(err, null);
|
||||
});
|
||||
}
|
||||
|
||||
// success
|
||||
return callback(null, true);
|
||||
});
|
||||
};
|
||||
|
||||
const convertUsdToLbc = (amount, callback) => {
|
||||
request.get({ url: rateUrl }, (err, res, body) => {
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(body);
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
}
|
||||
|
||||
if (!response.data || !response.data.lbc_usd) {
|
||||
return callback(new Error('Could not retrieve the LBC/USD conversion rate.'));
|
||||
}
|
||||
|
||||
const rateUsd = parseFloat(response.data.lbc_usd);
|
||||
if (isNaN(rateUsd) || rateUsd === 0) {
|
||||
return callback(new Error('Invalid LBC/USD rate retrieved.'));
|
||||
}
|
||||
const amountLbc = (amount / rateUsd).toFixed(8);
|
||||
return callback(null, amountLbc);
|
||||
});
|
||||
};
|
||||
|
||||
const convertLbcToUsd = (amount, callback) => {
|
||||
request.get({ url: rateUrl }, (err, res, body) => {
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(body);
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
}
|
||||
|
||||
if (!response.data || !response.data.lbc_usd) {
|
||||
return callback(new Error('Could not retrieve the LBC/USD conversion rate.'));
|
||||
}
|
||||
|
||||
const rateUsd = parseFloat(response.data.lbc_usd);
|
||||
if (isNaN(rateUsd) || rateUsd === 0) {
|
||||
return callback(new Error('Invalid LBC/USD rate retrieved.'));
|
||||
}
|
||||
const amountLbc = (amount * rateUsd).toFixed(2);
|
||||
return callback(null, amountLbc);
|
||||
});
|
||||
};
|
||||
|
||||
const markMessageRead = (messageFullId, callback) => {
|
||||
const url = `${baseUrl}/api/read_message`;
|
||||
request.post({ url, form: { id: messageFullId }, headers: { 'User-Agent': userAgent, 'Authorization': 'Bearer ' + globalAccessToken } }, (err, res, body) => {
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(body);
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
}
|
||||
|
||||
// success
|
||||
return callback(null, true);
|
||||
});
|
||||
};
|
||||
|
||||
const sendPMUsingTemplate = (template, substitions, subject, recipient, callback) => {
|
||||
if (!messageTemplates[template]) {
|
||||
return callback(new Error(`Message template ${template} not found.`));
|
||||
}
|
||||
|
||||
let messageText = messageTemplates[template];
|
||||
console.log(messageText);
|
||||
for (let variable in substitutions) {
|
||||
if (substitutions.hasOwnProperty(variable)) {
|
||||
const re = new RegExp(['{', variable, '}'].join(''), 'ig');
|
||||
messageText = messageText.replace(re, substitutions[variable]);
|
||||
}
|
||||
}
|
||||
|
||||
// send the message
|
||||
const url = `${baseUrl}/api/compose`;
|
||||
request.post({
|
||||
url,
|
||||
form: { api_type: 'json', text: messageText, subject, to: recipient },
|
||||
headers: { 'User-Agent': userAgent, 'Authorization': 'Bearer ' + globalAccessToken }
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(body);
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
}
|
||||
|
||||
if (response.json.ratelimit > 0 ||
|
||||
response.json.errors.length > 0) {
|
||||
return callback(new Error('Rate limited.'), null);
|
||||
}
|
||||
|
||||
// success
|
||||
return callback(null, true);
|
||||
});
|
||||
};
|
||||
|
||||
const replyMessageUsingTemplate = (template, substitutions, sourceMessageFullId, callback) => {
|
||||
if (!messageTemplates[template]) {
|
||||
return callback(new Error(`Message template ${template} not found.`));
|
||||
}
|
||||
|
||||
let messageText = messageTemplates[template];
|
||||
for (let variable in substitutions) {
|
||||
if (substitutions.hasOwnProperty(variable)) {
|
||||
const re = new RegExp(['{', variable, '}'].join(''), 'ig');
|
||||
messageText = messageText.replace(re, substitutions[variable]);
|
||||
}
|
||||
}
|
||||
|
||||
// send the message
|
||||
const url = `${baseUrl}/api/comment`;
|
||||
request.post({
|
||||
url,
|
||||
form: { api_type: 'json', text: messageText, thing_id: sourceMessageFullId },
|
||||
headers: { 'User-Agent': userAgent, 'Authorization': 'Bearer ' + globalAccessToken }
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(body);
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
}
|
||||
|
||||
if (response.json.ratelimit > 0 ||
|
||||
response.json.errors.length > 0) {
|
||||
return callback(new Error('Rate limited.'), null);
|
||||
}
|
||||
|
||||
// success
|
||||
return callback(null, true);
|
||||
});
|
||||
};
|
||||
|
||||
const getMessageAuthor = (thingId, accessToken, callback) => {
|
||||
const url = util.format('%s/api/info?id=%s', baseUrl, thingId);
|
||||
request.get({ url: url, headers: { 'User-Agent': userAgent, 'Authorization': 'Bearer ' + globalAccessToken } }, (err, res, body) => {
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(body);
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
}
|
||||
|
||||
return callback(null, (response.data.children.length > 0) ? response.data.children[0].data.author : null);
|
||||
});
|
||||
};
|
||||
|
||||
const doSendTip = function(body, message, callback) {
|
||||
/**
|
||||
* accepted formats:
|
||||
* 1 usd u/lbryian OR u/lbryian 1 usd
|
||||
* 1 lbc u/lbryian OR u/lbryian 1 lbc
|
||||
* $1 u/lbryian OR u/lbryian $1
|
||||
*/
|
||||
const parts = body.split(' ', 3);
|
||||
const parentId = message.data.parent_id ? message.data.parent_id.trim() : null;
|
||||
if ((!parentId || parentId.length === 0) || (parts.length === 0) || (parts.length !== 3 && (parts.length === 2 && parts[0].substring(0,1) !== '$'))) {
|
||||
// ignore the comment
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
if (parts[0] && parts[0].substring(0, 1) === '/') {
|
||||
parts[0] = parts[0].substring(1);
|
||||
}
|
||||
|
||||
let amountUsd = 0;
|
||||
let amountLbc = 0;
|
||||
const nameFirst = parts[0] === config.redditName;
|
||||
if (parts.length === 2) {
|
||||
// get the amount
|
||||
amountUsd = parseFloat(parts[nameFirst ? 1 : 0].substring(1));
|
||||
if (isNaN(amountUsd) || amountUsd <= 0) {
|
||||
return sendPMUsingTemplate('onsendtip.invalidamount', { how_to_use_url: howToUseUrl }, message.data.author, () => {
|
||||
callback(null, null);
|
||||
});
|
||||
}
|
||||
} else if (parts.length === 3) {
|
||||
const amount = parseFloat(parts[nameFirst ? 1 : 0]);
|
||||
const unit = parts[nameFirst ? 2 : 1].toLowerCase();
|
||||
if (isNaN(amount) || amount <= 0 || ['usd', 'lbc'].indexOf(unit) === -1) {
|
||||
// invalid amount or unit
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
if (unit === 'lbc') {
|
||||
amountLbc = amount;
|
||||
} else {
|
||||
amountUsd = amount;
|
||||
}
|
||||
}
|
||||
|
||||
if (amountLbc > 0 || amountUsd > 0) {
|
||||
const parsedAmount = (parts.length === 2) ? parts[nameFirst ? 1 : 0] : [parts[nameFirst ? 1 : 0], parts[nameFirst ? 2 : 1]].join(' ');
|
||||
// get the author of the parent message
|
||||
async.waterfall([
|
||||
(cb) => {
|
||||
getMessageAuthor(message.data.parent_id, globalAccessToken, cb);
|
||||
},
|
||||
(recipient, cb) => {
|
||||
const sender = message.data.author;
|
||||
if (sender !== recipient) {
|
||||
return cb(null, { amountLbc, amountUsd, message, recipient, sender, parsedAmount });
|
||||
}
|
||||
|
||||
return cb(null, null);
|
||||
},
|
||||
(tipdata, cb) => {
|
||||
if (tipdata) {
|
||||
if (tipdata.amountUsd > 0) {
|
||||
return convertUsdToLbc(tipdata.amountUsd, (err, convertedAmount) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
tipdata.amountLbc = convertedAmount;
|
||||
return cb(null, tipdata);
|
||||
});
|
||||
} else if (tipdata.amountLbc > 0 && (!tipdata.amountUsd || tipdata.amountUsd === 0)) {
|
||||
return convertLbcToUsd(tipdata.amountLbc, (err, convertedAmount) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
tipdata.amountUsd = convertedAmount;
|
||||
return cb(null, tipdata);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return cb(null, null);
|
||||
},
|
||||
(data, cb) => {
|
||||
if (data) {
|
||||
return sendTip(data.sender, data.recipient, data.amountLbc, data, cb);
|
||||
}
|
||||
|
||||
return cb(null, null);
|
||||
}
|
||||
], callback);
|
||||
}
|
||||
};
|
||||
|
||||
const doSendBalance = (message, callback) => {
|
||||
async.waterfall([
|
||||
(cb) => {
|
||||
createOrGetUserId(message.data.author, cb);
|
||||
},
|
||||
(authorId, cb) => {
|
||||
getBalance(authorId, cb);
|
||||
},
|
||||
(balance, cb) => {
|
||||
// send message with balance
|
||||
replyMessageUsingTemplate('onbalance', { how_to_use_url: howToUseUrl, amount: balance }, message.data.name, cb);
|
||||
},
|
||||
(success, cb) => {
|
||||
// mark messge as read
|
||||
markMessageRead(message.data.name, cb);
|
||||
}
|
||||
], (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
// success
|
||||
return callback(null, true);
|
||||
});
|
||||
};
|
||||
|
||||
const sendLbcToAddress = (address, amount, callback) => {
|
||||
request.post({ url: config.lbrycrd.rpcurl, json: { method: 'sendtoaddress', params: [address, amount] } }, (err, resp, body) => {
|
||||
if (err || body.error) {
|
||||
return callback(err || body.error, null);
|
||||
}
|
||||
|
||||
return callback(null, body.result);
|
||||
});
|
||||
};
|
||||
|
||||
const doWithdrawal = (amount, address, message, callback) => {
|
||||
const data = {};
|
||||
async.waterfall([
|
||||
(cb) => {
|
||||
// Start DB transaction
|
||||
db.beginTransaction((err) => {
|
||||
if (err) {
|
||||
return cb(err, null);
|
||||
}
|
||||
return cb(null, true);
|
||||
});
|
||||
},
|
||||
// prevent withdrawal to deposit address
|
||||
(started, cb) => {
|
||||
createOrGetUserId(message.data.author, cb);
|
||||
},
|
||||
(authorId, cb) => {
|
||||
data.userId = authorId;
|
||||
getDepositAddress(authorId, cb);
|
||||
},
|
||||
(depositAddress, cb) => {
|
||||
if (address === depositAddress) {
|
||||
return cb(new Error('Attempt to withdraw to deposit address.'), null);
|
||||
}
|
||||
|
||||
return getBalance(data.userId, cb);
|
||||
},
|
||||
(balance, cb) => {
|
||||
// check sufficient balance
|
||||
if (balance < amount || balance - amount < 0) {
|
||||
return sendPMUsingTemplate('onwithdraw.insufficientfunds', { how_to_use_url: howToUseUrl }, message.data.author, () => {
|
||||
cb(new Error('Insufficient funds'), null);
|
||||
});
|
||||
}
|
||||
|
||||
// Update the balance
|
||||
db.query('UPDATE Users SET Balance = Balance - ? WHERE Id = ?', [amount, data.userId], cb);
|
||||
},
|
||||
(res, fields, cb) => {
|
||||
// Send the transaction on the blockchain
|
||||
sendLbcToAddress(address, amount, cb);
|
||||
},
|
||||
(txhash, cb) => {
|
||||
data.txhash = txhash;
|
||||
// Insert the withdrawal entry
|
||||
db.query('INSERT INTO Withdrawals (UserId, TxHash, Amount, Created) VALUES (?, ?, ?, UTC_TIMESTAMP())', [data.userId, txhash, amount], cb);
|
||||
},
|
||||
(res, fields, cb) => {
|
||||
// commit the transaction
|
||||
db.commit((err) => {
|
||||
if (err) {
|
||||
return cb(err, null);
|
||||
}
|
||||
|
||||
return cb(null, true);
|
||||
});
|
||||
},
|
||||
(success, cb) => {
|
||||
// mark messge as read
|
||||
markMessageRead(message.data.name, cb);
|
||||
},
|
||||
(success, cb) => {
|
||||
// send a reply
|
||||
replyMessageUsingTemplate('onwithdraw', { how_to_use_url: howToUseUrl, address: address, amount: amount, txid: data.txhash }, message.data.name, cb);
|
||||
}
|
||||
], (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return db.rollback(() => {
|
||||
callback(err, null);
|
||||
});
|
||||
}
|
||||
|
||||
// success
|
||||
return callback(null, true);
|
||||
});
|
||||
};
|
||||
|
||||
const doSendDepositAddress = (message, callback) => {
|
||||
async.waterfall([
|
||||
(cb) => {
|
||||
createOrGetUserId(message.data.author, cb);
|
||||
},
|
||||
(authorId, cb) => {
|
||||
getDepositAddress(authorId, cb);
|
||||
},
|
||||
(address, cb) => {
|
||||
// send message with balance
|
||||
replyMessageUsingTemplate('ondeposit', { how_to_use_url: howToUseUrl, address: address }, message.data.name, cb);
|
||||
},
|
||||
(success, cb) => {
|
||||
// mark messge as read
|
||||
markMessageRead(message.data.name, cb);
|
||||
}
|
||||
], (err) => {
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
// success
|
||||
return callback(null, true);
|
||||
});
|
||||
};
|
||||
|
||||
// Commands
|
||||
// balance (PM)
|
||||
// deposit (PM)
|
||||
// tip (Comment): <amount> <unit> u/lbryian
|
||||
// withdraw (PM): withdraw <amount> <address>
|
||||
const processMessage = function(message, callback) {
|
||||
if (!message.kind || !message.data) {
|
||||
return callback(new Error('Invalid message specified for processing.'));
|
||||
}
|
||||
|
||||
const body = String(message.data.body).trim();
|
||||
if (message.kind === privateMessageKind) {
|
||||
// balance, deposit or withdraw
|
||||
// Check the command
|
||||
if ('balance' === body.toLowerCase()) {
|
||||
// do balance check
|
||||
return doSendBalance(message, callback);
|
||||
} else if ('deposit' === body.toLowerCase()) {
|
||||
// send deposit address
|
||||
return doSendDepositAddress(message, callback);
|
||||
} else {
|
||||
// withdrawal
|
||||
const parts = body.split(' ');
|
||||
if (parts.length !== 3 ||
|
||||
parts[0].toLowerCase() !== 'withdraw') {
|
||||
// invalid message, ignore
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
const amount = parseFloat(parts[1]);
|
||||
if (isNaN(amount) || amount < 0) {
|
||||
// TODO: send a message that the withdrawal amount is invalid
|
||||
return sendPMUsingTemplate('onwithdraw.invalidamount', { how_to_use_url: howToUseUrl }, message.data.author, () => {
|
||||
callback(null, null);
|
||||
});
|
||||
}
|
||||
|
||||
if (amount <= config.lbrycrd.txfee) {
|
||||
return sendPMUsingTemplate('onwithdraw.amountltefee', { how_to_use_url: howToUseUrl, amount: amount, fee: config.lbrycrd.txfee }, message.data.author, () => {
|
||||
callback(null, null);
|
||||
});
|
||||
}
|
||||
|
||||
// base58 check the address
|
||||
const address = parts[2];
|
||||
try {
|
||||
base58.decode(address);
|
||||
} catch(e) {
|
||||
return sendPMUsingTemplate('onwithdraw.invalidaddress', { how_to_use_url: howToUseUrl }, message.data.author, () => {
|
||||
callback(null, null);
|
||||
});
|
||||
}
|
||||
|
||||
return doWithdrawal(amount, address, message, callback);
|
||||
}
|
||||
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
if (message.kind === commentKind) {
|
||||
doSendTip(body, message, callback);
|
||||
}
|
||||
};
|
||||
|
||||
// Run the bot
|
||||
const runBot = () => {
|
||||
async.waterfall([
|
||||
(cb) => {
|
||||
if (!accessTokenTime || moment.duration(moment().diff(accessTokenTime)).asMinutes() >= 55) {
|
||||
// remove old or expired tokens
|
||||
// TODO: Implement refreshToken
|
||||
if (fs.existsSync(config.accessTokenPath)) {
|
||||
fs.unlinkSync(config.accessTokenPath);
|
||||
}
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
},
|
||||
(cb) => {
|
||||
loadAccessToken(cb);
|
||||
},
|
||||
(token, cb) => {
|
||||
if (!token || token.trim().length === 0) {
|
||||
return oauth(cb);
|
||||
}
|
||||
|
||||
return cb(null, token);
|
||||
},
|
||||
(token, cb) => {
|
||||
globalAccessToken = token;
|
||||
retrieveUnreadMessages(token, cb);
|
||||
},
|
||||
(unread, cb) => {
|
||||
async.eachSeries(unread, (message, ecb) => {
|
||||
processMessage(message, ecb);
|
||||
}, cb);
|
||||
}
|
||||
], (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
// Wait 1 minute for next iteration
|
||||
console.log('Waiting 1 minute...');
|
||||
setTimeout(runBot, 60000);
|
||||
});
|
||||
};
|
||||
runBot();
|
24
config/default.js
Normal file
24
config/default.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
module.exports = {
|
||||
debug: true,
|
||||
accessTokenPath: 'token',
|
||||
clientId: '<CLIENT_ID>',
|
||||
clientSecret: '<CLIENT_SECRET>',
|
||||
username: '<USERNAME>',
|
||||
password: '<PASSWORD>',
|
||||
|
||||
// for handling tip comments
|
||||
redditName: 'u/lbryian',
|
||||
|
||||
mariadb: {
|
||||
host: 'localhost',
|
||||
username: '<DB_USERNAME>',
|
||||
password: '<DB_PASSWORD>',
|
||||
database: '<DB_NAME>'
|
||||
},
|
||||
|
||||
lbrycrd: {
|
||||
account: 'tips',
|
||||
rpcurl: 'http://127.0.0.1:9245',
|
||||
txfee: 0.00002000
|
||||
}
|
||||
};
|
136
deposits.js
Normal file
136
deposits.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
// Background tx processor for handling deposits and withdrawals
|
||||
const async = require('async');
|
||||
const config = require('./config/config');
|
||||
const mysql = require('mysql');
|
||||
const request = require('request');
|
||||
if (config.debug) {
|
||||
require('request-debug')(request);
|
||||
}
|
||||
|
||||
// Connect to the database
|
||||
let db;
|
||||
const initSqlConnection = () => {
|
||||
const _db = mysql.createConnection({
|
||||
host: config.mariadb.host,
|
||||
user: config.mariadb.username,
|
||||
password: config.mariadb.password,
|
||||
database: config.mariadb.database,
|
||||
charset: 'utf8mb4',
|
||||
timezone: 'Z'
|
||||
});
|
||||
|
||||
_db.on('error', (err) => {
|
||||
if (err.code === 2006 || ['PROTOCOL_CONNECTION_LOST', 'PROTOCOL_PACKETS_OUT_OF_ORDER', 'PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR'].indexOf(err.code) > -1) {
|
||||
_db.destroy();
|
||||
db = initSqlConnection();
|
||||
}
|
||||
});
|
||||
|
||||
return _db;
|
||||
};
|
||||
db = initSqlConnection();
|
||||
|
||||
const userIdForDepositAddress = (address, callback) => {
|
||||
db.query('SELECT Id FROM Users WHERE DepositAddress = ?', [address], (err, res) => {
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
if (res.length === 0) {
|
||||
return callback(new Error(`User with deposit address ${address} not found.`));
|
||||
}
|
||||
|
||||
return callback(null, res[0].Id);
|
||||
});
|
||||
};
|
||||
|
||||
const createDeposit = (address, txhash, amount, confirmations, callback) => {
|
||||
async.waterfall([
|
||||
(cb) => {
|
||||
userIdForDepositAddress(address, cb);
|
||||
},
|
||||
(depositorId, cb) => {
|
||||
db.query('INSERT INTO Deposits (UserId, TxHash, Amount, Confirmations, Created) VALUES (?, ?, ?, ?, UTC_TIMESTAMP()) ON DUPLICATE KEY UPDATE Confirmations = ?',
|
||||
[depositorId, txhash, amount, confirmations, confirmations], cb);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
const confirmationsForTx = (txhash, callback) => {
|
||||
request.post({ url: config.lbrycrd.rpcurl, json: { method: 'gettransaction', params: [txhash] } }, (err, res, body) => {
|
||||
if (body.error) {
|
||||
return callback(body.error, null);
|
||||
}
|
||||
|
||||
return callback(null, body.result.confirmations);
|
||||
});
|
||||
};
|
||||
|
||||
const processNewDeposits = (callback) => {
|
||||
async.waterfall([
|
||||
(cb) => {
|
||||
request.post({ url: config.lbrycrd.rpcurl, json: { method: 'listtransactions', params: [config.lbrycrd.account, 1000] } }, cb);
|
||||
},
|
||||
(res, body, cb) => {
|
||||
if (body.error) {
|
||||
return cb(body.error, null);
|
||||
}
|
||||
|
||||
// simply insert the deposits
|
||||
return async.each(body.result, (tx, ecb) => {
|
||||
if (tx.amount <= 0) {
|
||||
return ecb(null, null);
|
||||
}
|
||||
return createDeposit(tx.address, tx.txid, tx.amount, tx.confirmations, ecb);
|
||||
}, cb);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
// deposits with confirmations < 3
|
||||
const processPendingDeposits = (callback) => {
|
||||
async.waterfall([
|
||||
(cb) => {
|
||||
db.query('SELECT Id, TxHash FROM Deposits WHERE Confirmations < 3', cb);
|
||||
},
|
||||
(res, fields, cb) => {
|
||||
if (res.length === 0) {
|
||||
return cb(null, []);
|
||||
}
|
||||
|
||||
return async.each(res, (deposit, ecb) => {
|
||||
confirmationsForTx(deposit.TxHash, (err, confirmations) => {
|
||||
if (err) {
|
||||
return ecb(err, null);
|
||||
}
|
||||
|
||||
db.query('UPDATE Deposits SET Confirmations = ? WHERE Id = ?', [confirmations, deposit.Id], ecb);
|
||||
});
|
||||
}, cb);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
const runProcess = () => {
|
||||
async.waterfall([
|
||||
(cb) => {
|
||||
console.log('Processing new deposits.');
|
||||
processNewDeposits(cb);
|
||||
},
|
||||
(cb) => {
|
||||
console.log('Processing pending deposits.');
|
||||
processPendingDeposits(cb);
|
||||
}
|
||||
], (err) => {
|
||||
if (err) {
|
||||
console.log('Error occurred.');
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
// run again in 1 minute
|
||||
console.log('Waiting 1 minute...');
|
||||
setTimeout(runProcess, 60000);
|
||||
});
|
||||
};
|
||||
|
||||
runProcess();
|
553
package-lock.json
generated
Normal file
553
package-lock.json
generated
Normal file
|
@ -0,0 +1,553 @@
|
|||
{
|
||||
"name": "lbry-social-tipbot",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.2.tgz",
|
||||
"integrity": "sha1-R8aNaehvXZUxA7AHSpQw3GPaXjk=",
|
||||
"requires": {
|
||||
"co": "4.6.0",
|
||||
"fast-deep-equal": "1.0.0",
|
||||
"json-schema-traverse": "0.3.1",
|
||||
"json-stable-stringify": "1.0.1"
|
||||
}
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
|
||||
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
|
||||
},
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||
},
|
||||
"async": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz",
|
||||
"integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==",
|
||||
"requires": {
|
||||
"lodash": "4.17.4"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"aws-sign2": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
|
||||
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
|
||||
},
|
||||
"base-x": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.2.tgz",
|
||||
"integrity": "sha1-v4c4YbdRQnm3lp80CSnquHwR0TA=",
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.1"
|
||||
}
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
|
||||
"integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"tweetnacl": "0.14.5"
|
||||
}
|
||||
},
|
||||
"bignumber.js": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-4.0.2.tgz",
|
||||
"integrity": "sha1-LR3DfuWWiGfs6pC22k0W5oYI0h0="
|
||||
},
|
||||
"boom": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
|
||||
"integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
|
||||
"requires": {
|
||||
"hoek": "4.2.0"
|
||||
}
|
||||
},
|
||||
"bs58": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||
"integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=",
|
||||
"requires": {
|
||||
"base-x": "3.0.2"
|
||||
}
|
||||
},
|
||||
"bs58check": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.0.2.tgz",
|
||||
"integrity": "sha1-BvY7AcL6YXMDPJDrh/H+PS4T2Jo=",
|
||||
"requires": {
|
||||
"bs58": "4.0.1",
|
||||
"create-hash": "1.1.3"
|
||||
}
|
||||
},
|
||||
"caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
|
||||
},
|
||||
"cipher-base": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
|
||||
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
|
||||
"requires": {
|
||||
"inherits": "2.0.3",
|
||||
"safe-buffer": "5.1.1"
|
||||
}
|
||||
},
|
||||
"co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
|
||||
"integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=",
|
||||
"requires": {
|
||||
"delayed-stream": "1.0.0"
|
||||
}
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"create-hash": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz",
|
||||
"integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=",
|
||||
"requires": {
|
||||
"cipher-base": "1.0.4",
|
||||
"inherits": "2.0.3",
|
||||
"ripemd160": "2.0.1",
|
||||
"sha.js": "2.4.8"
|
||||
}
|
||||
},
|
||||
"cryptiles": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz",
|
||||
"integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=",
|
||||
"requires": {
|
||||
"boom": "5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"boom": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
|
||||
"integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
|
||||
"requires": {
|
||||
"hoek": "4.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"jsbn": "0.1.1"
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
|
||||
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
|
||||
},
|
||||
"extsprintf": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
|
||||
"integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8="
|
||||
},
|
||||
"forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz",
|
||||
"integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=",
|
||||
"requires": {
|
||||
"asynckit": "0.4.0",
|
||||
"combined-stream": "1.0.5",
|
||||
"mime-types": "2.1.17"
|
||||
}
|
||||
},
|
||||
"getpass": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0"
|
||||
}
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
|
||||
},
|
||||
"har-validator": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
|
||||
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
|
||||
"requires": {
|
||||
"ajv": "5.2.2",
|
||||
"har-schema": "2.0.0"
|
||||
}
|
||||
},
|
||||
"hash-base": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz",
|
||||
"integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=",
|
||||
"requires": {
|
||||
"inherits": "2.0.3"
|
||||
}
|
||||
},
|
||||
"hawk": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz",
|
||||
"integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==",
|
||||
"requires": {
|
||||
"boom": "4.3.1",
|
||||
"cryptiles": "3.1.2",
|
||||
"hoek": "4.2.0",
|
||||
"sntp": "2.0.2"
|
||||
}
|
||||
},
|
||||
"hoek": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz",
|
||||
"integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ=="
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0",
|
||||
"jsprim": "1.4.1",
|
||||
"sshpk": "1.13.1"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"isstream": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
|
||||
},
|
||||
"jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
|
||||
"optional": true
|
||||
},
|
||||
"json-schema": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
|
||||
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
|
||||
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
|
||||
},
|
||||
"json-stable-stringify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
|
||||
"integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
|
||||
"requires": {
|
||||
"jsonify": "0.0.0"
|
||||
}
|
||||
},
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
||||
},
|
||||
"jsonify": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
|
||||
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0",
|
||||
"extsprintf": "1.3.0",
|
||||
"json-schema": "0.2.3",
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
|
||||
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz",
|
||||
"integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.17",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz",
|
||||
"integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
|
||||
"requires": {
|
||||
"mime-db": "1.30.0"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.18.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz",
|
||||
"integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8="
|
||||
},
|
||||
"mysql": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.14.1.tgz",
|
||||
"integrity": "sha512-ZPXqQeYH7L1QPDyC77Rcp32cNCQnNjz8Y4BbF17tOjm5yhSfjFa3xS4PvuxWJtEEmwVc4ccI7sSntj4eyYRq0A==",
|
||||
"requires": {
|
||||
"bignumber.js": "4.0.2",
|
||||
"readable-stream": "2.3.3",
|
||||
"safe-buffer": "5.1.1",
|
||||
"sqlstring": "2.2.0"
|
||||
}
|
||||
},
|
||||
"oauth-sign": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
|
||||
"integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM="
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
|
||||
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
|
||||
},
|
||||
"punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
|
||||
"integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
|
||||
"integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
|
||||
"requires": {
|
||||
"core-util-is": "1.0.2",
|
||||
"inherits": "2.0.3",
|
||||
"isarray": "1.0.0",
|
||||
"process-nextick-args": "1.0.7",
|
||||
"safe-buffer": "5.1.1",
|
||||
"string_decoder": "1.0.3",
|
||||
"util-deprecate": "1.0.2"
|
||||
}
|
||||
},
|
||||
"request": {
|
||||
"version": "2.82.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.82.0.tgz",
|
||||
"integrity": "sha512-/QWqfmyTfQ4OYs6EhB1h2wQsX9ZxbuNePCvCm0Mdz/mxw73mjdg0D4QdIl0TQBFs35CZmMXLjk0iCGK395CUDg==",
|
||||
"requires": {
|
||||
"aws-sign2": "0.7.0",
|
||||
"aws4": "1.6.0",
|
||||
"caseless": "0.12.0",
|
||||
"combined-stream": "1.0.5",
|
||||
"extend": "3.0.1",
|
||||
"forever-agent": "0.6.1",
|
||||
"form-data": "2.3.1",
|
||||
"har-validator": "5.0.3",
|
||||
"hawk": "6.0.2",
|
||||
"http-signature": "1.2.0",
|
||||
"is-typedarray": "1.0.0",
|
||||
"isstream": "0.1.2",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"mime-types": "2.1.17",
|
||||
"oauth-sign": "0.8.2",
|
||||
"performance-now": "2.1.0",
|
||||
"qs": "6.5.1",
|
||||
"safe-buffer": "5.1.1",
|
||||
"stringstream": "0.0.5",
|
||||
"tough-cookie": "2.3.3",
|
||||
"tunnel-agent": "0.6.0",
|
||||
"uuid": "3.1.0"
|
||||
}
|
||||
},
|
||||
"request-debug": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/request-debug/-/request-debug-0.2.0.tgz",
|
||||
"integrity": "sha1-/AVOyBcYGwTKQaBSwTb2HEirr3g=",
|
||||
"requires": {
|
||||
"stringify-clone": "1.1.1"
|
||||
}
|
||||
},
|
||||
"ripemd160": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz",
|
||||
"integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=",
|
||||
"requires": {
|
||||
"hash-base": "2.0.2",
|
||||
"inherits": "2.0.3"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
|
||||
},
|
||||
"sha.js": {
|
||||
"version": "2.4.8",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.8.tgz",
|
||||
"integrity": "sha1-NwaMLEdra69ALRSknGf1l5IfY08=",
|
||||
"requires": {
|
||||
"inherits": "2.0.3"
|
||||
}
|
||||
},
|
||||
"sntp": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz",
|
||||
"integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=",
|
||||
"requires": {
|
||||
"hoek": "4.2.0"
|
||||
}
|
||||
},
|
||||
"sqlstring": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.2.0.tgz",
|
||||
"integrity": "sha1-wxNcTqirzX5+50GklmqJHYak8ZE="
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz",
|
||||
"integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
|
||||
"requires": {
|
||||
"asn1": "0.2.3",
|
||||
"assert-plus": "1.0.0",
|
||||
"bcrypt-pbkdf": "1.0.1",
|
||||
"dashdash": "1.14.1",
|
||||
"ecc-jsbn": "0.1.1",
|
||||
"getpass": "0.1.7",
|
||||
"jsbn": "0.1.1",
|
||||
"tweetnacl": "0.14.5"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
|
||||
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.1"
|
||||
}
|
||||
},
|
||||
"stringify-clone": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stringify-clone/-/stringify-clone-1.1.1.tgz",
|
||||
"integrity": "sha1-MJojX7Ts/M19OI2+GLqQT6yvQzs="
|
||||
},
|
||||
"stringstream": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
|
||||
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg="
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",
|
||||
"integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=",
|
||||
"requires": {
|
||||
"punycode": "1.4.1"
|
||||
}
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.1"
|
||||
}
|
||||
},
|
||||
"tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
|
||||
"optional": true
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
|
||||
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
|
||||
},
|
||||
"verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "1.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
package.json
Normal file
18
package.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "lbry-social-tipbot",
|
||||
"description": "LBC reddit tip bot",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://gitlab.com/aureolin/lbry-social-tipbot.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^2.5.0",
|
||||
"bs58check": "^2.0.2",
|
||||
"moment": "^2.18.1",
|
||||
"mysql": "^2.14.1",
|
||||
"request": "^2.82.0",
|
||||
"request-debug": "^0.2.0"
|
||||
}
|
||||
}
|
93
sql/ddl.sql
Normal file
93
sql/ddl.sql
Normal file
|
@ -0,0 +1,93 @@
|
|||
CREATE TABLE Users
|
||||
(
|
||||
`Id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`Username` VARCHAR(20) NOT NULL,
|
||||
`Balance` DECIMAL(18,8) UNSIGNED DEFAULT 0 NOT NULL,
|
||||
`DepositAddress` VARCHAR(34) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
|
||||
`Created` DATETIME NOT NULL,
|
||||
PRIMARY KEY `PK_UserId` (`Id`),
|
||||
UNIQUE KEY `Idx_RedditUsername` (`Username`),
|
||||
UNIQUE KEY `Idx_UserDepositAddress` (`DepositAddress`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;
|
||||
|
||||
CREATE TABLE Messages
|
||||
(
|
||||
`Id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`Type` SMALLINT NOT NULL COMMENT '1 - Private Message, 2 - Comment',
|
||||
`FullId` VARCHAR(15) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
|
||||
`RedditId` VARCHAR(15) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
|
||||
`ParentRedditId` VARCHAR(20) CHARACTER SET latin1 COLLATE latin1_general_ci,
|
||||
`Subreddit` VARCHAR(50),
|
||||
`AuthorId` BIGINT UNSIGNED NOT NULL,
|
||||
`Body` TEXT,
|
||||
`Context` TEXT,
|
||||
`RedditCreated` DATETIME NOT NULL,
|
||||
`Created` DATETIME NOT NULL,
|
||||
PRIMARY KEY `PK_MessageId` (`Id`),
|
||||
FOREIGN KEY `FK_MessageAuthor` (`AuthorId`) REFERENCES `Users` (`Id`),
|
||||
UNIQUE KEY `Idx_MessageRedditId` (`RedditId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;
|
||||
|
||||
CREATE TABLE Tips
|
||||
(
|
||||
`Id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`MessageId` BIGINT UNSIGNED NOT NULL,
|
||||
`SenderId` BIGINT UNSIGNED NOT NULL,
|
||||
`RecipientId` BIGINT UNSIGNED NOT NULL,
|
||||
`ParsedAmount` VARCHAR(20) NOT NULL COMMENT 'user amount string, $0.x, 0.x usd or 0.x lbc',
|
||||
`AmountUsd` DECIMAL(18,2) UNSIGNED,
|
||||
`Amount` DECIMAL(18,8) UNSIGNED NOT NULL,
|
||||
`Created` DATETIME NOT NULL,
|
||||
PRIMARY KEY `PK_TipId` (`Id`),
|
||||
FOREIGN KEY `FK_TipSender` (`SenderId`) REFERENCES `Users` (`Id`),
|
||||
FOREIGN KEY `FK_TipRecipient` (`RecipientId`) REFERENCES `Users` (`Id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;
|
||||
|
||||
CREATE TABLE Deposits
|
||||
(
|
||||
`Id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`UserId` BIGINT UNSIGNED NOT NULL,
|
||||
`TxHash` VARCHAR(70) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
|
||||
`Amount` DECIMAL(18,8) UNSIGNED NOT NULL,
|
||||
`Confirmations` INTEGER UNSIGNED DEFAULT 0 NOT NULL COMMENT 'at least 3 confirmations required',
|
||||
`Created` DATETIME NOT NULL,
|
||||
PRIMARY KEY `PK_DepositId` (`Id`),
|
||||
FOREIGN KEY `FK_Depositor` (`UserId`) REFERENCES `Users` (`Id`),
|
||||
UNIQUE KEY `Idx_UserDepositTx` (`UserId`, `TxHash`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;
|
||||
|
||||
CREATE TABLE Withdrawals
|
||||
(
|
||||
`Id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`UserId` BIGINT UNSIGNED NOT NULL,
|
||||
`TxHash` VARCHAR(70) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
|
||||
`Amount` DECIMAL(18,8) UNSIGNED NOT NULL,
|
||||
`Created` DATETIME NOT NULL,
|
||||
PRIMARY KEY `PK_DepositId` (`Id`),
|
||||
FOREIGN KEY `FK_Withdrawer` (`UserId`) REFERENCES `Users` (`Id`),
|
||||
UNIQUE KEY `Idx_WithdrawalTxHash` (`TxHash`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;
|
||||
|
||||
|
||||
DELIMITER //
|
||||
|
||||
CREATE TRIGGER `Trg_OnDepositCreated`
|
||||
AFTER INSERT ON `Deposits`
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF NEW.Confirmations >= 3 THEN
|
||||
UPDATE Users U SET U.Balance = U.Balance + NEW.Amount WHERE U.Id = NEW.UserId;
|
||||
END IF;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER `Trg_OnDepositUpdated`
|
||||
AFTER UPDATE ON `Deposits`
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF OLD.Confirmations < 3 AND NEW.Confirmations >= 3 THEN
|
||||
UPDATE Users U SET U.Balance = U.Balance + NEW.Amount WHERE U.Id = NEW.UserId;
|
||||
END IF;
|
||||
END;
|
||||
//
|
||||
|
||||
DELIMITER ;
|
4
templates/onbalance.txt
Normal file
4
templates/onbalance.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
Your balance is **{amount} LBC**.
|
||||
|
||||
----
|
||||
[^How ^to ^use]({how_to_use_url}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry
|
4
templates/ondeposit.txt
Normal file
4
templates/ondeposit.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
Send any amount of LBC you'd like to deposit to `{address}`.
|
||||
|
||||
----
|
||||
[^How ^to ^use]({how_to_use_url}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry
|
4
templates/onsendtip.insufficientfunds.txt
Normal file
4
templates/onsendtip.insufficientfunds.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
You do not have sufficient funds to send a tip to {recipient}. You tried to send **{tip} LBC** but your balance is **{amount} LBC**.
|
||||
|
||||
----
|
||||
[^How ^to ^use]({how_to_use_url}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry
|
4
templates/onsendtip.invalidamount.txt
Normal file
4
templates/onsendtip.invalidamount.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
I'm sorry, I do not understand the amount that you specified for the tip.
|
||||
|
||||
----
|
||||
[^How ^to ^use]({how_to_use_url}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry
|
4
templates/onsendtip.txt
Normal file
4
templates/onsendtip.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
{recipient}, you've received `{tip}`!
|
||||
|
||||
----
|
||||
[^How ^to ^use]({how_to_use_url}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry
|
4
templates/onwithdraw.amountltefee.txt
Normal file
4
templates/onwithdraw.amountltefee.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
You cannot withdraw an amount that is less than or equal to the transaction fee. You tried to withdraw **{amount} LBC** but the transaction fee is **{fee} LBC**.
|
||||
|
||||
----
|
||||
[^How ^to ^use]({how_to_use_url}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry
|
4
templates/onwithdraw.insufficientfunds.txt
Normal file
4
templates/onwithdraw.insufficientfunds.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
You do not have sufficient funds for your withdrawal request. You tried to withdraw **{amount} LBC** but your balance is **{balance} LBC**.
|
||||
|
||||
----
|
||||
[^How ^to ^use]({how_to_use_url}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry
|
4
templates/onwithdraw.invalidaddress.txt
Normal file
4
templates/onwithdraw.invalidaddress.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
I'm sorry, I cannot send any LBC to `{address}` because it is invalid. Also, you cannot withdraw to your deposit address.
|
||||
|
||||
----
|
||||
[^How ^to ^use]({how_to_use_url}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry
|
4
templates/onwithdraw.invalidamount.txt
Normal file
4
templates/onwithdraw.invalidamount.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
I'm sorry, I do not understand the amount that you specified for the withdrawal.
|
||||
|
||||
----
|
||||
[^How ^to ^use]({how_to_use_url}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry
|
4
templates/onwithdraw.txt
Normal file
4
templates/onwithdraw.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
You have successfully withdrawn **{amount} LBC** to `{address}`! ([tx](https://explorer.lbry.io/tx/{txid})).
|
||||
|
||||
----
|
||||
[^How ^to ^use]({how_to_use_url}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry
|
Loading…
Reference in a new issue