Skip to content

Instantly share code, notes, and snippets.

@zackster
Last active June 8, 2019 23:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zackster/f4775eb7a280bfa2642ed8447f9cb503 to your computer and use it in GitHub Desktop.
Save zackster/f4775eb7a280bfa2642ed8447f9cb503 to your computer and use it in GitHub Desktop.
// tsc --lib es2015 index.ts
import "source-map-support/register";
const _ = await import("lodash");
const axios = await import("axios");
const Bot = await import("./node_modules/keybase-bot");
const bot = new Bot("/Users/zackburt");
const bot2 = new Bot("/Users/zackburt");
const botUsername = "beemo";
const paperkey = "something";
const paperkey2 = "special";
const isNumber = (value) => !Number.isNaN(parseFloat(value));
import { ChatChannel, MessageSummary, Transaction } from "keybase-bot";
interface IParticipant {
username: string;
transaction: Transaction;
}
interface ISnipe {
wager: number;
participants: T<IParticipant>;
}
function processRefund(txn: Transaction, channel: ChatChannel): void {
console.log("refunding txn", txn);
const txnDetailsApi = `https://horizon.stellar.org/transactions/${txn.txId}`;
axios.get(txnDetailsApi).then((response) => {
// API returns a response, number of stroops
const transactionFees = parseFloat(response.data.fee_paid) * 0.0000001;
console.log("refunding txn fees", transactionFees);
const refund = _.round(txn.amount - transactionFees, 7);
console.log("total refund is", refund);
bot.wallet.send(txn.fromUsername, refund.toString()).then((refundTxn) => {
let refundMsg = `\`\`\`+${refund}XLM@${txn.fromUsername}\`\`\` `;
refundMsg += ` :arrow_right: `;
refundMsg += `\`https://stellar.expert/explorer/public/tx/${refundTxn.txId}\``;
bot.chat.send(channel, {
body: refundMsg,
});
}).catch((err) => {
console.log(err);
});
});
}
function extractTxn(msg: MessageSummary): void {
const txnId = msg.content.text.payments[0].result.sent;
bot.wallet.details(txnId).then((details) => processTxnDetails(details, msg.channel));
}
function sendAmountToWinner(winnerUsername: string, wager: number, channel: ChatChannel): void {
let txnDetailsApi;
let transactionFees;
let bounty;
const snipe = activeSnipes[JSON.stringify(channel)];
Promise.all(snipe.participants.map((participant) => {
txnDetailsApi = `https://horizon.stellar.org/transactions/${participant.transaction.txId}`;
return axios.get(txnDetailsApi);
})).then((apiResponses) => {
transactionFees = 0;
bounty = 0;
apiResponses.forEach((apiResponse) => {
transactionFees += (parseFloat(apiResponse.data.fee_paid) * 0.0000001);
bounty += snipe.wager;
});
bounty = _.round(bounty - transactionFees, 7);
bot.wallet.send(winnerUsername, bounty.toString()).then((txn) => {
let bountyMsg = `\`\`\`+${bounty}XLM@${winnerUsername}\`\`\` `;
bountyMsg += `:arrow_right: `;
bountyMsg += `\`https://stellar.expert/explorer/public/tx/${txn.txId}\``,
bot.chat.send(channel, {
body: bountyMsg,
});
});
});
}
function resolveFlip(channel: ChatChannel, results: T<string>): void {
const winnerUsername = results[0];
const snipe = activeSnipes[JSON.stringify(channel)];
sendAmountToWinner(winnerUsername, snipe.wager, channel);
bot.chat.send(JSON.parse(snipe.channel), {
body: `Congrats to @${winnerUsername}`,
});
}
function flip(channel: ChatChannel): void {
const flipParticipants = activeSnipes[JSON.stringify(channel)].participants.map((el) => {
return el.username;
}).join(", ");
bot2.chat.send(channel, {
body: `/flip ${flipParticipants}`,
});
}
function processTxnDetails(txn: Transaction, channel: ChatChannel): void {
const snipe = activeSnipes[JSON.stringify(channel)];
if (typeof(snipe) === "undefined") {
return;
}
const isNative = txn.asset.type === "native";
if (!isNative) {
return;
}
if (txn.toUsername !== botUsername) {
return;
}
if (snipe.betting_open === false) {
processRefund(txn, channel);
} else {
activeSnipes[JSON.stringify(channel)].participants.push({
transaction: txn,
username: txn.fromUsername,
});
bot.chat.send(channel, {
body: `@${txn.fromUsername} is locked into the snipe!`,
});
}
}
const activeSnipes = {};
function launchSnipe(wager: number, channel: ChatChannel): void {
// Tell the channel: OK, your snipe has been accepted for routing.
let message = "The snipe is on. ";
message += `Anybody is free to send me _exactly_ ${wager}XLM within 30 seconds: `;
message += `\`\`\`+${wager}XLM@beemo\`\`\`.`;
message += ` If there are not at >= 2 confirmed participants, the snipe is going `;
message += `to be cancelled with deposits refunded, less transaction fess.`;
bot.chat.send(channel, { body: message });
bot.chat.send(channel, {
body: "Betting stops in 30 seconds",
}).then((sentMessage) => {
runClock(channel, sentMessage.id, 30);
});
setTimeout(() => {
finalizeBets(channel);
}, 30 * 1000);
activeSnipes[JSON.stringify(channel)] = {
betting_open: true,
participants: [],
wager: {},
};
}
function finalizeBets(channel: ChatChannel): void {
bot.chat.send(channel, {
body: "No more bets!",
});
activeSnipes[JSON.stringify(channel)].betting_open = false;
// Give 5 seconds to finalize transactions + 1 extra.
setTimeout(() => {
executeFlipOrCancel(channel);
}, 6 * 1000);
}
function executeFlipOrCancel(channel: ChatChannel): void {
const snipe = activeSnipes[JSON.stringify(channel)];
if (typeof(snipe) !== "undefined") {
if (snipe.participants.length > 1) {
flip(channel);
} else if (snipe.participants.length === 1) {
processRefund(snipe.participants[0].transaction, channel);
bot.chat.send(channel, {
body: "The snipe has been cancelled due to a lack of participants.",
});
activeSnipes[JSON.stringify(channel)] = undefined;
} else {
bot.chat.send(channel, {
body: "The snipe has been cancelled due to a lack of participants.",
});
activeSnipes[JSON.stringify(channel)] = undefined;
}
}
}
function checkForSnipe(msg: MessageSummary): void {
if (msg.channel.public || msg.channel.membersType !== "team" || msg.channel.topicType !== "chat") {
// Beemo only listens to public conversations.
return;
}
if (typeof activeSnipes[JSON.stringify(msg.channel)] !== "undefined") {
bot.chat.send(msg.channel, {
body: "Please! Just one active snipe per channel at any given moment",
});
return;
}
const msgText = msg.content.text.body;
const matchResults = msgText.match(/^\/cryptosnipe \+([0-9]+(?:[\.][0-9]*)?|\.[0-9]+)XLM@beemo/);
if (matchResults === null) {
bot.chat.send(msg.channel, {
body: "Format is: \`\`\`/cryptosnipe +0.005XLM@beemo\`\`\`",
});
return;
}
const wager = parseFloat(matchResults[1]);
if (!isNumber(wager)) {
bot.chat.send(msg.channel, {
body: "Wager must be in decimal format",
});
return;
}
if (wager <= 0) {
bot.chat.send(msg.channel, {
body: "Wager must be a positive amount",
});
return;
}
if (wager > 0.01) {
// throw error, amount must be less than threshold
bot.chat.send(msg.channel, {
body: "Beemo is prototype stage software. Please do not wager more than 0.01XLM",
});
return;
}
launchSnipe(wager, msg.channel);
}
function cancelFlip(conversationId: string, channel: ChatChannel, err): void {
clearInterval(flipMonitorIntervals[conversationId]);
bot.chat.send(channel, {
body: `The flip has been cancelled due to error,
\`${err}\`,
and everyone is getting a refund`,
});
activeSnipes[JSON.stringify(channel)].participants.forEach((participant) => {
processRefund(participant.transaction, channel);
});
activeSnipes[JSON.stringify(channel)] = undefined;
}
// Something to consider paging to disk or network
const flipMonitorIntervals = {};
function monitorFlipResults(msg: MessageSummary): void {
flipMonitorIntervals[msg.conversationId] = setInterval((() => {
try {
bot.chat.loadFlip(
msg.conversationId,
msg.content.flip.flipConvId,
msg.id,
msg.content.flip.gameId,
).then((flipDetails) => {
if (flipDetails.phase === 2) {
resolveFlip(msg.channel, flipDetails.resultInfo.shuffle);
clearInterval(flipMonitorIntervals[msg.conversationId]);
activeSnipes[JSON.stringify(msg.channel)] = undefined;
}
});
} catch (err) {
cancelFlip(msg.conversationId, msg.channel, err);
}
}), 1000);
}
const allClocks = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].reverse();
const runningClocks = {};
function runClock(channel, messageId, seconds) {
bot.chat.edit(channel, messageId, {
message: {
body: ":clock" + allClocks[seconds % 12].toString() + ":" + ` betting stops in ${seconds}s`,
},
});
if (seconds > 1) {
setTimeout(() => {
runClock(channel, messageId, seconds - 1);
}, 1000);
} else {
setTimeout(() => {
bot.chat.edit(channel, messageId, {
message: {
body: "~:clock" + allClocks[seconds % 12].toString() + ":" + ` betting stops in 1s~ no longer accepting bets`,
},
});
}, 1000);
}
}
async function main() {
try {
await bot.init(botUsername, paperkey);
const info = bot.myInfo();
console.log(`Bot initialized with username ${info.username}.`);
await bot2.init(botUsername, paperkey2);
console.log("Second key initialized");
console.log("Listening for all messages...");
const channel = {
membersType: "team", name: "mkbot", public: false, topicName: "test3", topicType: "chat",
};
const message = {
body: "beemo has been restarted ... but is still in development mode. please do not @ me. Now in TypeScript!",
};
bot.chat.send(channel, message);
await bot.chat.watchAllChannelsForNewMessages(
async (msg) => {
try {
if (msg.content.type === "flip" && msg.sender.username === botUsername) {
monitorFlipResults(msg);
return;
}
if (msg.content.type === "text" && msg.content.text.payments && msg.content.text.payments.length === 1) {
extractTxn(msg);
}
if (msg.content.text && /^\/cryptosnipe/.test(msg.content.text.body)) {
checkForSnipe(msg);
}
} catch (err) {
console.error(err);
}
},
(e) => console.error(e),
);
} catch (error) {
console.error(error);
}
}
async function shutDown() {
await bot.deinit();
await bot2.deinit();
process.exit();
}
process.on("SIGINT", shutDown);
process.on("SIGTERM", shutDown);
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment