-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from Kava-Labs/dm-refund-bot
Automatic swap refund bot
- Loading branch information
Showing
5 changed files
with
303 additions
and
2 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 |
---|---|---|
@@ -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 | ||
}; |
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
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,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 |
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,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 |
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,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(); |