Merge pull request #84 from fdundjer/feat/v2

feat: warp transactions
This commit is contained in:
Filip Dunđer
2024-04-17 10:02:45 +02:00
committed by GitHub
11 changed files with 179 additions and 43 deletions

View File

@ -9,10 +9,15 @@ COMMITMENT_LEVEL=confirmed
# Bot
LOG_LEVEL=debug
ONE_TOKEN_AT_A_TIME=true
COMPUTE_UNIT_LIMIT=421197
COMPUTE_UNIT_PRICE=101337
PRE_LOAD_EXISTING_MARKETS=false
CACHE_NEW_MARKETS=false
# default or warp
TRANSACTION_EXECUTOR=default
# if using default executor fee below will be applied
COMPUTE_UNIT_LIMIT=421197
COMPUTE_UNIT_PRICE=101337
# if using warp executor fee below will be applied
WARP_FEE=0.006
# Buy
QUOTE_MINT=WSOL
@ -27,7 +32,7 @@ MAX_SELL_RETRIES=10
AUTO_SELL_DELAY=0
PRICE_CHECK_INTERVAL=2000
PRICE_CHECK_DURATION=60000
TAKE_PROFIT=25
TAKE_PROFIT=20
STOP_LOSS=15
SELL_SLIPPAGE=5

View File

