From 032075e749fa7dbb7382e8e5c16aea01dd30d5d7 Mon Sep 17 00:00:00 2001 From: denalimarsh Date: Mon, 15 Jun 2020 12:52:41 +0200 Subject: [PATCH 01/10] implement RefundBot --- refund/refund.js | 234 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 refund/refund.js diff --git a/refund/refund.js b/refund/refund.js new file mode 100644 index 0000000..b8dab4b --- /dev/null +++ b/refund/refund.js @@ -0,0 +1,234 @@ +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("Error: cannot connect to Kava's lcd server") + 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("Error: cannot connect to Binance Chain's lcd server") + 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 + var accountData = await Kava.tx.loadMetaData(this.kavaClient.wallet.address, this.kavaClient.baseURI) + + // Refund each swap + for(let 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) + } + } + } + + /** + * 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(let i = 0; i < expiredSwaps.length; i++) { + const swapID = Kava.utils.calculateSwapID( + expiredSwaps[i].random_number_hash, + expiredSwaps[i].sender, + expiredSwaps[i].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(var i = 0; i < swapIDs.length; i++) { + console.log(`\tRefunding swap ${swapIDs[i]}`) + const res = await this.bnbClient.swap.refundHTLT(this.bnbChainAddress, swapIDs[i]); + if (res && res.status == 200) { + console.log(`\tTx hash: ${res.result[0].hash}`); + } else { + console.log(`\tCould not refund swap ${res}`) + } + // TODO: Open PR to binance-chain/javascript-sdk to support custom sequences, then can remove sleep() + await sleep(3000); + } + } + + /** + * 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 + + while(checkNextBatch) { + let swapBatch + try { + let res + if(incoming) { + res = await this.bnbClient.getSwapByCreator(this.bnbChainDeputy, this.limit, this.offsetIncoming); + } else { + res = await this.bnbClient.getSwapByRecipient(this.bnbChainDeputy, this.limit, this.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) + // Increment offset by limit for next iteration + if(incoming) { + this.offsetIncoming = this.offsetIncoming + this.limit + } else { + this.offsetOutgoing = this.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 \ No newline at end of file From ccd056c1c66209e7f4b0b4d1f6bd84322bd62020 Mon Sep 17 00:00:00 2001 From: denalimarsh Date: Mon, 15 Jun 2020 12:53:45 +0200 Subject: [PATCH 02/10] add RefundBot to index --- index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 3b76c76..a3c4af1 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,9 @@ const PriceOracle = require("./oracle/oracle").PriceOracle; const AuctionBot = require("./auction/auction").AuctionBot +const RefundBot = require("./refund/refund").RefundBot; module.exports = { PriceOracle, - AuctionBot + AuctionBot, + RefundBot }; \ No newline at end of file From ef4ec9328cf291226596676a5b3b40ef0fa87485 Mon Sep 17 00:00:00 2001 From: denalimarsh Date: Mon, 15 Jun 2020 12:54:01 +0200 Subject: [PATCH 03/10] implement script --- package.json | 3 ++- scripts/refund.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 scripts/refund.js diff --git a/package.json b/package.json index 07d1c33..d70e8c3 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ }, "homepage": "https://github.com/Kava-Labs/kava-tools#readme", "dependencies": { - "@kava-labs/javascript-sdk": "^2.0.0-beta.1", + "@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", "dotenv": "^8.2.0", diff --git a/scripts/refund.js b/scripts/refund.js new file mode 100644 index 0000000..7549b31 --- /dev/null +++ b/scripts/refund.js @@ -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); + + // Start cron job + cron.schedule(cronTimer, () => { + refundBot.refundSwaps() + }); + + // Print Binance Chain offsets hourly for debugging and future optimization. + cron.schedule("* 1 * * *", () => { + refundBot.printOffsets() + }); +} + +main(); \ No newline at end of file From c9cd5f74266b0e8f256e10fef81c0c0f5ba34163 Mon Sep 17 00:00:00 2001 From: denalimarsh Date: Mon, 15 Jun 2020 12:54:17 +0200 Subject: [PATCH 04/10] add example .env --- refund/example-env | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 refund/example-env diff --git a/refund/example-env b/refund/example-env new file mode 100644 index 0000000..97ad57d --- /dev/null +++ b/refund/example-env @@ -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 = "tbnb10uypsspvl6jlxcx5xse02pag39l8xpe7a3468h" + +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 From e570d94ef9e0606ab9e809091f243bda9810fe1b Mon Sep 17 00:00:00 2001 From: denalimarsh Date: Mon, 15 Jun 2020 12:56:16 +0200 Subject: [PATCH 05/10] update binance chain deputy address to mainet --- refund/example-env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refund/example-env b/refund/example-env index 97ad57d..57b2927 100644 --- a/refund/example-env +++ b/refund/example-env @@ -7,7 +7,7 @@ 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 = "tbnb10uypsspvl6jlxcx5xse02pag39l8xpe7a3468h" +BINANCE_CHAIN_DEPUTY_ADDRESS = "bnb1jh7uv2rm6339yue8k4mj9406k3509kr4wt5nxn" BINANCE_CHAIN_LCD_URL="https://dex.binance.org" From 63e52b937844c12075c0314355841b48bbeba938 Mon Sep 17 00:00:00 2001 From: denalimarsh Date: Mon, 15 Jun 2020 14:10:33 +0200 Subject: [PATCH 06/10] fix potential binance chain offset bug --- refund/refund.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/refund/refund.js b/refund/refund.js index b8dab4b..fa71b00 100644 --- a/refund/refund.js +++ b/refund/refund.js @@ -202,11 +202,14 @@ class RefundBot { if(swapBatch.length > 0) { const refundableSwapsInBatch = swapBatch.filter(swap => swap.status == 1) // Status 1 is open openSwaps = openSwaps.concat(refundableSwapsInBatch) - // Increment offset by limit for next iteration - if(incoming) { - this.offsetIncoming = this.offsetIncoming + this.limit - } else { - this.offsetOutgoing = this.offsetOutgoing + this.limit + + // If it's a full batch, increment offset by limit for next iteration + if(swapBatch.length == this.limit) { + if(incoming) { + this.offsetIncoming = this.offsetIncoming + this.limit + } else { + this.offsetOutgoing = this.offsetOutgoing + this.limit + } } // If no swaps in batch, don't check the next batch } else { From 40d12b9733f17dddc017be554990cdef9aed9ea7 Mon Sep 17 00:00:00 2001 From: denalimarsh Date: Tue, 16 Jun 2020 13:47:00 +0200 Subject: [PATCH 07/10] Binance Chain graceful error handling --- refund/refund.js | 17 +++++++++++------ scripts/refund.js | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/refund/refund.js b/refund/refund.js index fa71b00..9403ad7 100644 --- a/refund/refund.js +++ b/refund/refund.js @@ -83,7 +83,7 @@ class RefundBot { * Manages swap refunds */ async refundSwaps() { - await this.refundKavaSwaps() + // await this.refundKavaSwaps() await this.refundBinanceChainSwaps() } @@ -158,17 +158,22 @@ class RefundBot { 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(var i = 0; i < swapIDs.length; i++) { console.log(`\tRefunding swap ${swapIDs[i]}`) - const res = await this.bnbClient.swap.refundHTLT(this.bnbChainAddress, swapIDs[i]); - if (res && res.status == 200) { - console.log(`\tTx hash: ${res.result[0].hash}`); - } else { - console.log(`\tCould not refund swap ${res}`) + try { + const res = await this.bnbClient.swap.refundHTLT(this.bnbChainAddress, swapIDs[i]); + if (res && res.status == 200) { + console.log(`\tTx hash: ${res.result[0].hash}`); + } + } + catch(e) { + console.log(`\t${e}`) } + // TODO: Open PR to binance-chain/javascript-sdk to support custom sequences, then can remove sleep() await sleep(3000); } diff --git a/scripts/refund.js b/scripts/refund.js index 7549b31..dc472e6 100644 --- a/scripts/refund.js +++ b/scripts/refund.js @@ -14,7 +14,7 @@ var main = async () => { // Initiate refund bot refundBot = new RefundBot(bnbChainDeputy); await refundBot.initKavaClient(kavaLcdURL, kavaMnemonic); - await refundBot.initBnbChainClient(bnbChainLcdURL, bnbChainMnemonic); + await refundBot.initBnbChainClient(bnbChainLcdURL, bnbChainMnemonic, "mainnet"); // Start cron job cron.schedule(cronTimer, () => { From c6a54ebe42d9b29ea97106eb88794d08d9d96c2f Mon Sep 17 00:00:00 2001 From: denalimarsh Date: Tue, 16 Jun 2020 14:27:57 +0200 Subject: [PATCH 08/10] uncomment refundKavaSwaps() --- refund/refund.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refund/refund.js b/refund/refund.js index 9403ad7..63a9caa 100644 --- a/refund/refund.js +++ b/refund/refund.js @@ -83,7 +83,7 @@ class RefundBot { * Manages swap refunds */ async refundSwaps() { - // await this.refundKavaSwaps() + await this.refundKavaSwaps() await this.refundBinanceChainSwaps() } From f4b5bea0b413c7fee760ea63d19216085dd0d4af Mon Sep 17 00:00:00 2001 From: denalimarsh Date: Tue, 16 Jun 2020 17:38:03 +0200 Subject: [PATCH 09/10] go live edits --- refund/refund.js | 21 ++++++++++++++------- scripts/refund.js | 0 2 files changed, 14 insertions(+), 7 deletions(-) mode change 100644 => 100755 scripts/refund.js diff --git a/refund/refund.js b/refund/refund.js index 63a9caa..fdfebff 100644 --- a/refund/refund.js +++ b/refund/refund.js @@ -95,7 +95,12 @@ class RefundBot { console.log(`Kava refundable swap count: ${swapIDs.length}`) // Fetch account data so we can manually manage sequence when posting - var accountData = await Kava.tx.loadMetaData(this.kavaClient.wallet.address, this.kavaClient.baseURI) + let accountData + try { + accountData = await Kava.tx.loadMetaData(this.kavaClient.wallet.address, this.kavaClient.baseURI) + } catch(e) { + console.log(e) + } // Refund each swap for(let i = 0; i < swapIDs.length; i++) { @@ -158,7 +163,7 @@ class RefundBot { 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 @@ -186,15 +191,17 @@ class RefundBot { 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, this.offsetIncoming); + res = await this.bnbClient.getSwapByCreator(this.bnbChainDeputy, this.limit, offsetIncoming); } else { - res = await this.bnbClient.getSwapByRecipient(this.bnbChainDeputy, this.limit, this.offsetOutgoing); + res = await this.bnbClient.getSwapByRecipient(this.bnbChainDeputy, this.limit, offsetOutgoing); } swapBatch = _.get(res, 'result.atomicSwaps'); @@ -209,11 +216,11 @@ class RefundBot { openSwaps = openSwaps.concat(refundableSwapsInBatch) // If it's a full batch, increment offset by limit for next iteration - if(swapBatch.length == this.limit) { + if(swapBatch.length <= this.limit) { if(incoming) { - this.offsetIncoming = this.offsetIncoming + this.limit + offsetIncoming = offsetIncoming + this.limit } else { - this.offsetOutgoing = this.offsetOutgoing + this.limit + offsetOutgoing = offsetOutgoing + this.limit } } // If no swaps in batch, don't check the next batch diff --git a/scripts/refund.js b/scripts/refund.js old mode 100644 new mode 100755 From 2c9734ae69925fad2bbc85f791a46506316abb9f Mon Sep 17 00:00:00 2001 From: denalimarsh Date: Thu, 18 Jun 2020 14:22:56 +0200 Subject: [PATCH 10/10] revisions --- refund/refund.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/refund/refund.js b/refund/refund.js index fdfebff..80a36aa 100644 --- a/refund/refund.js +++ b/refund/refund.js @@ -39,7 +39,7 @@ class RefundBot { try { await this.kavaClient.initChain(); } catch (e) { - console.log("Error: cannot connect to Kava's lcd server") + console.log("Cannot connect to Kava's lcd server:", e) return } return this; @@ -68,7 +68,7 @@ class RefundBot { try { await this.bnbClient.initChain(); } catch (e) { - console.log("Error: cannot connect to Binance Chain's lcd server") + console.log("Cannot connect to Binance Chain's lcd server:", e) return } @@ -100,10 +100,11 @@ class RefundBot { accountData = await Kava.tx.loadMetaData(this.kavaClient.wallet.address, this.kavaClient.baseURI) } catch(e) { console.log(e) + return } // Refund each swap - for(let i = 0; i < swapIDs.length; i++) { + for(var i = 0; i < swapIDs.length; i++) { const sequence = String(Number(accountData.sequence) + i) try { console.log(`\tRefunding swap ${swapIDs[i]}`) @@ -113,6 +114,7 @@ class RefundBot { console.log(`\tCould not refund swap ${swapIDs[i]}`) console.log(e) } + await sleep(7000); // Wait for the block to be confirmed } } @@ -145,11 +147,11 @@ class RefundBot { // Calculate each swap's ID as it's not stored in the struct (it's on the interface) let swapIDs = [] - for(let i = 0; i < expiredSwaps.length; i++) { + for(const expiredSwap of expiredSwaps) { const swapID = Kava.utils.calculateSwapID( - expiredSwaps[i].random_number_hash, - expiredSwaps[i].sender, - expiredSwaps[i].sender_other_chain, + expiredSwap.random_number_hash, + expiredSwap.sender, + expiredSwap.sender_other_chain, ) swapIDs.push(swapID) } @@ -167,10 +169,10 @@ class RefundBot { console.log(`Binance Chain refundable swap count: ${swapIDs.length}`) // Refund each swap - for(var i = 0; i < swapIDs.length; i++) { - console.log(`\tRefunding swap ${swapIDs[i]}`) + for(const swapID of swapIDs) { + console.log(`\tRefunding swap ${swapID}`) try { - const res = await this.bnbClient.swap.refundHTLT(this.bnbChainAddress, swapIDs[i]); + const res = await this.bnbClient.swap.refundHTLT(this.bnbChainAddress, swapID); if (res && res.status == 200) { console.log(`\tTx hash: ${res.result[0].hash}`); } @@ -178,9 +180,7 @@ class RefundBot { catch(e) { console.log(`\t${e}`) } - - // TODO: Open PR to binance-chain/javascript-sdk to support custom sequences, then can remove sleep() - await sleep(3000); + await sleep(3000); // Wait for the block to be confirmed } }