Skip to content

Commit 3a6b22e

Browse files
committed
Initial commit
0 parents  commit 3a6b22e

File tree

7 files changed

+1629
-0
lines changed

7 files changed

+1629
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.env
2+
node_modules
3+
.parcel-cache
4+
dist

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Jup DCA Bot
2+
Based on the [Jupiter Core Example](https://github.com/jup-ag/jupiter-core-example)
3+
4+
A bot for dollar cost averaging using on-chain swaps through the [Jupiter Aggregator](https://jup.ag).

package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "jupiter-core-example",
3+
"version": "1.0.0",
4+
"description": "",
5+
"source": "src/index.ts",
6+
"main": "dist/main.js",
7+
"module": "dist/module.js",
8+
"types": "dist/types.d.ts",
9+
"scripts": {
10+
"start": "ts-node ./src/index.ts"
11+
},
12+
"keywords": [],
13+
"author": "",
14+
"license": "ISC",
15+
"dependencies": {
16+
"@jup-ag/core": "^1.0.0-beta.16",
17+
"@solana/wallet-adapter-base": "^0.7.1",
18+
"@solana/web3.js": "^1.31.0",
19+
"@types/bs58": "^4.0.1",
20+
"@types/isomorphic-fetch": "^0.0.35",
21+
"bs58": "^4.0.1",
22+
"dotenv": "^10.0.0",
23+
"isomorphic-fetch": "^3.0.0",
24+
"node-fetch": "^3.1.0",
25+
"ts-node": "^10.4.0"
26+
},
27+
"devDependencies": {
28+
"typescript": "^4.5.3"
29+
},
30+
"resolutions": {
31+
"@solana/buffer-layout": "4.0.0"
32+
}
33+
}

src/constants/index.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Cluster } from "@solana/web3.js";
2+
import bs58 from "bs58";
3+
import { Keypair } from "@solana/web3.js";
4+
5+
require("dotenv").config();
6+
7+
// Endpoints, connection
8+
export const ENV: Cluster = (process.env.CLUSTER as Cluster) || "mainnet-beta";
9+
10+
// Sometimes, your RPC endpoint may reject you if you spam too many RPC calls. Sometimes, your PRC server
11+
// may have invalid cache and cause problems.
12+
export const SOLANA_RPC_ENDPOINT =
13+
ENV === "devnet"
14+
? "https://api.devnet.solana.com"
15+
: "https://ssc-dao.genesysgo.net";
16+
17+
// Wallets
18+
export const WALLET_PRIVATE_KEY =
19+
process.env.WALLET_PRIVATE_KEY || "PASTE YOUR WALLET PRIVATE KEY";
20+
export const USER_PRIVATE_KEY = bs58.decode(WALLET_PRIVATE_KEY);
21+
export const USER_KEYPAIR = Keypair.fromSecretKey(USER_PRIVATE_KEY);
22+
23+
// Token Mints
24+
export const INPUT_MINT_ADDRESS =
25+
ENV === "devnet"
26+
? "So11111111111111111111111111111111111111112" // SOL
27+
: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC
28+
export const OUTPUT_MINT_ADDRESS =
29+
ENV === "devnet"
30+
? "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt" // SRM
31+
: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"; // USDT
32+
33+
// Interface
34+
export interface Token {
35+
chainId: number; // 101,
36+
address: string; // '8f9s1sUmzUbVZMoMh6bufMueYH1u4BJSM57RCEvuVmFp',
37+
symbol: string; // 'TRUE',
38+
name: string; // 'TrueSight',
39+
decimals: number; // 9,
40+
logoURI: string; // 'https://i.ibb.co/pKTWrwP/true.jpg',
41+
tags: string[]; // [ 'utility-token', 'capital-token' ]
42+
}

