Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic swap refund bot #15

Merged
merged 12 commits into from
Jun 18, 2020
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) {
denalimarsh marked this conversation as resolved.
Show resolved Hide resolved
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)
nddeluca marked this conversation as resolved.
Show resolved Hide resolved
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'})
denalimarsh marked this conversation as resolved.
Show resolved Hide resolved
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();