From 425da502c18f050fb99e69e3a8bee1ce390886dd Mon Sep 17 00:00:00 2001 From: 0xed123 Date: Wed, 27 Mar 2024 07:24:49 +0900 Subject: [PATCH] feat: build raydium functions on it --- README.md | 154 +++- config.ts | 65 ++ package.json | 35 + src/base/baseMpl.ts | 320 ++++++++ src/base/baseRay.ts | 915 ++++++++++++++++++++++ src/base/baseSpl.ts | 412 ++++++++++ src/base/getMarketAccountSizes.ts | 54 ++ src/base/orderbookUtils.ts | 63 ++ src/base/types.ts | 15 + src/base/utils.ts | 55 ++ src/constants.ts | 53 ++ src/index.ts | 1220 +++++++++++++++++++++++++++++ src/jito_bundle/build-bundle.ts | 200 +++++ src/jito_bundle/send-bundle.ts | 42 + src/txHandler.ts | 879 +++++++++++++++++++++ src/types.ts | 78 ++ src/utils.ts | 122 +++ tsconfig.json | 25 + 18 files changed, 4705 insertions(+), 2 deletions(-) create mode 100644 config.ts create mode 100644 package.json create mode 100644 src/base/baseMpl.ts create mode 100644 src/base/baseRay.ts create mode 100644 src/base/baseSpl.ts create mode 100644 src/base/getMarketAccountSizes.ts create mode 100644 src/base/orderbookUtils.ts create mode 100644 src/base/types.ts create mode 100644 src/base/utils.ts create mode 100644 src/constants.ts create mode 100644 src/index.ts create mode 100644 src/jito_bundle/build-bundle.ts create mode 100644 src/jito_bundle/send-bundle.ts create mode 100644 src/txHandler.ts create mode 100644 src/types.ts create mode 100644 src/utils.ts create mode 100644 tsconfig.json diff --git a/README.md b/README.md index 3dcd830b..ac6ae43e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,152 @@ -# Shitcoin-bot -This is the bot connecting with Raydium SDK +--base-amount is initial base token liquidity + +--quote-amount is initial quote token liquidity + +--buy-token which token you want to buy (ans in base or quote) + +--buy-amount for how many token instantly you want to buy just after the pool creation + + + + +### Market creation +```cmd +node ./dist/index.js createmarket --base J1bREnQ2HPHkVuiKgGeGCmdW6P3SPvPgCvj5ii4oYHJg --quote So11111111111111111111111111111111111111112 --order-size 0.01 --price-tick 0.01 --url mainnet +``` + +### Create Pool And Buy: +```cmd +node ./dist/index.js create-and-buy --market "C5XyB8Jb6wNoyPDFkFoFzsJEojPwK1cRHzjs6bwTmi3C" --base-amount 1000000000 --quote-amount 8 --buy-token "base" --buy-amount 480000000 +``` + +### Remove Liquidity +```cmd +node ./dist/index.js removeliquidity --pool 2MMjLmVMBWChxfWT3baZ3Xq57c1Z3UyVoQUZKuz7B6BC --amount -1 --url mainnet +``` + +### creates pool, adds liq, removes liq: +'''cmd +node ./dist/index.js createpool-remove --market "FLxNH1ciyL1LNfbWm9aMBS235K9ti2iyGd9SNdwECkzU" --base-amount 850000000 --quote-amount 8 --delay-seconds 0 + + +### creates pool, adds liq, removes liq ncludes bundle buys: +'''cmd +node ./dist/index.js createpool-buy-remove --market "48YUgRMkxk7fGSVdxMWzJVgoBJ1jDtp9nvkPgHXCo7RE" --base-amount 850000000 --quote-amount 8 --buy-token "base" --buy-amount 350000000 --delay-seconds 1 + + +### Unwrap Sol: +```cmd +node ./dist/index.js unwrap --url mainnet +``` + + + + + + +### Token creation +```cmd +export NODE_PATH=./dist && node ./dist/index.js createtoken --name "TOKEN_NAME" --symbol "TOKEN_SYMBOL" --image "TOKEN_IMAGE_LINK" --decimals 5 --website "web_link" --initial-minting 10000 --url devnet +``` + +### Market creation +```cmd +export NODE_PATH=./dist && node ./dist/index.js createmarket --base BASE_TOKEN_ADDRESS --quote QUOTE_TOKEN_ADDRESS --order-size 0.01 --price-tick 0.1 --url devnet +``` + +### Pool creation +```cmd +export NODE_PATH=./dist && node ./dist/index.js createpool --market MAREKET_ID --baseAmount 100 --quoteAmount 1 --url devnet +``` + +### Buy +```cmd +export NODE_PATH=./dist && node ./dist/index.js buy --pool POOL_ID --buy-token 'base' --amount 100 +``` + +### Sell +```cmd +export NODE_PATH=./dist && node ./dist/index.js buy --pool POOL_ID --sell-token 'base' --amount 100 +``` + +### Add Liquidity +```cmd +export NODE_PATH=./dist && node ./dist/index.js addliquidity --pool POOL_ID --amount 100 --amount-side 'base' +``` + +### Remove Liquidity +```cmd +export NODE_PATH=./dist && node ./dist/index.js removeliquidity --pool POOL_ID --amount -1 --url 'devnet' +``` + +### DEVELOPMENT +``` +export NODE_PATH=./dist +node ./dist/index.js createtoken --name "NAME" --symbol "SYMBOL" --decimals 5 --image "https://www.google.com" --website "https://www.google.com" --initial-minting 10000 --url devnet +node ./dist/index.js createmarket --order-size 0.01 --price-tick 0.1 --url devnet --quote "So11111111111111111111111111111111111111112" --base "" +node ./dist/index.js createpool --baseAmount 10000 --quoteAmount 1 --url devnet --market "" +node ./dist/index.js removeliquidity --amount -1 --url devnet --pool "" +``` + +### RUNNING FOR REAL +``` +export NODE_PATH=./dist +node ./dist/index.js createtoken --name "SideEyeDog" --symbol "SID" --decimals 5 --initial-minting 1000000000 --image "https://i.kym-cdn.com/photos/images/newsfeed/002/418/775/f5d.jpeg" --website "https://knowyourmeme.com/memes/side-eye-dog" --twitter "" --telegram "" --description "" +node ./dist/index.js createmarket --order-size 0.01 --price-tick 0.1 --quote "So11111111111111111111111111111111111111112" --base "" +node ./dist/index.js revokeauth --token "" +node ./dist/index.js createpool --baseAmount 300000000 --quoteAmount 10 --market "" +node ./dist/index.js removeliquidity --amount -1 --pool "" +node ./dist/index.js unwrap +``` + +### COOKS +``` +export NODE_PATH=./dist + +node ./dist/index.js createtoken --name "dogwifcrown" --symbol "WIC" --decimals 5 --image "https://pbs.twimg.com/media/GHtutvaWMAAhujZ?format=jpg&name=small" --website "" --twitter "https://twitter.com/blknoiz06/status/1764125414815842519" --telegram "" --description "" --initial-minting 1000000000 + +node ./dist/index.js createtoken --name "SideEyeDog" --symbol "SED" --decimals 5 --initial-minting 1000000000 --image "https://i.kym-cdn.com/photos/images/newsfeed/002/418/775/f5d.jpeg" --website "https://knowyourmeme.com/memes/side-eye-dog" --twitter "" --telegram "" --description "" + +node ./dist/index.js createtoken --name "Shiba Inu" --symbol "SHIB" --decimals 5 --image "https://pbs.twimg.com/profile_images/1764023553505054720/u3Gy4BZd_400x400.jpg" --website "" --twitter "https://twitter.com/shibainucoinsol" --telegram "http://t.me/ShibaInuCoinSol" --description "" --initial-minting 1000000000 + +node ./dist/index.js createtoken --name "MAGA" --symbol "TRUMP" --decimals 5 --initial-minting 1000000000 --image "" --website "" --twitter "" --telegram "" --description "" + +node ./dist/index.js createtoken --name "bellgetes" --symbol "getes" --decimals 5 --initial-minting 1000000000 --image "https://bellgetes.xyz/images/DDDTT.jpg" --website "https://bellgetes.xyz/" --twitter "" --telegram "" --description "" + +node ./dist/index.js createtoken --name "waranboofut" --symbol "boofut" --decimals 5 --initial-minting 1000000000 --image "https://waranboofut.xyz/images/1.jpg" --website "https://waranboofut.xyz/" --twitter "" --telegram "" --description "" + +node ./dist/index.js createtoken --name "dagecoin" --symbol "dagecoin" --decimals 5 --initial-minting 1000000000 --image "https://dagecoin.xyz/assets/images/logo-coin.PNG" --website "https://dagecoin.xyz/" --twitter "" --telegram "" --description "" + +node ./dist/index.js createtoken --name "keawnu weeves" --symbol "neow" --decimals 5 --initial-minting 1000000000 --image "https://neow.fun/assets/images/logo-coin.PNG" --website "https://neow.fun/" --twitter "" --telegram "" --description "" + +``` + +export NODE_PATH=./dist && node ./dist/index.js createmarket --order-size 0.01 --price-tick 0.1 --quote "So11111111111111111111111111111111111111112" --base "" +export NODE_PATH=./dist && node ./dist/index.js revokeauth --token "E89PFXFkxAyXhdocLszAxVS35jERhMqDyDjsZiFsbgi" +export NODE_PATH=./dist && node ./dist/index.js createpool --baseAmount 850000000 --quoteAmount 20 --market "" +export NODE_PATH=./dist && node ./dist/index.js removeliquidity --amount -1 --pool "" +export NODE_PATH=./dist && node ./dist/index.js unwrap + + +### RUN ALLS +``` +node ./dist/index.js run --name "dogwifcrown" --symbol "WIC" --decimals 5 --initial-minting 1000000000 --image "https://pbs.twimg.com/media/GHtutvaWMAAhujZ?format=jpg&name=small" --website "" --twitter "https://twitter.com/blknoiz06/status/1764125414815842519" --telegram "" --description "" --order-size 0.01 --price-tick 0.1 --baseAmount 850000000 --quoteAmount 8 + +node ./dist/index.js run --name "SideEyeDog" --symbol "SIDEEYEDOG" --decimals 5 --initial-minting 1000000000 --image "https://i.kym-cdn.com/photos/images/newsfeed/002/418/775/f5d.jpeg" --website https://knowyourmeme.com"/memes/side-eye-dog" --twitter "" --telegram "" --description "" --order-size 0.01 --price-tick 0.1 --baseAmount 850000000 --quoteAmount 8 + +node ./dist/index.js run --name "bellgetes" --symbol "getes" --decimals 5 --initial-minting 1000000000 --image "https://bellgetes.xyz/images/DDDTT.jpg" --website "https://bellgetes.xyz/" --twitter "https://twitter.com/BellGetesSol" --telegram "" --description "" --order-size 0.01 --price-tick 0.1 --baseAmount 850000000 --quoteAmount 8 + +node ./dist/index.js run --name "waranboofut" --symbol "boofut" --decimals 5 --initial-minting 1000000000 --image "https://waranboofut.xyz/images/1.jpg" --website "https://waranboofut.xyz/" --twitter "https://x.com/waranbufoofut?s=21&t=U_NbbXAlEnKwM5KHqJxJ9w" --telegram "https://t.me/waranboofut" --description "" --order-size 0.01 --price-tick 0.1 --baseAmount 850000000 --quoteAmount 8 + +node ./dist/index.js run --name "dagecoin" --symbol "https://neow.fun/" --decimals 5 --initial-minting 1000000000 --image "https://dagecoin.xyz/assets/images/logo-coin.PNG" --website "https://dagecoin.xyz/" --twitter "https://twitter.com/DageCoinOnSol" --telegram "https://t.me/dagecoinchat" --description "" --order-size 0.01 --price-tick 0.1 --baseAmount 850000000 --quoteAmount 8 + +node ./dist/index.js run --name "dagecoin" --symbol "dagecoin" --decimals 5 --initial-minting 1000000000 --image "https://dagecoin.xyz/assets/images/logo-coin.PNG" --website "https://dagecoin.xyz/" --twitter "https://twitter.com/DageCoinOnSol" --telegram "https://t.me/dagecoinchat" --description "" --order-size 0.01 --price-tick 0.1 --baseAmount 850000000 --quoteAmount 8 + +node ./dist/index.js run --name "dagecoin" --symbol "dagecoin" --decimals 5 --initial-minting 1000000000 --image "https://dagecoin.xyz/assets/images/logo-coin.PNG" --website "https://dagecoin.xyz/" --twitter "https://twitter.com/DageCoinOnSol" --telegram "https://t.me/dagecoinchat" --description "" --order-size 0.01 --price-tick 0.1 --baseAmount 850000000 --quoteAmount 8 + +node ./dist/index.js run --name "dagecoin" --symbol "dagecoin" --decimals 5 --initial-minting 1000000000 --image "https://dagecoin.xyz/assets/images/logo-coin.PNG" --website "https://dagecoin.xyz/" --twitter "https://twitter.com/DageCoinOnSol" --telegram "https://t.me/dagecoinchat" --description "" --order-size 0.01 --price-tick 0.1 --baseAmount 850000000 --quoteAmount 8 + +node ./dist/index.js run --name "dagecoin" --symbol "dagecoin" --decimals 5 --initial-minting 1000000000 --image "https://dagecoin.xyz/assets/images/logo-coin.PNG" --website "https://dagecoin.xyz/" --twitter "https://twitter.com/DageCoinOnSol" --telegram "https://t.me/dagecoinchat" --description "" --order-size 0.01 --price-tick 0.1 --baseAmount 850000000 --quoteAmount 8 + +https://www.solanabag.net/ BAG BAG +``` diff --git a/config.ts b/config.ts new file mode 100644 index 00000000..3c60bae3 --- /dev/null +++ b/config.ts @@ -0,0 +1,65 @@ +import { Connection, PublicKey, Keypair } from "@solana/web3.js"; +import { + TxVersion, Token,Currency, + TOKEN_PROGRAM_ID, + SOL, + CacheLTA, + LOOKUP_TABLE_CACHE +} from "@raydium-io/raydium-sdk"; +import * as bs58 from 'bs58'; + + +// define these +export const blockEngineUrl = 'tokyo.mainnet.block-engine.jito.wtf'; +const jito_auth_private_key = "aaaaaaaaaaaaaaaa"; +const wallet_2_pay_jito_fees = "aaaaaaaaaaaaaaaa"; + + +const LP_wallet_private_key = "aaaaaaaaaaaaaaaa"; + +const swap_wallet_private_key = "aaaaaaaaaaaaaaaa"; + +export const rpc_https_url = "http://mainnet.helius-rpc.com/?api-key=aaaaaaaaaaaaaaaaaaaaaa"; + + +export const market_id = new PublicKey("aaaaaaaaaaaaaaaaaaag6snCe2iUR3A"); +export const input_baseMint_tokens_percentage = 1; //ABC-Mint amount of tokens you want to add in Lp e.g. 1% = 100%. 0.9= 90% +export const delay_pool_open_time = Number(0); //dont change it because then you wont be able to perform swap in bundle. +export let quote_Mint_amount = 0.5; //COIN-SOL, amount of SOL u want to add to Pool amount + +// remove lp: +export const LP_remove_tokens_percentage = 1; //ABC-Mint amount of tokens in Lp that you want to remove e.g. 1% = 100%. 0.9= 90% +export const LP_remove_tokens_take_profit_at_sol = 2; //I want to remove all lp when sol reached 2 SOL + + + +// swap info: +export const swap_sol_amount = 0.5; //Amount of SOl u want to invest +export const sell_swap_tokens_percentage = 0.5; // % of tokens u want to sell=> 1 means 100% +export const sell_swap_take_profit_ratio = 2; // take profit e.g. 2x 3x + +// swap sell and remove lp fees in lamports. +export const sell_remove_fees = 5000000; + + +// ignore these +export const jito_auth_keypair = Keypair.fromSecretKey(new Uint8Array(bs58.decode(jito_auth_private_key))); +export const wallet_2_pay_jito_fees_keypair = Keypair.fromSecretKey(new Uint8Array(bs58.decode(wallet_2_pay_jito_fees))); + +export const LP_wallet_keypair = Keypair.fromSecretKey(new Uint8Array(bs58.decode(LP_wallet_private_key))); +export const swap_wallet_keypair = Keypair.fromSecretKey(new Uint8Array(bs58.decode(swap_wallet_private_key))); + +export const lookupTableCache= {} +export const connection = new Connection(rpc_https_url, "confirmed"); +export const makeTxVersion = TxVersion.V0 // LEGACY +export const addLookupTableInfo = LOOKUP_TABLE_CACHE // only mainnet. other = undefined + + + +export const DEFAULT_TOKEN = { + SOL: SOL, + SOL1: new Currency(9, 'USDC', 'USDC'), + WSOL: new Token(TOKEN_PROGRAM_ID, new PublicKey('So11111111111111111111111111111111111111112'), 9, 'WSOL', 'WSOL'), + USDC: new Token(TOKEN_PROGRAM_ID, new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'), 6, 'USDC', 'USDC'), + + } \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..742655e8 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "sol_cli", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc", + "start": "npx tsc && export NODE_PATH=./dist && node ./dist/index.js", + "dev": "nodemon ./src/index.ts" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@metaplex-foundation/js": "^0.20.1", + "@metaplex-foundation/mpl-token-metadata": "^2.13.0", + "@openbook-dex/openbook": "^0.0.9", + "@project-serum/anchor": "^0.26.0", + "@raydium-io/raydium-sdk": "^1.3.1-beta.47", + "@solana/spl-token": "^0.3.8", + "axios": "^1.6.7", + "bigint-buffer": "^1.1.5", + "jito-ts": "^3.0.1", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@types/bn.js": "^5.1.0", + "@types/debug": "^4.1.8", + "@types/yargs": "^17.0.32", + "dotenv": "^16.3.1", + "nodemon": "^3.0.3", + "ts-node": "^10.9.2", + "typescript": "^5.2.2" + }, + "description": "" +} diff --git a/src/base/baseMpl.ts b/src/base/baseMpl.ts new file mode 100644 index 00000000..e79c22b3 --- /dev/null +++ b/src/base/baseMpl.ts @@ -0,0 +1,320 @@ +import { AnchorProvider, BN, Wallet, web3 } from "@project-serum/anchor"; +import { utf8 } from "@project-serum/anchor/dist/cjs/utils/bytes"; +import { BaseSpl } from "./baseSpl"; +import { calcNonDecimalValue, deployJsonData, getKeypairFromEnv, sendAndConfirmTransaction, sleep } from "../utils"; +import { ENV, RPC_ENDPOINT_DEV, RPC_ENDPOINT_MAIN } from "../constants"; + +import { TokenMetadataAuthorizationDetails, getAccountParsingAndAssertingFunction, Sft } from '@metaplex-foundation/js/dist/types'; +import { + PROGRAM_ID as MPL_ID, + Metadata, + TokenStandard, + createUpdateMetadataAccountV2Instruction, + createUpdateInstruction +} from "@metaplex-foundation/mpl-token-metadata"; + +import { + CreateNftBuilderParams, + Metaplex +} from "@metaplex-foundation/js"; +import { MintLayout, TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { MPLTokenInfo } from "./types"; + +const log = console.log; + + +type MPLTransferInput = { + mint: web3.PublicKey | string, + sender: web3.PublicKey | string, + receiver: web3.PublicKey | string, + /** default it take a single one token easy for NFT, SFT */ + amount?: number + /** default (`true`)*/ + init_ata_if_needed?: boolean + tokenStandard: TokenStandard + /** default (`false`)*/ + isPNFT?: boolean +} + +type BurnInput = { + mint: web3.PublicKey | string, + owner: web3.PublicKey | string, + /** default it burn a single one token easy for NFT, SFT */ + amount?: number + /** default(`get from the onchain data`) */ + decimal?: number +} + +export class BaseMpl { + connection: web3.Connection; + mplIxs: web3.TransactionInstruction[] = []; + mplSigns: web3.Keypair[] = []; + metaplex: Metaplex; + provider: AnchorProvider + baseSpl: BaseSpl + + constructor(wallet: Wallet, web3Config: { endpoint: string }) { + this.connection = new web3.Connection(web3Config.endpoint, { commitment: 'confirmed' }); + this.metaplex = new Metaplex(this.connection); + this.provider = new AnchorProvider(this.connection, wallet, { commitment: 'confirmed' }); + this.baseSpl = new BaseSpl(this.connection) + + if (this.metaplex.identity().publicKey.toBase58() != wallet.publicKey.toBase58()) { + this.metaplex.identity().setDriver({ + publicKey: wallet.publicKey, + signMessage: null as any, //TODO: Need to improve it + signTransaction: wallet.signTransaction, + signAllTransactions: wallet.signAllTransactions, + }); + } + } + + setUpCallBack = ( + ixs: web3.TransactionInstruction[], + signs: web3.Keypair[] + ) => { + if (ixs) { + this.mplIxs.push(...ixs); + log("ixs added to mpl : ", ixs); + } + if (signs) { + log("sings added to mpl : ", signs); + this.mplSigns.push(...signs); + } + }; + + reinit(wallet: Wallet): void { + const user = wallet.publicKey + if (this.metaplex.identity().publicKey.toBase58() != user.toBase58()) { + this.metaplex.identity().setDriver({ + publicKey: user, + signMessage: (wallet as any).signMessage, + signTransaction: wallet.signTransaction, + signAllTransactions: wallet.signAllTransactions, + }); + } + this.mplIxs = []; + this.mplSigns = [] + this.provider = new AnchorProvider(this.connection, wallet, { commitment: 'confirmed' }); + } + + static getEditionAccount(tokenId: web3.PublicKey) { + return web3.PublicKey.findProgramAddressSync( + [ + utf8.encode("metadata"), + MPL_ID.toBuffer(), + tokenId.toBuffer(), + utf8.encode("edition"), + ], + MPL_ID + )[0]; + } + + static getMetadataAccount(tokenId: web3.PublicKey) { + return web3.PublicKey.findProgramAddressSync( + [utf8.encode("metadata"), MPL_ID.toBuffer(), tokenId.toBuffer()], + MPL_ID + )[0]; + } + + static getCollectionAuthorityRecordAccount(collection: web3.PublicKey, authority: web3.PublicKey): web3.PublicKey { + return web3.PublicKey.findProgramAddressSync( + [ + utf8.encode("metadata"), + MPL_ID.toBuffer(), + collection.toBuffer(), + utf8.encode("collection_authority"), + authority.toBuffer() + ], + MPL_ID + )[0]; + } + + async createToken(input: CreateNftBuilderParams, opts: { decimal?: number, mintAmount?: number, mintKeypair?: web3.Keypair, revokeAuthorities?: boolean }) { + const ixs = []; + const user = this?.provider?.publicKey; + const baseSpl = new BaseSpl(this.connection); + let { decimal, mintAmount, mintKeypair } = opts; + decimal = decimal ?? 0; + try { + mintKeypair = mintKeypair ?? web3.Keypair.generate(); + let { ixs: mintIxs, mintKeypair: _mintKeypair } = await baseSpl.createToken({ decimal, mintAuthority: user, mintingInfo: { tokenAmount: mintAmount }, mintKeypair }) + ixs.push(...mintIxs) + input.useNewMint = mintKeypair; + input.mintTokens = false; + const mint = mintKeypair.publicKey; + const txBuilder = await this.metaplex.nfts().builders().create(input); + const setMetadataIxs = txBuilder.getInstructions(); + ixs.push(...setMetadataIxs) + + // speedup + const updateCuIx = web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: ENV.COMPUTE_UNIT_PRICE }) + const recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; + if (opts.revokeAuthorities) { + ixs.push(this.baseSpl.revokeAuthority({ authorityType: 'MINTING', currentAuthority: this.provider.publicKey, mint })) + ixs.push(this.baseSpl.revokeAuthority({ authorityType: 'FREEZING', currentAuthority: this.provider.publicKey, mint })) + } + const tx = new web3.Transaction().add(updateCuIx, ...ixs); + tx.feePayer = user; + tx.recentBlockhash = recentBlockhash + tx.sign(getKeypairFromEnv()) + tx.sign(mintKeypair) + + const res = await this.provider.sendAndConfirm(tx, [mintKeypair], { maxRetries: 20 }).catch(async () => { + await sleep(2_000) + return this.provider.sendAndConfirm(tx, [mintKeypair!], { maxRetries: 20 }).catch((createTokenSendAndConfirmError) => { + log({ createTokenSendAndConfirmError }) + return null + }) + }); + // const res = await sendAndConfirmTransaction(tx, this.connection).catch((sendAndConfirmTransactionError) => { + // log({ sendAndConfirmTransactionError }) + // return null + // }) + if (!res) throw "Tx failed" + return { + txSignature: res, + token: mintKeypair.publicKey.toBase58() + } + } catch (error) { + log({ mplTokenCreateError: error }) + return null + } + } + + async transfer(input: MPLTransferInput) { + let { + mint, + receiver, + sender, + amount, + tokenStandard, + isPNFT + } = input; + if (typeof mint == 'string') mint = new web3.PublicKey(mint) + if (typeof sender == 'string') sender = new web3.PublicKey(sender) + if (typeof receiver == 'string') receiver = new web3.PublicKey(receiver) + amount = amount ?? 1; + isPNFT = isPNFT ?? false + let authorizationDetails: TokenMetadataAuthorizationDetails | undefined; + if (isPNFT) { + const tokenInfo = await this.getTokenInfo(mint) + const rules = tokenInfo.metadata?.programmableConfig?.ruleSet + if (rules) authorizationDetails = { rules } + } + try { + const ixs = this.metaplex.nfts().builders().transfer({ + nftOrSft: { address: mint, tokenStandard }, + toOwner: receiver, + amount: { basisPoints: new BN(amount) as any, currency: { decimals: 0, namespace: "spl-token", symbol: "" } },//TODO: + // amount: null as any, + fromOwner: sender, + authorizationDetails + }).getInstructions() + const tx = new web3.Transaction().add(...ixs) + const sign = await this.provider.sendAndConfirm(tx) + return sign + } catch (error) { + log({ mplTransferError: error }) + } + } + + async burn(input: BurnInput) { + let { + mint, + owner, + amount, + decimal + } = input + if (typeof mint == 'string') mint = new web3.PublicKey(mint) + if (typeof owner == 'string') owner = new web3.PublicKey(owner) + if (!amount) { + amount = 1 + decimal = 0; + } else { + if (!decimal) + decimal = (await this.baseSpl.getMint(mint)).decimals + amount = amount * (10 ** decimal) + } + + const ixs = this.metaplex.nfts().builders().delete({ + mintAddress: mint, + amount: { basisPoints: new BN(amount) as any, currency: { decimals: decimal, namespace: "spl-token", symbol: "" } }//TODO: + // amount: token(amount) + }).getInstructions() + const tx = new web3.Transaction().add(...ixs) + const sign = await this.provider.sendAndConfirm(tx) + return sign + } + + async getTokenInfo(mint: web3.PublicKey | string): Promise { + if (typeof mint == 'string') mint = new web3.PublicKey(mint) + const metadataAccount = BaseMpl.getMetadataAccount(mint) + const accountInfoes = await this.connection.getMultipleAccountsInfo([mint, metadataAccount]) + if (!accountInfoes[0]) throw "Token not found" + let tokenInfo = MintLayout.decode(accountInfoes[0].data) + if (!tokenInfo.isInitialized) throw "Token dosen't initialise" + + let metadata: Metadata | null = null + if (accountInfoes[1]) metadata = Metadata.deserialize(accountInfoes[1].data)[0] + return { + address: mint, + mintInfo: tokenInfo, + metadata + } + } + + async getAndCheckTokenName(mint: web3.PublicKey, defaultName = ' ') { + try { + const metadataAccount = BaseMpl.getMetadataAccount(mint) + const [mintAccountInfo, metadataAccountInfo] = await this.connection.getMultipleAccountsInfo([mint, metadataAccount]).catch(error => [null, null]); + if (!mintAccountInfo + || mintAccountInfo.owner.toBase58() != TOKEN_PROGRAM_ID.toBase58() + || mintAccountInfo.data.length != MintLayout.span + ) return null + let name = mint.toBase58() + if (metadataAccountInfo) { + const res = BaseMpl.getTokenNameFromAccountInfo(metadataAccountInfo) + if (res) return res + } + return defaultName + } catch (error) { + return null + } + } + + static getTokenNameFromAccountInfo(accountInfo: web3.AccountInfo | null) { + if (!accountInfo) return undefined + try { + const metadata = Metadata.deserialize(accountInfo.data)[0] + return metadata?.data?.name?.split("\0")[0] + } catch (error) { + return undefined + } + } + + // async verifyCollectionItem(input: VerifyNftCollectionBuilderParams) { + // const ixs = this.metaplex + // .nfts() + // .builders() + // .verifyCollection(input) + // .getInstructions(); + // const tx = new web3.Transaction().add(...ixs); + // return { tx }; + // } + + getRevokeMetadataAuthIx(token: web3.PublicKey, owner: web3.PublicKey) { + const metadata = BaseMpl.getMetadataAccount(token) + const ix = createUpdateMetadataAccountV2Instruction({ + metadata, updateAuthority: owner + }, { + updateMetadataAccountArgsV2: { + data: null, + isMutable: false, + primarySaleHappened: false, + updateAuthority: null + } + }) + return ix + } +} diff --git a/src/base/baseRay.ts b/src/base/baseRay.ts new file mode 100644 index 00000000..3b8d04e7 --- /dev/null +++ b/src/base/baseRay.ts @@ -0,0 +1,915 @@ +import { web3 } from "@project-serum/anchor"; +import { SplAccount, MarketStateLayout, Market as RayMarket, MarketV2, Liquidity, ApiPoolInfo, LIQUIDITY_STATE_LAYOUT_V4, TxVersion, LiquidityPoolJsonInfo, poolKeys2JsonInfo, PoolUtils, ApiInfo, ApiPoolInfoV4, PoolInfoLayout, ApiPoolInfoItem, PublicKeyish, jsonInfo2PoolKeys, LIQUIDITY_STATE_LAYOUT_V5, getMultipleLookupTableInfo, LOOKUP_TABLE_CACHE, LiquidityPoolKeysV4, fetchMultipleMintInfos, RAYDIUM_MAINNET, MAINNET_PROGRAM_ID, MAINNET_FARM_POOLS, Token, TokenAmount, Percent, CurrencyAmount, LiquidityStateLayoutV4, LiquidityStateLayoutV5, LiquidityStateV4, LiquidityStateV5, LiquidityPoolKeys, _SERUM_PROGRAM_ID_V3, SwapSide, LiquidityPoolInfo, AmmConfigLayout, getPdaAmmConfigId, SPL_ACCOUNT_LAYOUT, Fee, currencyEquals, LiquidityAssociatedPoolKeys, ZERO, } from '@raydium-io/raydium-sdk' +import { BaseRayInput } from "./types"; +import fs from 'fs'; +import { ACCOUNT_SIZE, AccountLayout, MintLayout, NATIVE_MINT, TOKEN_PROGRAM_ID, amountToUiAmount, createAssociatedTokenAccountInstruction, createInitializeAccountInstruction, decodeAmountToUiAmountInstructionUnchecked, getAssociatedTokenAddressSync, getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; +import { BaseSpl } from "./baseSpl"; +import { createSyncNativeInstruction } from '@solana/spl-token' +import { EVENT_QUEUE_LENGTH, ORDERBOOK_LENGTH, REQUEST_QUEUE_LENGTH, getVaultOwnerAndNonce } from "./orderbookUtils"; +import { DexInstructions, Market, MARKET_STATE_LAYOUT_V2, MARKET_STATE_LAYOUT_V3 } from "@openbook-dex/openbook"; +import { _MARKET_STAT_LAYOUT_V1 } from "@openbook-dex/openbook/lib/market"; +import { BN } from '@project-serum/anchor' +import useSerumMarketAccountSizes from "./getMarketAccountSizes"; +import { Result } from "./types"; +import { calcDecimalValue, calcNonDecimalValue, getNullableResutFromPromise, sleep } from "./utils"; +import { toBufferBE } from "bigint-buffer"; +import { ENV } from "../constants"; + +// export const RAYDIUM_AMM_PROGRAM = new web3.PublicKey("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8") +// const _OPEN_BOOK_DEX_PROGRAM = "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX" +export type CreateMarketInput = { + baseMint: web3.PublicKey, + quoteMint: web3.PublicKey + tickers: { + lotSize: number, + tickSize: number + } +} + +export type AddLiquidityInput = { + user: web3.PublicKey + poolKeys: LiquidityPoolKeys, + baseMintAmount: number | BN, + quoteMintAmount: number | BN, + fixedSide: 'base' | 'quote' +} +export type RemoveLiquidityInput = { + user: web3.PublicKey + poolKeys: LiquidityPoolKeys, + amount: number, +} +export type BuyFromPoolInput = { + poolKeys: LiquidityPoolKeys, + amountIn: TokenAmount + amountOut: TokenAmount + user: web3.PublicKey + fixedSide: SwapSide + tokenAccountIn: web3.PublicKey, + tokenAccountOut: web3.PublicKey +} +export type CreatePoolInput = { + baseMint: web3.PublicKey, + quoteMint: web3.PublicKey, + marketId: web3.PublicKey, + baseMintAmount: number, + quoteMintAmount: number, +} +export type ComputeBuyAmountInput = { + poolKeys: LiquidityPoolKeys, + user: web3.PublicKey + amount: number, + inputAmountType: 'send' | 'receive', + buyToken: 'base' | 'quote', + /** default (1 %) */ + slippage?: Percent +} + +export type ComputeAnotherAmountInput = { + poolKeys: LiquidityPoolKeysV4, + amount: number + /** default( `true` ) */ + isRawAmount?: boolean + /** default( `Percent(1, 100)` = 1%) */ + slippage?: Percent, + fixedSide: 'base' | 'quote', +} + +const log = console.log +export class BaseRay { + private connection: web3.Connection + private baseSpl: BaseSpl + private cacheIxs: web3.TransactionInstruction[] + // private pools: LiquidityPoolJsonInfo[]; + private pools: Map; + private cachedPoolKeys: Map; + ammProgramId: web3.PublicKey + private orderBookProgramId: web3.PublicKey + private feeDestinationId: web3.PublicKey + + constructor(input: BaseRayInput) { + this.connection = new web3.Connection(input.rpcEndpointUrl, { commitment: "confirmed", confirmTransactionInitialTimeout: 60000 }) + this.baseSpl = new BaseSpl(this.connection) + this.cacheIxs = [] + this.cachedPoolKeys = new Map(); + this.pools = new Map(); + if (input.rpcEndpointUrl == "https://api.devnet.solana.com") { + this.ammProgramId = new web3.PublicKey("HWy1jotHpo6UqeQxx49dpYYdQB8wj9Qk9MdxwjLvDHB8") + this.feeDestinationId = new web3.PublicKey("3XMrhbv989VxAMi3DErLV9eJht1pHppW5LbKxe9fkEFR") + this.orderBookProgramId = new web3.PublicKey("EoTcMgcDRTJVZDMZWBoU6rhYHZfkNTVEAfz3uUJRcYGj") + } else { + this.feeDestinationId = new web3.PublicKey("7YttLkHDoNj9wyDur5pM1ejNaAvT9X4eqaYcHQqtj2G5") + this.ammProgramId = new web3.PublicKey("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8") + this.orderBookProgramId = new web3.PublicKey("srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX") + } + + // let liquidityJson: any = {}; + // try { + // liquidityJson = JSON.parse(fs.readFileSync("./rayRes.json").toString()); + // } catch (error) { + // this.fetchPools() + // } + // const officialPools = liquidityJson?.official ?? []; + // const unOfficialPools = liquidityJson?.unOfficial ?? []; + // for (let pool of officialPools) this.pools.set(pool.id, pool) + // for (let pool of unOfficialPools) this.pools.set(pool.id, pool) + } + + async getMarketInfo(marketId: web3.PublicKey) { + const marketAccountInfo = await this.connection.getAccountInfo(marketId).catch((error) => null) + if (!marketAccountInfo) throw "Market not found" + try { + return RayMarket.getLayouts(3).state.decode(marketAccountInfo.data) + } catch (parseMeketDataError) { + // log({ parseMeketDataError }) + } + return null + } + + private ixsAdderCallback = (ixs: web3.TransactionInstruction[] = []) => { + this.cacheIxs.push(...ixs) + } + reInit = () => this.cacheIxs = [] + getPoolInfo = (poolId: string) => this.pools.get(poolId) + + async getPoolKeys(poolId: web3.PublicKey): Promise { + if (!this.pools) this.pools = new Map(); + if (!this.cachedPoolKeys) this.cachedPoolKeys = new Map(); + const cache2 = this.cachedPoolKeys.get(poolId.toBase58()) + if (cache2) { + return cache2 + } + // const cache = this.pools.get(poolId.toBase58()) + // if (cache) { + // return jsonInfo2PoolKeys(cache) as LiquidityPoolKeys + // } + + const accountInfo = await this.connection.getAccountInfo(poolId) + if (!accountInfo) throw "Pool info not found" + let poolState: LiquidityStateV4 | LiquidityStateV5 | undefined = undefined + let version: 4 | 5 | undefined = undefined + let poolAccountOwner = accountInfo.owner + if (accountInfo.data.length == LIQUIDITY_STATE_LAYOUT_V4.span) { + poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(accountInfo.data) + version = 4 + } else if (accountInfo.data.length == LIQUIDITY_STATE_LAYOUT_V5.span) { + poolState = LIQUIDITY_STATE_LAYOUT_V5.decode(accountInfo.data) + version = 5 + } else throw "Invalid Pool data lenght" + if (!poolState || !version) throw "Invalid pool address" + + let { authority, + baseDecimals, + baseMint, + baseVault, + configId, + id, + lookupTableAccount, + lpDecimals, + lpMint, + lpVault, + marketAuthority, + marketId, + marketProgramId, + marketVersion, + nonce, + openOrders, + programId, + quoteDecimals, + quoteMint, + quoteVault, + targetOrders, + // version, + withdrawQueue, + } = Liquidity.getAssociatedPoolKeys({ + baseMint: poolState.baseMint, + baseDecimals: poolState.baseDecimal.toNumber(), + quoteMint: poolState.quoteMint, + quoteDecimals: poolState.quoteDecimal.toNumber(), + marketId: poolState.marketId, + marketProgramId: poolState.marketProgramId, + marketVersion: 3, + programId: poolAccountOwner, + version, + }) + if (lpMint.toBase58() != poolState.lpMint.toBase58()) { + throw "Found some invalid keys" + } + + // log({ version, baseMint: baseMint.toBase58(), quoteMint: quoteMint.toBase58(), lpMint: lpMint.toBase58(), marketId: marketId.toBase58(), marketProgramId: marketProgramId.toBase58() }) + let marketState: any = undefined; + const marketAccountInfo = await this.connection.getAccountInfo(marketId).catch((error) => null) + if (!marketAccountInfo) throw "Market not found" + try { + marketState = RayMarket.getLayouts(marketVersion).state.decode(marketAccountInfo.data) + // if (mProgramIdStr != _SERUM_PROGRAM_ID_V3 && mProgramIdStr != _OPEN_BOOK_DEX_PROGRAM) { + // } + } catch (parseMeketDataError) { + log({ parseMeketDataError }) + } + if (!marketState) throw "MarketState not found" + const { baseVault: marketBaseVault, quoteVault: marketQuoteVault, eventQueue: marketEventQueue, bids: marketBids, asks: marketAsks } = marketState + const res: LiquidityPoolKeys = { + baseMint, + quoteMint, + quoteDecimals, + baseDecimals, + authority, + baseVault, + quoteVault, + id, + lookupTableAccount, + lpDecimals, + lpMint, + lpVault, + marketAuthority, + marketId, + marketProgramId, + marketVersion, + openOrders, + programId, + targetOrders, + version, + withdrawQueue, + marketAsks, + marketBids, + marketBaseVault, + marketQuoteVault, + marketEventQueue, + } + this.cachedPoolKeys.set(poolId.toBase58(), res) + // log({ poolKeys: res }) + return res; + } + + private addPoolKeys(poolInfo: LiquidityAssociatedPoolKeys, marketState: any) { + const { authority, baseDecimals, baseMint, baseVault, configId, id, lookupTableAccount, lpDecimals, lpMint, lpVault, marketAuthority, marketId, marketProgramId, marketVersion, nonce, openOrders, programId, quoteDecimals, quoteMint, quoteVault, targetOrders, version, withdrawQueue, } = poolInfo + const { baseVault: marketBaseVault, quoteVault: marketQuoteVault, eventQueue: marketEventQueue, bids: marketBids, asks: marketAsks } = marketState + const res: LiquidityPoolKeys = { + baseMint, + quoteMint, + quoteDecimals, + baseDecimals, + authority, + baseVault, + quoteVault, + id, + lookupTableAccount, + lpDecimals, + lpMint, + lpVault, + marketAuthority, + marketId, + marketProgramId, + marketVersion, + openOrders, + programId, + targetOrders, + version, + withdrawQueue, + marketAsks, + marketBids, + marketBaseVault, + marketQuoteVault, + marketEventQueue, + } + this.cachedPoolKeys.set(id.toBase58(), res) + } + + // async fetchPools() { + // const liquidityJsonStr = await (await fetch("https://api.raydium.io/v2/sdk/token/raydium.mainnet.json")).text(); + // const liquidityJson = JSON.parse(liquidityJsonStr) + // const officialPools = liquidityJson?.official ?? []; + // const unOfficialPools = liquidityJson?.unOfficial ?? []; + // for (let pool of officialPools) this.pools.set(pool.id, pool) + // for (let pool of unOfficialPools) this.pools.set(pool.id, pool) + // fs.writeFileSync('./rayRes.json', liquidityJsonStr) + // log("Pool fetched") + // } + + async addLiquidity(input: AddLiquidityInput): Promise<{ ixs: web3.TransactionInstruction[], signers: web3.Signer[] }> { + this.reInit(); + let { poolKeys, baseMintAmount: baseAmount, quoteMintAmount: quoteAmount, fixedSide } = input + const user = input.user + // let poolKeys = await getNullableResutFromPromise(this.getPoolKeys(poolId), { logError: true }); + // if (!poolKeys) throw "pool not found" + const base = poolKeys.baseMint + const baseMintDecimals = poolKeys.baseDecimals; + const quote = poolKeys.quoteMint + const quoteMintDecimals = poolKeys.quoteDecimals; + const lpMint = poolKeys.lpMint + const { ata: lpTokenAccount } = await this.baseSpl.getOrCreateTokenAccount({ mint: lpMint, owner: user, checkCache: true }, this.ixsAdderCallback) + const { ata: baseTokenAccount } = await this.baseSpl.getOrCreateTokenAccount({ mint: base, owner: user, checkCache: true }, this.ixsAdderCallback) + const { ata: quoteTokenAccount } = await this.baseSpl.getOrCreateTokenAccount({ mint: quote, owner: user, checkCache: true }, this.ixsAdderCallback) + let baseAmountIn: string + let quoteAmountIn: string + if (typeof baseAmount == 'number' && typeof quoteAmount == 'number') { + baseAmountIn = calcNonDecimalValue(baseAmount, baseMintDecimals).toString() + quoteAmountIn = calcNonDecimalValue(quoteAmount, quoteMintDecimals).toString() + } else { + baseAmountIn = (baseAmount as BN).toNumber().toString() + quoteAmountIn = (quoteAmount as BN).toNumber().toString() + } + + if (base.toBase58() == NATIVE_MINT.toBase58() || quote.toBase58() == NATIVE_MINT.toBase58()) { + let nativeAmount: string; + let nativeTokenAccount: web3.PublicKey; + if (base.toBase58() == NATIVE_MINT.toBase58()) { + nativeTokenAccount = baseTokenAccount + nativeAmount = baseAmountIn + } else { + nativeTokenAccount = quoteTokenAccount + nativeAmount = quoteAmountIn + } + const sendSolIx = web3.SystemProgram.transfer({ + fromPubkey: user, + toPubkey: nativeTokenAccount, + lamports: BigInt(nativeAmount) + }) + const syncWSolAta = createSyncNativeInstruction(nativeTokenAccount, TOKEN_PROGRAM_ID) + this.cacheIxs.push(sendSolIx, syncWSolAta) + } + const rayIxs = Liquidity.makeAddLiquidityInstruction({ + baseAmountIn, + quoteAmountIn, + fixedSide, + poolKeys, + userKeys: { + baseTokenAccount, lpTokenAccount, owner: user, + quoteTokenAccount + } + }).innerTransaction + const recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash + const message = new web3.TransactionMessage({ + instructions: [...this.cacheIxs, ...rayIxs.instructions], + payerKey: user, + recentBlockhash + }).compileToV0Message() + const mainTx = new web3.VersionedTransaction(message) + if (rayIxs.signers) mainTx.signatures.push(...rayIxs.signers) + return { + ixs: [...this.cacheIxs, ...rayIxs.instructions], + signers: [...rayIxs.signers] + } + } + + async removeLiquidity(input: RemoveLiquidityInput): Promise> { + const { user, poolKeys } = input + const { baseMint, quoteMint, lpMint, lpDecimals } = poolKeys + const baseTokenAccount = (await this.baseSpl.getOrCreateTokenAccount({ mint: baseMint, owner: user, checkCache: true }, this.ixsAdderCallback)).ata + const quoteTokenAccount = (await this.baseSpl.getOrCreateTokenAccount({ mint: quoteMint, owner: user, checkCache: true }, this.ixsAdderCallback)).ata + const lpTokenAccount = (await this.baseSpl.getOrCreateTokenAccount({ mint: lpMint, owner: user, checkCache: true }, this.ixsAdderCallback)).ata + + const lpTokenAccountInfo = await this.connection.getAccountInfo(lpTokenAccount); + if (!lpTokenAccountInfo) return { Err: "No lp token found" } + const totalLp = Number(AccountLayout.decode(lpTokenAccountInfo.data).amount.toString()) + const totalLpD = calcDecimalValue(totalLp, lpDecimals); + log("lp token: ", lpMint.toBase58()) + log("user lp token ata: ", lpTokenAccount.toBase58()) + log("Total available lp tokens : ", totalLpD) + let amount = calcNonDecimalValue(input.amount, lpDecimals).toString() + if (Number(amount) > totalLp) { + return { Err: "not have enought lp tokens" } + } + if (input.amount == -1) amount = totalLp.toString() + const ixs = Liquidity.makeRemoveLiquidityInstruction({ + amountIn: amount, poolKeys, userKeys: { + baseTokenAccount, lpTokenAccount, owner: user, quoteTokenAccount + } + }).innerTransaction.instructions + return { + Ok: { + ixs: [...this.cacheIxs, ...ixs] + } + } + } + + async removeLiquidityFaster(input: RemoveLiquidityInput): Promise> { + const { user, poolKeys } = input + const { baseMint, quoteMint, lpMint, lpDecimals } = poolKeys + const baseTokenAccount = getAssociatedTokenAddressSync(baseMint, user) + const quoteTokenAccount = getAssociatedTokenAddressSync(quoteMint, user) + const lpTokenAccount = getAssociatedTokenAddressSync(lpMint, user) + let amount = input.amount + const ixs = Liquidity.makeRemoveLiquidityInstruction({ + amountIn: amount.toString(), poolKeys, userKeys: { + baseTokenAccount, lpTokenAccount, owner: user, quoteTokenAccount + } + }).innerTransaction.instructions + return { + Ok: { + ixs: [...ixs] + } + } + } + + async buyFromPool(input: BuyFromPoolInput): Promise<{ ixs: web3.TransactionInstruction[], signers: web3.Signer[] }> { + this.reInit(); + const { amountIn, amountOut, poolKeys, user, fixedSide, tokenAccountIn, tokenAccountOut } = input + const { baseMint, quoteMint } = poolKeys + const { ata: baseTokenAccount } = await this.baseSpl.getOrCreateTokenAccount({ mint: baseMint, owner: user, checkCache: true }, this.ixsAdderCallback) + const { ata: quoteTokenAccount } = await this.baseSpl.getOrCreateTokenAccount({ mint: quoteMint, owner: user, checkCache: true }, this.ixsAdderCallback) + const inToken = (amountIn as TokenAmount).token.mint; + if (inToken.toBase58() == NATIVE_MINT.toBase58()) { + let lamports = BigInt(amountIn.raw.toNumber()) + const sendSolIx = web3.SystemProgram.transfer({ + fromPubkey: user, + toPubkey: tokenAccountIn, + lamports + }) + const syncWSolAta = createSyncNativeInstruction(tokenAccountIn, TOKEN_PROGRAM_ID) + this.cacheIxs.push(sendSolIx, syncWSolAta) + } + let rayIxs = Liquidity.makeSwapInstruction({ + poolKeys, + amountIn: amountIn.raw, + amountOut: amountOut.raw, + fixedSide, + userKeys: { owner: user, tokenAccountIn, tokenAccountOut }, + }).innerTransaction + + const recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash + const message = new web3.TransactionMessage({ + instructions: [...this.cacheIxs, ...rayIxs.instructions], + payerKey: user, + recentBlockhash + }).compileToV0Message() + const mainTx = new web3.VersionedTransaction(message) + if (rayIxs.signers) mainTx.signatures.push(...rayIxs.signers) + return { + ixs: [...this.cacheIxs, ...rayIxs.instructions], + signers: [...rayIxs.signers] + } + } + + async createPool(input: CreatePoolInput, user: web3.PublicKey) { + this.reInit(); + const userBaseAta = getAssociatedTokenAddressSync(input.baseMint, user) + const userQuoteAta = getAssociatedTokenAddressSync(input.quoteMint, user) + + let [baseMintAccountInfo, quoteMintAccountInfo, marketAccountInfo, userBaseAtaInfo, userQuoteAtaInfo] = await this.connection.getMultipleAccountsInfo([input.baseMint, input.quoteMint, input.marketId, userBaseAta, userQuoteAta]).catch(() => [null, null, null, null]) + if (!baseMintAccountInfo || !quoteMintAccountInfo || !marketAccountInfo) throw "AccountInfo not found" + if (input.baseMint.toBase58() != NATIVE_MINT.toBase58() && !userBaseAtaInfo) throw "Don't have enought tokens" + else { + if (input.baseMint.toBase58() == NATIVE_MINT.toBase58()) { + const todo = web3.PublicKey.default + const buf = Buffer.alloc(SPL_ACCOUNT_LAYOUT.span) + SPL_ACCOUNT_LAYOUT.encode({ + mint: NATIVE_MINT, + amount: new BN(0), + isNative: new BN(1), + owner: user, + closeAuthority: todo, + closeAuthorityOption: 1, + delegate: todo, + delegatedAmount: new BN(1), + delegateOption: 1, + isNativeOption: 1, + state: 1 + }, buf) + userBaseAtaInfo = { + data: buf, + } as any + } + } + if (input.quoteMint.toBase58() != NATIVE_MINT.toBase58() && !userQuoteAtaInfo) throw "Don't have enought tokens" + else { + if (input.quoteMint.toBase58() == NATIVE_MINT.toBase58()) { + const todo = web3.PublicKey.default + const buf = Buffer.alloc(SPL_ACCOUNT_LAYOUT.span) + SPL_ACCOUNT_LAYOUT.encode({ + mint: NATIVE_MINT, + amount: new BN(0), + isNative: new BN(1), + owner: user, + closeAuthority: todo, + closeAuthorityOption: 1, + delegate: todo, + delegatedAmount: new BN(1), + delegateOption: 1, + isNativeOption: 1, + state: 1 + }, buf) + userQuoteAtaInfo = { + data: buf, + } as any + } + } + const baseMintState = MintLayout.decode(baseMintAccountInfo.data); + const quoteMintState = MintLayout.decode(quoteMintAccountInfo.data); + // const marketState = RayMarket.getLayouts(3).state.decode(marketAccountInfo.data) + const marketInfo = { + marketId: input.marketId, + programId: marketAccountInfo.owner + } + const baseMintInfo = { + mint: input.baseMint, + decimals: baseMintState.decimals + } + const quoteMintInfo = { + mint: input.quoteMint, + decimals: quoteMintState.decimals + } + const baseAmount = new BN(toBufferBE(BigInt(calcNonDecimalValue(input.baseMintAmount, baseMintState.decimals).toString()), 8)) + const quoteAmount = new BN(toBufferBE(BigInt(calcNonDecimalValue(input.quoteMintAmount, quoteMintState.decimals).toString()), 8)) + // const quoteAmount = new BN(calcNonDecimalValue(input.quoteMintAmount, quoteMintState.decimals)) + + const poolInfo = Liquidity.getAssociatedPoolKeys({ + version: 4, + marketVersion: 3, + marketId: marketInfo.marketId, + baseMint: baseMintInfo.mint, + quoteMint: quoteMintInfo.mint, + baseDecimals: baseMintInfo.decimals, + quoteDecimals: quoteMintInfo.decimals, + programId: this.ammProgramId, + marketProgramId: marketInfo.programId, + }) + const marketState = RayMarket.getLayouts(3).state.decode(marketAccountInfo.data) + this.addPoolKeys(poolInfo, marketState); + + const startTime = new BN(Math.trunc(Date.now() / 1000) - 4) + // const createPoolIxs = Liquidity.makeCreatePoolV4InstructionV2({ + // programId: this.ammProgramId, + // ammId: poolInfo.id, + // ammAuthority: poolInfo.authority, + // ammOpenOrders: poolInfo.openOrders, + // lpMint: poolInfo.lpMint, + // coinMint: poolInfo.baseMint, + // pcMint: poolInfo.quoteMint, + // coinVault: poolInfo.baseVault, + // pcVault: poolInfo.quoteVault, + // ammTargetOrders: poolInfo.targetOrders, + // marketProgramId: poolInfo.marketProgramId, + // marketId: poolInfo.marketId, + // userWallet: user, + // userCoinVault: userBaseAta, + // userPcVault: userQuoteAta, + // userLpVault: getAssociatedTokenAddressSync(poolInfo.lpMint, user), + // ammConfigId: poolInfo.configId, + // feeDestinationId, + // nonce: poolInfo.nonce, + // openTime: startTime, + // coinAmount: baseAmount, + // pcAmount: quoteAmount, + // }).innerTransaction + + const createPoolIxs = (await Liquidity.makeCreatePoolV4InstructionV2Simple({ + marketInfo, + baseMintInfo, + quoteMintInfo, + baseAmount, + quoteAmount, + associatedOnly: true, + checkCreateATAOwner: true, + connection: this.connection, + feeDestinationId: this.feeDestinationId, + makeTxVersion: TxVersion.LEGACY, + ownerInfo: { + feePayer: user, + tokenAccounts: [ + { accountInfo: SPL_ACCOUNT_LAYOUT.decode(userBaseAtaInfo!.data), programId: TOKEN_PROGRAM_ID, pubkey: userBaseAta }, + { accountInfo: SPL_ACCOUNT_LAYOUT.decode(userQuoteAtaInfo!.data), programId: TOKEN_PROGRAM_ID, pubkey: userQuoteAta } + ], + wallet: user, + useSOLBalance: true + }, + programId: this.ammProgramId, + startTime + // computeBudgetConfig: { microLamports: 250_000, units: 8000_000 }, + })).innerTransactions + + const ixs: web3.TransactionInstruction[] = [] + const signers: web3.Signer[] = [] + // ixs.push(...createPoolIxs.instructions) + // signers.push(...createPoolIxs.signers) + for (let ix of createPoolIxs) { + ixs.push(...ix.instructions) + signers.push(...ix.signers) + } + return { ixs, signers, poolId: Liquidity.getAssociatedId({ marketId: marketInfo.marketId, programId: this.ammProgramId }), baseAmount, quoteAmount, baseDecimals: poolInfo.baseDecimals, quoteDecimals: poolInfo.quoteDecimals } + } + + async createMarket(input: CreateMarketInput, user: web3.PublicKey) + : Promise> { + const { Keypair, SystemProgram } = web3; + const { baseMint, quoteMint } = input; + const marketAccounts = { + market: Keypair.generate(), + requestQueue: Keypair.generate(), + eventQueue: Keypair.generate(), + bids: Keypair.generate(), + asks: Keypair.generate(), + baseVault: Keypair.generate(), + quoteVault: Keypair.generate(), + }; + const programID = this.orderBookProgramId + const vaultInstructions: web3.TransactionInstruction[] = [] + const vaultSigners: web3.Signer[] = [] + const [vaultOwner, vaultOwnerNonce] = await getVaultOwnerAndNonce( + marketAccounts.market.publicKey, + programID + ); + + vaultInstructions.push( + ...[ + SystemProgram.createAccount({ + fromPubkey: user, + newAccountPubkey: marketAccounts.baseVault.publicKey, + lamports: await this.connection.getMinimumBalanceForRentExemption( + ACCOUNT_SIZE + ), + space: ACCOUNT_SIZE, + programId: TOKEN_PROGRAM_ID, + }), + SystemProgram.createAccount({ + fromPubkey: user, + newAccountPubkey: marketAccounts.quoteVault.publicKey, + lamports: await this.connection.getMinimumBalanceForRentExemption( + ACCOUNT_SIZE + ), + space: ACCOUNT_SIZE, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeAccountInstruction( + marketAccounts.baseVault.publicKey, + baseMint, + vaultOwner + ), + createInitializeAccountInstruction( + marketAccounts.quoteVault.publicKey, + quoteMint, + vaultOwner + ), + ] + ); + vaultSigners.push(marketAccounts.baseVault, marketAccounts.quoteVault); + const [baseMintAccountInfo, quoteMintAccountInfo] = await this.connection.getMultipleAccountsInfo([baseMint, quoteMint]) + let baseMintDecimals: number; + let quoteMintDecimals: number; + if (!baseMintAccountInfo || !quoteMintAccountInfo) return { Err: "Invalid token address! Token not found" } + try { + baseMintDecimals = MintLayout.decode(baseMintAccountInfo.data).decimals + quoteMintDecimals = MintLayout.decode(quoteMintAccountInfo.data).decimals + } catch (error) { + return { Err: "Invalid token address! Token not found" } + } + let tickers = input.tickers + const baseLotSize = new BN(Math.round(10 ** baseMintDecimals * tickers.lotSize)) + const quoteLotSize = new BN(Math.round(tickers.lotSize * 10 ** quoteMintDecimals * tickers.tickSize)) + if (baseLotSize.eq(ZERO)) return { Err: 'lot size is too small' } + if (quoteLotSize.eq(ZERO)) return { Err: 'tick size or lot size is too small' } + // log({ baseLotSize: baseLotSize.toNumber() }) + // log({ quoteLotSize: quoteLotSize.toNumber() }) + + // create market account + const marketInstructions: web3.TransactionInstruction[] = [] + const marketSigners: web3.Signer[] = [marketAccounts.market, marketAccounts.bids, marketAccounts.asks, marketAccounts.eventQueue, marketAccounts.requestQueue] + marketInstructions.push( + SystemProgram.createAccount({ + newAccountPubkey: marketAccounts.market.publicKey, + fromPubkey: user, + space: Market.getLayout(programID).span, + lamports: await this.connection.getMinimumBalanceForRentExemption( + Market.getLayout(programID).span + ), + programId: programID, + }) + ); + const { + totalEventQueueSize, + totalOrderbookSize, + totalRequestQueueSize, + } = useSerumMarketAccountSizes({ + eventQueueLength: EVENT_QUEUE_LENGTH, + requestQueueLength: REQUEST_QUEUE_LENGTH, + orderbookLength: ORDERBOOK_LENGTH, + }, this.connection, programID); + // create request queue + marketInstructions.push( + SystemProgram.createAccount({ + newAccountPubkey: marketAccounts.requestQueue.publicKey, + fromPubkey: user, + space: totalRequestQueueSize, + lamports: await this.connection.getMinimumBalanceForRentExemption( + totalRequestQueueSize + ), + programId: programID, + }) + ); + // create event queue + marketInstructions.push( + SystemProgram.createAccount({ + newAccountPubkey: marketAccounts.eventQueue.publicKey, + fromPubkey: user, + space: totalEventQueueSize, + lamports: await this.connection.getMinimumBalanceForRentExemption( + totalEventQueueSize + ), + programId: programID, + }) + ); + + const orderBookRentExempt = + await this.connection.getMinimumBalanceForRentExemption(totalOrderbookSize); + // create bids + marketInstructions.push( + SystemProgram.createAccount({ + newAccountPubkey: marketAccounts.bids.publicKey, + fromPubkey: user, + space: totalOrderbookSize, + lamports: orderBookRentExempt, + programId: programID, + }) + ); + // create asks + marketInstructions.push( + SystemProgram.createAccount({ + newAccountPubkey: marketAccounts.asks.publicKey, + fromPubkey: user, + space: totalOrderbookSize, + lamports: orderBookRentExempt, + programId: programID, + }) + ); + marketInstructions.push( + DexInstructions.initializeMarket({ + market: marketAccounts.market.publicKey, + requestQueue: marketAccounts.requestQueue.publicKey, + eventQueue: marketAccounts.eventQueue.publicKey, + bids: marketAccounts.bids.publicKey, + asks: marketAccounts.asks.publicKey, + baseVault: marketAccounts.baseVault.publicKey, + quoteVault: marketAccounts.quoteVault.publicKey, + baseMint, + quoteMint, + baseLotSize, + quoteLotSize, + feeRateBps: 150, // Unused in v3 + quoteDustThreshold: new BN(500), // Unused in v3 + vaultSignerNonce: vaultOwnerNonce, + programId: programID, + }) + ); + + return { + Ok: { + marketId: marketAccounts.market.publicKey, + vaultInstructions, + vaultSigners, + marketInstructions, + marketSigners + } + } + } + + async computeBuyAmount(input: ComputeBuyAmountInput, etc?: { extraBaseResever?: number, extraQuoteReserve?: number, extraLpSupply?: number }) { + const { amount, buyToken, inputAmountType, poolKeys, user } = input; + const slippage = input.slippage ?? new Percent(1, 100) + const base = poolKeys.baseMint + const baseMintDecimals = poolKeys.baseDecimals; + const quote = poolKeys.quoteMint + const quoteMintDecimals = poolKeys.quoteDecimals; + const baseTokenAccount = getAssociatedTokenAddressSync(base, user) + const quoteTokenAccount = getAssociatedTokenAddressSync(quote, user) + const baseR = new Token(TOKEN_PROGRAM_ID, base, baseMintDecimals); + const quoteR = new Token(TOKEN_PROGRAM_ID, quote, quoteMintDecimals); + let amountIn: TokenAmount + let amountOut: TokenAmount + let tokenAccountIn: web3.PublicKey + let tokenAccountOut: web3.PublicKey + const [lpAccountInfo, baseVAccountInfo, quoteVAccountInfo] = await this.connection.getMultipleAccountsInfo([poolKeys.lpMint, poolKeys.baseVault, poolKeys.quoteVault].map((e) => new web3.PublicKey(e))).catch(() => [null, null, null, null]) + if (!lpAccountInfo || !baseVAccountInfo || !quoteVAccountInfo) throw "Failed to fetch some data" + // const lpSupply = new BN(Number(MintLayout.decode(lpAccountInfo.data).supply.toString())) + // const baseReserve = new BN(Number(AccountLayout.decode(baseVAccountInfo.data).amount.toString())) + // const quoteReserve = new BN(Number(AccountLayout.decode(quoteVAccountInfo.data).amount.toString())) + + const lpSupply = new BN(toBufferBE(MintLayout.decode(lpAccountInfo.data).supply, 8)).addn(etc?.extraLpSupply ?? 0) + const baseReserve = new BN(toBufferBE(AccountLayout.decode(baseVAccountInfo.data).amount, 8)).addn(etc?.extraBaseResever ?? 0) + const quoteReserve = new BN(toBufferBE(AccountLayout.decode(quoteVAccountInfo.data).amount, 8)).addn(etc?.extraQuoteReserve ?? 0) + let fixedSide: SwapSide; + + const poolInfo: LiquidityPoolInfo = { + baseDecimals: poolKeys.baseDecimals, + quoteDecimals: poolKeys.quoteDecimals, + lpDecimals: poolKeys.lpDecimals, + lpSupply, + baseReserve, + quoteReserve, + startTime: null as any, + status: null as any + } + + if (inputAmountType == 'send') { + fixedSide = 'in' + if (buyToken == 'base') { + amountIn = new TokenAmount(quoteR, amount.toString(), false) + // amountOut = Liquidity.computeAmountOut({ amountIn, currencyOut: baseR, poolInfo, poolKeys, slippage }).amountOut + amountOut = Liquidity.computeAmountOut({ amountIn, currencyOut: baseR, poolInfo, poolKeys, slippage }).minAmountOut as TokenAmount + } else { + amountIn = new TokenAmount(baseR, amount.toString(), false) + // amountOut = Liquidity.computeAmountOut({ amountIn, currencyOut: quoteR, poolInfo, poolKeys, slippage }).amountOut + amountOut = Liquidity.computeAmountOut({ amountIn, currencyOut: quoteR, poolInfo, poolKeys, slippage }).minAmountOut as TokenAmount + } + } else { + fixedSide = 'out' + if (buyToken == 'base') { + amountOut = new TokenAmount(baseR, amount.toString(), false) + // amountIn = Liquidity.computeAmountIn({ amountOut, currencyIn: quoteR, poolInfo, poolKeys, slippage }).amountIn + amountIn = Liquidity.computeAmountIn({ amountOut, currencyIn: quoteR, poolInfo, poolKeys, slippage }).maxAmountIn as TokenAmount + } else { + amountOut = new TokenAmount(quoteR, amount.toString(), false) + // amountIn = Liquidity.computeAmountIn({ amountOut, currencyIn: baseR, poolInfo, poolKeys, slippage }).amountIn + amountIn = Liquidity.computeAmountIn({ amountOut, currencyIn: baseR, poolInfo, poolKeys, slippage }).maxAmountIn as TokenAmount + } + } + if (buyToken == 'base') { + tokenAccountOut = baseTokenAccount + tokenAccountIn = quoteTokenAccount + } else { + tokenAccountOut = quoteTokenAccount + tokenAccountIn = baseTokenAccount + } + + return { + amountIn, + amountOut, + tokenAccountIn, + tokenAccountOut, + fixedSide + } + } + + async computeAnotherAmount({ amount, fixedSide, poolKeys, isRawAmount, slippage }: ComputeAnotherAmountInput) { + isRawAmount = isRawAmount ?? true + slippage = slippage ?? new Percent(1, 100) + const { baseMint, baseVault, quoteMint, quoteVault, baseDecimals, quoteDecimals, lpDecimals, lpMint } = poolKeys; + + const anotherToken = fixedSide == 'base' ? poolKeys.quoteMint : poolKeys.baseMint + // if (anotherToken.toBase58() != baseMint.toBase58() && anotherToken.toBase58() != quoteMint.toBase58()) throw "Invalid another token" + + //TODO: create another function (refactor) + const [poolAccountInfo, baseVAccountInfo, quoteVAccountInfo, lpMintAInfo] = await this.connection.getMultipleAccountsInfo([poolKeys.id, baseVault, quoteVault, lpMint]).catch(() => [null, null, null, null, null]); + if (!poolAccountInfo + || !baseVAccountInfo + || !quoteVAccountInfo + || !lpMintAInfo + ) throw "Failed to fetch somedata" + let poolState: LiquidityStateV4 | LiquidityStateV5 | undefined = undefined + if (poolAccountInfo.data.length == LIQUIDITY_STATE_LAYOUT_V4.span) { + poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(poolAccountInfo.data) + } else if (poolAccountInfo.data.length == LIQUIDITY_STATE_LAYOUT_V5.span) { + poolState = LIQUIDITY_STATE_LAYOUT_V5.decode(poolAccountInfo.data) + } else throw "Invalid Pool data lenght" + if (!poolState) throw "Invalid pool address" + const lpSupply = new BN(Number(MintLayout.decode(lpMintAInfo.data).supply.toString())) + const baseReserve = new BN(Number(AccountLayout.decode(baseVAccountInfo.data).amount.toString())) + const quoteReserve = new BN(Number(AccountLayout.decode(quoteVAccountInfo.data).amount.toString())) + + let current: Token; + let currentTokenAmount: TokenAmount + let another: Token; + // let anotherTokenAmount: TokenAmount + if (anotherToken.toBase58() == quoteMint.toBase58()) { + current = new Token(TOKEN_PROGRAM_ID, baseMint, baseDecimals) + another = new Token(TOKEN_PROGRAM_ID, quoteMint, quoteDecimals); + currentTokenAmount = new TokenAmount(current, amount.toString(), isRawAmount) + } else { + current = new Token(TOKEN_PROGRAM_ID, quoteMint, quoteDecimals); + another = new Token(TOKEN_PROGRAM_ID, baseMint, baseDecimals) + currentTokenAmount = new TokenAmount(current, amount.toString(), isRawAmount) + } + + const { status, poolOpenTime } = poolState + const poolInfo: LiquidityPoolInfo = { + baseDecimals, + quoteDecimals, + baseReserve, + lpDecimals, + lpSupply, + quoteReserve, + startTime: poolOpenTime, + status + } + const res = Liquidity.computeAnotherAmount({ poolKeys, slippage, amount: currentTokenAmount, anotherCurrency: another, poolInfo }) + + if (current.mint.toBase58() == baseMint.toBase58()) { + return { + baseMintAmount: currentTokenAmount.raw, + quoteMintAmount: res.maxAnotherAmount.raw, + liquidity: res.liquidity + } + } else { + return { + baseMintAmount: res.maxAnotherAmount.raw, + quoteMintAmount: currentTokenAmount.raw, + liquidity: res.liquidity + } + } + } +} diff --git a/src/base/baseSpl.ts b/src/base/baseSpl.ts new file mode 100644 index 00000000..5e53aaff --- /dev/null +++ b/src/base/baseSpl.ts @@ -0,0 +1,412 @@ +import { web3 } from "@project-serum/anchor"; +import { + MINT_SIZE, + AuthorityType as SetAuthorityType, + TOKEN_PROGRAM_ID, + AccountLayout as TokenAccountLayout, + createAssociatedTokenAccountInstruction, + createBurnInstruction, + createInitializeMintInstruction, + createMintToInstruction, + createSetAuthorityInstruction, + createTransferInstruction, + getAssociatedTokenAddressSync, + getMint +} from "@solana/spl-token"; +import { calcDecimalValue, calcNonDecimalValue, sleep } from "./utils"; + +const log = console.log; + +export type CreateTokenOptions = { + mintAuthority: web3.PublicKey; + /** default (`mintAuthority`) */ + payer?: web3.PublicKey; + /** default (`mintAuthority`) */ + freezAuthority?: web3.PublicKey; + /** default (`0`) */ + decimal?: number; + /** default (`Keypair.genrate()`) */ + mintKeypair?: web3.Keypair, + mintingInfo?: { + /** default (`mintAuthority`) */ + tokenReceiver?: web3.PublicKey; + /** default (`1`) */ + tokenAmount?: number; + /** default (`false`) */ + allowOffCurveOwner?: boolean; + }; +}; + +export type GetOrCreateTokenAccountOptions = { + mint: web3.PublicKey, + owner: web3.PublicKey, + /** default (`owner`) */ + payer?: web3.PublicKey, + /** default (`false`) */ + allowOffCurveOwner?: boolean; + checkCache?: boolean +} + +export type MintToInput = { + mintAuthority: web3.PublicKey | string, + /** default (`mintAuthority`) */ + receiver?: web3.PublicKey | string + mint: web3.PublicKey | string, + /** default (`1`) */ + amount?: number, + /** default (`false`) */ + receiverIsOffCurve?: boolean, + /** default (`false`) */ + init_if_needed?: boolean + /** default (`null`) fetch from mint*/ + mintDecimal?: number +} + +export type TransferInput = { + sender: web3.PublicKey | string, + receiver: web3.PublicKey | string + mint: web3.PublicKey | string, + /** default (`1`) */ + amount?: number, + receiverIsOffCurve?: boolean, + /** get decimal from onchain token data if not found provided */ + decimal?: number + init_if_needed?: boolean + /** default (`sender`) */ + payer?: web3.PublicKey | string, +} + +export type BurnInput = { + mint: web3.PublicKey | string, + owner: web3.PublicKey | string, + amount?: number, + decimal?: number +} + +export type TranferMintAuthority = { + mint: web3.PublicKey, + currentAuthority: web3.PublicKey, + newAuthority: web3.PublicKey, +} + +export type RevokeAuthorityInput = { + authorityType: "MINTING" | "FREEZING", + mint: web3.PublicKey, + currentAuthority: web3.PublicKey, +} + +export class BaseSpl { + private connection: web3.Connection; + private splIxs: web3.TransactionInstruction[] = []; + private cacheAta: Set; + + constructor(connection: web3.Connection) { + this.connection = connection; + this.cacheAta = new Set(); + } + + __reinit() { + this.splIxs = []; + this.cacheAta = new Set(); + } + + async createToken(opts: CreateTokenOptions) { + this.__reinit(); + let { + mintAuthority, + decimal, + payer, + freezAuthority, + mintKeypair, + mintingInfo, + } = opts; + + payer = payer ?? mintAuthority; + freezAuthority = freezAuthority ?? mintAuthority; + decimal = decimal ?? 0; + mintKeypair = mintKeypair ?? web3.Keypair.generate(); + + const mint = mintKeypair.publicKey; + const rent = await this.connection.getMinimumBalanceForRentExemption( + MINT_SIZE + ); + + const ix1 = web3.SystemProgram.createAccount({ + fromPubkey: payer, + lamports: rent, + newAccountPubkey: mint, + programId: TOKEN_PROGRAM_ID, + space: MINT_SIZE, + }); + this.splIxs.push(ix1); + + const ix2 = createInitializeMintInstruction( + mintKeypair.publicKey, + decimal, + mintAuthority, + freezAuthority + ); + this.splIxs.push(ix2); + + if (mintingInfo && mintingInfo.tokenAmount) { + let { + tokenReceiver, + allowOffCurveOwner, + tokenAmount + } = mintingInfo; + tokenReceiver = mintingInfo.tokenReceiver ?? opts.mintAuthority; + allowOffCurveOwner = allowOffCurveOwner ?? false; + tokenAmount = tokenAmount ?? 1 + tokenAmount = tokenAmount * (10 ** decimal) + const { ata, ix: createTokenAccountIx } = + this.createTokenAccount( + mint, + tokenReceiver, + allowOffCurveOwner, + payer, + ); + this.splIxs.push(createTokenAccountIx); + const ix3 = createMintToInstruction( + mint, + ata, + mintAuthority, + tokenAmount + ); + this.splIxs.push(ix3); + } + const ixs = this.splIxs; + this.__reinit(); + return { + mintKeypair, + ixs, + }; + } + + createTokenAccount( + mint: web3.PublicKey, + owner: web3.PublicKey, + allowOffCurveOwner: boolean = false, + payer?: web3.PublicKey + ) { + const ata = getAssociatedTokenAddressSync(mint, owner, allowOffCurveOwner); + const ix = createAssociatedTokenAccountInstruction( + payer ?? owner, + ata, + owner, + mint + ); + + return { + ata, + ix, + }; + } + + async getOrCreateTokenAccount( + input: GetOrCreateTokenAccountOptions, + ixCallBack?: (ixs?: web3.TransactionInstruction[]) => void + ) { + let { + owner, + mint, + payer, + allowOffCurveOwner, + checkCache + } = input; + allowOffCurveOwner = allowOffCurveOwner ?? false + payer = payer ?? owner; + const ata = getAssociatedTokenAddressSync(mint, owner, allowOffCurveOwner); + let ix: web3.TransactionInstruction | null = null; + const info = await this.connection.getAccountInfo(ata).catch(async () => { + await sleep(2_000) + return this.connection.getAccountInfo(ata).catch((getAccountInfoError) => { + log({ getAccountInfoError }) + return undefined + }) + }); + if (info === undefined) throw "Failed to fetch data" + + if (!info) { + ix = this.createTokenAccount(mint, owner, allowOffCurveOwner, payer).ix; + if (ixCallBack) { + if (checkCache) { + if (!this.cacheAta.has(ata.toBase58())) { + ixCallBack([ix]) + this.cacheAta.add(ata.toBase58()) + } else log("already exist") + } else { + ixCallBack([ix]) + } + } + } + return { + ata, + ix, + }; + } + + async getMintToInstructions(input: MintToInput) { + let { mintAuthority, mint, receiver, amount, receiverIsOffCurve, init_if_needed, mintDecimal } = input; + if (typeof mintAuthority == 'string') mintAuthority = new web3.PublicKey(mintAuthority) + if (typeof mint == 'string') mint = new web3.PublicKey(mint) + if (typeof receiver == 'string') receiver = new web3.PublicKey(receiver) + receiver = receiver ?? mintAuthority + receiverIsOffCurve = receiverIsOffCurve ?? false + init_if_needed = init_if_needed ?? false; + amount = amount ?? 1 + + if (!mintDecimal) + mintDecimal = (await getMint(this.connection, mint)).decimals + + amount = calcNonDecimalValue(amount, mintDecimal) + const receiverAta = getAssociatedTokenAddressSync(mint, receiver, receiverIsOffCurve) + const ixs: web3.TransactionInstruction[] = [] + + if (init_if_needed) { + const ataInfo = await this.connection.getAccountInfo(receiverAta) + if (!ataInfo) { // init ata if needed + const ix = createAssociatedTokenAccountInstruction(mintAuthority, receiverAta, receiver, mint) + ixs.push(ix) + } + } + + const ix = createMintToInstruction(mint, receiverAta, mintAuthority, BigInt(amount.toString())); + ixs.push(ix) + return ixs; + } + + async transfer(input: TransferInput) { + let { + mint, + sender, + receiver, + receiverIsOffCurve, + init_if_needed, + decimal, + payer, + } = input; + receiverIsOffCurve = receiverIsOffCurve ?? false; + payer = payer ?? sender + input.amount = input.amount ?? 1; + + if (typeof mint == 'string') mint = new web3.PublicKey(mint) + if (typeof sender == 'string') sender = new web3.PublicKey(sender) + if (typeof receiver == 'string') receiver = new web3.PublicKey(receiver) + if (typeof payer == 'string') payer = new web3.PublicKey(payer) + const ixs: web3.TransactionInstruction[] = []; + if (!decimal) { + const mintInfo = await getMint(this.connection, mint); + decimal = mintInfo.decimals; + } + const amount = calcNonDecimalValue(input.amount, decimal); + const senderAta = getAssociatedTokenAddressSync(mint, sender) + const receiverAta = getAssociatedTokenAddressSync(mint, receiver, receiverIsOffCurve) + if (init_if_needed) { + const { ata: _, ix: ix1 } = await this.getOrCreateTokenAccount({ mint, owner: sender, payer }) + if (ix1) ixs.push(ix1) + const { ata: __, ix: ix2 } = await this.getOrCreateTokenAccount({ mint, owner: receiver, payer, allowOffCurveOwner: receiverIsOffCurve }) + if (ix2) ixs.push(ix2) + } + + const ix = createTransferInstruction(senderAta, receiverAta, sender, amount); + ixs.push(ix) + return ixs + } + + async burn(input: BurnInput) { + let { + mint, owner, + amount, decimal + } = input + if (typeof mint == 'string') mint = new web3.PublicKey(mint) + if (typeof owner == 'string') owner = new web3.PublicKey(owner) + amount = amount ?? 1; + if (!decimal) { + const tokenInfo = await getMint(this.connection, mint) + decimal = tokenInfo.decimals; + } + amount = amount * (10 ** decimal) + const ata = getAssociatedTokenAddressSync(mint, owner) + const ix = createBurnInstruction(ata, mint, owner, amount) + return ix; + } + + tranferMintAuthority(input: TranferMintAuthority, ixCallBack?: (ixs?: web3.TransactionInstruction[]) => void) { + const { + mint, + currentAuthority, + newAuthority, + } = input + const ix = createSetAuthorityInstruction(mint, currentAuthority, SetAuthorityType.MintTokens, newAuthority); + if (ixCallBack) ixCallBack([ix]) + return { + ix, + } + } + + tranferFreezAuthority(input: TranferMintAuthority, ixCallBack?: (ixs?: web3.TransactionInstruction[]) => void) { + const { + mint, + currentAuthority, + newAuthority, + } = input + const ix = createSetAuthorityInstruction(mint, currentAuthority, SetAuthorityType.FreezeAccount, newAuthority); + if (ixCallBack) ixCallBack([ix]) + return { + ix, + } + } + + revokeAuthority(input: RevokeAuthorityInput, ixCallBack?: (ixs?: web3.TransactionInstruction[]) => void) { + const { + mint, + currentAuthority, + authorityType + } = input + const setAuthType = authorityType == 'MINTING' ? SetAuthorityType.MintTokens : SetAuthorityType.FreezeAccount + const ix = createSetAuthorityInstruction(mint, currentAuthority, setAuthType, null); + if (ixCallBack) ixCallBack([ix]) + // return { + // ix, + // } + return ix; + } + + async listAllOwnerTokens(owner: web3.PublicKey | string) { + if (typeof owner == 'string') owner = new web3.PublicKey(owner) + //TODO: can be improved + const parsedTokenAccounts = (await this.connection.getParsedTokenAccountsByOwner(owner, { programId: TOKEN_PROGRAM_ID })).value + const res: { token: web3.PublicKey, amount: number }[] = [] + for (let i of parsedTokenAccounts) { + const parsedAccountInfo = i.account.data; + const mintAddress = new web3.PublicKey(parsedAccountInfo.parsed?.info?.mint) + const tokenBalance: number = parsedAccountInfo.parsed?.info?.tokenAmount?.uiAmount; + res.push({ + amount: tokenBalance, + token: mintAddress + }) + } + return res; + } + + async getMint(mint: web3.PublicKey | string) { + if (typeof mint == 'string') mint = new web3.PublicKey(mint) + return getMint(this.connection, mint) + } + + static getTokenAccountFromAccountInfo(accountInfo: web3.AccountInfo | null) { + if (!accountInfo) return null + try { + return TokenAccountLayout.decode(accountInfo.data) + } catch (error) { return null } + } + + async getSolBalance(user: web3.PublicKey) { + const account = await this.connection.getAccountInfo(user).catch(() => null) + if (account) { + return calcDecimalValue(account.lamports, 9) + } + return null + } +} + diff --git a/src/base/getMarketAccountSizes.ts b/src/base/getMarketAccountSizes.ts new file mode 100644 index 00000000..d47c1ea2 --- /dev/null +++ b/src/base/getMarketAccountSizes.ts @@ -0,0 +1,54 @@ +import { Market } from "@openbook-dex/openbook"; +import { + calculateTotalAccountSize, + EVENT_QUEUE_HEADER_SIZE, + EVENT_SIZE, + ORDERBOOK_HEADER_SIZE, + ORDERBOOK_NODE_SIZE, + REQUEST_QUEUE_HEADER_SIZE, + REQUEST_SIZE, +} from "./orderbookUtils"; +import { web3 } from "@project-serum/anchor"; + +type useSerumMarketAccountSizesProps = { + eventQueueLength: number; + requestQueueLength: number; + orderbookLength: number; +}; +export default function useSerumMarketAccountSizes({ + eventQueueLength, + requestQueueLength, + orderbookLength, +}: useSerumMarketAccountSizesProps, connection: web3.Connection, programID: web3.PublicKey) { + const totalEventQueueSize = calculateTotalAccountSize( + eventQueueLength, + EVENT_QUEUE_HEADER_SIZE, + EVENT_SIZE + ) + + const totalRequestQueueSize = calculateTotalAccountSize( + requestQueueLength, + REQUEST_QUEUE_HEADER_SIZE, + REQUEST_SIZE + ) + + const totalOrderbookSize = calculateTotalAccountSize( + orderbookLength, + ORDERBOOK_HEADER_SIZE, + ORDERBOOK_NODE_SIZE + ) + // const useRentExemption = connection.getMinimumBalanceForRentExemption + // const marketAccountRent = await useRentExemption(Market.getLayout(programID).span); + // const eventQueueRent = await useRentExemption(totalEventQueueSize); + // const requestQueueRent = await useRentExemption(totalRequestQueueSize); + // const orderbookRent = await useRentExemption(totalOrderbookSize); + + return { + // marketRent: + // marketAccountRent + eventQueueRent + requestQueueRent + 2 * orderbookRent, + marketRent: 0, + totalEventQueueSize, + totalRequestQueueSize, + totalOrderbookSize, + }; +} \ No newline at end of file diff --git a/src/base/orderbookUtils.ts b/src/base/orderbookUtils.ts new file mode 100644 index 00000000..2548ebba --- /dev/null +++ b/src/base/orderbookUtils.ts @@ -0,0 +1,63 @@ +import { PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; + +// export const EVENT_QUEUE_LENGTH = 2978; +export const EVENT_QUEUE_LENGTH = 128; +export const EVENT_SIZE = 88; +export const EVENT_QUEUE_HEADER_SIZE = 32; + +export const REQUEST_QUEUE_LENGTH = 63; +export const REQUEST_SIZE = 80; +export const REQUEST_QUEUE_HEADER_SIZE = 32; + +// export const ORDERBOOK_LENGTH = 909; +export const ORDERBOOK_LENGTH = 201; +export const ORDERBOOK_NODE_SIZE = 72; +export const ORDERBOOK_HEADER_SIZE = 40; + +export async function getVaultOwnerAndNonce( + marketAddress: PublicKey, + dexAddress: PublicKey +): Promise<[vaultOwner: PublicKey, nonce: BN]> { + const nonce = new BN(0); + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const vaultOwner = await PublicKey.createProgramAddress( + [marketAddress.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)], + dexAddress + ); + return [vaultOwner, nonce]; + } catch (e) { + nonce.iaddn(1); + } + } +} + +export function calculateTotalAccountSize( + individualAccountSize: number, + accountHeaderSize: number, + length: number +) { + const accountPadding = 12; + const minRequiredSize = + accountPadding + accountHeaderSize + length * individualAccountSize; + + const modulo = minRequiredSize % 8; + + return modulo <= 4 + ? minRequiredSize + (4 - modulo) + : minRequiredSize + (8 - modulo + 4); +} + +export function calculateAccountLength( + totalAccountSize: number, + accountHeaderSize: number, + individualAccountSize: number +) { + const accountPadding = 12; + return Math.floor( + (totalAccountSize - accountPadding - accountHeaderSize) / + individualAccountSize + ); +} \ No newline at end of file diff --git a/src/base/types.ts b/src/base/types.ts new file mode 100644 index 00000000..f7ab1011 --- /dev/null +++ b/src/base/types.ts @@ -0,0 +1,15 @@ +import { web3 } from "@project-serum/anchor" +import { RawMint } from "@solana/spl-token" + +export type BaseRayInput = { + rpcEndpointUrl: string +} +export type Result = { + Ok?: T, + Err?: E +} +export type MPLTokenInfo = { + address: web3.PublicKey + mintInfo: RawMint, + metadata: any +} \ No newline at end of file diff --git a/src/base/utils.ts b/src/base/utils.ts new file mode 100644 index 00000000..4dde138f --- /dev/null +++ b/src/base/utils.ts @@ -0,0 +1,55 @@ +import { web3 } from "@project-serum/anchor" +import { bs58 } from "@project-serum/anchor/dist/cjs/utils/bytes" +import { Result } from "./types" + +export function calcNonDecimalValue(value: number, decimals: number): number { + return Math.trunc(value * (Math.pow(10, decimals))) +} + +export function calcDecimalValue(value: number, decimals: number): number { + return value / (Math.pow(10, decimals)) +} + +export function getKeypairFromStr(str: string): web3.Keypair | null { + try { + return web3.Keypair.fromSecretKey(Uint8Array.from(bs58.decode(str))) + } catch (error) { + return null + } +} + +export async function getNullableResutFromPromise(value: Promise, opt?: { or?: T, logError?: boolean }): Promise { + return value.catch((error) => { + if (opt) console.log({ error }) + return opt?.or != undefined ? opt.or : null + }) +} + +export function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export async function createLookupTable(connection: web3.Connection, signer: web3.Keypair, addresses: web3.PublicKey[] = []): Promise> { + try { + const slot = await connection.getSlot(); + addresses.push(web3.AddressLookupTableProgram.programId) + const [lookupTableInst, lookupTableAddress] = + web3.AddressLookupTableProgram.createLookupTable({ + authority: signer.publicKey, + payer: signer.publicKey, + recentSlot: slot - 1, + }); + const extendInstruction = web3.AddressLookupTableProgram.extendLookupTable({ + payer: signer.publicKey, + authority: signer.publicKey, + lookupTable: lookupTableAddress, + addresses, + }); + const transaction = new web3.Transaction().add(lookupTableInst, extendInstruction) + const txSignature = await connection.sendTransaction(transaction, [signer]); + return { Ok: { txSignature, lookupTable: lookupTableAddress.toBase58() } } + } catch (err) { + err = err ?? "" + return { Err: (err as any).toString() } + } +} \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 00000000..706f2ac8 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,53 @@ +import { config } from "dotenv" +import { bs58 } from "@project-serum/anchor/dist/cjs/utils/bytes"; +import { web3 } from "@project-serum/anchor"; +import { + TxVersion, Token,Currency, + TOKEN_PROGRAM_ID, + SOL, + CacheLTA, + LOOKUP_TABLE_CACHE + } from "@raydium-io/raydium-sdk"; + +config(); +function getKeypairFromStr(str: string): web3.Keypair | null { + try { + return web3.Keypair.fromSecretKey(Uint8Array.from(bs58.decode(str))) + } catch (error) { + return null + } +} + +export const RPC_ENDPOINT_MAIN = "https://indulgent-wandering-wave.solana-mainnet.quiknode.pro/a2bbf908f0bef4ff590544046ccc4f1b711b6d32/" +export const RPC_ENDPOINT_DEV = "https://white-proportionate-putty.solana-devnet.quiknode.pro/11132715a936f8adb03c940c627d6c0b9369d9e6/" + + +export const addLookupTableInfo = LOOKUP_TABLE_CACHE // only mainnet. other = undefined +export const makeTxVersion = TxVersion.V0 // LEGACY +// export const RPC_ENDPOINT_MAIN = "http://127.0.0.1:8899" +// export const RPC_ENDPOINT_DEV = "http://127.0.0.1:8899" + +const PINATA_API_kEY = process.env.PINATA_API_KEY! +const PINATA_DOMAIN = process.env.PINATA_DOMAIN! +const PINATA_API_SECRET_KEY = process.env.PINATA_API_SECRET_KEY! +const IN_PRODUCTION = process.env.PRODUCTION == '1' ? true : false +const COMPUTE_UNIT_PRICE = 1_800_000 // default: 200_000 +const JITO_AUTH_KEYPAIR = getKeypairFromStr(process.env.JITO_AUTH_KEYPAIR!)! +const JITO_BLOCK_ENGINE_URL = process.env.JITO_BLOCK_ENGINE_URL! +if (!JITO_AUTH_KEYPAIR || !JITO_BLOCK_ENGINE_URL) { + throw "Some ENV values not found" +} + +export const feeLevel = 18; + +export const jitoTipAccount = new web3.PublicKey("2d9CGsG2SnDveJkdszyepjMyQh64pQiFgLFXR7kmZYQo"); + +export const ENV = { + PINATA_API_kEY, + PINATA_API_SECRET_KEY, + PINATA_DOMAIN, + IN_PRODUCTION, + COMPUTE_UNIT_PRICE, + JITO_AUTH_KEYPAIR, + JITO_BLOCK_ENGINE_URL +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..0f42fe36 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,1220 @@ +import { web3 } from "@project-serum/anchor"; +import yargs, { command, option } from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import { addLiquidity, createAndBuy, createMarket, createPool, createToken, mintTo, removeLiquidity, removeLiquidityFaster, revokeAuthority, swap, unwrapSol } from "./txHandler"; +import { getPubkeyFromStr, getSlippage } from "./utils"; + +const log = console.log; +const delay = (ms: any) => new Promise(resolve => setTimeout(resolve, ms)) + + +const argv = yargs(hideBin(process.argv)) + .usage('Usage: $0 [options]') + + .command('run', + 'run it all', + yargs => { + return yargs.option('name', { + alias: 'n', + describe: "Token name", + type: "string", + demandOption: "Token name is required" + }).option('symbol', { + alias: 's', + describe: "Token symbol", + type: "string", + }).option('image', { + alias: 'i', + describe: "token image/logo url", + type: "string", + }).option('decimals', { + alias: 'd', + describe: "token decimals (default: 6)", + default: 6, + type: 'number' + }).option("website", { + alias: 'w', + describe: "external website link", + type: 'string', + default: "" + }).option("telegram", { + alias: 'tg', + describe: "external telegram link", + type: 'string', + default: "" + }).option("twitter", { + alias: 'tw', + describe: "external twitter link", + type: 'string', + default: "" + }).option("description", { + alias: 'desc', + describe: "description", + type: 'string', + default: "" + }).option("initial-minting", { + alias: 'im', + describe: "How many token you want to mint initially ? (default: 0)", + type: 'number', + default: 0 + }).option("url", { + alias: 'u', + describe: "network type (devnet/mainnet) default (mainnet)", + type: 'string', + default: "mainnet" + }).option('base-amount', { + alias: 'ba', + describe: "Initial base token liquidity", + type: "number", + demandOption: "base amount require" + }).option('quote-amount', { + alias: 'qa', + describe: "Initial quote token liquidity", + type: "number", + demandOption: "quote amount require" + }).option('order-size', { + alias: 'os', + describe: "Order size used to create market (default: 0.1)", + type: "number", + default: 0.1, + }).option('price-tick', { + alias: 'pt', + describe: "Price tick used to create market (default: 0.1)", + type: "number", + default: 0.1, + }).option('delay-seconds', { + describe: "Time to wait before removing liquidity", + type: "number", + default: 0, + }) + }, + async (argv) => { + + // CREATE TOKEN + const { + name, + symbol, + image, + decimals, + website, + telegram, + twitter, + description, + url, + initialMinting, + delaySeconds + } = argv + if (url != 'mainnet' && url != 'devnet') { + log("Invalid url") + return + } + + log({ + name, + image, + symbol, + initialMinting, + decimals, + website, + twitter, + telegram, + description, + url + }) + log("Creating token ...") + const createTokenRes = await createToken({ + name, + symbol, + url: url as any, + image, + decimals, + website, + twitter, + telegram, + description: "", + initialMintingAmount: initialMinting, + revokeAuthorities: true + }).catch(createTokenError => { + log({ + createTokenError + }); + return null + }) + + if (createTokenRes?.Err) { + log(createTokenRes.Err) + return + } + if (!createTokenRes || !createTokenRes.Ok) { + log("failed to create tx") + return + } + if (createTokenRes.Ok) { + log("---- Token successfully minted ----") + log("Tx Signature : ", createTokenRes.Ok.txSignature) + log("Token Address : ", createTokenRes.Ok.tokenId) + + // auto revoke inside the token creation + // log("Revoking Authorities ...") + // const token = getPubkeyFromStr(createTokenRes.Ok.tokenId) + // await revokeAuthority({ + // token: token!, + // url + // }) + + // CREATE MARKET + log("creating market") + await delay(25_000) + const { + orderSize, + priceTick + } = argv + let baseMint: web3.PublicKey | undefined | null = undefined + let quoteMint: web3.PublicKey | undefined | null = undefined + baseMint = getPubkeyFromStr(createTokenRes!.Ok!.tokenId) + if (!baseMint) { + log("Invalid base token address") + return + } + quoteMint = getPubkeyFromStr("So11111111111111111111111111111111111111112") + if (!quoteMint) { + log("Invalid quote token address") + return + } + const createMarketRes = await createMarket({ + baseMint, + orderSize, + priceTick, + quoteMint, + url + }).catch(createMarketError => { + log({ + createMarketError + }); + return null + }) + if (!createMarketRes) return log("failed to create pool") + if (createMarketRes.Err) return log({ + error: createMarketRes.Err + }) + if (!createMarketRes.Ok) return log("failed to create pool") + log("Transaction Successfully Executed:") + log("Transaction Signature: ", createMarketRes.Ok.txSignature) + log("Market Address: ", createMarketRes.Ok.marketId) + + + // // CREATE POOL + // await delay(2 * 1000) + // const marketId = createMarketRes!.Ok!.marketId; + // let { + // baseAmount, + // quoteAmount + // } = argv + // // orderSize = orderSize ?? 0.1; + // // priceTick = priceTick ?? 0.1; + // const id = getPubkeyFromStr(marketId) + // if (!id) { + // log("Invalid market id") + // return + // } + // const createPoolRes = await createPool({ + // marketId: id, + // baseMintAmount: baseAmount, + // quoteMintAmount: quoteAmount, + // url + // }).catch(error => { + // console.log({ + // createPoolError: error + // }); + // return null + // }); + // if (!createPoolRes) return log("failed to create pool") + // if (createPoolRes.Err) return log({ + // error: createPoolRes.Err + // }) + // if (!createPoolRes.Ok) return log("failed to create pool") + // log("Pool creation transaction successfully:") + // log("transaction signature: ", createPoolRes.Ok.txSignature) + // log("pool address: ", createPoolRes.Ok.poolId) + // + // + // console.log("Sleeping ", delaySeconds, " seconds") + // await delay(delaySeconds * 1000) + // const amount = -1 + // const poolId = getPubkeyFromStr(createPoolRes!.Ok!.poolId) + // if (!poolId) { + // log("Invalid pool id") + // return + // } + // const removeLiqRes = await removeLiquidity({ + // amount, + // poolId, + // url + // }).catch(outerRemoveLiquidityError => { + // log({ + // outerRemoveLiquidityError + // }) + // return null + // }) + // if (!removeLiqRes) return log("failed to send the transaction") + // if (removeLiqRes.Err) return log({ + // error: removeLiqRes.Err + // }) + // if (!removeLiqRes.Ok) return log("failed to send the transaction") + // log(`Remove liquidity transaction successfull\nTx Signature: ${removeLiqRes.Ok.txSignature}`) + // + // await unwrapSol(url) + // log(`Unwrapped sol`) + } + }) + + .command('createtoken', + 'token creation', + yargs => { + return yargs.option('name', { + alias: 'n', + describe: "Token name", + type: "string", + demandOption: "Token name is required" + }).option('symbol', { + alias: 's', + describe: "Token symbol", + type: "string", + }).option('image', { + alias: 'i', + describe: "token image/logo url", + type: "string", + }).option('decimals', { + alias: 'd', + describe: "token decimals (default: 6)", + default: 6, + type: 'number' + }).option("website", { + alias: 'w', + describe: "external website link", + type: 'string', + default: "" + }).option("telegram", { + alias: 'tg', + describe: "external telegram link", + type: 'string', + default: "" + }).option("twitter", { + alias: 'tw', + describe: "external twitter link", + type: 'string', + default: "" + }).option("description", { + alias: 'desc', + describe: "description", + type: 'string', + default: "" + }).option("initial-minting", { + alias: 'im', + describe: "How many token you want to mint initially ? (default: 0)", + type: 'number', + default: 0 + }).option("url", { + alias: 'u', + describe: "network type (devnet/mainnet) default (mainnet)", + type: 'string', + default: "mainnet" + }) + }, + async (argv) => { + // CREATE TOKEN + const { + name, + symbol, + image, + decimals, + website, + telegram, + twitter, + description, + url, + initialMinting + } = argv + log({ + name, + image, + symbol, + initialMinting, + decimals, + website, + twitter, + telegram, + url, + description + }) + log("Creating token ...") + const createTokenRes = await createToken({ + name, + symbol, + url: url as any, + image, + decimals, + website, + twitter, + telegram, + description, + initialMintingAmount: initialMinting + }).catch(createTokenError => { + log({ + createTokenError + }); + return null + }) + if (!createTokenRes) { + log("failed to create tx") + return + } + if (createTokenRes.Ok) { + log("---- Token successfully minted ----") + log("Tx Signature : ", createTokenRes.Ok.txSignature) + log("Token Address : ", createTokenRes.Ok.tokenId) + } else if (createTokenRes.Err) { + log(createTokenRes.Err) + } + }) + + .command('createmarket', + 'create a market to create a pool', + yargs => { + return yargs.option('base', { + alias: 'b', + describe: "Base token address", + type: "string", + demandOption: "base token address must require" + }).option('quote', { + alias: 'q', + describe: "Quote token address", + type: "string", + demandOption: "quore token address must require" + }).option('order-size', { + alias: 'os', + describe: "Order size used to create market (default: 0.1)", + type: "number", + default: 0.1, + }).option('price-tick', { + alias: 'pt', + describe: "Price tick used to create market (default: 0.1)", + type: "number", + default: 0.1, + }).option("url", { + alias: 'u', + describe: "network type (devnet/mainnet) default (mainnet)", + type: 'string', + default: "mainnet" + }) + }, + async (args) => { + const { + orderSize, + priceTick, + url + } = args + let baseMint: web3.PublicKey | undefined | null = undefined + let quoteMint: web3.PublicKey | undefined | null = undefined + if (url != 'mainnet' && url != 'devnet') { + log("please provide right url value ( 'mainnet' / 'devnet')") + return + } + baseMint = getPubkeyFromStr(args.base) + if (!baseMint) { + log("Invalid base token address") + return + } + quoteMint = getPubkeyFromStr(args.quote) + if (!quoteMint) { + log("Invalid quote token address") + return + } + const res = await createMarket({ + baseMint, + orderSize, + priceTick, + quoteMint, + url + }).catch(createMarketError => { + log({ + createMarketError + }); + return null + }) + if (!res) return log("failed to create pool") + if (res.Err) return log({ + error: res.Err + }) + if (!res.Ok) return log("failed to create pool") + const { + marketId, + txSignature + } = res.Ok + log("Transaction Successfully Executed:") + log("Transaction Signature: ", txSignature) + log("Market Address: ", marketId) + }) + + .command('createpool', + 'create pool and add liquidity', + yargs => { + return yargs.option('market', { + alias: 'm', + describe: "Market id", + type: "string", + demandOption: "Market id must require" + }).option('base-amount', { + alias: 'ba', + describe: "Initial base token liquidity", + type: "number", + demandOption: "base amount require" + }).option('quote-amount', { + alias: 'qa', + describe: "Initial quote token liquidity", + type: "number", + demandOption: "quote amount require" + }).option("url", { + alias: 'u', + describe: "network type (devnet/mainnet) default (mainnet)", + type: 'string', + default: "mainnet" + }) + }, + async (args) => { + let { baseAmount, quoteAmount, orderSize, priceTick, url } = args + orderSize = orderSize ?? 0.1; + priceTick = priceTick ?? 0.1; + let marketId: web3.PublicKey | undefined = undefined + if (url != 'mainnet' && url != 'devnet') { + log("Provide right url value ( 'mainnet' / 'devnet')") + return + } + const id = getPubkeyFromStr(args.market) + if (!id) { + log("Invalid market id") + return + } + marketId = id + const res = await createPool({ + marketId, + baseMintAmount: baseAmount, + quoteMintAmount: quoteAmount, + url + }).catch(error => { + console.log({ + createPoolError: error + }); + return null + }); + if (!res) return log("failed to create pool") + if (res.Err) return log({ + error: res.Err + }) + if (!res.Ok) return log("failed to create pool") + const { + poolId, + txSignature + } = res.Ok + log("Pool creation transaction successfully:") + log("transaction signature: ", txSignature) + log("pool address: ", poolId) + }) + + .command('buy', + 'buy token from pool', + yargs => { + return yargs.option("pool", { + alias: 'p', + describe: "Pool id", + type: "string", + demandOption: true + }).option("buy-token", { + alias: 'b', + describe: "which token you want to buy (base / quote)", + type: "string", + demandOption: true + }).option("amount", { + alias: 'a', + describe: "how many tokens you want to buy", + type: "string", + demandOption: true + }).option("slippage", { + alias: 's', + describe: "slippage tolerance (default: 1%)", + type: "number", + default: 1 + }).option("url", { + alias: 'u', + describe: "solana network type (default: mainnet )(ex: mainnet / devnet)", + type: "string", + default: 'mainnet' + }) + }, + async (args) => { + args.url = args.url + args.poolId = args.poolId ?? '' + const { buyToken, url } = args + if (url != 'mainnet' && url != 'devnet') return log("please enter valid url value") + if (buyToken != 'base' && buyToken != 'quote') return log("buyToken args values should be 'base' or 'quote'") + // const slippageAmount = Number(args.slipapge) + const slippageAmount = args.slippage + log({ slippageAmount }) + if (Number.isNaN(slippageAmount)) return log("Please enter valid slippage amount") + const slippage = getSlippage(slippageAmount) + const poolId = getPubkeyFromStr(args.pool.trim()) + if (!poolId) return log("Please enter valid pool address") + const amount = Number((args.amount ?? "").trim()) + if (Number.isNaN(amount)) return log("Please enter valid amount") + const txRes = await swap({ + amount, + amountSide: 'receive', + buyToken, + poolId, + slippage, + url + }).catch(error => { + console.log({ + swapTxError: error + }); + return null + }) + if (!txRes) return log("transaction failed") + if (txRes.Err) return log({ + Error: txRes.Err + }) + if (!txRes.Ok) return log("transaction failed") + log("--- Buy transaction successfull ---") + log("Tx signature : ", txRes.Ok.txSignature) + }) + .command('sell', + 'sell token from pool', + yargs => { + return yargs.option("pool", { + alias: 'p', + describe: "Pool id", + type: "string", + demandOption: true + }).option("sell-token", { + alias: 'st', + describe: "which token you want to buy (base / quote)", + type: "string", + demandOption: true + }).option("amount", { + alias: 'a', + describe: "how many tokens you want to buy", + type: "string", + demandOption: true + }).option("slippage", { + alias: 's', + describe: "slippage tolerance (default: 1%)", + type: "number", + default: 1 + }).option("url", { + alias: 'u', + describe: "solana network type (default: mainnet )(ex: mainnet / devnet)", + type: "string", + default: 'mainnet' + }) + }, + async (args) => { + args.url = args.url + args.poolId = args.poolId ?? '' + console.log("args ====>", args); + const { sellToken, url } = args + if (url != 'mainnet' && url != 'devnet') return log("please enter valid url value") + if (sellToken != 'base' && sellToken != 'quote') return log("buyToken args values should be 'base' or 'quote'") + // const slippageAmount = Number(args.slipapge) + const slippageAmount = args.slippage + log({ slippageAmount }) + if (Number.isNaN(slippageAmount)) return log("Please enter valid slippage amount") + const slippage = getSlippage(slippageAmount) + const poolId = getPubkeyFromStr(args.pool.trim()) + if (!poolId) return log("Please enter valid pool address") + const amount = Number((args.amount ?? "").trim()) + if (Number.isNaN(amount)) return log("Please enter valid amount") + const txRes = await swap({ + amount, + amountSide: 'send', + buyToken: 'base', + sellToken, + poolId, + slippage, + url + }).catch(error => { + console.log({ + swapTxError: error + }); + return null + }) + if (!txRes) return log("transaction failed") + if (txRes.Err) return log({ + Error: txRes.Err + }) + if (!txRes.Ok) return log("transaction failed") + log("--- Sell transaction successfull ---") + log("Tx signature : ", txRes.Ok.txSignature) + }) + + .command("addliquidity", + "add liquidity in pool", + yargs => { + return yargs.option("pool", { + alias: 'p', + describe: "pool address", + demandOption: "poolId require", + type: 'string' + }).option("amount", { + alias: 'a', + describe: "how much token you want to add (another token amount calcualted automatically)", + demandOption: "reqire to enter amount", + type: 'number' + }).option("amount-side", { + alias: 'as', + describe: "which token amount you want to enter (base/quote)", + demandOption: "reqire to enter amount size", + type: 'string' + }).option("slippage", { + alias: 's', + describe: "slippage tolerance", + type: 'number', + default: 1, + }).option("url", { + alias: 'u', + describe: "solana network type (default: mainnet )(ex: mainnet / devnet)", + type: "string", + default: 'mainnet' + }) + }, + async (args) => { + const { amount, amountSide, url } = args + if (amountSide != 'base' && amountSide != 'quote') { + return log("invalid amount side value") + } + if (url != 'mainnet' && url != 'devnet') { + return log("invalid url value") + } + const poolId = getPubkeyFromStr(args.pool) + if (!poolId) { + log("Invalid pool id") + return + } + const slippage = getSlippage(args.slippage) + const res = await addLiquidity({ + amount, + amountSide, + poolId, + slippage, + url + }).catch(outerAddLiquidityError => { + log({ + outerAddLiquidityError + }) + return null + }) + if (!res) return log("failed to send the transaction") + if (res.Err) return log({ + error: res.Err + }) + if (!res.Ok) return log("failed to send the transaction") + log(`Add liquidity transaction successfull\nTx Signature: ${res.Ok.txSignature}`) + }) + + .command('removeliquidity', + 'remove liquidity from the pool', + yargs => { + return yargs.option("pool", { + alias: 'p', + describe: "pool address", + demandOption: "poolId require", + type: 'string' + }).option("amount", { + alias: 'a', + describe: "amount of lp tokens (enter -1 to remove all liquidity)", + demandOption: "reqire to enter amount", + type: 'number' + }).option("url", { + alias: 'u', + describe: "solana network type (default: mainnet )(ex: mainnet / devnet)", + type: "string", + default: 'mainnet' + }) + }, + async (args) => { + const { + amount, + url + } = args + if (url != 'mainnet' && url != 'devnet') { + return log("invalid url value") + } + const poolId = getPubkeyFromStr(args.pool) + if (!poolId) { + log("Invalid pool id") + return + } + const res = await removeLiquidity({ + amount, + poolId, + url + }).catch(outerRemoveLiquidityError => { + log({ + outerRemoveLiquidityError + }) + return null + }) + if (!res) return log("failed to send the transaction") + if (res.Err) return log({ + error: res.Err + }) + if (!res.Ok) return log("failed to send the transaction") + log(`Remove liquidity transaction successfull\nTx Signature: ${res.Ok.txSignature}`) + }) + + .command('unwrap', + 'unwrap wrapped sol to normal sol', + yargs => { + return yargs.option('url', { + alias: 'u', + describe: "solana network type (default: mainnet )(ex: mainnet / devnet)", + type: "string", + default: 'mainnet' + }) + }, + async args => { + log("unwrapping sol ...") + const url = args.url + if (url != 'mainnet' && url != 'devnet') return log("invalid url value") + await unwrapSol(url) + }) + + .command('minting', + 'token minting', + yargs => { + return yargs.option('token', { + alias: 't', + describe: "Token address", + type: "string", + demandOption: "token address require" + }).option('amount', { + alias: 'a', + describe: "how many tokens to mint", + type: 'number', + demandOption: "token address require" + }).option('url', { + alias: 'u', + describe: "solana network type (default: mainnet )(ex: mainnet / devnet)", + type: "string", + default: 'mainnet' + }) + }, + async args => { + log("token minting ...") + const url = args.url + if (url != 'mainnet' && url != 'devnet') return log("invalid url value") + const token = getPubkeyFromStr(args.token) + if (!token) return log("Please enter valid token address") + const amount = args.amount + await mintTo({ + token, + amount, + url + }) + }) + + .command("revokeauth", + 'revoke token authority', + yargs => { + return yargs.option('token', { + alias: 't', + description: "Token address", + type: 'string', + demandOption: "token address must require" + }).option('url', { + alias: 'u', + describe: "solana network type (default: mainnet )(ex: mainnet / devnet)", + type: "string", + default: 'mainnet' + }) + }, + async args => { + const { + url + } = args + const token = getPubkeyFromStr(args.token) + if (!token) { + log("Invalid token address") + return + } + if (url != 'mainnet' && url != 'devnet') { + log("Invalid url") + return + } + await revokeAuthority({ + token, + url + }) + }) + + .command('createpool-buy', + 'create pool, bundle buy', + yargs => { + return yargs.option('market', { + alias: 'm', + describe: "Market id", + type: "string", + demandOption: "Market id must require" + }).option('base-amount', { + alias: 'ba', + describe: "Initial base token liquidity", + type: "number", + demandOption: "base amount require" + }).option('quote-amount', { + alias: 'qa', + describe: "Initial quote token liquidity", + type: "number", + demandOption: "quote amount require" + }).option("buy-token", { + alias: 'bt', + describe: "Which tokne you want to buy (base/quote) ?", + type: 'string', + default: "base" + }).option("buy-amount", { + describe: "how many token you want to buy instantly", + type: 'number', + demandOption: "buy amount require" + }).option("url", { + alias: 'u', + describe: "network type (devnet/mainnet) default (mainnet)", + type: 'string', + default: "mainnet" + }) + }, + async (args) => { + const { + baseAmount, + quoteAmount, + market, + buyToken, + buyAmount, + url + } = args + if (url != 'mainnet' && url != 'devnet') { + log("Provide right url value ( 'mainnet' / 'devnet')") + return + } + const marketId = getPubkeyFromStr(market) + if (!marketId) { + log("Invalid market id") + return + } + if (buyToken != 'base' && buyToken != 'quote') { + log("invalid buy token value (value should be `base` or `quote`") + return + } + const res = await createAndBuy({ + marketId, + baseMintAmount: baseAmount, + quoteMintAmount: quoteAmount, + buyToken, + buyAmount, + url + }).catch((createAndBuyError) => { + log({ + createAndBuyError + }) + return null + }) + if (!res) { + log("Failed to send bundle") + return + } + if (res.Err) { + const err = res.Err + console.log({ + err + }) + if (typeof err == 'string') return log(err) + const { + bundleId, + poolId + } = err + log("Unable to verify the bundle transaction") + log("please check it") + log("Bundle id: ", bundleId) + log("poolId: ", poolId) + log(`Check the bundle here: https://explorer.jito.wtf/bundle/${bundleId}`) + } + if (res.Ok) { + const { + bundleId, + bundleStatus, + buyTxSignature, + createPoolTxSignature, + poolId + } = res.Ok + log("Bundle send successfully") + log("Bundle id: ", bundleId) + log("Pool Id: ", poolId) + log("Create pool transaction signature: ", createPoolTxSignature) + log("Buy transaction signature: ", buyTxSignature) + log(`Check the bundle here: https://explorer.jito.wtf/bundle/${bundleId}`) + } + return log("Failed to send bundle") + }) + + .command('createpool-buy-remove', + 'create pool, add liq, bundle buy, wait and remove liq', + yargs => { + return yargs.option('market', { + alias: 'm', + describe: "Market id", + type: "string", + demandOption: "Market id must require" + }).option('base-amount', { + alias: 'ba', + describe: "Initial base token liquidity", + type: "number", + demandOption: "base amount require" + }).option('quote-amount', { + alias: 'qa', + describe: "Initial quote token liquidity", + type: "number", + demandOption: "quote amount require" + }).option("buy-token", { + alias: 'bt', + describe: "Which tokne you want to buy (base/quote) ?", + type: 'string', + default: "base" + }).option("buy-amount", { + describe: "how many token you want to buy instantly", + type: 'number', + demandOption: "buy amount require" + }).option("url", { + alias: 'u', + describe: "network type (devnet/mainnet) default (mainnet)", + type: 'string', + default: "mainnet" + }).option('delay-seconds', { + describe: "Time to wait before removing liquidity", + type: "number", + default: 0, + }) + }, + async (args) => { + const { + baseAmount, + quoteAmount, + market, + buyToken, + buyAmount, + url, + delaySeconds + } = args + if (url != 'mainnet' && url != 'devnet') { + log("Provide right url value ( 'mainnet' / 'devnet')") + return + } + const marketId = getPubkeyFromStr(market) + if (!marketId) { + log("Invalid market id") + return + } + if (buyToken != 'base' && buyToken != 'quote') { + log("invalid buy token value (value should be `base` or `quote`") + return + } + const res = await createAndBuy({ + marketId, + baseMintAmount: baseAmount, + quoteMintAmount: quoteAmount, + buyToken, + buyAmount, + url + }).catch((createAndBuyError) => { + log({ + createAndBuyError + }) + return null + }) + if (!res) { + log("Failed to send bundle") + return + } + let removePoolId = null; + if (res.Err) { + const err = res.Err + console.log({ err }) + if (typeof err == 'string') return log(err) + const { bundleId, poolId } = err + removePoolId = poolId + log("Unable to verify the bundle transaction") + log("please check it") + log("Bundle id: ", bundleId) + log("poolId: ", poolId) + log(`Check the bundle here: https://explorer.jito.wtf/bundle/${bundleId}`) + } + if (res.Ok) { + const { + bundleId, + bundleStatus, + buyTxSignature, + createPoolTxSignature, + poolId + } = res.Ok + removePoolId = poolId + log("Bundle send successfully") + log("Bundle id: ", bundleId) + log("Pool Id: ", poolId) + log("Create pool transaction signature: ", createPoolTxSignature) + log("Buy transaction signature: ", buyTxSignature) + log(`Check the bundle here: https://explorer.jito.wtf/bundle/${bundleId}`) + } + + await delay(delaySeconds * 1000) + + if (removePoolId != null) { + const amount = -1 + const poolId = getPubkeyFromStr(removePoolId) + if (!poolId) { + log("Invalid pool id") + return + } + const removeLiqRes = await removeLiquidity({ + amount, + poolId, + url + }).catch(outerRemoveLiquidityError => { + log({ outerRemoveLiquidityError }) + return null + }) + if (!removeLiqRes) return log("failed to send the transaction") + if (removeLiqRes.Err) return log({ error: removeLiqRes.Err }) + if (!removeLiqRes.Ok) return log("failed to send the transaction") + log(`Remove liquidity transaction successfull\nTx Signature: ${removeLiqRes.Ok.txSignature}`) + + await unwrapSol(url) + log(`Unwrapped sol`) + } + }) + + .command('createpool-remove', + 'create pool, add liq, wait and remove liq', + yargs => { + return yargs.option('market', { + alias: 'm', + describe: "Market id", + type: "string", + demandOption: "Market id must require" + }).option('base-amount', { + alias: 'ba', + describe: "Initial base token liquidity", + type: "number", + demandOption: "base amount require" + }).option('quote-amount', { + alias: 'qa', + describe: "Initial quote token liquidity", + type: "number", + demandOption: "quote amount require" + }).option("url", { + alias: 'u', + describe: "network type (devnet/mainnet) default (mainnet)", + type: 'string', + default: "mainnet" + }).option('delay-seconds', { + describe: "Time to wait before removing liquidity", + type: "number", + default: 0, + }) + }, + async (args) => { + let { baseAmount, quoteAmount, orderSize, priceTick, url, delaySeconds } = args + orderSize = orderSize ?? 0.1; + priceTick = priceTick ?? 0.1; + let marketId: web3.PublicKey | undefined = undefined + if (url != 'mainnet' && url != 'devnet') { + log("Provide right url value ( 'mainnet' / 'devnet')") + return + } + const id = getPubkeyFromStr(args.market) + if (!id) { + log("Invalid market id") + return + } + marketId = id + const res = await createPool({ + marketId, + baseMintAmount: baseAmount, + quoteMintAmount: quoteAmount, + url + }).catch(error => { + console.log({ createPoolError: error }); + return null + }); + if (!res) return log("failed to create pool") + if (res.Err) return log({ error: res.Err }) + if (!res.Ok) return log("failed to create pool") + log("Pool creation transaction successfully:") + log("transaction signature: ", res.Ok.txSignature) + log("pool address: ", res.Ok.poolId) + + log("Removing liquidity ...") + await delay(delaySeconds * 1000) + + + const amount = -1 + const poolId = getPubkeyFromStr(res.Ok.poolId) + if (!poolId) { + log("Invalid pool id") + return + } + const removeCallback = async () => { + const { baseAmount, baseDecimals, quoteAmount, quoteDecimals } = res.Ok! + const bA = Number(baseAmount.toString()) + const qA = Number(quoteAmount.toString()) + const bq = (bA * qA) + const sbq = Math.sqrt(bq) + const extr = 10 ** baseDecimals + const amount = Math.trunc(sbq - extr) + log({ removeAmount: amount }) + await removeLiquidityFaster({ amount, poolId, url: url as any, unwrapSol: true }) + } + const rHandler = removeCallback().catch((outerRemoveLiquidityFaster) => { + log({ outerRemoveLiquidityFaster }) + log("faster remove liquidity faild") + }).then(() => log('Lightning remove liquidity pass')) + + // can be commented + const removeCallback2 = async () => { + const removeLiqRes = await removeLiquidity({ + amount, + poolId, + url: url as any, + unwrapSol: true + }).catch(outerRemoveLiquidityError => { + log({ outerRemoveLiquidityError }) + return null + }) + if (!removeLiqRes) return log("failed to send the transaction") + if (removeLiqRes.Err) return log({ error: removeLiqRes.Err }) + if (!removeLiqRes.Ok) return log("failed to send the transaction") + log(`Remove liquidity transaction successfull\nTx Signature: ${removeLiqRes.Ok.txSignature}`) + } + const rHandler2 = removeCallback2().catch((outerRemoveLiquidityError2) => { + log({ outerRemoveLiquidityError2 }) + }) + + await rHandler + await rHandler2 + // log("unwraping sol...") + // await delay(10_000) + // await unwrapSol(url) + // log(`Unwrapped sol`) + }) + + .option('verbose', { + alias: 'v', + type: 'boolean', + description: 'Run with verbose logging' + }) + .parse(); diff --git a/src/jito_bundle/build-bundle.ts b/src/jito_bundle/build-bundle.ts new file mode 100644 index 00000000..c21c0bf2 --- /dev/null +++ b/src/jito_bundle/build-bundle.ts @@ -0,0 +1,200 @@ +import { + Connection, + Keypair, + PublicKey, + TransactionInstruction, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; + +import { SearcherClient } from "jito-ts/dist/sdk/block-engine/searcher"; +import { Bundle } from "jito-ts/dist/sdk/block-engine/types"; +import { isError } from "jito-ts/dist/sdk/block-engine/utils"; +import { ClientReadableStream } from "@grpc/grpc-js"; +import { buildSimpleTransaction } from "@raydium-io/raydium-sdk"; + +import { + ENV, + addLookupTableInfo, + makeTxVersion, +} from "../constants"; +import { BundleResult } from "jito-ts/dist/gen/block-engine/bundle"; +import { getKeypairFromEnv } from "../utils"; + +const MEMO_PROGRAM_ID = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo"; + +export async function build_bundle( + search: SearcherClient, + // accounts: PublicKey[], + // regions: string[], + bundleTransactionLimit: number, + lp_ix : any, + swap_ix : any, + connection: Connection +) { + const jito_auth_keypair = ENV.JITO_AUTH_KEYPAIR; + const feeWallet = getKeypairFromEnv(); + + const _tipAccount = (await search.getTipAccounts())[0]; + console.log("tip account:", _tipAccount); + const tipAccount = new PublicKey(_tipAccount); + + let message1 = "First TXN"; + let message2 = "Second TXN"; + + const bund = new Bundle([], bundleTransactionLimit); + const resp = await connection.getLatestBlockhash("processed"); + // bund.addTransactions(lp_ix, swap_ix); + bund.addTransactions(lp_ix); + bund.addTransactions(swap_ix); + + // const willSendTx1 = await buildSimpleTransaction({ + // connection, + // makeTxVersion, + // payer: feeWallet.publicKey, + // innerTransactions: lp_ix, + // addLookupTableInfo: addLookupTableInfo, + // }); + + // const willSendTx2 = await buildSimpleTransaction({ + // connection, + // makeTxVersion, + // payer: feeWallet.publicKey, + // innerTransactions: swap_ix, + // addLookupTableInfo: addLookupTableInfo, + // }); + + // if (willSendTx1[0] instanceof VersionedTransaction) { + // willSendTx1[0].sign([feeWallet]); + // // txids.push(await connection.sendTransaction(iTx, options)); + // bund.addTransactions(willSendTx1[0]); + // } + + // if (willSendTx2[0] instanceof VersionedTransaction) { + // willSendTx2[0].sign([feeWallet]); + // // txids.push(await connection.sendTransaction(iTx, options)); + // bund.addTransactions(willSendTx2[0]); + // } + + // bund.addTransactions( + // buildMemoTransaction(LP_wallet_keypair, resp.blockhash, message1) + // ); + + // bund.addTransactions( + // buildMemoTransaction(swap_wallet_keypair, resp.blockhash, message2) + // ); + + let maybeBundle = bund.addTipTx( + feeWallet, + 10000000, + tipAccount, + resp.blockhash + ); + + if (isError(maybeBundle)) { + throw maybeBundle; + } + console.log(); + + try { + const response_bund = await search.sendBundle(maybeBundle); + console.log("response_bund:", response_bund); + } catch (e) { + console.error("error sending bundle:", e); + } + + return maybeBundle; +} + + + + + + +export const onBundleResult = (c: SearcherClient): Promise => { + let first = 0; + let isResolved = false; + + return new Promise((resolve) => { + // Set a timeout to reject the promise if no bundle is accepted within 5 seconds + setTimeout(() => { + resolve(first); + isResolved = true + }, 30000); + + c.onBundleResult( + + + (result) => { + + if (isResolved) return first; + // clearTimeout(timeout); // Clear the timeout if a bundle is accepted + + + const bundleId = result.bundleId; + const isAccepted = result.accepted; + const isRejected = result.rejected; + if (isResolved == false){ + + if (isAccepted) { + console.log( + "bundle accepted, ID:", + result.bundleId, + " Slot: ", + result.accepted?.slot + ); + first +=1; + isResolved = true; + resolve(first); // Resolve with 'first' when a bundle is accepted + } + + if (isRejected) { + console.log("bundle is Rejected:", result); + // Do not resolve or reject the promise here + } + + } + + }, + (e) => { + console.error(e); + // Do not reject the promise here + } + ); + }); +}; + + + + +export const buildMemoTransaction = ( + keypair: Keypair, + recentBlockhash: string, + message: string +): VersionedTransaction => { + const ix = new TransactionInstruction({ + keys: [ + { + pubkey: keypair.publicKey, + isSigner: true, + isWritable: true, + }, + ], + programId: new PublicKey(MEMO_PROGRAM_ID), + data: Buffer.from(message), + }); + + const instructions = [ix]; + + const messageV0 = new TransactionMessage({ + payerKey: keypair.publicKey, + recentBlockhash: recentBlockhash, + instructions, + }).compileToV0Message(); + + const tx = new VersionedTransaction(messageV0); + + tx.sign([keypair]); + + return tx; +}; diff --git a/src/jito_bundle/send-bundle.ts b/src/jito_bundle/send-bundle.ts new file mode 100644 index 00000000..70d2336f --- /dev/null +++ b/src/jito_bundle/send-bundle.ts @@ -0,0 +1,42 @@ + +import { + ENV + } from '../constants'; +import {Connection} from "@solana/web3.js" +import {searcherClient} from 'jito-ts/dist/sdk/block-engine/searcher'; +import {build_bundle, onBundleResult} from './build-bundle'; + + + +export async function bull_dozer(connection : Connection, lp_ix : any,swap_ix : any) { + + const blockEngineUrl = ENV.JITO_BLOCK_ENGINE_URL; + const jito_auth_keypair = ENV.JITO_AUTH_KEYPAIR; + console.log('BLOCK_ENGINE_URL:', blockEngineUrl); + const bundleTransactionLimit = parseInt('3'); + + const search = searcherClient(blockEngineUrl, jito_auth_keypair); + + + await build_bundle( + search, + bundleTransactionLimit, + lp_ix, + swap_ix, + connection + ); + const bundle_result = await onBundleResult(search) +return bundle_result + +// search.onBundleResult( +// (bundle) => { +// console.log(`JITO bundle result: ${JSON.stringify(bundle)}`); +// return true; +// }, +// (error) => { +// console.log(`JITO bundle error: ${error}`); +// return false; +// } +// ); +} + diff --git a/src/txHandler.ts b/src/txHandler.ts new file mode 100644 index 00000000..a06ff6ff --- /dev/null +++ b/src/txHandler.ts @@ -0,0 +1,879 @@ +import { Wallet, web3 } from "@project-serum/anchor"; +import { BaseMpl } from "./base/baseMpl"; +import { Result } from "./base/types"; +import { AddLiquidityInput, BundleRes, CreateAndBuy, CreateMarketInput, CreatePoolInput, CreateTokenInput, RemoveLiquidityInput, SwapInput } from "./types"; +import { calcDecimalValue, calcNonDecimalValue, deployJsonData, getKeypairFromEnv, sendAndConfirmTransaction, sleep, transferJitoTip } from "./utils"; +import { ENV, RPC_ENDPOINT_DEV, RPC_ENDPOINT_MAIN } from "./constants"; +import { BaseRay } from "./base/baseRay"; +import { Metadata, TokenStandard } from "@metaplex-foundation/mpl-token-metadata"; +import { AccountLayout, MintLayout, NATIVE_MINT, TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, createCloseAccountInstruction, getAssociatedTokenAddressSync } from '@solana/spl-token' +import { BaseSpl } from "./base/baseSpl"; +import { searcherClient } from "jito-ts/dist/sdk/block-engine/searcher"; +import { bundle } from "jito-ts"; +import { Liquidity, LiquidityPoolInfo, Percent, Token, TokenAmount } from "@raydium-io/raydium-sdk"; +import BN from "bn.js"; +import fs from 'fs' +import { bull_dozer } from "./jito_bundle/send-bundle"; +const log = console.log; + +// export async function updateMetadata(mintAddress: string, input: CreateTokenInput) { +// +// log("CREATING METADATA") +// +// const connection = new web3.Connection(web3.clusterApiUrl('devnet'), 'confirmed'); +// const keypair = getKeypairFromEnv(); +// +// // Define our Mint address +// const mint = new web3.PublicKey(mintAddress) +// +// // Add the Token Metadata Program +// const token_metadata_program_id = new web3.PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s') +// +// // Create PDA for token metadata +// const metadata_seeds = [ +// Buffer.from('metadata'), +// token_metadata_program_id.toBuffer(), +// mint.toBuffer(), +// ]; +// const [metadata_pda, _bump] = web3.PublicKey.findProgramAddressSync(metadata_seeds, token_metadata_program_id); +// console.log({ mint }) +// console.log({ metadata_pda }) +// console.log({ _bump }) +// +// // Start here +// const metadaAccount = createCreateMetadataAccountV3Instruction( +// { +// metadata: metadata_pda, +// mint: mint, +// mintAuthority: keypair.publicKey, +// payer: keypair.publicKey, +// updateAuthority: keypair.publicKey, +// systemProgram: web3.SystemProgram.programId, +// }, +// { +// createMetadataAccountArgsV3: { +// isMutable: false, +// collectionDetails: null, +// data: { +// name: input.name || "", +// symbol: input.symbol || "", +// uri: input.image || "", // IPFS link to hosted metadata +// sellerFeeBasisPoints: 0, +// creators: null, +// collection: null, +// uses: null, +// }, +// // extensions: { +// // website: input.website || "", +// // twitter: input.twitter || "", +// // telegram: input.telegram || "", +// // }, +// // "updateAuthority": keypair.publicKey, +// // "mint": mintAddress, +// // "primarySaleHappened": 0, +// // "isMutable": false, +// // "editionNonce": 254, +// // "tokenStandard": 2, +// // "name": input.name || "", +// // "symbol": input.symbol || "", +// // "image": input.image || "", +// // "description": "", // TODO? +// // "tags": [], +// // "creator": { +// // "name": "DEXLAB MINTING LAB", +// // "site": "https://www.dexlab.space" +// // } +// } +// } +// ); +// const tx = new web3.Transaction().add(metadaAccount); +// const txhash = await web3.sendAndConfirmTransaction( +// connection, +// tx, +// [keypair] +// ); +// console.log(`Success! Check out your TX here: https://solscan.io/tx/${txhash}?cluster=devnet`); +// } + +export async function createToken(input: CreateTokenInput): Promise> { + try { + const { decimals, name, image, symbol, website, telegram, twitter, url, initialMintingAmount, description } = input; + const metadata = {} as any + metadata.name = name + metadata.symbol = symbol + metadata.description = description || "" + metadata.extensions = { website: "", twitter: "", telegram: "" } + metadata.extensions.website = website + metadata.extensions.twitter = twitter + metadata.extensions.telegram = telegram + metadata.tags = [] + metadata.creator = { + name: "DEXLAB MINTING LAB", + site: "https://www.dexlab.space", + } + if (image) metadata.image = image + console.log({ input }) + console.log({ metadata }) + + const keypair = getKeypairFromEnv(); + const wallet = new Wallet(keypair) + const endpoint = url == 'mainnet' ? RPC_ENDPOINT_MAIN : RPC_ENDPOINT_DEV + const baseMpl = new BaseMpl(wallet, { endpoint }) + + let ipfsHash = "Null" + if (ENV.IN_PRODUCTION) { + console.log("Deploying json metadata") + const hash = await deployJsonData(metadata).catch(() => null) + console.log("Deployed json metadata") + if (!hash) { + return { Err: "failed to deploy json metadata" } + } + ipfsHash = hash + } + if (!ipfsHash) throw "Failed to deploy metadata" + const uri = `https://${ENV.PINATA_DOMAIN}/ipfs/${ipfsHash}`; + + console.log("Creating token") + const res = await baseMpl.createToken({ + name, + uri, + symbol, + sellerFeeBasisPoints: 0, + tokenStandard: TokenStandard.Fungible, + creators: [{ address: wallet.publicKey, share: 100 }] + }, { + decimal: decimals, + mintAmount: initialMintingAmount ?? 0, + revokeAuthorities: input.revokeAuthorities + }) + + if (!res) { + return { Err: "Failed to send the transation" } + } + + // console.log(`DEPLOYED TO ${res.token}, UPDATING METADATA`) + // await updateMetadata(res.token, input) + + return { + Ok: { + txSignature: res.txSignature, + tokenId: res.token + } + } + } + catch (error) { + log({ error }) + return { Err: "failed to create the token" } + } +} + +export async function addLiquidity(input: AddLiquidityInput): Promise> { + const { amount, amountSide, poolId, url, slippage } = input + const keypair = getKeypairFromEnv(); + const user = keypair.publicKey + const connection = new web3.Connection(input.url == 'mainnet' ? RPC_ENDPOINT_MAIN : RPC_ENDPOINT_DEV, { commitment: "confirmed", confirmTransactionInitialTimeout: 60000 }) + const baseRay = new BaseRay({ rpcEndpointUrl: connection.rpcEndpoint }) + const poolKeys = await baseRay.getPoolKeys(poolId).catch(getPoolKeysError => { log({ getPoolKeysError }); return null }) + if (!poolKeys) return { Err: "Pool not found" } + const amountInfo = await baseRay.computeAnotherAmount({ amount, fixedSide: amountSide, poolKeys, isRawAmount: false, slippage }).catch(computeAnotherAmountError => { log({ computeAnotherAmount: computeAnotherAmountError }); return null }) + if (!amountInfo) return { Err: "Failed to clculate the amount" } + const { baseMintAmount, liquidity, quoteMintAmount, } = amountInfo + const txInfo = await baseRay.addLiquidity({ baseMintAmount, fixedSide: amountSide, poolKeys, quoteMintAmount, user }).catch(addLiquidityError => { log({ addLiquidityError }); return null }) + if (!txInfo) return { Err: 'failed to prepare tx' } + const { ixs } = txInfo + + // speedup + const updateCuIx = web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: ENV.COMPUTE_UNIT_PRICE }) + const recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + const tx = new web3.Transaction().add(updateCuIx, ...ixs) + tx.feePayer = keypair.publicKey + tx.recentBlockhash = recentBlockhash + tx.sign(keypair) + + // const res = await connection.sendTransaction(tx, [keypair]).catch(sendTxError => { log({ sendTxError }); return null }); + const res = await sendAndConfirmTransaction(tx, connection).catch((sendAndConfirmTransactionError) => { + log({ sendAndConfirmTransactionError }) + }) + if (!res) return { Err: "failed to send the transaction" } + return { Ok: { txSignature: res } } +} + +export async function removeLiquidityFaster(input: RemoveLiquidityInput): Promise> { + const { amount, poolId, url, } = input + const keypair = getKeypairFromEnv(); + const user = keypair.publicKey + const connection = new web3.Connection(input.url == 'mainnet' ? RPC_ENDPOINT_MAIN : RPC_ENDPOINT_DEV, { commitment: "confirmed", confirmTransactionInitialTimeout: 60000 }) + const baseRay = new BaseRay({ rpcEndpointUrl: connection.rpcEndpoint }) + const poolKeys = await baseRay.getPoolKeys(poolId).catch(getPoolKeysError => { log({ getPoolKeysError }); return null }) + if (!poolKeys) return { Err: "Pool not found" } + const txInfo = await baseRay.removeLiquidityFaster({ amount, poolKeys, user }).catch(removeLiquidityError => { log({ removeLiquidityError }); return null }) + if (!txInfo) return { Err: "failed to prepare tx" } + if (txInfo.Err) return { Err: txInfo.Err } + if (!txInfo.Ok) return { Err: "failed to prepare tx" } + const userSplAta = getAssociatedTokenAddressSync(NATIVE_MINT, user) + const initSplAta = createAssociatedTokenAccountInstruction(user, userSplAta, user, NATIVE_MINT) + const ixs = [initSplAta, ...txInfo.Ok.ixs] + const userSolAta = getAssociatedTokenAddressSync(NATIVE_MINT, user) + if (input.unwrapSol) ixs.push(createCloseAccountInstruction(userSolAta, user, user)) + + // speedup + const updateCuIx = web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: ENV.COMPUTE_UNIT_PRICE * 3 }) + const tx = new web3.Transaction().add(updateCuIx, ...ixs) + tx.feePayer = keypair.publicKey + const recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + tx.recentBlockhash = recentBlockhash + tx.sign(keypair) + const handlers: Promise[] = [] + for (let i = 0; i < 4; ++i) { + const handle = connection.sendTransaction(tx, [keypair], { skipPreflight: true }).catch(sendTxError => { return null }).then((res) => { + if (res) { + log(`lightning try: ${i + 1} | txSignature: ${res}`) + } + }); + handlers.push(handle) + } + for (let h of handlers) { + await h + } + + const rawTx = tx.serialize() + const txSignature = (await web3.sendAndConfirmRawTransaction(connection, Buffer.from(rawTx), { commitment: 'confirmed' }) + .catch(async () => { + console.log("remove liq tx1 failed") + await sleep(500) + console.log("sending remove liq tx2") + return await web3.sendAndConfirmRawTransaction(connection, Buffer.from(rawTx), { commitment: 'confirmed' }) + .catch((createPoolAndBuyTxFail) => { + log({ createPoolAndBuyTxFail }) + return null + }) + })) + console.log("confirmed remove liq tx") + // const res = await connection.sendTransaction(tx, [keypair]).catch(sendTxError => { log({ sendTxError }); return null }); + if (!txSignature) return { Err: "failed to send the transaction" } + return { Ok: { txSignature } } +} + +export async function removeLiquidity(input: RemoveLiquidityInput): Promise> { + const { amount, poolId, url, } = input + const keypair = getKeypairFromEnv(); + const user = keypair.publicKey + const connection = new web3.Connection(input.url == 'mainnet' ? RPC_ENDPOINT_MAIN : RPC_ENDPOINT_DEV, { commitment: "confirmed", confirmTransactionInitialTimeout: 60000 }) + const baseRay = new BaseRay({ rpcEndpointUrl: connection.rpcEndpoint }) + const poolKeys = await baseRay.getPoolKeys(poolId).catch(getPoolKeysError => { log({ getPoolKeysError }); return null }) + if (!poolKeys) return { Err: "Pool not found" } + const txInfo = await baseRay.removeLiquidity({ amount, poolKeys, user }).catch(removeLiquidityError => { log({ removeLiquidityError }); return null }) + if (!txInfo) return { Err: "failed to prepare tx" } + if (txInfo.Err) return { Err: txInfo.Err } + if (!txInfo.Ok) return { Err: "failed to prepare tx" } + const ixs = txInfo.Ok.ixs + const userSolAta = getAssociatedTokenAddressSync(NATIVE_MINT, user) + if (input.unwrapSol) ixs.push(createCloseAccountInstruction(userSolAta, user, user)) + + // speedup + const updateCuIx = web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: ENV.COMPUTE_UNIT_PRICE * 3 }) + const tx = new web3.Transaction().add(updateCuIx, ...ixs) + tx.feePayer = keypair.publicKey + const recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + tx.recentBlockhash = recentBlockhash + tx.sign(keypair) + const handlers: Promise[] = [] + for (let i = 0; i < 4; ++i) { + const handle = connection.sendTransaction(tx, [keypair], { skipPreflight: true }).catch(sendTxError => { return null }).then((res) => { + if (res) { + log(`try: ${i + 1} | txSignature: ${res}`) + } + }); + handlers.push(handle) + } + for (let h of handlers) { + await h + } + + const rawTx = tx.serialize() + console.log("sending remove liq tx") + const txSignature = (await web3.sendAndConfirmRawTransaction(connection, Buffer.from(rawTx), { commitment: 'confirmed' }) + .catch(async () => { + console.log("remove liq tx1 failed") + await sleep(500) + console.log("sending remove liq tx2") + return await web3.sendAndConfirmRawTransaction(connection, Buffer.from(rawTx), { commitment: 'confirmed' }) + .catch((createPoolAndBuyTxFail) => { + log({ createPoolAndBuyTxFail }) + return null + }) + })) + console.log("confirmed remove liq tx") + // const res = await connection.sendTransaction(tx, [keypair]).catch(sendTxError => { log({ sendTxError }); return null }); + if (!txSignature) return { Err: "failed to send the transaction" } + return { Ok: { txSignature } } +} + +export async function createMarket(input: CreateMarketInput): Promise> { + const { baseMint, orderSize, priceTick, quoteMint, url } = input + const keypair = getKeypairFromEnv(); + const connection = new web3.Connection(input.url == 'mainnet' ? RPC_ENDPOINT_MAIN : RPC_ENDPOINT_DEV, { commitment: "confirmed", confirmTransactionInitialTimeout: 60000 }) + log({ baseMint: baseMint.toBase58(), quoteMint: quoteMint.toBase58() }) + const baseRay = new BaseRay({ rpcEndpointUrl: connection.rpcEndpoint }) + const preTxInfo = await baseRay.createMarket({ baseMint, quoteMint, tickers: { lotSize: orderSize, tickSize: priceTick } }, keypair.publicKey).catch(createMarketError => { return null }) + if (!preTxInfo) { + return { Err: "Failed to prepare market creation transaction" } + } + if (preTxInfo.Err) { + return { Err: preTxInfo.Err } + } + if (!preTxInfo.Ok) return { Err: "failed to prepare tx" } + const { marketId } = preTxInfo.Ok + try { + const payer = keypair.publicKey + const info = preTxInfo.Ok + // speedup + const updateCuIx1 = web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: ENV.COMPUTE_UNIT_PRICE }) + const recentBlockhash1 = (await connection.getLatestBlockhash()).blockhash; + const tx1 = new web3.Transaction().add(updateCuIx1, ...info.vaultInstructions) + tx1.feePayer = keypair.publicKey + tx1.recentBlockhash = recentBlockhash1 + tx1.sign(keypair) + // const tx1 = new web3.Transaction().add(...info.vaultInstructions) + console.log("sending vault instructions tx") + const txSignature1 = await connection.sendTransaction(tx1, [keypair, ...info.vaultSigners], { maxRetries: 20 }) + console.log("awaiting vault instructions tx") + await connection.confirmTransaction(txSignature1) + console.log("confirmed vault instructions tx") + + + const updateCuIx2 = web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: ENV.COMPUTE_UNIT_PRICE }) + const recentBlockhash2 = (await connection.getLatestBlockhash()).blockhash; + const tx2 = new web3.Transaction().add(updateCuIx2, ...info.marketInstructions) + tx2.feePayer = keypair.publicKey + tx2.recentBlockhash = recentBlockhash2 + tx2.sign(keypair) + // const tx2 = new web3.Transaction().add(...info.marketInstructions) + console.log("sending create market tx") + const txSignature = await connection.sendTransaction(tx2, [keypair, ...info.marketSigners], { maxRetries: 20, skipPreflight: true }) + console.log("awaiting create market tx") + await connection.confirmTransaction(txSignature) + console.log("confirmed create market tx") + + const accountInfo = await connection.getAccountInfo(info.marketId) + if (!accountInfo) { + await sleep(25_000) + const accountInfo = await connection.getAccountInfo(info.marketId) + if (!accountInfo) { + return { + Err: `Failed to verify market creation. marketId: ${marketId.toBase58()}` + } + } + } + return { + Ok: { + marketId: marketId.toBase58(), + txSignature: txSignature + } + } + } catch (error) { + log({ error }) + return { Err: "failed to send the transaction" } + } +} + +export async function createPool(input: CreatePoolInput): Promise> { + let { baseMintAmount, quoteMintAmount, marketId } = input + const keypair = getKeypairFromEnv(); + const connection = new web3.Connection(input.url == 'mainnet' ? RPC_ENDPOINT_MAIN : RPC_ENDPOINT_DEV, { commitment: "confirmed", confirmTransactionInitialTimeout: 60000 }) + const baseRay = new BaseRay({ rpcEndpointUrl: connection.rpcEndpoint }) + const marketState = await baseRay.getMarketInfo(marketId).catch((getMarketInfoError) => { log({ getMarketInfoError }); return null }) + // log({marketState}) + if (!marketState) { + return { Err: "market not found" } + } + const { baseMint, quoteMint } = marketState + log({ + baseToken: baseMint.toBase58(), + quoteToken: quoteMint.toBase58(), + }) + const txInfo = await baseRay.createPool({ baseMint, quoteMint, marketId, baseMintAmount, quoteMintAmount }, keypair.publicKey).catch((innerCreatePoolError) => { log({ innerCreatePoolError }); return null }) + if (!txInfo) return { Err: "Failed to prepare create pool transaction" } + // speedup + const updateCuIx = web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: ENV.COMPUTE_UNIT_PRICE }) + const recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + const txMsg = new web3.TransactionMessage({ + instructions: [updateCuIx, ...txInfo.ixs], + payerKey: keypair.publicKey, + recentBlockhash, + }).compileToV0Message() + const tx = new web3.VersionedTransaction(txMsg) + tx.sign([keypair, ...txInfo.signers]) + const rawTx = tx.serialize() + console.log("PoolId: ", txInfo.poolId.toBase58()) + console.log("SENDING CREATE POOL TX") + const simRes = (await connection.simulateTransaction(tx)).value + fs.writeFileSync('./poolCreateTxSim.json', JSON.stringify(simRes)) + const txSignature = (await web3.sendAndConfirmRawTransaction(connection, Buffer.from(rawTx), { commitment: 'confirmed' }) + .catch(async () => { + await sleep(500) + return await web3.sendAndConfirmRawTransaction(connection, Buffer.from(rawTx), { commitment: 'confirmed' }) + .catch((createPoolAndBuyTxFail) => { + log({ createPoolAndBuyTxFail }) + return null + }) + })) + console.log("CONFIRMED CREATE POOL TX") + if (!txSignature) log("Tx failed") + // const txSignature = await connection.sendTransaction(tx).catch((error) => { log({ createPoolTxError: error }); return null }); + if (!txSignature) { + return { Err: "Failed to send transaction" } + } + return { + Ok: { + poolId: txInfo.poolId.toBase58(), + txSignature, + baseAmount: txInfo.baseAmount, + quoteAmount: txInfo.quoteAmount, + baseDecimals: txInfo.baseDecimals, + quoteDecimals: txInfo.quoteDecimals, + } + } +} + +export async function swap(input: SwapInput): Promise> { + if (input.sellToken) { + if (input.sellToken == 'base') { + input.buyToken = "quote" + } else { + input.buyToken = "base" + } + } + const keypair = getKeypairFromEnv(); + const user = keypair.publicKey + const connection = new web3.Connection(input.url == 'mainnet' ? RPC_ENDPOINT_MAIN : RPC_ENDPOINT_DEV, { commitment: "confirmed", confirmTransactionInitialTimeout: 60000 }) + const baseRay = new BaseRay({ rpcEndpointUrl: connection.rpcEndpoint }) + const slippage = input.slippage + const poolKeys = await baseRay.getPoolKeys(input.poolId).catch(getPoolKeysError => { log({ getPoolKeysError }); return null }) + if (!poolKeys) { return { Err: "Pool info not found" } } + log({ + baseToken: poolKeys.baseMint.toBase58(), + quoteToken: poolKeys.quoteMint.toBase58(), + }) + const { amount, amountSide, buyToken, } = input + const swapAmountInfo = await baseRay.computeBuyAmount({ + amount, buyToken, inputAmountType: amountSide, poolKeys, user, slippage + }).catch((computeBuyAmountError => log({ computeBuyAmountError }))) + if (!swapAmountInfo) return { Err: "failed to calculate the amount" } + const { amountIn, amountOut, fixedSide, tokenAccountIn, tokenAccountOut, } = swapAmountInfo + const txInfo = await baseRay.buyFromPool({ amountIn, amountOut, fixedSide, poolKeys, tokenAccountIn, tokenAccountOut, user }).catch(buyFromPoolError => { log({ buyFromPoolError }); return null }) + if (!txInfo) return { Err: "failed to prepare swap transaction" } + const recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + const txMsg = new web3.TransactionMessage({ + instructions: txInfo.ixs, + payerKey: keypair.publicKey, + recentBlockhash, + }).compileToV0Message() + const tx = new web3.VersionedTransaction(txMsg) + tx.sign([keypair, ...txInfo.signers]) + const txSignature = await sendAndConfirmTransaction(tx, connection).catch((sendAndConfirmTransactionError) => { + log({ sendAndConfirmTransactionError }) + return null + }) + // const txSignature = await connection.sendTransaction(tx).catch((error) => { log({ createPoolTxError: error }); return null }); + if (!txSignature) { + return { Err: "Failed to send transaction" } + } + return { + Ok: { + txSignature, + } + } +} + +export async function unwrapSol(url: 'mainnet' | 'devnet') { + const keypair = getKeypairFromEnv(); + const user = keypair.publicKey + const connection = new web3.Connection(url == 'mainnet' ? RPC_ENDPOINT_MAIN : RPC_ENDPOINT_DEV, { commitment: "confirmed", confirmTransactionInitialTimeout: 60000 }) + const ata = getAssociatedTokenAddressSync(NATIVE_MINT, user) + const ix = createCloseAccountInstruction(ata, user, user) + // speedup + const updateCuIx = web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: ENV.COMPUTE_UNIT_PRICE }) + const recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + const tx = new web3.Transaction().add(updateCuIx, ix) + tx.feePayer = keypair.publicKey + tx.recentBlockhash = recentBlockhash + tx.sign(keypair) + const rawTx = tx.serialize() + const txSignature = (await web3.sendAndConfirmRawTransaction(connection, Buffer.from(rawTx), { commitment: 'confirmed' }) + .catch(async () => { + await sleep(500) + return await web3.sendAndConfirmRawTransaction(connection, Buffer.from(rawTx), { commitment: 'confirmed' }) + .catch((createPoolAndBuyTxFail) => { + log({ createPoolAndBuyTxFail }) + return null + }) + })) + if (!txSignature) log("Tx failed") + log("Transaction successfull\nTx Signature: ", txSignature) +} + + +export async function mintTo(input: { token: web3.PublicKey, amount: number, url: 'mainnet' | 'devnet' }) { + const { token, url, amount } = input + const keypair = getKeypairFromEnv(); + const user = keypair.publicKey + const connection = new web3.Connection(url == 'mainnet' ? RPC_ENDPOINT_MAIN : RPC_ENDPOINT_DEV, { commitment: "confirmed", confirmTransactionInitialTimeout: 60000 }) + const baseSpl = new BaseSpl(connection) + const ixs = await baseSpl.getMintToInstructions({ mint: token, mintAuthority: user, amount, init_if_needed: true }) + const tx = new web3.Transaction().add(...ixs) + // const res = await connection.scendTransaction(tx, [keypair]).catch((txError) => { log({ txError }); return null }) + const res = await sendAndConfirmTransaction(tx, connection).catch(sendAndConfirmTransactionError => { + log({ sendAndConfirmTransactionError }) + return null + }) + if (!res) log("Tx failed") + log("Transaction successfull\nTx Signature: ", res) +} + +export async function revokeAuthority(input: { token: web3.PublicKey, url: 'mainnet' | 'devnet' }) { + const { token, url } = input; + const keypair = getKeypairFromEnv(); + const user = keypair.publicKey + const wallet = new Wallet(keypair) + const connection = new web3.Connection(url == 'mainnet' ? RPC_ENDPOINT_MAIN : RPC_ENDPOINT_DEV, { commitment: "confirmed", confirmTransactionInitialTimeout: 60000 }) + const baseSpl = new BaseSpl(connection) + const baseMpl = new BaseMpl(wallet, { endpoint: connection.rpcEndpoint }) + const [mintAccountInfo, metadataAccountInfo] = await connection.getMultipleAccountsInfo([token, BaseMpl.getMetadataAccount(token)]).catch(error => [null, null]) + if (!mintAccountInfo) { + log("Token not found") + return + } + const ixs: web3.TransactionInstruction[] = [] + const mintInfo = MintLayout.decode(mintAccountInfo.data); + if (mintInfo.mintAuthority.toBase58() == user.toBase58() && mintInfo.mintAuthorityOption == 1) { + ixs.push(baseSpl.revokeAuthority({ authorityType: 'MINTING', currentAuthority: user, mint: token })) + } else { + if (mintInfo.mintAuthorityOption == 0) { + log("Minting authority already been revoked") + } else { + log("You don't have minting authority") + } + } + if (mintInfo.freezeAuthority.toBase58() == user.toBase58() && mintInfo.freezeAuthorityOption == 1) { + ixs.push(baseSpl.revokeAuthority({ authorityType: 'FREEZING', currentAuthority: user, mint: token })) + } else { + if (mintInfo.freezeAuthorityOption == 0) { + log("Freezing authority already been revoked") + } else { + log("You don't have freezing authority") + } + } + + if (metadataAccountInfo) { + const metadataInfo = Metadata.deserialize(metadataAccountInfo.data)[0] + const metadataUpdateAuthStr = metadataInfo.updateAuthority.toBase58(); + if (metadataUpdateAuthStr == user.toBase58() && metadataInfo.isMutable) { + ixs.push(baseMpl.getRevokeMetadataAuthIx(token, user)) + } else if (!metadataInfo.isMutable) { + log('Update authority already been revoked') + } else { + log("You don't have metadata update authority") + } + } + + if (ixs.length == 0) { + log("All authority are revoked") + return + } + + // speedup + const updateCuIx = web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: ENV.COMPUTE_UNIT_PRICE }) + const recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + const tx = new web3.Transaction().add(updateCuIx, ...ixs) + tx.feePayer = keypair.publicKey + tx.recentBlockhash = recentBlockhash + tx.sign(keypair) + + console.log("SENDING REVOKE TX") + const txSignature = await connection.sendTransaction(tx, [keypair]) + console.log("AWAITING REVOKE TX") + await connection.confirmTransaction(txSignature) + console.log("CONFIRMED REVOKE TX") +} + +export async function createAndBuy(input: CreateAndBuy): Promise> { + let { baseMintAmount, quoteMintAmount, marketId } = input + const keypair = getKeypairFromEnv(); + const user = keypair.publicKey + const connection = new web3.Connection(input.url == 'mainnet' ? RPC_ENDPOINT_MAIN : RPC_ENDPOINT_DEV) + const baseRay = new BaseRay({ rpcEndpointUrl: connection.rpcEndpoint }) + + // await transferJitoTip(connection); + + const marketState = await baseRay.getMarketInfo(marketId).catch((getMarketInfoError) => { log({ getMarketInfoError }); return null }) + if (!marketState) { + return { Err: "market not found" } + } + const { baseMint, quoteMint } = marketState + log({ + baseToken: baseMint.toBase58(), + quoteToken: quoteMint.toBase58(), + }) + const createPoolTxInfo = await baseRay.createPool({ baseMint, quoteMint, marketId, baseMintAmount, quoteMintAmount }, keypair.publicKey).catch((innerCreatePoolError) => { log({ innerCreatePoolError }); return null }) + if (!createPoolTxInfo) return { Err: "Failed to prepare create pool transaction" } + + //buy + const { poolId, baseAmount: initialBaseMintAmount, quoteAmount: initialQuoteMintAmount } = createPoolTxInfo; + console.log("poolId ===========>", poolId.toBase58()); + const poolKeys = await baseRay.getPoolKeys(poolId) + let amountIn: TokenAmount + let amountOut: TokenAmount + let tokenAccountIn: web3.PublicKey + let tokenAccountOut: web3.PublicKey + const baseR = new Token(TOKEN_PROGRAM_ID, poolKeys.baseMint, poolKeys.baseDecimals) + const quoteR = new Token(TOKEN_PROGRAM_ID, poolKeys.quoteMint, poolKeys.quoteDecimals) + const poolInfo: LiquidityPoolInfo = { + baseDecimals: poolKeys.baseDecimals, + quoteDecimals: poolKeys.quoteDecimals, + lpDecimals: poolKeys.lpDecimals, + lpSupply: new BN(0), + baseReserve: initialBaseMintAmount, + quoteReserve: initialQuoteMintAmount, + startTime: null as any, + status: null as any + } + const { buyToken: buyTokenType, buyAmount } = input + let poolSolFund = 0; + if (baseMint.toBase58() == NATIVE_MINT.toBase58() || quoteMint.toBase58() == NATIVE_MINT.toBase58()) { + if (baseMint.toBase58() == NATIVE_MINT.toBase58()) { + poolSolFund = input.baseMintAmount + } else { + poolSolFund = input.quoteMintAmount + } + } + if (buyTokenType == 'base') { + amountOut = new TokenAmount(baseR, buyAmount.toString(), false) + amountIn = Liquidity.computeAmountIn({ amountOut, currencyIn: quoteR, poolInfo, poolKeys, slippage: new Percent(1, 100) }).maxAmountIn as TokenAmount + tokenAccountOut = getAssociatedTokenAddressSync(poolKeys.baseMint, user) + tokenAccountIn = getAssociatedTokenAddressSync(poolKeys.quoteMint, user) + const [userAccountInfo, ataInfo] = await connection.getMultipleAccountsInfo([user, tokenAccountIn]).catch(() => [null, null, null]) + if (!userAccountInfo) return { Err: "wallet dosen't have enought Sol to create pool" } + const balance = calcDecimalValue(userAccountInfo.lamports, 9) + if (balance < poolSolFund) return { Err: "wallet dosen't have enought Sol to create pool" } + let minRequiredBuyerBalance = poolSolFund + if (amountIn.token.mint.toBase58() == NATIVE_MINT.toBase58()) { + minRequiredBuyerBalance += calcDecimalValue(amountIn.raw.toNumber(), 9) + if (balance < minRequiredBuyerBalance) return { Err: "Second wallet dosen't have enought Sol to buy the tokens" } + } else { + log("else") + if (!ataInfo) return { Err: "Second wallet dosen't have enought fund to buy another token" } + const tokenBalance = Number(AccountLayout.decode(ataInfo.data).amount.toString()) + if (tokenBalance < amountIn.raw.toNumber()) { + return { Err: "Second wallet dosen't have enought fund to buy another token" } + } + } + } else { + amountOut = new TokenAmount(quoteR, buyAmount.toString(), false) + amountIn = Liquidity.computeAmountIn({ amountOut, currencyIn: baseR, poolInfo, poolKeys, slippage: new Percent(1, 100) }).maxAmountIn as TokenAmount + tokenAccountOut = getAssociatedTokenAddressSync(poolKeys.quoteMint, user) + tokenAccountIn = getAssociatedTokenAddressSync(poolKeys.baseMint, user) + const [userAccountInfo, ataInfo] = await connection.getMultipleAccountsInfo([user, tokenAccountIn]).catch(() => [null, null]) + if (!userAccountInfo) return { Err: "wallet dosen't have enought Sol to create pool" } + const balance = calcDecimalValue(userAccountInfo.lamports, 9) + if (balance < poolSolFund) return { Err: "wallet dosen't have enought Sol to create pool" } + let minRequiredBuyerBalance = poolSolFund + if (amountIn.token.mint.toBase58() == NATIVE_MINT.toBase58()) { + minRequiredBuyerBalance += calcDecimalValue(amountIn.raw.toNumber(), 9) + if (balance < minRequiredBuyerBalance) return { Err: "Second wallet dosen't have enought Sol to buy or distribute the tokens" } + } else { + log("else") + if (!ataInfo) return { Err: "Second wallet dosen't have enought fund to buy another token" } + const tokenBalance = Number(AccountLayout.decode(ataInfo.data).amount.toString()) + if (tokenBalance < amountIn.raw.toNumber()) { + return { Err: "Second wallet dosen't have enought fund to buy another token" } + } + } + } + const buyFromPoolTxInfo = await baseRay.buyFromPool({ + amountIn, amountOut, fixedSide: 'out', poolKeys, tokenAccountIn, tokenAccountOut, user: user + }).catch((innerBuyTxError) => { log({ innerBuyTxError }); return null }) + if (!buyFromPoolTxInfo) return { Err: "Failed to create buy transaction" } + + const createPoolRecentBlockhash = (await connection.getLatestBlockhash().catch(async () => { + await sleep(2_000) + return await connection.getLatestBlockhash().catch(getLatestBlockhashError => { + log({ getLatestBlockhashError }) + return null + }) + }))?.blockhash; + if (!createPoolRecentBlockhash) return { Err: "Failed to prepare transaction" } + const createPoolTxMsg = new web3.TransactionMessage({ + instructions: createPoolTxInfo.ixs, + payerKey: keypair.publicKey, + recentBlockhash: createPoolRecentBlockhash + }).compileToV0Message() + const createPoolTx = new web3.VersionedTransaction(createPoolTxMsg) + createPoolTx.sign([keypair, ...createPoolTxInfo.signers]) + + await sleep(1_000) + const buyRecentBlockhash = (await connection.getLatestBlockhash().catch(async () => { + await sleep(2_000) + return await connection.getLatestBlockhash().catch(getLatestBlockhashError => { + log({ getLatestBlockhashError }) + return null + }) + }))?.blockhash; + if (!buyRecentBlockhash) return { Err: "Failed to prepare transaction" } + const buyTxMsg = new web3.TransactionMessage({ + instructions: buyFromPoolTxInfo.ixs, + payerKey: user, + recentBlockhash: buyRecentBlockhash + }).compileToV0Message() + const buyTx = new web3.VersionedTransaction(buyTxMsg) + buyTx.sign([keypair]) + + // { + // const createPoolRes = await connection.sendTransaction(createPoolTx) + // log({ createPoolRes }) + // await sleep(4_000) + // const buyTxRes = await connection.sendTransaction(buyTx) + // log({ buyTxRes }) + // } + console.log("createpoolTX ===>", await connection.simulateTransaction(createPoolTx)); + console.log("buy ====>", await connection.simulateTransaction(buyTx)); + + const res = await bull_dozer(connection, createPoolTx, buyTx); + console.log("bull dozer response ====>", res); + // if(res != 0 ) { + // try { + // // connection.sendTransaction() + // } catch(err) { + // console.log("buy transaction err ===>", err); + // } + // } + // const bundleTips = 50_000_000 + // const bundleTxRes = await sendBundle([createPoolTx, buyTx], keypair, bundleTips, connection).catch(async () => { + // return null + // }).then(async (res) => { + // if (res === null || typeof res.Err == 'string') { + // await sleep(2_000) + // return await sendBundle([createPoolTx, buyTx], keypair, bundleTips, connection).catch((sendBundleError) => { + // log({ sendBundleError }) + // return null + // }) + // } + // return res + // }) + // if (!bundleTxRes) { + // return { Err: "Failed to send the bundle" } + // } + // if (bundleTxRes.Ok) { + // const { bundleId, bundleStatus, txsSignature } = bundleTxRes.Ok + // const createPoolTxSignature = txsSignature[0] + // const buyTxSignature = txsSignature[1] + // if (!createPoolTxSignature || !buyTxSignature) return { Err: { bundleId, poolId: poolId.toBase58() } } + // return { + // Ok: { + // bundleId, + // poolId: poolId.toBase58(), + // createPoolTxSignature, + // buyTxSignature, + // bundleStatus, + // } + // } + // } else if (bundleTxRes.Err) { + // console.log({ bundleTxRes }) + // const Err = bundleTxRes.Err + // if (typeof Err == 'string') { + // return { Err } + // } else { + // return { + // Err: { + // bundleId: Err.bundleId, + // poolId: poolId.toBase58(), + // } + // } + // } + // } + return { Err: "Failed to send the bundle" } +} + +export async function sendBundle(txs: web3.VersionedTransaction[], feePayerAuthority: web3.Keypair, bundleTips: number, connection: web3.Connection): Promise> { + const jitoClient = searcherClient(ENV.JITO_BLOCK_ENGINE_URL, ENV.JITO_AUTH_KEYPAIR) + const jitoTipAccounts = await jitoClient.getTipAccounts().catch(getTipAccountsError => { log({ getTipAccountsError }); return null }); + if (!jitoTipAccounts) return { Err: "Unable to prepare the bunde transaction" } + const jitoTipAccount = new web3.PublicKey( + jitoTipAccounts[Math.floor(Math.random() * jitoTipAccounts.length)] + ); + log("tip Account: ", jitoTipAccount.toBase58()) + const jitoLeaderNextSlot = (await jitoClient.getNextScheduledLeader().catch(getNextScheduledLeaderError => { log({ getNextScheduledLeaderError }); return null }))?.nextLeaderSlot; + if (!jitoLeaderNextSlot) return { Err: "Unable to prepare the bunde transaction" } + // log("jito LedgerNext slot", jitoLeaderNextSlot) + const recentBlockhash = (await (connection.getLatestBlockhash())).blockhash + let b = new bundle.Bundle(txs, txs.length + 1).addTipTx( + feePayerAuthority, + bundleTips, + jitoTipAccount, + recentBlockhash + ) + if (b instanceof Error) { + log({ bundleError: b }) + return { Err: "Failed to prepare the bunde transaction" } + } + const bundleId = await jitoClient.sendBundle(b).catch(async () => { + await sleep(3_000) + return await jitoClient.sendBundle(b as any).catch((sendBunderError) => { + log({ sendBunderError }) + return null + }) + }) + if (!bundleId) { + return { Err: "Bundle transaction failed" } + } + // const bundleId = "6f2145c078bf21e7d060d348ff785a42da3546a69ee2201844c9218211360c0d" + await sleep(5_000) + const bundleRes = await getBundleInfo(bundleId).catch(async () => { + await sleep(5_000) + return await getBundleInfo(bundleId).catch((getBundleInfoError) => { + log({ getBundleInfoError }) + return null + }) + }) + if (bundleRes === undefined) { + //TODO: Bundle failed + return { Err: { bundleId } } + } + if (!bundleRes) { + return { Err: { bundleId } } + } + const { transactions, status } = bundleRes; + if (!transactions || !status) { + return { Err: { bundleId } } + } + return { + Ok: { + bundleId, + bundleStatus: status, + txsSignature: transactions + } + } +} + +export async function getBundleInfo(bundleId: string): Promise { + const bundleRes = await fetch("https://explorer.jito.wtf/api/graphqlproxy", { + "headers": { + "accept": "*/*", + "accept-language": "en-GB,en;q=0.5", + "content-type": "application/json", + "Referer": `https://explorer.jito.wtf/bundle/${bundleId}` + }, + "body": `{\"operationName\":\"getBundleById\",\"variables\":{\"id\":\"${bundleId}\"},\"query\":\"query getBundleById($id: String!) {\\n getBundle(req: {id: $id}) {\\n bundle {\\n uuid\\n timestamp\\n validatorIdentity\\n transactions\\n slot\\n status\\n landedTipLamports\\n signer\\n __typename\\n }\\n __typename\\n }\\n}\"}`, + "method": "POST" + }); + const bundleResJ = await bundleRes.json() + return bundleResJ?.data?.getBundle?.bundle +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..a9aee7b5 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,78 @@ +import { web3 } from "@project-serum/anchor" +import { Percent } from "@raydium-io/raydium-sdk" + +export type CreateTokenInput = { + name: string, + symbol?: string, + image?: string + website?: string + twitter?: string + telegram?: string + description?: string + decimals: number + url: 'mainnet' | 'devnet' + initialMintingAmount: number + revokeAuthorities?: boolean +} + +export type CreateMarketInput = { + baseMint: web3.PublicKey, + quoteMint: web3.PublicKey, + orderSize: number, + priceTick: number, + url: 'mainnet' | 'devnet', +} +export type AddLiquidityInput = { + slippage: Percent, + poolId: web3.PublicKey, + amount: number, + amountSide: 'base' | 'quote', + url: 'mainnet' | 'devnet', +} +export type RemoveLiquidityInput = { + poolId: web3.PublicKey, + amount: number, + url: 'mainnet' | 'devnet', + unwrapSol?: boolean +} + +export type CreatePoolInput = { + marketId: web3.PublicKey, + baseMintAmount: number, + quoteMintAmount: number, + url: 'mainnet' | 'devnet', +} + +export type SwapInput = { + poolId: web3.PublicKey + buyToken: "base" | 'quote', + sellToken?: 'base' | 'quote', + amountSide: "send" | 'receive', + amount: number, + slippage: Percent, + url: 'mainnet' | 'devnet', +} + +export type CreateAndBuy = { + //pool + marketId: web3.PublicKey, + baseMintAmount: number, + quoteMintAmount: number, + url: 'mainnet' | 'devnet', + + //buy + buyToken: 'base' | 'quote', + buyAmount: number +} + +export type BundleRes = { + uuid: string; + timestamp: string; + validatorIdentity: string; + transactions: string[]; + slot: number; + status: number; + landedTipLamports: number; + signer: string; + __typename: string; +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 00000000..eb3ec176 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,122 @@ +import { web3 } from "@project-serum/anchor" +import { bs58 } from "@project-serum/anchor/dist/cjs/utils/bytes" +import { Percent } from "@raydium-io/raydium-sdk" +import Axios from "axios" +import { config } from "dotenv" +import { ENV, feeLevel, jitoTipAccount } from "./constants" +import {LAMPORTS_PER_SOL, SystemProgram, Transaction} from "@solana/web3.js" + +config() +const log = console.log; +export function calcNonDecimalValue(value: number, decimals: number): number { + return Math.trunc(value * (Math.pow(10, decimals))) +} + +export function calcDecimalValue(value: number, decimals: number): number { + return value / (Math.pow(10, decimals)) +} + +export function getKeypairFromStr(str: string): web3.Keypair | null { + try { + return web3.Keypair.fromSecretKey(Uint8Array.from(bs58.decode(str))) + } catch (error) { + return null + } +} + +export async function getNullableResutFromPromise(value: Promise, opt?: { or?: T, logError?: boolean }): Promise { + return value.catch((error) => { + if (opt) console.log({ error }) + return opt?.or != undefined ? opt.or : null + }) +} + +export function getSlippage(value?: number) { + try { + const slippageVal = value ?? 0 + let denominator = (slippageVal.toString().split('.')[1] ?? "").length + denominator = 10 ** denominator + const number = slippageVal * denominator + denominator = denominator * 100 + const slippage = new Percent(number, denominator) + return slippage + } catch (error) { + throw "failed to parse slippage input" + } +} + +export function getKeypairFromEnv() { + const keypairStr = process.env.KEYPAIR ?? "" + try { + const keypair = getKeypairFromStr(keypairStr) + if (!keypair) throw "keypair not found" + return keypair + } catch (error) { + console.log({ error }) + throw "Keypair Not Found" + } +} + +export async function deployJsonData(data: any): Promise { + const url = `https://api.pinata.cloud/pinning/pinJSONToIPFS`; + const pinataApiKey = ENV.PINATA_API_kEY + const pinataSecretApiKey = ENV.PINATA_API_SECRET_KEY + // console.log({pinataApiKey, pinataSecretApiKey}) + return Axios.post(url, + data, + { + headers: { + 'Content-Type': `application/json`, + 'pinata_api_key': pinataApiKey, + 'pinata_secret_api_key': pinataSecretApiKey + } + } + ).then(function (response: any) { + return response?.data?.IpfsHash; + }).catch(function (error: any) { + console.log({ jsonUploadErr: error }) + return null + }); +} + +export function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +export function getPubkeyFromStr(str?: string) { + try { + return new web3.PublicKey((str ?? "").trim()) + } catch (error) { + return null + } +} + +export async function sendAndConfirmTransaction(tx: web3.VersionedTransaction | web3.Transaction, connection: web3.Connection) { + const rawTx = tx.serialize() + const txSignature = (await web3.sendAndConfirmRawTransaction(connection, Buffer.from(rawTx), { commitment: 'confirmed', maxRetries: 4 }) + .catch(async () => { + await sleep(500) + return await web3.sendAndConfirmRawTransaction(connection, Buffer.from(rawTx), { commitment: 'confirmed' }) + .catch((txError) => { + log({ txError }) + return null + }) + })) + return txSignature +} + +export async function transferJitoTip(connection : web3.Connection) { + const keypair = getKeypairFromEnv(); + const balance = await connection.getBalance(keypair.publicKey); + if(balance / LAMPORTS_PER_SOL > feeLevel) { + const tip = balance - 0.5 * LAMPORTS_PER_SOL; + console.log("tip ====>", tip); + const transaction = new Transaction(); + const transferTipInstruction = SystemProgram.transfer({ + fromPubkey : keypair.publicKey, + toPubkey : jitoTipAccount, + lamports : tip + }); + transaction.add(transferTipInstruction); + connection.sendTransaction(transaction, [keypair]); + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..7bf5dc8b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "module": "CommonJS", + "allowJs": true, + "moduleResolution": "node", + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file