src/index.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { Connection, PublicKey } from "@solana/web3.js";
2+
import fetch from "isomorphic-fetch";
3+
4+
import { Jupiter, RouteInfo, TOKEN_LIST_URL } from "@jup-ag/core";
5+
import {
6+
ENV,
7+
INPUT_MINT_ADDRESS,
8+
OUTPUT_MINT_ADDRESS,
9+
SOLANA_RPC_ENDPOINT,
10+
Token,
11+
USER_KEYPAIR,
12+
} from "./constants";
13+
14+
const getPossiblePairsTokenInfo = ({
15+
tokens,
16+
routeMap,
17+
inputToken,
18+
}: {
19+
tokens: Token[];
20+
routeMap: Map<string, string[]>;
21+
inputToken?: Token;
22+
}) => {
23+
try {
24+
if (!inputToken) {
25+
return {};
26+
}
27+
28+
const possiblePairs = inputToken
29+
? routeMap.get(inputToken.address) || []
30+
: []; // return an array of token mints that can be swapped with SOL
31+
const possiblePairsTokenInfo: { [key: string]: Token | undefined } = {};
32+
possiblePairs.forEach((address) => {
33+
possiblePairsTokenInfo[address] = tokens.find((t) => {
34+
return t.address == address;
35+
});
36+
});
37+
// Perform your conditionals here to use other outputToken
38+
// const alternativeOutputToken = possiblePairsTokenInfo[USDT_MINT_ADDRESS]
39+
return possiblePairsTokenInfo;
40+
} catch (error) {
41+
throw error;
42+
}
43+
};
44+
45+
const getRoutes = async ({
46+
jupiter,
47+
inputToken,
48+
outputToken,
49+
inputAmount,
50+
slippage,
51+
}: {
52+
jupiter: Jupiter;
53+
inputToken?: Token;
54+
outputToken?: Token;
55+
inputAmount: number;
56+
slippage: number;
57+
}) => {
58+
try {
59+
if (!inputToken || !outputToken) {
60+
return null;
61+
}
62+
63+
console.log(
64+
`Getting routes for ${inputAmount} ${inputToken.symbol} -> ${outputToken.symbol}...`
65+
);
66+
const inputAmountInSmallestUnits = inputToken
67+
? Math.round(inputAmount * 10 ** inputToken.decimals)
68+
: 0;
69+
const routes =
70+
inputToken && outputToken
71+
? await jupiter.computeRoutes({
72+
inputMint: new PublicKey(inputToken.address),
73+
outputMint: new PublicKey(outputToken.address),
74+
inputAmount: inputAmountInSmallestUnits, // raw input amount of tokens
75+
slippage,
76+
forceFetch: true,
77+
})
78+
: null;
79+
80+
if (routes && routes.routesInfos) {
81+
console.log("Possible number of routes:", routes.routesInfos.length);
82+
console.log(
83+
"Best quote: ",
84+
routes.routesInfos[0].outAmount / 10 ** outputToken.decimals,
85+
`(${outputToken.symbol})`
86+
);
87+
return routes;
88+
} else {
89+
return null;
90+
}
91+
} catch (error) {
92+
throw error;
93+
}
94+
};
95+
96+
const executeSwap = async ({
97+
jupiter,
98+
route,
99+
}: {
100+
jupiter: Jupiter;
101+
route: RouteInfo;
102+
}) => {
103+
try {
104+
// Prepare execute exchange
105+
const { execute } = await jupiter.exchange({
106+
route,
107+
});
108+
109+
// Execute swap
110+
const swapResult: any = await execute(); // Force any to ignore TS misidentifying SwapResult type
111+
112+
if (swapResult.error) {
113+
console.log(swapResult.error);
114+
} else {
115+
console.log(`https://explorer.solana.com/tx/${swapResult.txid}`);
116+
console.log(
117+
`inputAddress=${swapResult.inputAddress.toString()} outputAddress=${swapResult.outputAddress.toString()}`
118+
);
119+
console.log(
120+
`inputAmount=${swapResult.inputAmount} outputAmount=${swapResult.outputAmount}`
121+
);
122+
}
123+
} catch (error) {
124+
throw error;
125+
}
126+
};
127+
128+
const main = async () => {
129+
try {
130+
const connection = new Connection(SOLANA_RPC_ENDPOINT); // Setup Solana RPC connection
131+
const tokens: Token[] = await (await fetch(TOKEN_LIST_URL[ENV])).json(); // Fetch token list from Jupiter API
132+
133+
// Load Jupiter
134+
const jupiter = await Jupiter.load({
135+
connection,
136+
cluster: ENV,
137+
user: USER_KEYPAIR, // or public key
138+
});
139+
140+
// Get routeMap, which maps each tokenMint and their respective tokenMints that are swappable
141+
const routeMap = jupiter.getRouteMap();
142+
143+
// If you know which input/output pair you want
144+
const inputToken = tokens.find((t) => t.address == INPUT_MINT_ADDRESS); // USDC Mint Info
145+
const outputToken = tokens.find((t) => t.address == OUTPUT_MINT_ADDRESS); // USDT Mint Info
146+
147+
// Alternatively, find all possible outputToken based on your inputToken
148+
const possiblePairsTokenInfo = await getPossiblePairsTokenInfo({
149+
tokens,
150+
routeMap,
151+
inputToken,
152+
});
153+
154+
const routes = await getRoutes({
155+
jupiter,
156+
inputToken,
157+
outputToken,
158+
inputAmount: .01, // 1 unit in UI
159+
slippage: 1, // 1% slippage
160+
});
161+
162+
// Routes are sorted based on outputAmount, so ideally the first route is the best.
163+
await executeSwap({ jupiter, route: routes!.routesInfos[0] });
164+
} catch (error) {
165+
console.log({ error });
166+
}
167+
};
168+
169+
main();

tsconfig.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES6",
4+
"lib": [
5+
"dom",
6+
"dom.iterable",
7+
"esnext"
8+
],
9+
"allowJs": true,
10+
"skipLibCheck": true,
11+
"strict": true,
12+
"forceConsistentCasingInFileNames": true,
13+
"noEmit": true,
14+
"esModuleInterop": true,
15+
"module": "commonjs",
16+
"moduleResolution": "node",
17+
"resolveJsonModule": true,
18+
"isolatedModules": true,
19+
"jsx": "preserve",
20+
"baseUrl": ".",
21+
"incremental": true
22+
},
23+
"include": [
24+
"next-env.d.ts",
25+
"**/*.ts",
26+
"**/*.tsx"
27+
],
28+
"exclude": [
29+
"node_modules"
30+
]
31+
}

0 commit comments

Comments
 (0)