@ -40,6 +40,11 @@ You should see the following output:
- This option should not be used with public RPC.
- `CACHE_NEW_MARKETS` - Set to `true` to cache new markets.
- This option should not be used with public RPC.
- `TRANSACTION_EXECUTOR` - Set to `warp` to use warp infrastructure for executing transactions
- For more details checkout [warp](#warp-transactions-beta) section
- `WARP_FEE` - If using warp executor this value will be used for transaction fees instead of `COMPUTE_UNIT_LIMIT` and `COMPUTE_UNIT_LIMIT`
- Minimum value is 0.0001 SOL, but we recommend using 0.006 SOL or above
- On top of this fee, minimal solana network fee will be applied
#### Buy
- `QUOTE_MINT` - Amount used to buy each new token.
@ -66,12 +71,30 @@ You should see the following output:
- Pool must not exist before the script starts.
- `SNIPE_LIST_REFRESH_INTERVAL` - Interval in milliseconds to refresh the snipe list.
- `CHECK_IF_MINT_IS_RENOUNCED` - Set to `true` to buy tokens only if their mint is renounced.
- `CHECK_IF_BURNED` - Set to `true` to buy tokens only if their liqudity pool is burned.
- `MIN_POOL_SIZE` - Bot will buy only if the pool size is greater than the specified amount.
- `CHECK_IF_BURNED` - Set to `true` to buy tokens only if their liquidity pool is burned.
- `MIN_POOL_SIZE` - Bot will buy only if the pool size is greater than or equal the specified amount.
- Set `0` to disable.
- `MAX_POOL_SIZE` - Bot will buy only if the pool size is less than the specified amount.
- `MAX_POOL_SIZE` - Bot will buy only if the pool size is less than or equal the specified amount.
- Set `0` to disable.
## Warp transactions (beta)
In case you experience a lot of failed transactions or transaction performance is too slow, you can try using `warp` for executing transactions.
Warp is hosted service that executes transactions using integrations with third party providers.
Using warp for transactions supports the team behind this project.
### Security
When using warp, transaction is sent to the hosted service.
**Payload that is being sent will NOT contain your wallet private key**. Fee transaction is signed on your machine.
Each request is processed by hosted service and sent to third party provider.
**We don't store your transactions, nor we store your private key.**
Note: Warp transactions are disabled by default.
### Fees
When using warp for transactions, fee is distributed between developers of warp and third party providers.
In case TX fails, no fee will be taken from your account.
## Common issues
If you have an error which is not listed here, please create a new issue in this repository.
To collect more information on an issue, please change `LOG_LEVEL` to `debug`.

36
bot.ts
View File

@ -14,20 +14,14 @@ import {
RawAccount,
TOKEN_PROGRAM_ID,
} from '@solana/spl-token';
import {
Liquidity,
LiquidityPoolKeysV4,
LiquidityStateV4,
Percent,
Token,
TokenAmount,
} from '@raydium-io/raydium-sdk';
import { Liquidity, LiquidityPoolKeysV4, LiquidityStateV4, Percent, Token, TokenAmount } from '@raydium-io/raydium-sdk';
import { MarketCache, PoolCache, SnipeListCache } from './cache';
import { PoolFilters } from './filters';
import { TransactionExecutor } from './transactions';
import { createPoolKeys, logger, NETWORK, sleep } from './helpers';
import { Mutex } from 'async-mutex';
import BN from 'bn.js';
import { WarpTransactionExecutor } from './transactions/warp-transaction-executor';
export interface BotConfig {
wallet: Keypair;
@ -64,14 +58,17 @@ export class Bot {
// one token at the time
private readonly mutex: Mutex;
private sellExecutionCount = 0;
public readonly isWarp: boolean = false;
constructor(
private readonly connection: Connection,
private readonly marketStorage: MarketCache,
private readonly poolStorage: PoolCache,
private readonly txExecutor: TransactionExecutor,
private readonly config: BotConfig,
readonly config: BotConfig,
) {
this.isWarp = txExecutor instanceof WarpTransactionExecutor;
this.mutex = new Mutex();
this.poolFilters = new PoolFilters(connection, {
quoteToken: this.config.quoteToken,
@ -318,8 +315,12 @@ export class Bot {
payerKey: wallet.publicKey,
recentBlockhash: latestBlockhash.blockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: this.config.unitPrice }),
ComputeBudgetProgram.setComputeUnitLimit({ units: this.config.unitLimit }),
...(this.isWarp
? []
: [
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: this.config.unitPrice }),
ComputeBudgetProgram.setComputeUnitLimit({ units: this.config.unitLimit }),
]),
...(direction === 'buy'
? [
createAssociatedTokenAccountIdempotentInstruction(
@ -338,10 +339,15 @@ export class Bot {
const transaction = new VersionedTransaction(messageV0);
transaction.sign([wallet, ...innerTransaction.signers]);
return this.txExecutor.executeAndConfirm(transaction, latestBlockhash);
return this.txExecutor.executeAndConfirm(transaction, wallet, latestBlockhash);
}
private async priceMatch(amountIn: TokenAmount, poolKeys: LiquidityPoolKeysV4) {
if (this.config.priceCheckDuration === 0 || this.config.priceCheckInterval === 0) {
return;
}
const timesToCheck = this.config.priceCheckDuration / this.config.priceCheckInterval;
const profitFraction = this.config.quoteAmount.mul(this.config.takeProfit).numerator.div(new BN(100));
const profitAmount = new TokenAmount(this.config.quoteToken, profitFraction, true);
const takeProfit = this.config.quoteAmount.add(profitAmount);
@ -350,8 +356,6 @@ export class Bot {
const lossAmount = new TokenAmount(this.config.quoteToken, lossFraction, true);
const stopLoss = this.config.quoteAmount.subtract(lossAmount);
const slippage = new Percent(this.config.sellSlippage, 100);
const timesToCheck = this.config.priceCheckDuration / this.config.priceCheckInterval;
let timesChecked = 0;
do {
@ -374,11 +378,11 @@ export class Bot {
`Take profit: ${takeProfit.toFixed()} | Stop loss: ${stopLoss.toFixed()} | Current: ${amountOut.toFixed()}`,
);
if (amountOut.lt(stopLoss)){
if (amountOut.lt(stopLoss)) {
break;
}
if (amountOut.gt(takeProfit)){
if (amountOut.gt(takeProfit)) {
break;
}

View File

@ -18,7 +18,7 @@ export class PoolSizeFilter implements Filter {
let inRange = true;
if (!this.maxPoolSize?.isZero()) {
inRange = poolSize.lt(this.maxPoolSize);
inRange = poolSize.raw.lte(this.maxPoolSize.raw);
if (!inRange) {
return { ok: false, message: `PoolSize -> Pool size ${poolSize.toFixed()} > ${this.maxPoolSize.toFixed()}` };
@ -26,7 +26,7 @@ export class PoolSizeFilter implements Filter {
}
if (!this.minPoolSize?.isZero()) {
inRange = poolSize.gt(this.minPoolSize);
inRange = poolSize.raw.gte(this.minPoolSize.raw);
if (!inRange) {
return { ok: false, message: `PoolSize -> Pool size ${poolSize.toFixed()} < ${this.minPoolSize.toFixed()}` };

View File

@ -30,6 +30,8 @@ export const COMPUTE_UNIT_LIMIT = Number(retrieveEnvVariable('COMPUTE_UNIT_LIMIT
export const COMPUTE_UNIT_PRICE = Number(retrieveEnvVariable('COMPUTE_UNIT_PRICE', logger));
export const PRE_LOAD_EXISTING_MARKETS = retrieveEnvVariable('PRE_LOAD_EXISTING_MARKETS', logger) === 'true';
export const CACHE_NEW_MARKETS = retrieveEnvVariable('CACHE_NEW_MARKETS', logger) === 'true';
export const TRANSACTION_EXECUTOR = retrieveEnvVariable('TRANSACTION_EXECUTOR', logger);
export const WARP_FEE = retrieveEnvVariable('WARP_FEE', logger);
// Buy
export const AUTO_BUY_DELAY = Number(retrieveEnvVariable('AUTO_BUY_DELAY', logger));

View File

@ -4,7 +4,7 @@ import { Connection, KeyedAccountInfo, Keypair } from '@solana/web3.js';
import { LIQUIDITY_STATE_LAYOUT_V4, MARKET_STATE_LAYOUT_V3, Token, TokenAmount } from '@raydium-io/raydium-sdk';
import { AccountLayout, getAssociatedTokenAddressSync } from '@solana/spl-token';
import { Bot, BotConfig } from './bot';
import { DefaultTransactionExecutor } from './transactions';
import { DefaultTransactionExecutor, TransactionExecutor } from './transactions';
import {
getToken,
getWallet,
@ -36,16 +36,20 @@ import {
BUY_SLIPPAGE,
SELL_SLIPPAGE,
PRICE_CHECK_DURATION,
PRICE_CHECK_INTERVAL, SNIPE_LIST_REFRESH_INTERVAL,
PRICE_CHECK_INTERVAL,
SNIPE_LIST_REFRESH_INTERVAL,
TRANSACTION_EXECUTOR,
WARP_FEE,
} from './helpers';
import { version } from './package.json';
import { WarpTransactionExecutor } from './transactions/warp-transaction-executor';
const connection = new Connection(RPC_ENDPOINT, {
wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
commitment: COMMITMENT_LEVEL,
});
function printDetails(wallet: Keypair, quoteToken: Token, botConfig: BotConfig) {
function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) {
logger.info(`
.. :-===++++-
.-==+++++++- =+++++++++-
@ -64,12 +68,22 @@ function printDetails(wallet: Keypair, quoteToken: Token, botConfig: BotConfig)
Version: ${version}
`);
const botConfig = bot.config;
logger.info('------- CONFIGURATION START -------');
logger.info(`Wallet: ${wallet.publicKey.toString()}`);
logger.info('- Bot -');
logger.info(`Compute Unit limit: ${botConfig.unitLimit}`);
logger.info(`Compute Unit price (micro lamports): ${botConfig.unitPrice}`);
logger.info(`Using warp: ${bot.isWarp}`);
if (bot.isWarp) {
logger.info(`Warp fee: ${WARP_FEE}`);
}
else {
logger.info(`Compute Unit limit: ${botConfig.unitLimit}`);
logger.info(`Compute Unit price (micro lamports): ${botConfig.unitPrice}`);
}
logger.info(`Single token at the time: ${botConfig.oneTokenAtATime}`);
logger.info(`Pre load existing markets: ${PRE_LOAD_EXISTING_MARKETS}`);
logger.info(`Cache new markets: ${CACHE_NEW_MARKETS}`);
@ -111,7 +125,19 @@ const runListener = async () => {
const marketCache = new MarketCache(connection);
const poolCache = new PoolCache();
const txExecutor = new DefaultTransactionExecutor(connection);
let txExecutor: TransactionExecutor;
switch (TRANSACTION_EXECUTOR) {
case 'warp': {
txExecutor = new WarpTransactionExecutor(WARP_FEE);
break;
}
default: {
txExecutor = new DefaultTransactionExecutor(connection);
break;
}
}
const wallet = getWallet(PRIVATE_KEY.trim());
const quoteToken = getToken(QUOTE_MINT);
const botConfig = <BotConfig>{
@ -186,7 +212,7 @@ const runListener = async () => {
await bot.sell(updatedAccountInfo.accountId, accountData);
});
printDetails(wallet, quoteToken, botConfig);
printDetails(wallet, quoteToken, bot);
};
runListener();

13
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@solana/spl-token": "^0.4.0",
"@solana/web3.js": "^1.89.1",
"async-mutex": "^0.5.0",
"axios": "^1.6.8",
"bigint-buffer": "^1.1.5",
"bip39": "^3.1.0",
"bn.js": "^5.2.1",
@ -396,10 +397,11 @@
}
},
"node_modules/axios": {
"version": "1.6.7",
"license": "MIT",
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
"dependencies": {
"follow-redirects": "^1.15.4",
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
@ -724,14 +726,15 @@
"license": "MIT"
},
"node_modules/follow-redirects": {
"version": "1.15.5",
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},

View File

@ -12,6 +12,7 @@
"@solana/spl-token": "^0.4.0",
"@solana/web3.js": "^1.89.1",
"async-mutex": "^0.5.0",
"axios": "^1.6.8",
"bigint-buffer": "^1.1.5",
"bip39": "^3.1.0",
"bn.js": "^5.2.1",

View File

@ -1,4 +1,10 @@
import { BlockhashWithExpiryBlockHeight, Connection, Transaction, VersionedTransaction } from '@solana/web3.js';
import {
BlockhashWithExpiryBlockHeight,
Connection,
Keypair,
Transaction,
VersionedTransaction,
} from '@solana/web3.js';
import { TransactionExecutor } from './transaction-executor.interface';
import { logger } from '../helpers';
@ -6,9 +12,10 @@ export class DefaultTransactionExecutor implements TransactionExecutor {
constructor(private readonly connection: Connection) {}
public async executeAndConfirm(
transaction: Transaction | VersionedTransaction,
transaction: VersionedTransaction,
payer: Keypair,
latestBlockhash: BlockhashWithExpiryBlockHeight,
): Promise<{ confirmed: boolean; signature: string }> {
): Promise<{ confirmed: boolean; signature?: string }> {
logger.debug('Executing transaction...');
const signature = await this.execute(transaction);

View File

@ -1,8 +1,9 @@
import { BlockhashWithExpiryBlockHeight, Transaction, VersionedTransaction } from '@solana/web3.js';
import { BlockhashWithExpiryBlockHeight, Keypair, MessageV0, Signer, VersionedTransaction } from '@solana/web3.js';
export interface TransactionExecutor {
executeAndConfirm(
transaction: Transaction | VersionedTransaction,
latestBlockhash: BlockhashWithExpiryBlockHeight,
): Promise<{ confirmed: boolean; signature: string }>;
transaction: VersionedTransaction,
payer: Keypair,
latestBlockHash: BlockhashWithExpiryBlockHeight,
): Promise<{ confirmed: boolean; signature?: string }>;
}

View File

@ -0,0 +1,64 @@
import {
BlockhashWithExpiryBlockHeight,
Keypair,
PublicKey,
SystemProgram,
TransactionMessage,
VersionedTransaction,
} from '@solana/web3.js';
import { TransactionExecutor } from './transaction-executor.interface';
import { logger } from '../helpers';
import axios, { AxiosError } from 'axios';
import bs58 from 'bs58';
import { Currency, CurrencyAmount } from '@raydium-io/raydium-sdk';
export class WarpTransactionExecutor implements TransactionExecutor {
private readonly warpFeeWallet = new PublicKey('WARPzUMPnycu9eeCZ95rcAUxorqpBqHndfV3ZP5FSyS');
constructor(private readonly warpFee: string) {}
public async executeAndConfirm(
transaction: VersionedTransaction,
payer: Keypair,
latestBlockhash: BlockhashWithExpiryBlockHeight,
): Promise<{ confirmed: boolean; signature?: string }> {
logger.debug('Executing transaction...');
try {
const fee = new CurrencyAmount(Currency.SOL, this.warpFee, false).raw.toNumber();
const warpFeeMessage = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: latestBlockhash.blockhash,
instructions: [
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: this.warpFeeWallet,
lamports: fee,
}),
],
}).compileToV0Message();
const warpFeeTx = new VersionedTransaction(warpFeeMessage);
warpFeeTx.sign([payer]);
const response = await axios.post<{ confirmed: boolean; signature: string }>(
'https://tx.warp.id/transaction/execute',
{
transactions: [bs58.encode(warpFeeTx.serialize()), bs58.encode(transaction.serialize())],
latestBlockhash,
},
{
timeout: 100000,
},
);
return response.data;
} catch (error) {
if (error instanceof AxiosError) {
logger.trace({ error: error.response?.data }, 'Failed to execute warp transaction');
}
}
return { confirmed: false };
}
}