Skip to content

Commit 2d4d332

Browse files
committed
adding buy and sell logic
added support for other blockchains such as bsc, optimism, arbitrum
1 parent d1d4ee4 commit 2d4d332

File tree

9 files changed

+224
-21
lines changed

9 files changed

+224
-21
lines changed

CHANGELOG.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,20 @@
1212
`v1.0.0`
1313

1414
- Bug fix and optimizations
15-
- Add support for binance smart chain(BSC)
15+
- Add support for binance smart chain(BSC) :heavy_check_mark:
1616
- Testing on mainnet
1717

1818
`v1.0.2`
1919

20-
- Add support for Optimism Blockchain
20+
- Add support for Optimism Blockchain :heavy_check_mark:
2121

2222
`v1.0.3`
2323

24-
- Add support for Arbitrum Blockchain
24+
- Add support for Arbitrum Blockchain :heavy_check_mark:
2525

2626
`v1.0.4`
2727

28-
- Add support for Polygon Matic Blockchain
28+
- Add support for Polygon Matic Blockchain :heavy_check_mark:
2929

3030
`v1.0.5`
3131

config.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1-
if (!process.env.PUBLIC_KEY && !process.env.PRIVATE_KEY && !process.env.ETH_IN_AMOUNT && !process.env.DB_URL) {
1+
if (!process.env.INFURA_API_KEY && !process.env.PUBLIC_KEY && !process.env.PRIVATE_KEY && !process.env.ETH_IN_AMOUNT && !process.env.DB_URL) {
22

3-
throw new Error("PUBLIC_KEY && PRIVATE_KEY && ETH_IN_AMOUNT && DB_URL, Must be defined in your .env file");
3+
throw new Error("INFURA_API_KEY && PUBLIC_KEY && PRIVATE_KEY && ETH_IN_AMOUNT && DB_URL, Must be defined in your .env file");
44
}
55
export const config = {
66
WALLET: {
77
PUBLIC_KEY: process.env.PUBLIC_KEY!,
88
PRIVATE_KEY: process.env.PRIVATE_KEY!
99
},
1010
PROVIDERS: {
11-
INFURA_API_KEY: ''
11+
INFURA_API_KEY: process.env.INFURA_API_KEY!
1212
},
1313
NETWORK: {
14-
ID: 1 // 1 eth, 56 is bsc, 137 polygon, 10 optimism, 42161 arbitrum
14+
ID: process.env.NETWORK_ID || 1 // 1 eth, 56 is bsc, 137 polygon, 10 optimism, 42161 arbitrum
1515
},
16+
PROFIT_THRESHOLD: { // profit % you atleast want
17+
BUY: 0.2,
18+
SELL: 2
19+
},
20+
SLIPPAGE: 0.5,
21+
EXPLORER: '',
1622
PRICE_CHECK_INTERVAL_IN_SECONDS: process.env.PRICE_CHECK_INTERVAL_IN_SECONDS || 45,
1723
ETH_IN_AMOUNT: parseFloat(process.env.ETH_IN_AMOUNT!),
1824
DB_URL: process.env.DB_URL!

src/index.ts

Lines changed: 139 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import chalk from "chalk";
22
import { connect } from "mongoose";
33
import { schedule } from "node-cron";
44
import { config } from "../config";
5-
import { OneInch } from "./lib/1inch.io";
6-
import { Quote, Token } from "./types/1inch";
5+
import { OneInch } from "./lib";
6+
import { Quote, Direction } from "./types";
77
const chalkTable = require('chalk-table');
88
import BigNumber from "bignumber.js";
9-
import { flat } from "./utils";
9+
import { buildTradeMsg, flat } from "./utils";
1010
import { MONITORED_TOKENS } from "./data/token";
1111

1212
const Main = async () => {
@@ -37,7 +37,6 @@ const Main = async () => {
3737
const options = {
3838
useNewUrlParser: true,
3939
useUnifiedTopology: true,
40-
useCreateIndex: true,
4140
keepAlive: true,
4241
connectTimeoutMS: 60000,
4342
socketTimeoutMS: 60000,
@@ -47,7 +46,7 @@ const Main = async () => {
4746
console.log("Connected to MongoDb :)");
4847
}).catch(async (err) => {
4948
let error = JSON.parse(JSON.stringify(err))
50-
console.log('Mongo Error:', error?.name);
49+
console.log('Mongo Error:', error);
5150
});
5251
console.log(`---`.repeat(10));
5352

@@ -60,6 +59,9 @@ const Main = async () => {
6059
console.log(`---`.repeat(10));
6160

6261
let ethInAmount = new BigNumber(config.ETH_IN_AMOUNT).shiftedBy(18).toString()
62+
let on_cooldown = false
63+
let message = ''
64+
6365
schedule(`*/${config.PRICE_CHECK_INTERVAL_IN_SECONDS} * * * * *`, async function () {
6466
console.log(`***`.repeat(10));
6567
MONITORED_TOKENS.forEach(async (token: any) => {
@@ -110,8 +112,140 @@ const Main = async () => {
110112
]);
111113
if (JSON.stringify(best_buy_protocols) != JSON.stringify(best_sell_protocols)) {
112114
console.log(table);
115+
116+
117+
if (profit_pct >= config.PROFIT_THRESHOLD.BUY && !on_cooldown) {
118+
let nonce: number = await oneInch.getNonce()
119+
console.log(`Nonce:`, nonce);
120+
121+
on_cooldown = true
122+
/**
123+
* Start of Buy => Approve? => Sell Txs
124+
*/
125+
try {
126+
127+
console.log(`Initiating a buy for token ${token.ticker} ...`);
128+
// build buy Tx
129+
let txData = await oneInch.buildTx({
130+
srcToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
131+
toToken: token.address,
132+
srcAmount: ethInAmount,
133+
slippage: config.SLIPPAGE
134+
})
135+
console.log(`Buy Tx Data:`, txData);
136+
137+
// send a buy Tx
138+
nonce++;
139+
oneInch.sendTx({
140+
data: txData.tx,
141+
nonce
142+
}).then(async (tx: any) => {
143+
if (tx.hash) {
144+
145+
// build Buy Tg Msg
146+
message = await buildTradeMsg({ data: tx, profit_pct: profit_pct, side: Direction.BUY })
147+
// send Msg to Tg
148+
sendMessage(users, message);
149+
150+
try {
151+
/**
152+
* Approve Token if it has not been approved before and save it to db
153+
*/
154+
// approve if token has not been approved
155+
const token_is_approved = await Approve.exists({ token: token, by: process.env.PUBLIC_KEY!.toLowerCase() })
156+
if (!token_is_approved) {
157+
// approve if not approved
158+
message = `Approving ${token.description}...`
159+
sendMessage(users, message)
160+
let txData = await oneInch.approve(token.address)
161+
nonce++;
162+
await oneInch.sendTx({
163+
data: txData.tx,
164+
nonce
165+
}).then((tx: any) => {
166+
console.log(`${token.ticker} has been approved successfully.`)
167+
sendMessage(users, message)
168+
}).catch((err) => {
169+
console.log(`Error: `, err)
170+
});
171+
}
172+
173+
174+
/**
175+
* Get the balance of the bought token shpuld be atleast be 1/2 of what was expected
176+
*/
177+
178+
let tries = 0
179+
let tokenBalance = '0'
180+
while (tries < 2000) {
181+
tokenBalance = await oneInch.balanceOf(tx.toToken.address)
182+
if (parseInt(tokenBalance) > parseInt(new BigNumber(token_amount).multipliedBy(0.5).toString())) {
183+
break
184+
}
185+
tries++
186+
}
187+
/**
188+
* End of Balance Check
189+
*/
190+
191+
/**
192+
* Sell the bought tokens/assets to the exchange with the best rates
193+
*/
194+
message = `Initiating a sell for token ${token.ticker}...`
195+
// build Sell Tx
196+
let txData = await oneInch.buildTx({
197+
srcToken: token.address,
198+
toToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
199+
srcAmount: tokenBalance,
200+
slippage: config.SLIPPAGE
201+
})
202+
console.log(`Sell Tx Data:`, txData);
203+
204+
// send the sell Tx
205+
nonce++;
206+
oneInch.sendTx({
207+
data: txData.tx,
208+
nonce
209+
}).then(async (tx: any) => {
210+
if (tx.hash) {
211+
212+
// build Sell Tg Msg
213+
message = await buildTradeMsg({ data: tx, profit_pct: profit_pct, side: Direction.SELL })
214+
// send Msg to Tg
215+
sendMessage(users, message);
216+
217+
218+
}
219+
}).catch((err) => {
220+
console.log(`Error:`, err)
221+
})
222+
223+
/**
224+
* End of Sell Tx
225+
*/
226+
227+
}
228+
catch (error) {
229+
console.error(`Error:`, error)
230+
}
231+
}
232+
}
233+
).catch((err) => {
234+
console.log(`Error:`, err);
235+
});
236+
237+
238+
} catch (error) {
239+
console.error(`Error:`, error)
240+
}
241+
/**
242+
* End of Buy => Approve? => Sell Txs
243+
*/
244+
245+
}
113246
}
114247

248+
115249
} catch (error: any) {
116250
console.error('Error:', JSON.parse(JSON.stringify(error)).code);
117251
}

src/lib/1inch.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import axios from "axios";
22
import { config } from "../../config";
33
import { Quote } from "../types/1inch";
4+
import { toHex } from "../utils";
45
import { Aggr } from "./aggr";
56

67
export class OneInch extends Aggr {
@@ -42,13 +43,24 @@ export class OneInch extends Aggr {
4243
* @param slippage - slippage tolerance
4344
* @returns tx data that can be send to the network
4445
*/
45-
buildTx = async (srcToken: string, toToken: string, srcAmount: number, slippage?: number): Promise<string> => {
46+
buildTx = async (params: { srcToken: string, toToken: string, srcAmount: number | string, slippage?: number, gasLimit?: string }) => {
47+
const { srcToken, toToken, srcAmount, slippage, gasLimit } = params;
4648
try {
4749
let defaultSlippage = 0.5
48-
const { data } = await axios({
50+
console.log(defaultSlippage)
51+
const { data }: any = await axios({
4952
method: "GET",
50-
url: `${this.API_URL}${config.NETWORK.ID}/swap?fromTokenAddress=${srcToken}&toTokenAddress=${toToken}&amount=${srcAmount}&fromAddress=${config.WALLET.PUBLIC_KEY}&disableEstimate=false&slippage=${slippage ? `${slippage}` : defaultSlippage}`
53+
url: `${this.API_URL}${config.NETWORK.ID}/swap?fromTokenAddress=${srcToken}&toTokenAddress=${toToken}&amount=${srcAmount}&fromAddress=${config.WALLET.PUBLIC_KEY}&disableEstimate=true&slippage=${slippage ? `${slippage}` : defaultSlippage}`
5154
})
55+
delete data.tx.gasPrice; //ethersjs will find the gasPrice needed
56+
delete data.tx.gas;
57+
58+
if (gasLimit) {
59+
data.tx.gasLimit = toHex(parseInt(gasLimit))
60+
}
61+
62+
data.tx["value"] = toHex(parseInt(data.tx["value"]))
63+
5264
return data
5365
} catch (error: any) {
5466
throw new Error(JSON.stringify(error));
@@ -80,10 +92,14 @@ export class OneInch extends Aggr {
8092
*/
8193
approve = async (tokenAddress: string, amount?: string) => {
8294
try {
83-
const { data } = await axios({
95+
const { data }: any = await axios({
8496
method: "GET",
8597
url: `${this.API_URL}${config.NETWORK.ID}/approve/calldata?tokenAddress=${tokenAddress}`
8698
})
99+
delete data.tx.gasPrice; //ethersjs will find the gasPrice needed
100+
delete data.tx.gas;
101+
102+
data.tx["value"] = toHex(parseInt(data.tx["value"]))
87103
return data
88104
} catch (error: any) {
89105
throw new Error(JSON.stringify(error));

src/lib/aggr.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,18 @@ export abstract class Aggr {
1313
this.account = new Wallet(config.WALLET.PRIVATE_KEY, this.provider);
1414
this.API_URL = api_url
1515
}
16-
16+
/**
17+
* Sends a tx to the blockchain
18+
* @param data - Tx data
19+
* @param nonce - wallet current nonce
20+
* @returns Tx hash if successful else error message
21+
*/
1722
sendTx = async (params: { data: any, nonce: number }) => {
1823
const { data, nonce } = params
1924
try {
2025

2126
if (!isNaN(nonce)) {
22-
data.nonce = nonce
27+
data.nonce = nonce + 1
2328
}
2429

2530
const tx = await this.account.sendTransaction(data)
@@ -47,9 +52,9 @@ export abstract class Aggr {
4752

4853
/**
4954
* Gets the current nonce of a wallet
50-
* @returns nonce
55+
* @returns wallet's current nonce
5156
*/
5257
getNonce = async (): Promise<number> => {
53-
return await this.provider.getTransactionCount(config.WALLET.PUBLIC_KEY)
58+
return await this.account.getTransactionCount()
5459
}
5560
}

src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './1inch'

src/types/enums.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum Direction {
2+
SELL = 'SELL',
3+
BUY = 'BUY'
4+
}

src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './1inch';
2+
export * from './enums';

src/utils/common.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,42 @@
1+
import BigNumber from "bignumber.js"
2+
import { config } from "../../config"
3+
import { Direction } from "../types"
4+
15
export const flat = async (arr: Array<any>, start: number = 0, end: number = 3): Promise<Array<any>> => {
26
if (start < end) {
37
start += 1
48
return flat([].concat(...arr), start)
59
}
610
return arr
11+
}
12+
13+
export const toHex = (value: number) => {
14+
return `0x${value.toString(16)}`
15+
}
16+
export const humanizeBalance = async (balance: string | number, decimals: number) => {
17+
return new BigNumber(balance).shiftedBy(-decimals).toString()
18+
}
19+
20+
export const buildTradeMsg = async (params: { data: any, profit_pct: number, side: Direction }): Promise<string> => {
21+
const { data, profit_pct, side } = params
22+
23+
let dexes = (await flat(data.protocols)).map((quote: any) => quote.name).join(', ')
24+
let msg = `* NEW TRADE NOTIFICATION *\n-- - `
25+
msg += `\n*Direction:* ${side}`
26+
if (side == Direction.SELL) {
27+
28+
msg += `\n*Token Amount:* ${await humanizeBalance(data.fromTokenAmount, data.fromToken.decimals)}`
29+
msg += `\n*Token:* ${data.fromToken.name}`
30+
msg += `\n*ETH Amount:* ${await humanizeBalance(data.toTokenAmount, data.toToken.decimals)}`
31+
} else {
32+
msg += `\n*ETH Amount:* ${await humanizeBalance(data.fromTokenAmount, data.fromToken.decimals)}`
33+
msg += `\n*Token Amount:* ${await humanizeBalance(data.toTokenAmount, data.toToken.decimals}`
34+
msg += `\n*Token:* ${data.fromToken.name}`
35+
}
36+
msg += `\n*Profit PCT:* ${profit_pct.toFixed(6)}%`
37+
msg += `\n*Dex:* ${dexes}`
38+
msg += `\n*Gas Limit:* ${data.gasLimit}`
39+
msg += `\n*Hash:* [${data.hash.toUpperCase()}](${config.EXPLORER}${data.hash})`
40+
41+
return msg
742
}

0 commit comments

Comments
 (0)