Skip to content

Commit

Permalink
Merge pull request #15 from Kava-Labs/dm-refund-bot
Browse files Browse the repository at this point in the history
Automatic swap refund bot
  • Loading branch information
denalimarsh authored Jun 18, 2020
2 parents 0d1bb34 + 0096aa3 commit 5c89e4e
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 2 deletions.
6 changes: 4 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const PriceOracle = require('./oracle/oracle').PriceOracle;
const AuctionBot = require('./auction/auction').AuctionBot;
const PriceOracle = require("./oracle/oracle").PriceOracle;
const AuctionBot = require("./auction/auction").AuctionBot
const RefundBot = require("./refund/refund").RefundBot;

module.exports = {
PriceOracle,
AuctionBot,
RefundBot
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"homepage": "https://github.com/Kava-Labs/kava-tools#readme",
"dependencies": {
"@binance-chain/javascript-sdk": "^3.0.2",
"@kava-labs/javascript-sdk": "^2.0.0-beta.6",
"axios": "^0.19.2",
"coingecko-api": "^1.0.10",
Expand Down
19 changes: 19 additions & 0 deletions refund/example-env
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Cron tab for how frequently refunds will be attempted e.g. 5 minutes
CRONTAB="5 * * * *"

# Kava network details
KAVA_LCD_URL="https://kava3.data.kava.io"

KAVA_MNEMONIC="secret words that unlock your kava address"

# Binance Chain network details
BINANCE_CHAIN_DEPUTY_ADDRESS = "bnb1jh7uv2rm6339yue8k4mj9406k3509kr4wt5nxn"

BINANCE_CHAIN_LCD_URL="https://dex.binance.org"

BINANCE_CHAIN_MNEMONIC="secret words that unlock your binance chain address"

# These allow the refund bot to be started from the most recent swap batch
BINANCE_CHAIN_START_OFFSET_INCOMING=0

BINANCE_CHAIN_START_OFFSET_OUTGOING=0
249 changes: 249 additions & 0 deletions refund/refund.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
require('dotenv').config()
require('log-timestamp');
const _ = require('lodash');
const Kava = require('@kava-labs/javascript-sdk');
const BnbChain = require("@binance-chain/javascript-sdk");
const bnbCrypto = BnbChain.crypto;

/**
* Automatically refunds any refundable swaps on both Kava and Binance Chain
*/
class RefundBot {
constructor(bnbChainDeputy, limit = 1000, offsetIncoming = 0, offsetOutgoing = 0) {
if (!bnbChainDeputy) {
throw new Error("must specify the deputy's Binance Chain address");
}
this.bnbChainDeputy = bnbChainDeputy
this.limit = limit;
this.offsetIncoming = offsetIncoming;
this.offsetOutgoing = offsetOutgoing;
}

/**
* Initialize the Kava client
* @param {String} lcdURL api endpoint for Kava's rest-server
* @param {String} mnemonic Kava address mnemonic
* @return {Promise}
*/
async initKavaClient(lcdURL, mnemonic) {
if (!lcdURL) {
throw new Error("Kava's chain's rest-server url is required");
}
if (!mnemonic) {
throw new Error("Kava address mnemonic is required");
}

// Initiate and set Kava client
this.kavaClient = new Kava.KavaClient(lcdURL);
this.kavaClient.setWallet(mnemonic);
try {
await this.kavaClient.initChain();
} catch (e) {
console.log("Cannot connect to Kava's lcd server:", e)
return
}
return this;
}

/**
* Initialize the Binance Chain client
* @param {String} lcdURL api endpoint for Binance Chain's rest-server
* @param {String} mnemonic Binance Chain address mnemonic
* @param {String} network "testnet" or "mainnet"
* @return {Promise}
*/
async initBnbChainClient(lcdURL, mnemonic, network = "testnet") {
if (!lcdURL) {
throw new Error("Binance Chain's rest-server url is required");
}
if (!mnemonic) {
throw new Error("Binance Chain address mnemonic is required");
}

// Start Binance Chain client
this.bnbClient = await new BnbChain(lcdURL);
this.bnbClient.chooseNetwork(network);
const privateKey = bnbCrypto.getPrivateKeyFromMnemonic(mnemonic);
this.bnbClient.setPrivateKey(privateKey);
try {
await this.bnbClient.initChain();
} catch (e) {
console.log("Cannot connect to Binance Chain's lcd server:", e)
return
}

// Load our Binance Chain address (required for refunds)
const bnbAddrPrefix = network == "mainnet" ? "bnb" : "tbnb"
this.bnbChainAddress = bnbCrypto.getAddressFromPrivateKey(privateKey, bnbAddrPrefix);

return this;
}

/**
* Manages swap refunds
*/
async refundSwaps() {
await this.refundKavaSwaps()
await this.refundBinanceChainSwaps()
}

/**
* Refund any expired swaps on Kava
*/
async refundKavaSwaps() {
const swapIDs = await this.getRefundableKavaSwaps();
console.log(`Kava refundable swap count: ${swapIDs.length}`)

// Fetch account data so we can manually manage sequence when posting
let accountData
try {
accountData = await Kava.tx.loadMetaData(this.kavaClient.wallet.address, this.kavaClient.baseURI)
} catch(e) {
console.log(e)
return
}

// Refund each swap
for(var i = 0; i < swapIDs.length; i++) {
const sequence = String(Number(accountData.sequence) + i)
try {
console.log(`\tRefunding swap ${swapIDs[i]}`)
const txHash = await this.kavaClient.refundSwap(swapIDs[i], sequence)
console.log("\tTx hash:", txHash)
} catch (e) {
console.log(`\tCould not refund swap ${swapIDs[i]}`)
console.log(e)
}
await sleep(7000); // Wait for the block to be confirmed
}
}

/**
* Gets the swap IDs of all incoming and outgoing expired swaps on Kava
*/
async getRefundableKavaSwaps() {
let expiredSwaps = [];
let checkNextBatch = true;
let page = 1; // After refunding swaps paginated query results will always start from page 1

while(checkNextBatch) {
let swapBatch;
const args = {status: 'Expired', page: page, limit: this.limit};
try {
swapBatch = await this.kavaClient.getSwaps(5000, args);
} catch (e) {
console.log(`couldn't query swaps on Kava...`);
return
}
// If swaps in batch, save them and increment page count
if(swapBatch.length > 0) {
expiredSwaps = expiredSwaps.concat(swapBatch);
page++;
// If no swaps in batch, don't check the next batch
} else {
checkNextBatch = false
}
}

// Calculate each swap's ID as it's not stored in the struct (it's on the interface)
let swapIDs = []
for(const expiredSwap of expiredSwaps) {
const swapID = Kava.utils.calculateSwapID(
expiredSwap.random_number_hash,
expiredSwap.sender,
expiredSwap.sender_other_chain,
)
swapIDs.push(swapID)
}
return swapIDs
}

/**
* Refund any expired swaps on Binance Chain
*/
async refundBinanceChainSwaps() {
const incomingSwaps = await this.getRefundableBinanceSwaps(true)
const outgoingSwaps = await this.getRefundableBinanceSwaps(false)
const swapIDs = incomingSwaps.concat(outgoingSwaps)

console.log(`Binance Chain refundable swap count: ${swapIDs.length}`)

// Refund each swap
for(const swapID of swapIDs) {
console.log(`\tRefunding swap ${swapID}`)
try {
const res = await this.bnbClient.swap.refundHTLT(this.bnbChainAddress, swapID);
if (res && res.status == 200) {
console.log(`\tTx hash: ${res.result[0].hash}`);
}
}
catch(e) {
console.log(`\t${e}`)
}
await sleep(3000); // Wait for the block to be confirmed
}
}

/**
* Gets the swap IDs of all incoming and outgoing open swaps on Binance Chain
* @param {Boolean} incoming swap direction, defaults to incoming
*/
async getRefundableBinanceSwaps(incoming = true) {
let openSwaps = []
let checkNextBatch = true
let offsetIncoming = this.offsetIncoming
let offsetOutgoing = this.offsetOutgoing

while(checkNextBatch) {
let swapBatch
try {
let res
if(incoming) {
res = await this.bnbClient.getSwapByCreator(this.bnbChainDeputy, this.limit, offsetIncoming);
} else {
res = await this.bnbClient.getSwapByRecipient(this.bnbChainDeputy, this.limit, offsetOutgoing);
}
swapBatch = _.get(res, 'result.atomicSwaps');

} catch (e) {
console.log(`couldn't query ${incoming ? "incoming" : "outgoing"} swaps on Binance Chain...`)
return
}

// If swaps in batch, filter for expired swaps
if(swapBatch.length > 0) {
const refundableSwapsInBatch = swapBatch.filter(swap => swap.status == 1) // Status 1 is open
openSwaps = openSwaps.concat(refundableSwapsInBatch)

// If it's a full batch, increment offset by limit for next iteration
if(swapBatch.length <= this.limit) {
if(incoming) {
offsetIncoming = offsetIncoming + this.limit
} else {
offsetOutgoing = offsetOutgoing + this.limit
}
}
// If no swaps in batch, don't check the next batch
} else {
checkNextBatch = false
}
}
return openSwaps.map(swap => swap.swapId)
}

/**
* Print the current Binance Chain offsets to console
*/
printOffsets() {
console.log("\nCurrent Binance Chain offsets:")
console.log(`Offset incoming: ${this.offsetIncoming}`)
console.log(`Offset outgoing: ${this.offsetOutgoing}\n`)
}
}

// Sleep is a wait function
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

module.exports.RefundBot = RefundBot
30 changes: 30 additions & 0 deletions scripts/refund.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require('dotenv').config({path:'../refund/.env'})
const RefundBot = require("..").RefundBot
const cron = require('node-cron');

var main = async () => {
// Load environment variables
const cronTimer = process.env.CRONTAB;
const kavaLcdURL = process.env.KAVA_LCD_URL;
const kavaMnemonic = process.env.KAVA_MNEMONIC;
const bnbChainLcdURL = process.env.BINANCE_CHAIN_LCD_URL;
const bnbChainMnemonic = process.env.BINANCE_CHAIN_MNEMONIC;
const bnbChainDeputy = process.env.BINANCE_CHAIN_DEPUTY_ADDRESS;

// Initiate refund bot
refundBot = new RefundBot(bnbChainDeputy);
await refundBot.initKavaClient(kavaLcdURL, kavaMnemonic);
await refundBot.initBnbChainClient(bnbChainLcdURL, bnbChainMnemonic, "mainnet");

// Start cron job
cron.schedule(cronTimer, () => {
refundBot.refundSwaps()
});

// Print Binance Chain offsets hourly for debugging and future optimization.
cron.schedule("* 1 * * *", () => {
refundBot.printOffsets()
});
}

main();

0 comments on commit 5c89e4e

Please sign in to comment.