├── .gitignore ├── LICENSE ├── README.md ├── bluna-bot-cli ├── .npmignore ├── README.md ├── package.json ├── src │ └── index.ts └── tsconfig.json ├── bluna-bot-pkg ├── .npmignore ├── README.md ├── package.json ├── src │ ├── api │ │ └── index.ts │ ├── index.ts │ ├── logic │ │ └── index.ts │ ├── types │ │ ├── api.ts │ │ ├── config.ts │ │ └── index.ts │ └── utils │ │ ├── constants.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ └── terra.ts └── tsconfig.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | yarn.lock 4 | *.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Aaron Choo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bLUNA Bot 2 | 3 | Automates swapping between LUNA and bLUNA in the Terra blockchain via [Terraswap](https://app.terraswap.io/#Swap). 4 | 5 | ## Usage 6 | 7 | This repository currently contains two packages: 8 | 9 | - [`bluna-bot-pkg`](https://github.com/AaronCQL/bluna-bot/tree/main/bluna-bot-pkg): a helper library which contains the main logic and API calls needed 10 | - [`bluna-bot-cli`](https://github.com/AaronCQL/bluna-bot/tree/main/bluna-bot-cli): a node CLI wrapper for the `bluna-bot-pkg` package 11 | 12 | If you just want to start using this, refer to [`bluna-bot-cli`](https://github.com/AaronCQL/bluna-bot/tree/main/bluna-bot-cli). 13 | 14 | If you are a developer and want more freedom, you may refer to [`bluna-bot-pkg`](https://github.com/AaronCQL/bluna-bot/tree/main/bluna-bot-pkg) to create your own custom runner. 15 | -------------------------------------------------------------------------------- /bluna-bot-cli/.npmignore: -------------------------------------------------------------------------------- 1 | !dist -------------------------------------------------------------------------------- /bluna-bot-cli/README.md: -------------------------------------------------------------------------------- 1 | 2 | # `bluna-bot-cli` 3 | 4 | > Node CLI wrapper for the [`bluna-bot-pkg`](https://github.com/AaronCQL/bluna-bot/tree/main/bluna-bot-pkg) package. 5 | 6 | Automates swapping between LUNA and bLUNA. 7 | 8 | - [Installation](#installation) 9 | - [Usage](#usage) 10 | - [Configurations](#configurations) 11 | - [Example](#example) 12 | 13 | ## Installation 14 | 15 | Make sure you have [node](https://nodejs.org/en/) installed first. Then, run: 16 | 17 | ```sh 18 | # for npm users: 19 | npm i -g bluna-bot-cli 20 | 21 | # for yarn users: 22 | yarn global add bluna-bot-cli 23 | ``` 24 | 25 | ## Usage 26 | 27 | After installing, use **`bluna-bot`** in your CLI to access the program. 28 | 29 | Display help messages: 30 | 31 | ```sh 32 | bluna-bot -h 33 | ``` 34 | 35 | Display the current version: 36 | 37 | ```sh 38 | bluna-bot -V 39 | ``` 40 | 41 | Use the `--verbose` option to print out debug information for every run (not recommended): 42 | 43 | ```sh 44 | bluna-bot [options] --verbose 45 | ``` 46 | 47 | ### Configurations 48 | 49 | Other than the wallet address and wallet mnemonic, all other fields are optional and comes with a default value (use `bluna-bot -h` to check). 50 | 51 | - **`-a `**: Terra wallet address 52 | - **`-m `**: Terra wallet mnemonic key 53 | - `--interval `: delay in seconds before running the next round 54 | - `--min-gain `: minimum percentage gain when swapping LUNA for bLUNA 55 | - eg. if `` is 13, the swap will only commence if swapping the current amount of LUNA will net a 13% increase in the corresponding bLUNA amount 56 | - `--max-loss `: maximum percentage loss when swapping bLUNA for LUNA 57 | - eg. if `` is 1.5, the swap will only commence if swapping the current amount of bLUNA will net a 1.5% decrease in the corresponding LUNA amount 58 | - `--min-luna-swap-amount `: minimum number of LUNA to use when swapping 59 | - `--max-luna-swap-amount `: maximum number of LUNA to use when swapping 60 | - `--min-bluna-swap-amount `: minimum number of bLUNA to use when swapping 61 | - `--max-bluna-swap-amount `: maximum number of bLUNA to use when swapping 62 | - `--slippage `: percentage slippage to use when swapping 63 | - Note: the `min-gain` and `max-loss` settings *do not* take into account the `slippage` set 64 | - Terraswap's default slippage is `1`, the default slippage used in this program is `0.5` 65 | - `--stop-on-errors`: when present, stops execution of the program when met with any unknown errors 66 | - Be default, unknown errors are ignored since queries to the blockchain may occasionally throw network errors 67 | - `--verbose`: when present, prints out debug information for every run 68 | - Not recommended unless developing or debugging 69 | 70 | ### Example 71 | 72 | ```sh 73 | bluna-bot -a "terraAddressHere" \ 74 | -m "terra wallet mnemonic here" \ 75 | --interval 4 \ 76 | --min-gain 13 \ 77 | --max-loss 1.5 \ 78 | --min-swap-amount 100 \ 79 | --slippage 0.1 \ 80 | --verbose 81 | ``` 82 | -------------------------------------------------------------------------------- /bluna-bot-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bluna-bot-cli", 3 | "version": "0.2.0", 4 | "main": "dist/index.js", 5 | "author": "Aaron Choo", 6 | "license": "MIT", 7 | "private": false, 8 | "bin": { 9 | "bluna-bot": "dist/index.js" 10 | }, 11 | "files": [ 12 | "dist" 13 | ], 14 | "scripts": { 15 | "start": "node ./dist", 16 | "dev": "yarn clean && tsc --watch", 17 | "build": "yarn clean && tsc", 18 | "clean": "rimraf dist", 19 | "prepublish": "yarn build" 20 | }, 21 | "dependencies": { 22 | "bluna-bot-pkg": "0.2.0", 23 | "chalk": "^4.1.1", 24 | "clear": "^0.1.0", 25 | "commander": "^8.0.0", 26 | "figlet": "^1.5.0" 27 | }, 28 | "devDependencies": { 29 | "@types/clear": "^0.1.1", 30 | "@types/figlet": "^1.5.1", 31 | "@types/node": "^15.12.4", 32 | "rimraf": "^3.0.2", 33 | "typescript": "^4.3.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /bluna-bot-cli/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from "commander"; 4 | import chalk from "chalk"; 5 | import clear from "clear"; 6 | import figlet from "figlet"; 7 | import { 8 | DEFAULT_INTERVAL, 9 | DEFAULT_MIN_PERCENTAGE_GAIN, 10 | DEFAULT_MAX_PERCENTAGE_LOSS, 11 | DEFAULT_MIN_SWAP_AMOUNT, 12 | DEFAULT_MAX_SWAP_AMOUNT, 13 | DEFAULT_SLIPPAGE_PERCENTAGE, 14 | Config, 15 | run, 16 | } from "bluna-bot-pkg"; 17 | 18 | const VERSION: string = require("../package.json").version; 19 | 20 | function greet() { 21 | const banner = figlet 22 | .textSync("bLUNA Bot", { 23 | font: "Big Money-ne", 24 | }) 25 | .trim(); 26 | console.log("\n", chalk.bold.green(banner), "\n"); 27 | } 28 | 29 | function initConfig() { 30 | program 31 | .version(VERSION) 32 | .name("bluna-bot") 33 | .usage('-a "wallet address here" -m "wallet mnemonic here" [options]') 34 | .description("CLI tool to automate swapping between LUNA and bLUNA") 35 | .requiredOption("-a, --address ", "Terra wallet address") 36 | .requiredOption( 37 | "-m, --mnemonic ", 38 | "Terra wallet mnemonic key" 39 | ) 40 | .option( 41 | "--interval ", 42 | "delay in seconds before running the next round", 43 | Number, 44 | DEFAULT_INTERVAL 45 | ) 46 | .option( 47 | "--min-gain ", 48 | "minimum percentage gain when swapping LUNA for bLUNA", 49 | Number, 50 | DEFAULT_MIN_PERCENTAGE_GAIN 51 | ) 52 | .option( 53 | "--max-loss ", 54 | "maximum percentage loss when swapping bLUNA for LUNA", 55 | Number, 56 | DEFAULT_MAX_PERCENTAGE_LOSS 57 | ) 58 | .option( 59 | "--min-luna-swap-amount ", 60 | "minimum number of LUNA to use when swapping", 61 | Number, 62 | DEFAULT_MIN_SWAP_AMOUNT 63 | ) 64 | .option( 65 | "--max-luna-swap-amount ", 66 | "maximum number of LUNA to use when swapping", 67 | Number, 68 | DEFAULT_MAX_SWAP_AMOUNT 69 | ) 70 | .option( 71 | "--min-bluna-swap-amount ", 72 | "minimum number of bLUNA to use when swapping", 73 | Number, 74 | DEFAULT_MIN_SWAP_AMOUNT 75 | ) 76 | .option( 77 | "--max-bluna-swap-amount ", 78 | "maximum number of bLUNA to use when swapping", 79 | Number, 80 | DEFAULT_MAX_SWAP_AMOUNT 81 | ) 82 | .option( 83 | "--slippage ", 84 | "percentage slippage when swapping", 85 | Number, 86 | DEFAULT_SLIPPAGE_PERCENTAGE 87 | ) 88 | .option("--stop-on-errors", "stops program on unknown errors") 89 | .option("--verbose", "prints out debug information for every run"); 90 | } 91 | 92 | function parseArgs() { 93 | return program.parse(process.argv).opts(); 94 | } 95 | 96 | function printConfig( 97 | interval: number, 98 | minGain: number, 99 | maxLoss: number, 100 | minLunaSwapAmount: number, 101 | maxLunaSwapAmount: number, 102 | minBlunaSwapAmount: number, 103 | maxBlunaSwapAmount: number, 104 | slippage: number, 105 | stopOnErrors: boolean 106 | ) { 107 | console.log( 108 | chalk.bold("Running with the following configurations:\n") + 109 | ` - Interval: ${interval} seconds\n` + 110 | ` - Minimum percentage gain: ${minGain}%\n` + 111 | ` - Maximum percentage loss: ${maxLoss}%\n` + 112 | ` - Minimum LUNA swap amount: ${minLunaSwapAmount}\n` + 113 | ` - Maximum LUNA swap amount: ${maxLunaSwapAmount}\n` + 114 | ` - Minimum bLUNA swap amount: ${minBlunaSwapAmount}\n` + 115 | ` - Maximum bLUNA swap amount: ${maxBlunaSwapAmount}\n` + 116 | ` - Slippage: ${slippage}%\n` + 117 | ` - On encountering unknown errors: ${ 118 | stopOnErrors ? "stop program" : "ignore and continue program" 119 | }\n` 120 | ); 121 | } 122 | 123 | async function runBot(botConfig: Config, stopOnErrors: boolean) { 124 | try { 125 | await run(botConfig); 126 | } catch (error) { 127 | console.log(error); 128 | if (stopOnErrors) { 129 | console.log( 130 | chalk.bold.red("\nUnknown error encountered!"), 131 | "Stopping program...\n" 132 | ); 133 | } else { 134 | console.log( 135 | chalk.bold.red("\nUnknown error encountered!"), 136 | "Ignoring and continuing the program...\n" 137 | ); 138 | await runBot(botConfig, stopOnErrors); 139 | } 140 | } 141 | } 142 | 143 | async function main() { 144 | // clear console text 145 | clear(); 146 | // greet user 147 | greet(); 148 | // init commander config 149 | initConfig(); 150 | // parse args via commander 151 | const { 152 | address, 153 | mnemonic, 154 | interval, 155 | minGain, 156 | maxLoss, 157 | minLunaSwapAmount, 158 | maxLunaSwapAmount, 159 | minBlunaSwapAmount, 160 | maxBlunaSwapAmount, 161 | slippage, 162 | stopOnErrors, 163 | verbose, 164 | } = parseArgs(); 165 | // print config back to user 166 | printConfig( 167 | interval, 168 | minGain, 169 | maxLoss, 170 | minLunaSwapAmount, 171 | maxLunaSwapAmount, 172 | minBlunaSwapAmount, 173 | maxBlunaSwapAmount, 174 | slippage, 175 | stopOnErrors 176 | ); 177 | 178 | const botConfig: Config = { 179 | walletAddress: address, 180 | walletMnemonic: mnemonic, 181 | interval: interval, 182 | minPercentageGain: minGain, 183 | maxPercentageLoss: maxLoss, 184 | minLunaSwapAmount: minLunaSwapAmount, 185 | maxLunaSwapAmount: maxLunaSwapAmount, 186 | minBlunaSwapAmount: minBlunaSwapAmount, 187 | maxBlunaSwapAmount: maxBlunaSwapAmount, 188 | slippagePercentage: slippage, 189 | debug: (info) => { 190 | if (verbose) { 191 | console.log("debug:", info, "\n"); 192 | } 193 | }, 194 | onSwapSuccess: (transactionResult) => { 195 | console.log(chalk.bold.green("Swap success!")); 196 | console.log("transaction result:", transactionResult, "\n"); 197 | }, 198 | onSwapError: (transactionResult) => { 199 | console.log(chalk.bold.red("Swap error!")); 200 | console.log("transaction result:", transactionResult, "\n"); 201 | }, 202 | }; 203 | 204 | // run the program 205 | await runBot(botConfig, stopOnErrors); 206 | } 207 | 208 | main(); 209 | -------------------------------------------------------------------------------- /bluna-bot-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ 6 | "dom", 7 | "es6", 8 | "es2017", 9 | "esnext.asynciterable" 10 | ], 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "moduleResolution": "node", 14 | "removeComments": true, 15 | "noImplicitAny": true, 16 | "strictNullChecks": true, 17 | "strictFunctionTypes": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "allowSyntheticDefaultImports": true, 24 | "esModuleInterop": true, 25 | "emitDecoratorMetadata": true, 26 | "experimentalDecorators": true, 27 | "resolveJsonModule": true, 28 | "baseUrl": "." 29 | }, 30 | "exclude": [ 31 | "node_modules" 32 | ], 33 | "include": [ 34 | "./src/**/*.ts" 35 | ] 36 | } -------------------------------------------------------------------------------- /bluna-bot-pkg/.npmignore: -------------------------------------------------------------------------------- 1 | !dist -------------------------------------------------------------------------------- /bluna-bot-pkg/README.md: -------------------------------------------------------------------------------- 1 | 2 | # `bluna-bot-pkg` 3 | 4 | Helper library to automate swapping between LUNA and bLUNA. 5 | 6 | - [Installation](#installation) 7 | - [Usage](#usage) 8 | 9 | ## Installation 10 | 11 | ```sh 12 | yarn add bluna-bot-pkg 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```ts 18 | import { run, stop } from "bluna-bot-pkg"; 19 | 20 | run({ 21 | walletAddress: "terraAddressHere", // REQUIRED! 22 | walletMnemonic: "terra wallet mnemonic here", // REQUIRED! 23 | }); 24 | ``` 25 | 26 | Refer to [`config.ts`](https://github.com/AaronCQL/bluna-bot/blob/main/bluna-bot-pkg/src/types/config.ts) for the full list of configurations that the `run` function can accept. 27 | -------------------------------------------------------------------------------- /bluna-bot-pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bluna-bot-pkg", 3 | "version": "0.2.0", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "author": "Aaron Choo", 7 | "license": "MIT", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "dev": "yarn clean && tsc --watch", 13 | "build": "yarn clean && tsc", 14 | "clean": "rimraf dist", 15 | "prepublish": "yarn build" 16 | }, 17 | "dependencies": { 18 | "@terra-money/terra.js": "^1.8.4" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^15.12.4", 22 | "rimraf": "^3.0.2", 23 | "typescript": "^4.3.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /bluna-bot-pkg/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Coins, 3 | MnemonicKey, 4 | MsgExecuteContract, 5 | BlockTxBroadcastResult, 6 | } from "@terra-money/terra.js"; 7 | 8 | import { 9 | BlunaBalanceQueryMessage, 10 | BlunaBalanceResponse, 11 | IncreaseAllowanceHandleMessage, 12 | SwapBlunaToLunaSendMsg, 13 | SwapBlunaToLunaHandleMessage, 14 | SwapLunaToBlunaHandleMessage, 15 | SwapLunaToBlunaSimulationQueryMessage, 16 | SwapBlunaToLunaSimulationQueryMessage, 17 | SwapLunaToBlunaSimulationResponse, 18 | SwapBlunaToLunaSimulationResponse, 19 | SwapSimulationContractResponse, 20 | WalletBalance, 21 | } from "../types"; 22 | import { 23 | terra, 24 | toMicroAmount, 25 | LUNA_BLUNA_SWAP_CONTRACT_ADDRESS, 26 | BLUNA_CONTRACT_ADDRESS, 27 | calculatePercentageGain, 28 | fromMicroAmount, 29 | calculatePercentageLoss, 30 | } from "../utils"; 31 | 32 | export async function simulateLunaToBlunaSwap( 33 | lunaAmount: number = 1 34 | ): Promise { 35 | const queryMessage: SwapLunaToBlunaSimulationQueryMessage = { 36 | simulation: { 37 | offer_asset: { 38 | amount: toMicroAmount(lunaAmount).toString(), 39 | info: { 40 | native_token: { 41 | denom: "uluna", 42 | }, 43 | }, 44 | }, 45 | }, 46 | }; 47 | 48 | const contractResponse: SwapSimulationContractResponse = 49 | await terra.wasm.contractQuery( 50 | LUNA_BLUNA_SWAP_CONTRACT_ADDRESS, 51 | queryMessage 52 | ); 53 | const percentageGain = calculatePercentageGain( 54 | lunaAmount, 55 | fromMicroAmount(contractResponse.return_amount) 56 | ); 57 | 58 | return { 59 | contractResponse, 60 | percentageGain, 61 | }; 62 | } 63 | 64 | export async function simulateBlunaToLunaSwap( 65 | blunaAmount: number = 1 66 | ): Promise { 67 | const queryMessage: SwapBlunaToLunaSimulationQueryMessage = { 68 | simulation: { 69 | offer_asset: { 70 | amount: toMicroAmount(blunaAmount).toString(), 71 | info: { 72 | token: { 73 | contract_addr: BLUNA_CONTRACT_ADDRESS, 74 | }, 75 | }, 76 | }, 77 | }, 78 | }; 79 | 80 | const contractResponse: SwapSimulationContractResponse = 81 | await terra.wasm.contractQuery( 82 | LUNA_BLUNA_SWAP_CONTRACT_ADDRESS, 83 | queryMessage 84 | ); 85 | const percentageLoss = calculatePercentageLoss( 86 | blunaAmount, 87 | fromMicroAmount(contractResponse.return_amount) 88 | ); 89 | 90 | return { 91 | contractResponse, 92 | percentageLoss, 93 | }; 94 | } 95 | 96 | export async function getWalletBalance( 97 | address: string 98 | ): Promise { 99 | const blunaBalanceQueryMessage: BlunaBalanceQueryMessage = { 100 | balance: { 101 | address: address, 102 | }, 103 | }; 104 | 105 | const [{ balance: blunaBalance }, coins] = await Promise.all([ 106 | // bluna 107 | terra.wasm.contractQuery( 108 | BLUNA_CONTRACT_ADDRESS, 109 | blunaBalanceQueryMessage 110 | ) as Promise, 111 | // ust and luna 112 | terra.bank.balance(address), 113 | ]); 114 | 115 | return { 116 | uust: coins.get("uusd")?.amount.toString() ?? "0", 117 | uluna: coins.get("uluna")?.amount.toString() ?? "0", 118 | ubluna: blunaBalance, 119 | }; 120 | } 121 | 122 | export async function swapLunaToBluna( 123 | walletMnemonic: string, 124 | lunaAmount: number, 125 | expectedBlunaAmount: number, 126 | slippagePercentage: number 127 | ): Promise { 128 | const wallet = terra.wallet( 129 | new MnemonicKey({ 130 | mnemonic: walletMnemonic, 131 | }) 132 | ); 133 | 134 | // increase allowance 135 | const increaseAllowanceHandleMessage: IncreaseAllowanceHandleMessage = { 136 | increase_allowance: { 137 | amount: toMicroAmount(expectedBlunaAmount).toString(), 138 | spender: LUNA_BLUNA_SWAP_CONTRACT_ADDRESS, 139 | }, 140 | }; 141 | const executeIncreaseAllowance = new MsgExecuteContract( 142 | wallet.key.accAddress, 143 | BLUNA_CONTRACT_ADDRESS, 144 | increaseAllowanceHandleMessage 145 | ); 146 | 147 | // swap 148 | const swapHandleMessage: SwapLunaToBlunaHandleMessage = { 149 | swap: { 150 | offer_asset: { 151 | amount: toMicroAmount(lunaAmount).toString(), 152 | info: { 153 | native_token: { 154 | denom: "uluna", 155 | }, 156 | }, 157 | }, 158 | belief_price: (lunaAmount / expectedBlunaAmount).toString(), 159 | max_spread: (slippagePercentage / 100).toString(), 160 | }, 161 | }; 162 | const swapCoins = new Coins({ 163 | uluna: toMicroAmount(lunaAmount), 164 | }); 165 | const executeSwap = new MsgExecuteContract( 166 | wallet.key.accAddress, 167 | LUNA_BLUNA_SWAP_CONTRACT_ADDRESS, 168 | swapHandleMessage, 169 | swapCoins 170 | ); 171 | 172 | const transaction = await wallet.createAndSignTx({ 173 | msgs: [executeIncreaseAllowance, executeSwap], 174 | }); 175 | 176 | const result = await terra.tx.broadcast(transaction); 177 | 178 | return result; 179 | } 180 | 181 | export async function swapBlunaToLuna( 182 | walletMnemonic: string, 183 | blunaAmount: number, 184 | expectedLunaAmount: number, 185 | slippagePercentage: number 186 | ): Promise { 187 | const wallet = terra.wallet( 188 | new MnemonicKey({ 189 | mnemonic: walletMnemonic, 190 | }) 191 | ); 192 | 193 | const swapMsg: SwapBlunaToLunaSendMsg = { 194 | swap: { 195 | belief_price: (blunaAmount / expectedLunaAmount).toString(), 196 | max_spread: (slippagePercentage / 100).toString(), 197 | }, 198 | }; 199 | const base64SwapMsg = Buffer.from(JSON.stringify(swapMsg)).toString("base64"); 200 | 201 | const swapHandleMessage: SwapBlunaToLunaHandleMessage = { 202 | send: { 203 | amount: toMicroAmount(blunaAmount).toString(), 204 | contract: LUNA_BLUNA_SWAP_CONTRACT_ADDRESS, 205 | msg: base64SwapMsg, 206 | }, 207 | }; 208 | const executeSwap = new MsgExecuteContract( 209 | wallet.key.accAddress, 210 | BLUNA_CONTRACT_ADDRESS, 211 | swapHandleMessage 212 | ); 213 | 214 | const transaction = await wallet.createAndSignTx({ 215 | msgs: [executeSwap], 216 | }); 217 | 218 | const result = await terra.tx.broadcast(transaction); 219 | 220 | return result; 221 | } 222 | -------------------------------------------------------------------------------- /bluna-bot-pkg/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | 3 | export * from "./utils"; 4 | 5 | export * from "./types"; 6 | 7 | export * from "./logic"; 8 | -------------------------------------------------------------------------------- /bluna-bot-pkg/src/logic/index.ts: -------------------------------------------------------------------------------- 1 | import { BlockTxBroadcastResult, isTxError } from "@terra-money/terra.js"; 2 | 3 | import { Config } from "../types"; 4 | import { 5 | simulateLunaToBlunaSwap, 6 | simulateBlunaToLunaSwap, 7 | getWalletBalance, 8 | swapLunaToBluna, 9 | swapBlunaToLuna, 10 | } from "../api"; 11 | import { 12 | toMicroSeconds, 13 | fromMicroAmount, 14 | DEFAULT_INTERVAL, 15 | DEFAULT_MIN_PERCENTAGE_GAIN, 16 | DEFAULT_MAX_PERCENTAGE_LOSS, 17 | DEFAULT_MAX_SWAP_AMOUNT, 18 | DEFAULT_MIN_SWAP_AMOUNT, 19 | DEFAULT_CALLBACK, 20 | DEFAULT_SLIPPAGE_PERCENTAGE, 21 | } from "../utils"; 22 | 23 | let shouldContinueRunning = true; 24 | 25 | export async function run(config: Config): Promise { 26 | // set to true in case it was set to false 27 | shouldContinueRunning = true; 28 | 29 | const { 30 | walletAddress, 31 | walletMnemonic, 32 | interval = DEFAULT_INTERVAL, 33 | minPercentageGain = DEFAULT_MIN_PERCENTAGE_GAIN, 34 | maxPercentageLoss = DEFAULT_MAX_PERCENTAGE_LOSS, 35 | minLunaSwapAmount = DEFAULT_MIN_SWAP_AMOUNT, 36 | maxLunaSwapAmount = DEFAULT_MAX_SWAP_AMOUNT, 37 | minBlunaSwapAmount = DEFAULT_MIN_SWAP_AMOUNT, 38 | maxBlunaSwapAmount = DEFAULT_MAX_SWAP_AMOUNT, 39 | slippagePercentage = DEFAULT_SLIPPAGE_PERCENTAGE, 40 | onSwapSuccess = DEFAULT_CALLBACK, 41 | onSwapError = DEFAULT_CALLBACK, 42 | debug = DEFAULT_CALLBACK, 43 | } = config; 44 | 45 | const walletBalance = await getWalletBalance(walletAddress); 46 | const lunaBalance = Math.min( 47 | fromMicroAmount(walletBalance.uluna), 48 | maxLunaSwapAmount 49 | ); 50 | const blunaBalance = Math.min( 51 | fromMicroAmount(walletBalance.ubluna), 52 | maxBlunaSwapAmount 53 | ); 54 | 55 | const [swapLunaToBlunaSimulation, swapBlunaToLunaSimulation] = 56 | await Promise.all([ 57 | simulateLunaToBlunaSwap(lunaBalance), 58 | simulateBlunaToLunaSwap(blunaBalance), 59 | ]); 60 | 61 | const shouldSwapLuna = 62 | lunaBalance >= minLunaSwapAmount && 63 | swapLunaToBlunaSimulation.percentageGain >= minPercentageGain; 64 | const shouldSwapBluna = 65 | blunaBalance >= minBlunaSwapAmount && 66 | swapBlunaToLunaSimulation.percentageLoss <= maxPercentageLoss; 67 | const transactionResult: BlockTxBroadcastResult | undefined = shouldSwapLuna 68 | ? await swapLunaToBluna( 69 | walletMnemonic, 70 | lunaBalance, 71 | fromMicroAmount( 72 | swapLunaToBlunaSimulation.contractResponse.return_amount 73 | ), 74 | slippagePercentage 75 | ) 76 | : shouldSwapBluna 77 | ? await swapBlunaToLuna( 78 | walletMnemonic, 79 | blunaBalance, 80 | fromMicroAmount( 81 | swapBlunaToLunaSimulation.contractResponse.return_amount 82 | ), 83 | slippagePercentage 84 | ) 85 | : undefined; 86 | 87 | if (transactionResult === undefined) { 88 | // neither swap occurred 89 | } else { 90 | if (isTxError(transactionResult)) { 91 | await onSwapError(transactionResult); 92 | } else { 93 | await onSwapSuccess(transactionResult); 94 | } 95 | } 96 | 97 | if (shouldContinueRunning) { 98 | setTimeout(() => run(config), toMicroSeconds(interval)); 99 | } 100 | 101 | await debug({ 102 | initialWalletBalance: walletBalance, 103 | lunaSwapAmount: lunaBalance, 104 | swapLunaToBlunaSimulation, 105 | blunaSwapAmount: blunaBalance, 106 | swapBlunaToLunaSimulation, 107 | transactionResult, 108 | }); 109 | } 110 | 111 | export function stop(): void { 112 | shouldContinueRunning = false; 113 | } 114 | -------------------------------------------------------------------------------- /bluna-bot-pkg/src/types/api.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BLUNA_CONTRACT_ADDRESS, 3 | LUNA_BLUNA_SWAP_CONTRACT_ADDRESS, 4 | } from "../utils"; 5 | 6 | export type SwapLunaToBlunaSimulationQueryMessage = { 7 | simulation: { 8 | offer_asset: { 9 | amount: string; 10 | info: { 11 | native_token: { 12 | denom: "uluna"; 13 | }; 14 | }; 15 | }; 16 | }; 17 | }; 18 | 19 | export type SwapBlunaToLunaSimulationQueryMessage = { 20 | simulation: { 21 | offer_asset: { 22 | amount: string; 23 | info: { 24 | token: { 25 | contract_addr: typeof BLUNA_CONTRACT_ADDRESS; 26 | }; 27 | }; 28 | }; 29 | }; 30 | }; 31 | 32 | export type SwapSimulationContractResponse = { 33 | return_amount: string; 34 | spread_amount: string; 35 | commission_amount: string; 36 | }; 37 | 38 | export type SwapLunaToBlunaSimulationResponse = { 39 | contractResponse: SwapSimulationContractResponse; 40 | percentageGain: number; 41 | }; 42 | 43 | export type SwapBlunaToLunaSimulationResponse = { 44 | contractResponse: SwapSimulationContractResponse; 45 | percentageLoss: number; 46 | }; 47 | 48 | export type BlunaBalanceQueryMessage = { 49 | balance: { 50 | address: string; 51 | }; 52 | }; 53 | 54 | export type BlunaBalanceResponse = { 55 | balance: string; 56 | }; 57 | 58 | export type WalletBalance = { 59 | uust: string; 60 | uluna: string; 61 | ubluna: string; 62 | }; 63 | 64 | export type IncreaseAllowanceHandleMessage = { 65 | increase_allowance: { 66 | amount: string; 67 | spender: typeof LUNA_BLUNA_SWAP_CONTRACT_ADDRESS; 68 | }; 69 | }; 70 | 71 | export type SwapLunaToBlunaHandleMessage = { 72 | swap: { 73 | offer_asset: { 74 | amount: string; 75 | info: { 76 | native_token: { 77 | denom: "uluna"; 78 | }; 79 | }; 80 | }; 81 | belief_price: string; 82 | max_spread: string; 83 | }; 84 | }; 85 | 86 | export type SwapBlunaToLunaSendMsg = { 87 | swap: { 88 | belief_price: string; 89 | max_spread: string; 90 | }; 91 | }; 92 | 93 | export type SwapBlunaToLunaHandleMessage = { 94 | send: { 95 | amount: string; 96 | contract: typeof LUNA_BLUNA_SWAP_CONTRACT_ADDRESS; 97 | msg: string; // SwapBlunaToLunaSendMsg in base64 encoding 98 | }; 99 | }; 100 | -------------------------------------------------------------------------------- /bluna-bot-pkg/src/types/config.ts: -------------------------------------------------------------------------------- 1 | import { BlockTxBroadcastResult } from "@terra-money/terra.js"; 2 | 3 | import { 4 | SwapLunaToBlunaSimulationResponse, 5 | SwapBlunaToLunaSimulationResponse, 6 | WalletBalance, 7 | } from "./api"; 8 | 9 | export type CallbackFunction = ( 10 | transactionResult?: BlockTxBroadcastResult | undefined 11 | ) => any; 12 | 13 | export type DebugFunction = (info: { 14 | initialWalletBalance?: WalletBalance | undefined; 15 | lunaSwapAmount?: number | undefined; 16 | swapLunaToBlunaSimulation?: SwapLunaToBlunaSimulationResponse | undefined; 17 | blunaSwapAmount?: number | undefined; 18 | swapBlunaToLunaSimulation?: SwapBlunaToLunaSimulationResponse | undefined; 19 | transactionResult?: BlockTxBroadcastResult | undefined; 20 | }) => any; 21 | 22 | export type Config = { 23 | walletAddress: string; 24 | walletMnemonic: string; 25 | interval?: number | undefined; // min delay in seconds before running the next round 26 | minPercentageGain?: number | undefined; // min percentage gain when swapping luna for bluna 27 | maxPercentageLoss?: number | undefined; // max percentage loss when swapping bluna for luna 28 | minLunaSwapAmount?: number | undefined; // min number of luna to swap at one go 29 | maxLunaSwapAmount?: number | undefined; // max number of luna to swap at one go 30 | minBlunaSwapAmount?: number | undefined; // min number of luna to swap at one go 31 | maxBlunaSwapAmount?: number | undefined; // max number of luna to swap at one go 32 | slippagePercentage?: number | undefined; // slippage percentage when swapping 33 | onSwapSuccess?: CallbackFunction | undefined; 34 | onSwapError?: CallbackFunction | undefined; 35 | debug?: DebugFunction | undefined; 36 | }; 37 | -------------------------------------------------------------------------------- /bluna-bot-pkg/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | export * from "./config"; 3 | -------------------------------------------------------------------------------- /bluna-bot-pkg/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const BLUNA_CONTRACT_ADDRESS = 2 | "terra1kc87mu460fwkqte29rquh4hc20m54fxwtsx7gp"; 3 | 4 | export const LUNA_BLUNA_SWAP_CONTRACT_ADDRESS = 5 | "terra1jxazgm67et0ce260kvrpfv50acuushpjsz2y0p"; 6 | 7 | // base64 encoding for `{"swap":{}}` 8 | export const BASE64_EMPTY_SWAP_MESSAGE = "eyJzd2FwIjp7fX0="; 9 | 10 | // Default configurations 11 | export const DEFAULT_INTERVAL = 2; 12 | export const DEFAULT_MIN_PERCENTAGE_GAIN = 10; 13 | export const DEFAULT_MAX_PERCENTAGE_LOSS = 1.5; 14 | export const DEFAULT_MAX_SWAP_AMOUNT = 2500; 15 | export const DEFAULT_MIN_SWAP_AMOUNT = 10; 16 | export const DEFAULT_SLIPPAGE_PERCENTAGE = 0.5; 17 | export const DEFAULT_CALLBACK = () => {}; 18 | -------------------------------------------------------------------------------- /bluna-bot-pkg/src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | export function toMicroAmount(amount: number): number { 2 | return Math.round(amount * 1000000); 3 | } 4 | 5 | export function fromMicroAmount(microAmount: number | string): number { 6 | return Number(microAmount) / 1000000; 7 | } 8 | 9 | export function toMicroSeconds(seconds: number): number { 10 | return seconds * 1000; 11 | } 12 | 13 | export function calculatePercentageGain(from: number, to: number) { 14 | return ((to - from) / from) * 100; 15 | } 16 | 17 | export function calculatePercentageLoss(from: number, to: number) { 18 | return ((from - to) / from) * 100; 19 | } 20 | -------------------------------------------------------------------------------- /bluna-bot-pkg/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./constants"; 2 | export * from "./helpers"; 3 | export * from "./terra"; 4 | -------------------------------------------------------------------------------- /bluna-bot-pkg/src/utils/terra.ts: -------------------------------------------------------------------------------- 1 | import { LCDClient } from "@terra-money/terra.js"; 2 | 3 | export const terra = new LCDClient({ 4 | URL: "https://lcd.terra.dev", 5 | chainID: "columbus-4", 6 | }); 7 | -------------------------------------------------------------------------------- /bluna-bot-pkg/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ 6 | "dom", 7 | "es6", 8 | "es2017", 9 | "esnext.asynciterable" 10 | ], 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "declaration": true, 14 | "moduleResolution": "node", 15 | "removeComments": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "strictFunctionTypes": true, 19 | "noImplicitThis": true, 20 | "noUnusedLocals": false, 21 | "noUnusedParameters": false, 22 | "noImplicitReturns": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "allowSyntheticDefaultImports": true, 25 | "esModuleInterop": true, 26 | "emitDecoratorMetadata": true, 27 | "experimentalDecorators": true, 28 | "resolveJsonModule": true, 29 | "baseUrl": "." 30 | }, 31 | "exclude": [ 32 | "node_modules" 33 | ], 34 | "include": [ 35 | "./src/**/*.ts" 36 | ] 37 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bluna-bot", 3 | "version": "0.1.0", 4 | "author": "Aaron Choo", 5 | "license": "MIT", 6 | "private": true, 7 | "workspaces": [ 8 | "bluna-bot-cli", 9 | "bluna-bot-pkg" 10 | ] 11 | } 12 | --------------------------------------------------------------------------------