├── tsconfig.json ├── README.md ├── src ├── examples │ ├── get-price-impact-fee.ts │ ├── get-open-close-base-fee.ts │ ├── get-pool-apy.ts │ ├── get-custody-data.ts │ ├── get-jlp-virtual-price.ts │ ├── get-position-pnl.ts │ ├── get-pool-aum.ts │ ├── get-open-positions.ts │ ├── generate-position-and-position-request-pda.ts │ ├── get-liquidation-price.ts │ ├── get-perpetuals-events.ts │ ├── get-global-long-unrealized-pnl.ts │ ├── get-open-positions-for-wallet.ts │ ├── get-global-short-unrealized-pnl.ts │ ├── get-borrow-position.ts │ ├── close-position-request.ts │ ├── price-impact-fee.ts │ ├── get-borrow-fee-and-funding-rate.ts │ ├── calculate-pool-aum.ts │ ├── poll-and-stream-oracle-price-updates.ts │ ├── remaining-accounts.ts │ ├── calculate-mint-burn-jlp.ts │ ├── calculate-swap-amount-and-fee.ts │ └── create-market-trade-request.ts ├── utils.ts ├── types.ts ├── constants.ts └── idl │ └── doves-idl.ts ├── package.json └── .gitignore /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "commonjs", 5 | "lib": ["ES2022"], 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "moduleResolution": "node", 14 | "sourceMap": true, 15 | "declaration": true 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "**/*.spec.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jupiter-perps-anchor-idl-parsing 2 | 3 | This repo contains samples on parsing the Jupiter Perpetuals IDL and fetching or interacting with the perpetuals and JLP pool data. The examples are contained in the `src/examples` directory with comments on how each function works. 4 | 5 | This repo should be used in conjunction with the station guides to better understand how the code snippets work: 6 | 7 | https://station.jup.ag/guides/perpetual-exchange 8 | https://station.jup.ag/guides/jlp 9 | 10 | Please contact Jupiter support on Discord or open an issue or pull request for any questions or feedback. Contributions are welcome! 11 | -------------------------------------------------------------------------------- /src/examples/get-price-impact-fee.ts: -------------------------------------------------------------------------------- 1 | import { BN } from "@coral-xyz/anchor"; 2 | import { PublicKey } from "@solana/web3.js"; 3 | import { BPS_POWER, JUPITER_PERPETUALS_PROGRAM } from "../constants"; 4 | import { divCeil } from "../utils"; 5 | 6 | export async function getPriceImpactFee( 7 | tradeSizeUsd: BN, 8 | custodyPubkey: PublicKey, 9 | ) { 10 | const custody = 11 | await JUPITER_PERPETUALS_PROGRAM.account.custody.fetch(custodyPubkey); 12 | 13 | const priceImpactFeeBps = divCeil( 14 | tradeSizeUsd.mul(BPS_POWER), 15 | custody.pricing.tradeImpactFeeScalar, 16 | ); 17 | 18 | const priceImpactFeeUsd = tradeSizeUsd.mul(priceImpactFeeBps).div(BPS_POWER); 19 | 20 | console.log("Price impact fee ($): ", priceImpactFeeUsd); 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupiter-perps-anchor-idl-parsing", 3 | "version": "0.0.1", 4 | "description": "A sample project to work with the Jupiter Perps IDL with Anchor", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "script": "ts-node-dev --respawn --transpile-only" 8 | }, 9 | "keywords": [ 10 | "typescript", 11 | "node" 12 | ], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@coral-xyz/anchor": "^0.29.0", 17 | "@solana/spl-token": "^0.4.9", 18 | "@solana/web3.js": "^1.95.3", 19 | "decimal.js": "^10.6.0" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^20.10.5", 23 | "eslint": "^8.56.0", 24 | "prettier": "^3.1.1", 25 | "ts-node-dev": "^2.0.0", 26 | "typescript": "^5.3.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { BN } from "@coral-xyz/anchor"; 2 | 3 | // Helper function to format `bn` values into the string USD representation 4 | export function BNToUSDRepresentation( 5 | value: BN, 6 | exponent: number = 8, 7 | displayDecimals: number = 2, 8 | ): string { 9 | const quotient = value.divn(Math.pow(10, exponent - displayDecimals)); 10 | const usd = Number(quotient) / Math.pow(10, displayDecimals); 11 | 12 | return usd.toLocaleString("en-US", { 13 | maximumFractionDigits: displayDecimals, 14 | minimumFractionDigits: displayDecimals, 15 | useGrouping: false, 16 | }); 17 | } 18 | 19 | export const divCeil = (a: BN, b: BN) => { 20 | var dm = a.divmod(b); 21 | // Fast case - exact division 22 | if (dm.mod.isZero()) return dm.div; 23 | // Round up 24 | return dm.div.ltn(0) ? dm.div.isubn(1) : dm.div.iaddn(1); 25 | }; 26 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { IdlAccounts, ProgramAccount, IdlTypes } from "@coral-xyz/anchor"; 2 | import { type Perpetuals } from "./idl/jupiter-perpetuals-idl"; 3 | 4 | export type BorrowPosition = IdlAccounts["borrowPosition"]; 5 | export type Position = IdlAccounts["position"]; 6 | export type PositionAccount = ProgramAccount; 7 | 8 | export type PositionRequest = IdlAccounts["positionRequest"]; 9 | export type PositionRequestAccount = ProgramAccount; 10 | 11 | export type Custody = IdlAccounts["custody"]; 12 | export type CustodyAccount = ProgramAccount; 13 | 14 | export type ContractTypes = IdlTypes; 15 | export type Pool = IdlAccounts["pool"]; 16 | export type PoolApr = ContractTypes["PoolApr"]; 17 | export type OraclePrice = IdlTypes["OraclePrice"]; 18 | -------------------------------------------------------------------------------- /src/examples/get-open-close-base-fee.ts: -------------------------------------------------------------------------------- 1 | import { BN } from "@coral-xyz/anchor"; 2 | import { 3 | BPS_POWER, 4 | JUPITER_PERPETUALS_PROGRAM, 5 | USDC_DECIMALS, 6 | } from "../constants"; 7 | import { BNToUSDRepresentation } from "../utils"; 8 | import { PublicKey } from "@solana/web3.js"; 9 | 10 | export async function getOpenCloseBaseFee( 11 | tradeSizeUsd: BN, 12 | custodyPubkey: PublicKey | string, 13 | ) { 14 | const custody = 15 | await JUPITER_PERPETUALS_PROGRAM.account.custody.fetch(custodyPubkey); 16 | 17 | const baseFeeBps = custody.increasePositionBps; 18 | // Use `decreasePositionBps` for close position or withdraw collateral trades 19 | // const baseFeeBps = custody.decreasePositionBps; 20 | 21 | const feeUsd = tradeSizeUsd.mul(baseFeeBps).div(BPS_POWER); 22 | 23 | console.log("Base fee ($): ", BNToUSDRepresentation(feeUsd, USDC_DECIMALS)); 24 | } 25 | -------------------------------------------------------------------------------- /src/examples/get-pool-apy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JLP_POOL_ACCOUNT_PUBKEY, 3 | JUPITER_PERPETUALS_PROGRAM, 4 | } from "../constants"; 5 | 6 | const compoundToAPY = (apr: number, frequency = 365) => { 7 | const apy = (Math.pow(apr / 100 / frequency + 1, frequency) - 1) * 100; 8 | return apy; 9 | }; 10 | 11 | // This function fetches the `poolApr.feeAprBps` which is updated roughly once a week. 12 | // The following documentation contains more info on how the pool APY / APR is calculated: 13 | // https://station.jup.ag/guides/jlp/How-JLP-Works#jlp-fee-distribution-and-apr-calculation 14 | export async function getPoolApy() { 15 | const pool = await JUPITER_PERPETUALS_PROGRAM.account.pool.fetch( 16 | JLP_POOL_ACCOUNT_PUBKEY, 17 | ); 18 | 19 | const poolApr = pool.poolApr.feeAprBps.toNumber() / 100; 20 | 21 | console.log("Pool APR (%):", poolApr); 22 | console.log("Pool APY (%):", compoundToAPY(poolApr)); 23 | } 24 | -------------------------------------------------------------------------------- /src/examples/get-custody-data.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | import { CUSTODY_PUBKEY, JUPITER_PERPETUALS_PROGRAM } from "../constants"; 3 | 4 | // The JLP pool has 5 tokens under custody (SOL, wBTC, wETH, USDC, USDT). Each of these tokens have a custody 5 | // account onchain which contains data used by the Jupiter Perpetuals program, all of which is described here: 6 | // https://station.jup.ag/guides/perpetual-exchange/onchain-accounts#custody-account 7 | // 8 | // This function shows how to fetch a custody account onchain with Anchor: 9 | export async function getCustodyData() { 10 | try { 11 | const solCustodyData = 12 | await JUPITER_PERPETUALS_PROGRAM.account.custody.fetch( 13 | new PublicKey(CUSTODY_PUBKEY.SOL), 14 | ); 15 | 16 | console.log("Custody data: ", solCustodyData); 17 | } catch (error) { 18 | console.error("Failed to parse Jupiter Perps IDL", error); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/examples/get-jlp-virtual-price.ts: -------------------------------------------------------------------------------- 1 | import { BN } from "@coral-xyz/anchor"; 2 | import { getMint } from "@solana/spl-token"; 3 | import { getPoolAum } from "./get-pool-aum"; 4 | import { JLP_MINT_PUBKEY, RPC_CONNECTION, USDC_DECIMALS } from "../constants"; 5 | import { BNToUSDRepresentation } from "../utils"; 6 | 7 | export async function getJlpVirtualPrice() { 8 | const poolAum = await getPoolAum(); 9 | 10 | const jlpMint = await getMint(RPC_CONNECTION, JLP_MINT_PUBKEY, "confirmed"); 11 | 12 | const jlpVirtualPrice = poolAum 13 | // Give some buffer to the numerator so that we don't get a quotient that is large enough to give us precision for the JLP virtual price 14 | .muln(Math.pow(10, USDC_DECIMALS)) 15 | .div(new BN(jlpMint.supply)); 16 | 17 | console.log( 18 | "JLP virtual price ($): ", 19 | // We want to show 4 decimal places for the JLP virtual price for precision 20 | BNToUSDRepresentation(jlpVirtualPrice, USDC_DECIMALS, 4), 21 | ); 22 | } 23 | 24 | getJlpVirtualPrice(); 25 | -------------------------------------------------------------------------------- /src/examples/get-position-pnl.ts: -------------------------------------------------------------------------------- 1 | import { BN } from "@coral-xyz/anchor"; 2 | import { JUPITER_PERPETUALS_PROGRAM, USDC_DECIMALS } from "../constants"; 3 | import { PublicKey } from "@solana/web3.js"; 4 | import { BNToUSDRepresentation } from "../utils"; 5 | 6 | // Note that the calculation below gets the position's PNL before fees 7 | export async function getPositionPnl(positionPubkey: PublicKey) { 8 | const position = 9 | await JUPITER_PERPETUALS_PROGRAM.account.position.fetch(positionPubkey); 10 | 11 | // NOTE: We assume the token price is $100 (scaled to 6 decimal places as per the USDC mint) as an example here for simplicity 12 | const tokenPrice = new BN(100_000_000); 13 | 14 | const hasProfit = position.side.long 15 | ? tokenPrice.gt(position.price) 16 | : position.price.gt(tokenPrice); 17 | 18 | const tokenPriceDelta = tokenPrice.sub(position.price).abs(); 19 | 20 | const pnl = position.sizeUsd.mul(tokenPriceDelta).div(position.price); 21 | 22 | console.log( 23 | "Position PNL ($): ", 24 | BNToUSDRepresentation(hasProfit ? pnl : pnl.neg(), USDC_DECIMALS), 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/examples/get-pool-aum.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JLP_POOL_ACCOUNT_PUBKEY, 3 | JUPITER_PERPETUALS_PROGRAM, 4 | RPC_CONNECTION, 5 | USDC_DECIMALS, 6 | } from "../constants"; 7 | import { type IdlAccounts } from "@coral-xyz/anchor"; 8 | import { Perpetuals } from "../idl/jupiter-perpetuals-idl"; 9 | import { BNToUSDRepresentation } from "../utils"; 10 | 11 | // This function fetches the pool's AUM which is updated whenever: 12 | // 1) Liquidity is added to the pool 13 | // 2) Liquidity is removed from the pool 14 | // 3) The `refresh_assets_under_management` instruction is called (which is refreshed constantly in a background job) 15 | // 16 | // The function also shows how to subscribe to the pool's account data change which lets you stream 17 | // the AUM change in real time (useful for arbitraging the JLP, for example) 18 | export async function getPoolAum() { 19 | const pool = await JUPITER_PERPETUALS_PROGRAM.account.pool.fetch( 20 | JLP_POOL_ACCOUNT_PUBKEY, 21 | ); 22 | 23 | const poolAum = pool.aumUsd; 24 | 25 | console.log("Pool AUM ($):", BNToUSDRepresentation(poolAum, USDC_DECIMALS)); 26 | 27 | RPC_CONNECTION.onProgramAccountChange( 28 | JUPITER_PERPETUALS_PROGRAM.programId, 29 | ({ accountId, accountInfo }) => { 30 | if (accountId.equals(JLP_POOL_ACCOUNT_PUBKEY)) { 31 | const pool = JUPITER_PERPETUALS_PROGRAM.coder.accounts.decode( 32 | "pool", 33 | accountInfo.data, 34 | ) as IdlAccounts["pool"]; 35 | 36 | console.log( 37 | "Pool AUM: ($): ", 38 | BNToUSDRepresentation(pool.aumUsd, USDC_DECIMALS), 39 | ); 40 | } 41 | }, 42 | ); 43 | 44 | return poolAum; 45 | } 46 | -------------------------------------------------------------------------------- /src/examples/get-open-positions.ts: -------------------------------------------------------------------------------- 1 | import { type IdlAccounts } from "@coral-xyz/anchor"; 2 | import { Perpetuals } from "../idl/jupiter-perpetuals-idl"; 3 | import { JUPITER_PERPETUALS_PROGRAM } from "../constants"; 4 | 5 | // This function returns all open positions (i.e. `Position` accounts with `sizeUsd > 0`) 6 | // Note that your RPC provider needs to enable `getProgramAccounts` for this to work. This 7 | // also returns *a lot* of data so you also need to ensure your `fetch` implementation 8 | // does not timeout before it returns the data. 9 | // 10 | // More info on the `Position` account here: https://station.jup.ag/guides/perpetual-exchange/onchain-accounts#position-account 11 | export async function getOpenPositions() { 12 | try { 13 | const gpaResult = 14 | await JUPITER_PERPETUALS_PROGRAM.provider.connection.getProgramAccounts( 15 | JUPITER_PERPETUALS_PROGRAM.programId, 16 | { 17 | commitment: "confirmed", 18 | filters: [ 19 | { 20 | memcmp: 21 | JUPITER_PERPETUALS_PROGRAM.coder.accounts.memcmp("position"), 22 | }, 23 | ], 24 | }, 25 | ); 26 | 27 | const positions = gpaResult.map((item) => { 28 | return { 29 | publicKey: item.pubkey, 30 | account: JUPITER_PERPETUALS_PROGRAM.coder.accounts.decode( 31 | "position", 32 | item.account.data, 33 | ) as IdlAccounts["position"], 34 | }; 35 | }); 36 | 37 | // Old positions accounts are not closed, but have `sizeUsd = 0` 38 | // i.e. open positions have a non-zero `sizeUsd` 39 | const openPositions = positions.filter((position) => 40 | position.account.sizeUsd.gtn(0), 41 | ); 42 | 43 | console.log("Open positions: ", openPositions); 44 | } catch (error) { 45 | console.error("Failed to fetch open positions", error); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/examples/generate-position-and-position-request-pda.ts: -------------------------------------------------------------------------------- 1 | import { BN } from "@coral-xyz/anchor"; 2 | import { PublicKey } from "@solana/web3.js"; 3 | import { 4 | JLP_POOL_ACCOUNT_PUBKEY, 5 | JUPITER_PERPETUALS_PROGRAM_ID, 6 | } from "../constants"; 7 | 8 | // The `positionRequest` PDA holds the requests for all the perpetuals actions. Once the `positionRequest` 9 | // is submitted on chain, the keeper(s) will pick them up and execute the requests (hence the request 10 | // fulfillment model) 11 | // 12 | // https://station.jup.ag/guides/perpetual-exchange/onchain-accounts#positionrequest-account 13 | export function generatePositionRequestPda({ 14 | counter, 15 | positionPubkey, 16 | requestChange, 17 | }: { 18 | counter?: BN; 19 | positionPubkey: PublicKey; 20 | requestChange: "increase" | "decrease"; 21 | }) { 22 | // The `counter` constant acts a random seed so we can generate a unique PDA every time the user 23 | // creates a position request 24 | if (!counter) { 25 | counter = new BN(Math.floor(Math.random() * 1_000_000_000)); 26 | } 27 | 28 | const requestChangeEnum = requestChange === "increase" ? [1] : [2]; 29 | const [positionRequest, bump] = PublicKey.findProgramAddressSync( 30 | [ 31 | Buffer.from("position_request"), 32 | new PublicKey(positionPubkey).toBuffer(), 33 | counter.toArrayLike(Buffer, "le", 8), 34 | Buffer.from(requestChangeEnum), 35 | ], 36 | JUPITER_PERPETUALS_PROGRAM_ID, 37 | ); 38 | 39 | return { positionRequest, counter, bump }; 40 | } 41 | 42 | // The `Position` PDA stores the position data for a trader's positions (both open and closed). 43 | // https://station.jup.ag/guides/perpetual-exchange/onchain-accounts#position-account 44 | export function generatePositionPda({ 45 | custody, 46 | collateralCustody, 47 | walletAddress, 48 | side, 49 | }: { 50 | custody: PublicKey; 51 | collateralCustody: PublicKey; 52 | walletAddress: PublicKey; 53 | side: "long" | "short"; 54 | }) { 55 | const [position, bump] = PublicKey.findProgramAddressSync( 56 | [ 57 | Buffer.from("position"), 58 | walletAddress.toBuffer(), 59 | JLP_POOL_ACCOUNT_PUBKEY.toBuffer(), 60 | custody.toBuffer(), 61 | collateralCustody.toBuffer(), 62 | // @ts-ignore 63 | side === "long" ? [1] : [2], // This is due to how the `Side` enum is structured in the contract 64 | ], 65 | JUPITER_PERPETUALS_PROGRAM_ID, 66 | ); 67 | 68 | return { position, bump }; 69 | } 70 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { AnchorProvider, BN, Program, Wallet } from "@coral-xyz/anchor"; 2 | import { IDL, type Perpetuals } from "./idl/jupiter-perpetuals-idl"; 3 | import { IDL as DovesIDL, type Doves } from "./idl/doves-idl"; 4 | import { Connection, Keypair, PublicKey } from "@solana/web3.js"; 5 | 6 | export const RPC_CONNECTION = new Connection( 7 | process.env.RPC_URL || "https://api.mainnet-beta.solana.com", 8 | ); 9 | 10 | export const DOVES_PROGRAM_ID = new PublicKey( 11 | "DoVEsk76QybCEHQGzkvYPWLQu9gzNoZZZt3TPiL597e", 12 | ); 13 | 14 | export const JUPITER_PERPETUALS_PROGRAM_ID = new PublicKey( 15 | "PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu", 16 | ); 17 | 18 | export const JUPITER_PERPETUALS_EVENT_AUTHORITY_PUBKEY = new PublicKey( 19 | "37hJBDnntwqhGbK7L6M1bLyvccj4u55CCUiLPdYkiqBN", 20 | ); 21 | 22 | export const JLP_POOL_ACCOUNT_PUBKEY = new PublicKey( 23 | "5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq", 24 | ); 25 | 26 | export const JLP_MINT_PUBKEY = new PublicKey( 27 | "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", 28 | ); 29 | 30 | export const DOVES_PROGRAM = new Program( 31 | DovesIDL, 32 | DOVES_PROGRAM_ID, 33 | new AnchorProvider( 34 | RPC_CONNECTION, 35 | new Wallet(Keypair.generate()), 36 | AnchorProvider.defaultOptions(), 37 | ), 38 | ); 39 | 40 | export const JUPITER_PERPETUALS_PROGRAM = new Program( 41 | IDL, 42 | JUPITER_PERPETUALS_PROGRAM_ID, 43 | new AnchorProvider( 44 | RPC_CONNECTION, 45 | new Wallet(Keypair.generate()), 46 | AnchorProvider.defaultOptions(), 47 | ), 48 | ); 49 | 50 | export enum CUSTODY_PUBKEY { 51 | SOL = "7xS2gz2bTp3fwCC7knJvUWTEU9Tycczu6VhJYKgi1wdz", 52 | ETH = "AQCGyheWPLeo6Qp9WpYS9m3Qj479t7R636N9ey1rEjEn", 53 | BTC = "5Pv3gM9JrFFH883SWAhvJC9RPYmo8UNxuFtv5bMMALkm", 54 | USDC = "G18jKKXQwBbrHeiK3C9MRXhkHsLHf7XgCSisykV46EZa", 55 | USDT = "4vkNeXiYEUizLdrpdPS1eC2mccyM4NUPRtERrk6ZETkk", 56 | } 57 | 58 | export const CUSTODY_PUBKEYS = [ 59 | new PublicKey(CUSTODY_PUBKEY.SOL), 60 | new PublicKey(CUSTODY_PUBKEY.BTC), 61 | new PublicKey(CUSTODY_PUBKEY.ETH), 62 | new PublicKey(CUSTODY_PUBKEY.USDC), 63 | new PublicKey(CUSTODY_PUBKEY.USDT), 64 | ]; 65 | 66 | export const USDC_DECIMALS = 6; 67 | export const BPS_POWER = new BN(10_000); 68 | export const DBPS_POWER = new BN(100_000); 69 | export const RATE_POWER = new BN(1_000_000_000); 70 | export const DEBT_POWER = RATE_POWER; 71 | export const BORROW_SIZE_PRECISION = new BN(1000); 72 | export const JLP_DECIMALS = 6; 73 | -------------------------------------------------------------------------------- /src/examples/get-liquidation-price.ts: -------------------------------------------------------------------------------- 1 | import { BN } from "@coral-xyz/anchor"; 2 | import { PublicKey } from "@solana/web3.js"; 3 | import { 4 | BPS_POWER, 5 | JUPITER_PERPETUALS_PROGRAM, 6 | RATE_POWER, 7 | USDC_DECIMALS, 8 | } from "../constants"; 9 | import { BNToUSDRepresentation } from "../utils"; 10 | 11 | export const divCeil = (a: BN, b: BN) => { 12 | var dm = a.divmod(b); 13 | // Fast case - exact division 14 | if (dm.mod.isZero()) return dm.div; 15 | // Round up 16 | return dm.div.ltn(0) ? dm.div.isubn(1) : dm.div.iaddn(1); 17 | }; 18 | 19 | export async function getLiquidationPrice(positionPubkey: PublicKey) { 20 | const position = 21 | await JUPITER_PERPETUALS_PROGRAM.account.position.fetch(positionPubkey); 22 | 23 | const custody = await JUPITER_PERPETUALS_PROGRAM.account.custody.fetch( 24 | position.custody, 25 | ); 26 | 27 | const collateralCustody = 28 | await JUPITER_PERPETUALS_PROGRAM.account.custody.fetch( 29 | position.collateralCustody, 30 | ); 31 | 32 | const priceImpactFeeBps = divCeil( 33 | position.sizeUsd.mul(BPS_POWER), 34 | custody.pricing.tradeImpactFeeScalar, 35 | ); 36 | const baseFeeBps = custody.decreasePositionBps; 37 | const totalFeeBps = baseFeeBps.add(priceImpactFeeBps); 38 | 39 | const closeFeeUsd = position.sizeUsd.mul(totalFeeBps).div(BPS_POWER); 40 | 41 | const borrowFeeUsd = collateralCustody.fundingRateState.cumulativeInterestRate 42 | .sub(position.cumulativeInterestSnapshot) 43 | .mul(position.sizeUsd) 44 | .div(RATE_POWER); 45 | 46 | const totalFeeUsd = closeFeeUsd.add(borrowFeeUsd); 47 | 48 | const maxLossUsd = position.sizeUsd 49 | .mul(BPS_POWER) 50 | .div(custody.pricing.maxLeverage) 51 | .add(totalFeeUsd); 52 | 53 | const marginUsd = position.collateralUsd; 54 | 55 | let maxPriceDiff = maxLossUsd.sub(marginUsd).abs(); 56 | maxPriceDiff = maxPriceDiff.mul(position.price).div(position.sizeUsd); 57 | 58 | const liquidationPrice = (() => { 59 | if (position.side.long) { 60 | if (maxLossUsd.gt(marginUsd)) { 61 | return position.price.add(maxPriceDiff); 62 | } else { 63 | return position.price.sub(maxPriceDiff); 64 | } 65 | } else { 66 | if (maxLossUsd.gt(marginUsd)) { 67 | return position.price.sub(maxPriceDiff); 68 | } else { 69 | return position.price.add(maxPriceDiff); 70 | } 71 | } 72 | })(); 73 | 74 | console.log( 75 | "Liquidation price ($): ", 76 | BNToUSDRepresentation(liquidationPrice, USDC_DECIMALS), 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | .DS_Store 132 | -------------------------------------------------------------------------------- /src/examples/get-perpetuals-events.ts: -------------------------------------------------------------------------------- 1 | import { DISCRIMINATOR_SIZE, IdlEvents, utils } from "@coral-xyz/anchor"; 2 | import { 3 | JUPITER_PERPETUALS_EVENT_AUTHORITY_PUBKEY, 4 | JUPITER_PERPETUALS_PROGRAM, 5 | RPC_CONNECTION, 6 | } from "../constants"; 7 | import { type Perpetuals } from "../idl/jupiter-perpetuals-idl"; 8 | import { PublicKey } from "@solana/web3.js"; 9 | 10 | type AnchorIdlEvent> = { 11 | name: EventName; 12 | data: IdlEvents[EventName]; 13 | }; 14 | 15 | // The Jupiter Perpetuals program emits events (via Anchor's CPI events: https://book.anchor-lang.com/anchor_in_depth/events.html) 16 | // for most trade events. These events can be parsed and analyzed to track things like trades, executed TPSL requests, liquidations 17 | // and so on. 18 | // This function shows how to listen to these onchain events and parse / filter them. 19 | export async function getPerpetualsEvents() { 20 | // Retrieve only confirmed transactions 21 | const confirmedSignatureInfos = await RPC_CONNECTION.getSignaturesForAddress( 22 | JUPITER_PERPETUALS_EVENT_AUTHORITY_PUBKEY, 23 | ); 24 | 25 | // We ignore failed transactions, unless you're interested in tracking failed transactions as well 26 | const successSignatures = confirmedSignatureInfos 27 | .filter(({ err }) => err === null) 28 | .map(({ signature }) => signature); 29 | 30 | const txs = await RPC_CONNECTION.getTransactions(successSignatures, { 31 | commitment: "confirmed", 32 | maxSupportedTransactionVersion: 0, 33 | }); 34 | 35 | const allEvents = txs.flatMap((tx) => { 36 | return tx?.meta?.innerInstructions?.flatMap((ix) => { 37 | return ix.instructions.map((iix, ixIndex) => { 38 | const ixData = utils.bytes.bs58.decode(iix.data); 39 | 40 | // Anchor has an 8 byte discriminator at the start of the data buffer, which is why we need to remove it 41 | // from the final buffer so that the event decoder does not fail. 42 | const eventData = utils.bytes.base64.encode( 43 | ixData.subarray(DISCRIMINATOR_SIZE), 44 | ); 45 | const event = JUPITER_PERPETUALS_PROGRAM.coder.events.decode(eventData); 46 | 47 | return { 48 | event, 49 | ixIndex, 50 | tx, 51 | }; 52 | }); 53 | }); 54 | }); 55 | 56 | // This is an example of filtering the `allEvents` array to only return increase position request events 57 | // The full list of event names and types can be found in the `jupiter-perpetuals-idl.ts` file under the 58 | // `events` key 59 | const increasePositionEvents = allEvents.filter( 60 | (data) => 61 | data?.event?.name === "IncreasePositionEvent" || 62 | data?.event?.name === "InstantIncreasePositionEvent", 63 | ); 64 | 65 | // Example to filter increase position events for a given wallet address 66 | const walletAddress = new PublicKey("WALLET_ADDRESS"); 67 | increasePositionEvents.filter((data) => { 68 | if (data) { 69 | const event = data.event as AnchorIdlEvent< 70 | "InstantIncreasePositionEvent" | "IncreasePositionEvent" 71 | >; 72 | 73 | return event.data.owner.equals(walletAddress); 74 | } 75 | 76 | return false; 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /src/examples/get-global-long-unrealized-pnl.ts: -------------------------------------------------------------------------------- 1 | import { BN, IdlAccounts } from "@coral-xyz/anchor"; 2 | import { JUPITER_PERPETUALS_PROGRAM, USDC_DECIMALS } from "../constants"; 3 | import { Perpetuals } from "../idl/jupiter-perpetuals-idl"; 4 | import { BNToUSDRepresentation } from "../utils"; 5 | 6 | function getPnlForSize( 7 | sizeUsdDelta: BN, 8 | positionAvgPrice: BN, 9 | positionSide: "long" | "short", 10 | tokenPrice: BN, 11 | ) { 12 | if (sizeUsdDelta.eqn(0)) return [false, new BN(0)]; 13 | 14 | const hasProfit = 15 | positionSide === "long" 16 | ? tokenPrice.gt(positionAvgPrice) 17 | : positionAvgPrice.gt(tokenPrice); 18 | 19 | const tokenPriceDelta = tokenPrice.sub(positionAvgPrice).abs(); 20 | 21 | const pnl = sizeUsdDelta.mul(tokenPriceDelta).div(positionAvgPrice); 22 | 23 | return [hasProfit, pnl]; 24 | } 25 | 26 | export async function getGlobalLongUnrealizedPnl() { 27 | const gpaResult = 28 | await JUPITER_PERPETUALS_PROGRAM.provider.connection.getProgramAccounts( 29 | JUPITER_PERPETUALS_PROGRAM.programId, 30 | { 31 | commitment: "confirmed", 32 | filters: [ 33 | { 34 | memcmp: 35 | JUPITER_PERPETUALS_PROGRAM.coder.accounts.memcmp("position"), 36 | }, 37 | ], 38 | }, 39 | ); 40 | 41 | const positions = gpaResult.map((item) => { 42 | return { 43 | publicKey: item.pubkey, 44 | account: JUPITER_PERPETUALS_PROGRAM.coder.accounts.decode( 45 | "position", 46 | item.account.data, 47 | ) as IdlAccounts["position"], 48 | }; 49 | }); 50 | 51 | // Old positions accounts are not closed, but have `sizeUsd = 0` 52 | // i.e. open positions have a non-zero `sizeUsd` 53 | const openPositions = positions.filter( 54 | (position) => position.account.sizeUsd.gtn(0) && position.account.side.long, 55 | ); 56 | 57 | // NOTE: We assume the token price is $100 (scaled to 6 decimal places as per the USDC mint) as an example here for simplicity 58 | const tokenPrice = new BN(100_000_000); 59 | 60 | let totalPnl = new BN(0); 61 | 62 | openPositions.forEach((position) => { 63 | const [hasProfit, pnl] = getPnlForSize( 64 | position.account.sizeUsd, 65 | position.account.price, 66 | position.account.side.long ? "long" : "short", 67 | tokenPrice, 68 | ); 69 | 70 | totalPnl = hasProfit ? totalPnl.add(pnl) : totalPnl.sub(pnl); 71 | }); 72 | 73 | console.log( 74 | "Global long unrealized PNL ($)", 75 | BNToUSDRepresentation(totalPnl, USDC_DECIMALS), 76 | ); 77 | } 78 | 79 | export async function getGlobalLongUnrealizedPnlEstimate() { 80 | const custodies = await JUPITER_PERPETUALS_PROGRAM.account.custody.all(); 81 | 82 | let totalPnl = new BN(0); 83 | 84 | custodies.forEach((custody) => { 85 | // NOTE: We assume the token price is $100 (scaled to 6 decimal places as per the USDC mint) as an example here for simplicity 86 | const tokenPrice = new BN(100_000_000); 87 | const lockedUsd = custody.account.assets.locked.mul(tokenPrice); 88 | totalPnl = totalPnl.add( 89 | lockedUsd.sub(custody.account.assets.guaranteedUsd), 90 | ); 91 | }); 92 | 93 | console.log( 94 | "Global long unrealized PNL estimate ($)", 95 | BNToUSDRepresentation(totalPnl, USDC_DECIMALS), 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/examples/get-open-positions-for-wallet.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | import { type IdlAccounts } from "@coral-xyz/anchor"; 3 | import { Perpetuals } from "../idl/jupiter-perpetuals-idl"; 4 | import { JUPITER_PERPETUALS_PROGRAM, RPC_CONNECTION } from "../constants"; 5 | 6 | // This function fetches all the open positions for a given wallet address, similar to `getOpenPositions`. 7 | export async function getOpenPositionsForWallet(walletAddress: string) { 8 | try { 9 | const gpaResult = await RPC_CONNECTION.getProgramAccounts( 10 | JUPITER_PERPETUALS_PROGRAM.programId, 11 | { 12 | commitment: "confirmed", 13 | filters: [ 14 | // Pass in a wallet address here to filter for positions for 15 | // a specific wallet address 16 | { 17 | memcmp: { 18 | bytes: new PublicKey(walletAddress).toBase58(), 19 | offset: 8, 20 | }, 21 | }, 22 | { 23 | memcmp: 24 | JUPITER_PERPETUALS_PROGRAM.coder.accounts.memcmp("position"), 25 | }, 26 | ], 27 | }, 28 | ); 29 | 30 | const positions = gpaResult.map((item) => { 31 | return { 32 | publicKey: item.pubkey, 33 | account: JUPITER_PERPETUALS_PROGRAM.coder.accounts.decode( 34 | "position", 35 | item.account.data, 36 | ) as IdlAccounts["position"], 37 | }; 38 | }); 39 | 40 | // Old positions accounts are not closed, but have `sizeUsd = 0` 41 | // i.e. open positions have a non-zero `sizeUsd` 42 | // Remove this filter to retrieve closed positions as well 43 | const openPositions = positions.filter((position) => 44 | position.account.sizeUsd.gtn(0), 45 | ); 46 | 47 | console.log( 48 | `Open positions for wallet address ${walletAddress}: `, 49 | openPositions, 50 | ); 51 | 52 | // This `onProgramAccountChange` call subscribes to position changes for the wallet address, which is the same 53 | // as the logic above but via streaming instead of polling. 54 | RPC_CONNECTION.onProgramAccountChange( 55 | JUPITER_PERPETUALS_PROGRAM.programId, 56 | async ({ 57 | accountId: positionPubkey, 58 | accountInfo: { data: positionBuffer }, 59 | }) => { 60 | try { 61 | const position = JUPITER_PERPETUALS_PROGRAM.coder.accounts.decode( 62 | "position", 63 | positionBuffer, 64 | ) as IdlAccounts["position"]; 65 | 66 | console.log("Position updated:", positionPubkey.toString()); 67 | } catch (err) { 68 | console.error( 69 | `Failed to decode position ${positionPubkey.toString()}`, 70 | err, 71 | ); 72 | } 73 | }, 74 | { 75 | commitment: "confirmed", 76 | filters: [ 77 | { 78 | memcmp: { 79 | bytes: new PublicKey(walletAddress).toBase58(), 80 | offset: 8, 81 | }, 82 | }, 83 | { 84 | memcmp: 85 | JUPITER_PERPETUALS_PROGRAM.coder.accounts.memcmp("position"), 86 | }, 87 | ], 88 | }, 89 | ); 90 | } catch (error) { 91 | console.error( 92 | `Failed to fetch open positions for wallet address ${walletAddress}`, 93 | error, 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/examples/get-global-short-unrealized-pnl.ts: -------------------------------------------------------------------------------- 1 | import { BN, IdlAccounts } from "@coral-xyz/anchor"; 2 | import { JUPITER_PERPETUALS_PROGRAM, USDC_DECIMALS } from "../constants"; 3 | import { Perpetuals } from "../idl/jupiter-perpetuals-idl"; 4 | import { BNToUSDRepresentation } from "../utils"; 5 | 6 | function getPnlForSize( 7 | sizeUsdDelta: BN, 8 | positionAvgPrice: BN, 9 | positionSide: "long" | "short", 10 | tokenPrice: BN, 11 | ) { 12 | if (sizeUsdDelta.eqn(0)) return [false, new BN(0)]; 13 | 14 | const hasProfit = 15 | positionSide === "long" 16 | ? tokenPrice.gt(positionAvgPrice) 17 | : positionAvgPrice.gt(tokenPrice); 18 | 19 | const tokenPriceDelta = tokenPrice.sub(positionAvgPrice).abs(); 20 | 21 | const pnl = sizeUsdDelta.mul(tokenPriceDelta).div(positionAvgPrice); 22 | 23 | return [hasProfit, pnl]; 24 | } 25 | 26 | export async function getGlobalShortUnrealizedPnl() { 27 | const gpaResult = 28 | await JUPITER_PERPETUALS_PROGRAM.provider.connection.getProgramAccounts( 29 | JUPITER_PERPETUALS_PROGRAM.programId, 30 | { 31 | commitment: "confirmed", 32 | filters: [ 33 | { 34 | memcmp: 35 | JUPITER_PERPETUALS_PROGRAM.coder.accounts.memcmp("position"), 36 | }, 37 | ], 38 | }, 39 | ); 40 | 41 | const positions = gpaResult.map((item) => { 42 | return { 43 | publicKey: item.pubkey, 44 | account: JUPITER_PERPETUALS_PROGRAM.coder.accounts.decode( 45 | "position", 46 | item.account.data, 47 | ) as IdlAccounts["position"], 48 | }; 49 | }); 50 | 51 | // Old positions accounts are not closed, but have `sizeUsd = 0` 52 | // i.e. open positions have a non-zero `sizeUsd` 53 | const openPositions = positions.filter( 54 | (position) => 55 | position.account.sizeUsd.gtn(0) && position.account.side.short, 56 | ); 57 | 58 | // NOTE: We assume the token price is $100 (scaled to 6 decimal places as per the USDC mint) as an example here for simplicity 59 | const tokenPrice = new BN(100_000_000); 60 | 61 | let totalPnl = new BN(0); 62 | 63 | openPositions.forEach((position) => { 64 | const [hasProfit, pnl] = getPnlForSize( 65 | position.account.sizeUsd, 66 | position.account.price, 67 | position.account.side.long ? "long" : "short", 68 | tokenPrice, 69 | ); 70 | 71 | totalPnl = hasProfit ? totalPnl.add(pnl) : totalPnl.sub(pnl); 72 | }); 73 | 74 | console.log( 75 | "Global short unrealized PNL ($)", 76 | BNToUSDRepresentation(totalPnl, USDC_DECIMALS), 77 | ); 78 | } 79 | 80 | export async function getGlobalShortUnrealizedPnlEstimate() { 81 | const custodies = await JUPITER_PERPETUALS_PROGRAM.account.custody.all(); 82 | 83 | let totalPnl = new BN(0); 84 | 85 | custodies.forEach((custody) => { 86 | // NOTE: We assume the token price is $100 (scaled to 6 decimal places as per the USDC mint) as an example here for simplicity 87 | const tokenPrice = new BN(100_000_000); 88 | const tokenPriceDelta = custody.account.assets.globalShortAveragePrices 89 | .sub(tokenPrice) 90 | .abs(); 91 | 92 | totalPnl = totalPnl.add( 93 | custody.account.assets.globalShortSizes 94 | .mul(tokenPriceDelta) 95 | .div(custody.account.assets.globalShortAveragePrices), 96 | ); 97 | }); 98 | 99 | console.log( 100 | "Global short unrealized PNL estimate ($)", 101 | BNToUSDRepresentation(totalPnl, USDC_DECIMALS), 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /src/examples/get-borrow-position.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | import { 3 | BORROW_SIZE_PRECISION, 4 | DOVES_PROGRAM, 5 | JLP_MINT_PUBKEY, 6 | JLP_POOL_ACCOUNT_PUBKEY, 7 | JUPITER_PERPETUALS_PROGRAM, 8 | JUPITER_PERPETUALS_PROGRAM_ID, 9 | RPC_CONNECTION, 10 | } from "../constants"; 11 | import { BN } from "@coral-xyz/anchor"; 12 | import { BorrowPosition, Custody, OraclePrice } from "../types"; 13 | import { divCeil } from "../utils"; 14 | import { getAssetAmountUsd } from "./calculate-pool-aum"; 15 | import { getMint } from "@solana/spl-token"; 16 | 17 | // The `BorrowPosition` PDA stores the position data for a borrower's onchain position 18 | // Use this to generate the PDA for borrow positions 19 | export function generateBorrowPositionPda({ 20 | walletAddress, 21 | custody, 22 | }: { 23 | walletAddress: PublicKey; 24 | custody: PublicKey; 25 | }) { 26 | const [position, bump] = PublicKey.findProgramAddressSync( 27 | [ 28 | Buffer.from("borrow_lend"), 29 | JLP_POOL_ACCOUNT_PUBKEY.toBuffer(), 30 | walletAddress.toBuffer(), 31 | custody.toBuffer(), 32 | ], 33 | JUPITER_PERPETUALS_PROGRAM_ID, 34 | ); 35 | 36 | return { position, bump }; 37 | } 38 | 39 | export function getBorrowTokenAmount(borrowPosition: BorrowPosition) { 40 | return divCeil(borrowPosition.borrowSize, BORROW_SIZE_PRECISION); 41 | } 42 | 43 | export function updateInterestsAccumulated( 44 | custody: Custody, 45 | borrowPosition: BorrowPosition, 46 | ) { 47 | // If it's a new position, there's no interests accumulated 48 | if ( 49 | borrowPosition.borrowSize.eqn(0) || 50 | borrowPosition.cumulativeCompoundedInterestSnapshot.eqn(0) 51 | ) { 52 | return new BN(0); 53 | } 54 | 55 | const compoundedInterestIndex = 56 | custody.borrowsFundingRateState.cumulativeInterestRate; 57 | 58 | const interests = divCeil( 59 | compoundedInterestIndex 60 | .sub(borrowPosition.cumulativeCompoundedInterestSnapshot) 61 | .mul(borrowPosition.borrowSize), 62 | borrowPosition.cumulativeCompoundedInterestSnapshot, 63 | ); 64 | 65 | return interests; 66 | } 67 | 68 | export function formatBorrowPosition({ 69 | borrowPosition, 70 | custody, 71 | custodyPrice, 72 | jlpTokenSupply, 73 | aumUsd, 74 | }: { 75 | borrowPosition: BorrowPosition; 76 | custody: Custody; 77 | custodyPrice: OraclePrice; 78 | jlpTokenSupply: BN; 79 | jlpPrice: OraclePrice; 80 | aumUsd: BN; 81 | }) { 82 | const interests = updateInterestsAccumulated(custody, borrowPosition); 83 | borrowPosition.borrowSize = borrowPosition.borrowSize.add(interests); 84 | 85 | // Scaled to 6 precision as per the USDC mint 86 | const borrowSizeTokenAmount = getBorrowTokenAmount(borrowPosition); 87 | // Scaled to 6 precision as per the USDC mint 88 | const borrowSizeUsd = getAssetAmountUsd( 89 | custodyPrice, 90 | borrowSizeTokenAmount, 91 | custody.decimals, 92 | ); 93 | 94 | // Scaled to 6 precision as per the USDC mint 95 | const lockedCollateralUsd = aumUsd 96 | .mul(borrowPosition.lockedCollateral) 97 | .div(jlpTokenSupply); 98 | 99 | return { 100 | borrowSize: borrowPosition.borrowSize, 101 | borrowSizeTokenAmount, 102 | borrowSizeUsd, 103 | borrowTokenMint: custody.mint.toString(), 104 | lockedCollateral: borrowPosition.lockedCollateral, 105 | lockedCollateralUsd, 106 | }; 107 | } 108 | 109 | // Only USDC borrowing is enabled 110 | export async function getBorrowPosition(borrowPositionPubkey: PublicKey) { 111 | const borrowPosition = 112 | await JUPITER_PERPETUALS_PROGRAM.account.borrowPosition.fetch( 113 | borrowPositionPubkey, 114 | ); 115 | 116 | const custody = await JUPITER_PERPETUALS_PROGRAM.account.custody.fetch( 117 | borrowPosition.custody, 118 | ); 119 | 120 | const oracleAccount = await DOVES_PROGRAM.account.agPriceFeed.fetch( 121 | custody.dovesAgOracle, 122 | ); 123 | 124 | const oraclePrice = { 125 | price: oracleAccount.price, 126 | exponent: oracleAccount.expo, 127 | } as OraclePrice; 128 | 129 | const pool = await JUPITER_PERPETUALS_PROGRAM.account.pool.fetch( 130 | JLP_POOL_ACCOUNT_PUBKEY, 131 | ); 132 | 133 | const jlpMint = await getMint(RPC_CONNECTION, JLP_MINT_PUBKEY, "confirmed"); 134 | 135 | const formattedBorrowPosition = formatBorrowPosition({ 136 | borrowPosition, 137 | custody, 138 | custodyPrice: oraclePrice, 139 | jlpTokenSupply: new BN(jlpMint.supply.toString()), 140 | aumUsd: pool.aumUsd, 141 | }); 142 | 143 | return formattedBorrowPosition; 144 | } 145 | -------------------------------------------------------------------------------- /src/examples/close-position-request.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createAssociatedTokenAccountIdempotentInstruction, 3 | getAssociatedTokenAddressSync, 4 | NATIVE_MINT, 5 | } from "@solana/spl-token"; 6 | import { 7 | JLP_POOL_ACCOUNT_PUBKEY, 8 | JUPITER_PERPETUALS_PROGRAM, 9 | RPC_CONNECTION, 10 | } from "../constants"; 11 | import { 12 | ComputeBudgetProgram, 13 | Keypair, 14 | PublicKey, 15 | TransactionInstruction, 16 | TransactionMessage, 17 | VersionedTransaction, 18 | } from "@solana/web3.js"; 19 | import { utils } from "@coral-xyz/anchor"; 20 | 21 | async function closePositionRequest(_positionRequestPubkey: string) { 22 | try { 23 | // Fetch position request (TPSL/LO) account 24 | const positionRequestPubkey = new PublicKey(_positionRequestPubkey); 25 | const positionRequest = 26 | await JUPITER_PERPETUALS_PROGRAM.account.positionRequest.fetch( 27 | positionRequestPubkey, 28 | ); 29 | 30 | // Setup transaction instructions 31 | const preInstructions: TransactionInstruction[] = []; 32 | const isSOL = positionRequest.mint.equals(NATIVE_MINT); 33 | 34 | const positionRequestAta = getAssociatedTokenAddressSync( 35 | positionRequest.mint, 36 | positionRequestPubkey, 37 | true, 38 | ); 39 | 40 | const ownerATA = getAssociatedTokenAddressSync( 41 | positionRequest.mint, 42 | positionRequest.owner, 43 | true, 44 | ); 45 | 46 | if (!isSOL) { 47 | const createOwnerATA = createAssociatedTokenAccountIdempotentInstruction( 48 | positionRequest.owner, 49 | ownerATA, 50 | positionRequest.owner, 51 | positionRequest.mint, 52 | ); 53 | 54 | preInstructions.push(createOwnerATA); 55 | } 56 | 57 | const closePositionRequestIx = await JUPITER_PERPETUALS_PROGRAM.methods 58 | .closePositionRequest({}) 59 | .accounts({ 60 | keeper: null, 61 | owner: positionRequest.owner, 62 | ownerAta: isSOL ? null : ownerATA, 63 | pool: JLP_POOL_ACCOUNT_PUBKEY, 64 | positionRequest: positionRequestPubkey, 65 | positionRequestAta, 66 | position: positionRequest.position, 67 | }) 68 | .instruction(); 69 | 70 | // Construct transaction 71 | const instructions = [ 72 | ComputeBudgetProgram.setComputeUnitPrice({ 73 | microLamports: 100000, // Get the estimated compute unit price here from RPC or a provider like Triton 74 | }), 75 | ...preInstructions, 76 | closePositionRequestIx, 77 | ]; 78 | 79 | const simulateTx = new VersionedTransaction( 80 | new TransactionMessage({ 81 | instructions, 82 | // `payerKey` for simulation can be any account as long as it has enough SOL to cover the gas fees 83 | payerKey: PublicKey.default, 84 | // We don't need to pass in a real blockhash here since the `replaceRecentBlockhash` 85 | // option in `simulateTransaction` gets the latest blockhash from the RPC's internal cache 86 | // Reference: https://github.com/anza-xyz/agave/blob/master/rpc/src/rpc.rs#L3890-L3907 87 | recentBlockhash: PublicKey.default.toString(), 88 | }).compileToV0Message([]), 89 | ); 90 | 91 | const simulation = await RPC_CONNECTION.simulateTransaction(simulateTx, { 92 | replaceRecentBlockhash: true, 93 | sigVerify: false, 94 | }); 95 | 96 | instructions.unshift( 97 | ComputeBudgetProgram.setComputeUnitLimit({ 98 | units: simulation.value.unitsConsumed || 1_400_000, 99 | }), 100 | ); 101 | const { blockhash, lastValidBlockHeight } = 102 | await RPC_CONNECTION.getLatestBlockhash("confirmed"); 103 | 104 | const txMessage = new TransactionMessage({ 105 | payerKey: positionRequest.owner, 106 | recentBlockhash: blockhash, 107 | instructions, 108 | }).compileToV0Message(); 109 | 110 | const tx = new VersionedTransaction(txMessage); 111 | 112 | // Sign transaction 113 | const secretKey = Uint8Array.from( 114 | utils.bytes.bs58.decode("SECRET_KEY" as string), 115 | ); 116 | const keypair = Keypair.fromSecretKey(secretKey); 117 | tx.sign([keypair]); 118 | 119 | // Submit transaction 120 | const txid = await RPC_CONNECTION.sendTransaction(tx); 121 | const confirmation = await RPC_CONNECTION.confirmTransaction( 122 | { 123 | blockhash, 124 | lastValidBlockHeight, 125 | signature: txid, 126 | }, 127 | "confirmed", 128 | ); 129 | console.log("transaction confirmation", confirmation); 130 | console.log(`https://solscan.io/tx/${txid}`); 131 | } catch (error) {} 132 | } 133 | -------------------------------------------------------------------------------- /src/examples/price-impact-fee.ts: -------------------------------------------------------------------------------- 1 | import { BN } from "@coral-xyz/anchor"; 2 | import Decimal from "decimal.js"; 3 | 4 | import { divCeil } from "../utils"; 5 | import { BPS_POWER } from "../constants"; 6 | import { Custody } from "../types"; 7 | 8 | export enum TradePoolType { 9 | Increase = "Increase", 10 | Decrease = "Decrease", 11 | } 12 | 13 | export const getBaseFeeUsd = (baseFeeBps: BN, amount: BN) => { 14 | if (amount.eqn(0)) { 15 | return new BN(0); 16 | } 17 | 18 | return amount.mul(baseFeeBps).div(BPS_POWER); 19 | }; 20 | 21 | export const getLinearPriceImpactFeeBps = ( 22 | tradeSizeUsd: BN, 23 | tradeImpactFeeScalar: BN 24 | ) => { 25 | return tradeImpactFeeScalar.eqn(0) 26 | ? new BN(0) 27 | : divCeil(tradeSizeUsd.mul(BPS_POWER), tradeImpactFeeScalar); 28 | }; 29 | 30 | export function calculateDeltaImbalance( 31 | priceImpactBuffer: { 32 | openInterest: BN[]; 33 | lastUpdated: BN; 34 | feeFactor: BN; 35 | maxFeeBps: BN; 36 | exponent: number; 37 | deltaImbalanceThresholdDecimal: BN; 38 | }, 39 | currentTime: number, 40 | newOpenInterest: BN, 41 | tradeType: TradePoolType 42 | ): BN { 43 | const currentIdx = currentTime % 60; 44 | const lastUpdatedIdx = priceImpactBuffer.lastUpdated.toNumber() % 60; 45 | 46 | const amount = 47 | tradeType === TradePoolType.Increase 48 | ? newOpenInterest 49 | : newOpenInterest.neg(); 50 | 51 | const updatedOpenInterest = [...priceImpactBuffer.openInterest]; 52 | 53 | // No values OR more than 1 minute 54 | if ( 55 | priceImpactBuffer.lastUpdated.lten(0) || 56 | new BN(currentTime).sub(priceImpactBuffer.lastUpdated).gten(60) 57 | ) { 58 | return amount; 59 | } 60 | 61 | if (lastUpdatedIdx === currentIdx) { 62 | updatedOpenInterest[currentIdx] = 63 | updatedOpenInterest[currentIdx].add(amount); 64 | } else { 65 | updatedOpenInterest[currentIdx] = amount; 66 | } 67 | 68 | // Set outdated values to 0 69 | if (currentIdx > lastUpdatedIdx) { 70 | // Clean from last_updated_idx+1 to current_idx 71 | updatedOpenInterest.fill(new BN(0), lastUpdatedIdx + 1, currentIdx); 72 | } else if (currentIdx < lastUpdatedIdx) { 73 | // Clean from last_updated_idx+1 to end 74 | updatedOpenInterest.fill( 75 | new BN(0), 76 | lastUpdatedIdx + 1, 77 | updatedOpenInterest.length 78 | ); 79 | // Clean from start to current_idx 80 | updatedOpenInterest.fill(new BN(0), 0, currentIdx); 81 | } 82 | 83 | // Calculate the sum of all values in the array 84 | return updatedOpenInterest.reduce((acc, val) => acc.add(val), new BN(0)); 85 | } 86 | 87 | export const getAdditivePriceImpactFeeBps = ( 88 | baseFeeBps: BN, 89 | amount: BN, 90 | tradePoolType: TradePoolType, 91 | custody: Custody, 92 | curtime: BN 93 | ) => { 94 | if (amount.eqn(0)) { 95 | return { 96 | positionFeeUsd: new BN(0), 97 | priceImpactFeeUsd: new BN(0), 98 | }; 99 | } 100 | 101 | const priceImpactBuffer = custody.priceImpactBuffer; 102 | const linearImpactFeeCoefficientBps = getLinearPriceImpactFeeBps( 103 | amount, 104 | custody.pricing.tradeImpactFeeScalar 105 | ); 106 | const totalBaseFeeBps = linearImpactFeeCoefficientBps.add(baseFeeBps); 107 | const linearImpactFeeUsd = divCeil( 108 | amount.mul(linearImpactFeeCoefficientBps), 109 | BPS_POWER 110 | ); 111 | let positionFeeUsd = divCeil(amount.mul(totalBaseFeeBps), BPS_POWER); 112 | 113 | if (custody.priceImpactBuffer.feeFactor.eq(new BN(0))) { 114 | return { 115 | positionFeeUsd, 116 | priceImpactFeeUsd: linearImpactFeeUsd, 117 | }; 118 | } 119 | 120 | const deltaImbalanceDecimal = calculateDeltaImbalance( 121 | priceImpactBuffer, 122 | curtime.toNumber(), 123 | amount, 124 | tradePoolType 125 | ).abs(); 126 | 127 | if ( 128 | deltaImbalanceDecimal.lte(priceImpactBuffer.deltaImbalanceThresholdDecimal) 129 | ) { 130 | return { 131 | positionFeeUsd, 132 | priceImpactFeeUsd: linearImpactFeeUsd, 133 | }; 134 | } 135 | 136 | const deltaImbalanceAmountDecimal = 137 | new (deltaImbalanceDecimal.toString().div)( 138 | new Decimal(priceImpactBuffer.deltaImbalanceThresholdDecimal.toString()) 139 | ) 140 | .pow(priceImpactBuffer.exponent) 141 | .ceil(); 142 | const deltaImbalanceAmount = new BN(deltaImbalanceAmountDecimal.toString()); 143 | 144 | const priceImpactFeeBps = divCeil( 145 | deltaImbalanceAmount, 146 | priceImpactBuffer.feeFactor 147 | ); 148 | const totalFeeBps = totalBaseFeeBps.add(priceImpactFeeBps); 149 | const cappedTotalFeeBps = BN.min(totalFeeBps, priceImpactBuffer.maxFeeBps); 150 | 151 | positionFeeUsd = divCeil(amount.mul(cappedTotalFeeBps), BPS_POWER); 152 | const baseFeeUsd = getBaseFeeUsd(baseFeeBps, amount); 153 | const priceImpactFeeUsd = positionFeeUsd.sub(baseFeeUsd); 154 | 155 | return { positionFeeUsd, priceImpactFeeUsd }; 156 | }; 157 | -------------------------------------------------------------------------------- /src/examples/get-borrow-fee-and-funding-rate.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | import { BN } from "@coral-xyz/anchor"; 3 | import { 4 | BPS_POWER, 5 | DBPS_POWER, 6 | DEBT_POWER, 7 | JUPITER_PERPETUALS_PROGRAM, 8 | RATE_POWER, 9 | USDC_DECIMALS, 10 | } from "../constants"; 11 | import { BNToUSDRepresentation, divCeil } from "../utils"; 12 | import { Custody } from "../types"; 13 | 14 | const HOURS_IN_A_YEAR = 24 * 365; 15 | 16 | enum BorrowRateMechanism { 17 | Linear, 18 | Jump, 19 | } 20 | 21 | export const getBorrowFee = async ( 22 | positionPubkey: PublicKey | string, 23 | curtime: BN, 24 | ) => { 25 | const position = 26 | await JUPITER_PERPETUALS_PROGRAM.account.position.fetch(positionPubkey); 27 | 28 | const custody = await JUPITER_PERPETUALS_PROGRAM.account.custody.fetch( 29 | position.custody, 30 | ); 31 | 32 | if (position.sizeUsd.eqn(0)) return new BN(0); 33 | 34 | const cumulativeInterest = getCumulativeInterest(custody, curtime); 35 | const positionInterest = cumulativeInterest.sub( 36 | position.cumulativeInterestSnapshot, 37 | ); 38 | 39 | const borrowFee = divCeil(positionInterest.mul(position.sizeUsd), RATE_POWER); 40 | 41 | console.log( 42 | "Outstanding borrow fee ($): ", 43 | BNToUSDRepresentation(borrowFee, USDC_DECIMALS), 44 | ); 45 | }; 46 | 47 | export function getBorrowRateMechanism(custody: Custody) { 48 | if (!custody.fundingRateState.hourlyFundingDbps.eq(new BN(0))) { 49 | return BorrowRateMechanism.Linear; 50 | } else { 51 | return BorrowRateMechanism.Jump; 52 | } 53 | } 54 | 55 | export const getCumulativeInterest = (custody: Custody, curtime: BN) => { 56 | if (curtime.gt(custody.fundingRateState.lastUpdate)) { 57 | const fundingRate = getCurrentFundingRate(custody, curtime); 58 | return custody.fundingRateState.cumulativeInterestRate.add(fundingRate); 59 | } else { 60 | return custody.fundingRateState.cumulativeInterestRate; 61 | } 62 | }; 63 | 64 | export function getDebt(custody: Custody) { 65 | return divCeil( 66 | BN.max(custody.debt.sub(custody.borrowLendInterestsAccured), new BN(0)), 67 | DEBT_POWER, 68 | ); 69 | } 70 | 71 | export function theoreticallyOwned(custody: Custody) { 72 | return custody.assets.owned.add(getDebt(custody)); 73 | } 74 | 75 | export function totalLocked(custody: Custody) { 76 | return custody.assets.locked.add(getDebt(custody)); 77 | } 78 | 79 | export const getHourlyBorrowRate = ( 80 | custody: Custody, 81 | isBorrowCurve = false, 82 | ) => { 83 | const borrowRateMechanism = getBorrowRateMechanism(custody); 84 | const owned = theoreticallyOwned(custody); 85 | const locked = totalLocked(custody); 86 | 87 | if (borrowRateMechanism === BorrowRateMechanism.Linear) { 88 | const fundingRateState = isBorrowCurve 89 | ? custody.borrowsFundingRateState 90 | : custody.fundingRateState; 91 | const hourlyFundingRate = fundingRateState.hourlyFundingDbps 92 | .mul(RATE_POWER) 93 | .div(DBPS_POWER); 94 | 95 | return owned.gtn(0) && locked.gtn(0) 96 | ? divCeil(locked.mul(hourlyFundingRate), owned) 97 | : new BN(0); 98 | } else { 99 | const { minRateBps, maxRateBps, targetRateBps, targetUtilizationRate } = 100 | custody.jumpRateState; 101 | 102 | const utilizationRate = 103 | owned.gtn(0) && locked.gtn(0) 104 | ? locked.mul(RATE_POWER).div(owned) 105 | : new BN(0); 106 | 107 | let yearlyRate: BN; 108 | 109 | if (utilizationRate.lte(targetUtilizationRate)) { 110 | yearlyRate = divCeil( 111 | targetRateBps.sub(minRateBps).mul(utilizationRate), 112 | targetUtilizationRate, 113 | ) 114 | .add(minRateBps) 115 | .mul(RATE_POWER) 116 | .div(BPS_POWER); 117 | } else { 118 | const rateDiff = BN.max(new BN(0), maxRateBps.sub(targetRateBps)); 119 | const utilDiff = BN.max( 120 | new BN(0), 121 | utilizationRate.sub(targetUtilizationRate), 122 | ); 123 | const denom = BN.max(new BN(0), RATE_POWER.sub(targetUtilizationRate)); 124 | 125 | if (denom.eqn(0)) { 126 | throw new Error("Denominator is 0"); 127 | } 128 | 129 | yearlyRate = divCeil(rateDiff.mul(utilDiff), denom) 130 | .add(targetRateBps) 131 | .mul(RATE_POWER) 132 | .div(BPS_POWER); 133 | } 134 | 135 | return yearlyRate.divn(HOURS_IN_A_YEAR); 136 | } 137 | }; 138 | 139 | // Returns the borrow APR for the year for a given custody 140 | export const getBorrowApr = (custody: Custody) => { 141 | return ( 142 | (getHourlyBorrowRate(custody, true).toNumber() / RATE_POWER.toNumber()) * 143 | (24 * 365) * 144 | 100 145 | ); 146 | }; 147 | 148 | export const getCurrentFundingRate = (custody: Custody, curtime: BN) => { 149 | if (custody.assets.owned.eqn(0)) return new BN(0); 150 | 151 | const interval = curtime.sub(custody.fundingRateState.lastUpdate); 152 | const currentFundingRate = getHourlyBorrowRate(custody); 153 | 154 | return divCeil(currentFundingRate.mul(interval), new BN(3600)); 155 | }; 156 | -------------------------------------------------------------------------------- /src/examples/calculate-pool-aum.ts: -------------------------------------------------------------------------------- 1 | import { BN } from "@coral-xyz/anchor"; 2 | import { divCeil } from "../utils"; 3 | import { Custody, OraclePrice } from "../types"; 4 | import { USDC_DECIMALS } from "../constants"; 5 | 6 | /* Constants */ 7 | 8 | export const RATE_DECIMALS = 9; 9 | export const RATE_POWER = new BN(10).pow(new BN(RATE_DECIMALS)); 10 | export const DEBT_POWER = RATE_POWER; 11 | 12 | /* Math helpers */ 13 | 14 | export const checkedDecimalMul = ( 15 | coefficient1: BN, 16 | exponent1: number, 17 | coefficient2: BN, 18 | exponent2: number, 19 | targetExponent: number, 20 | ) => { 21 | if (coefficient1.eqn(0) || coefficient2.eqn(0)) return new BN(0); 22 | 23 | let targetPower = exponent1 + exponent2 - targetExponent; 24 | 25 | if (targetPower >= 0) { 26 | return coefficient1 27 | .mul(coefficient2) 28 | .mul(new BN(Math.pow(10, targetPower))); 29 | } else { 30 | return coefficient1 31 | .mul(coefficient2) 32 | .div(new BN(Math.pow(10, -targetPower))); 33 | } 34 | }; 35 | 36 | // Formats the oracle price to a target exponent 37 | export function getPrice(oraclePrice: OraclePrice, targetExponent: number): BN { 38 | if (targetExponent === oraclePrice.exponent) { 39 | return { ...oraclePrice }; 40 | } 41 | 42 | const delta = targetExponent - oraclePrice.exponent; 43 | 44 | if (delta > 0) { 45 | return { 46 | price: oraclePrice.price.div(new BN(10).pow(new BN(delta))), 47 | exponent: targetExponent, 48 | }; 49 | } else { 50 | return { 51 | price: oraclePrice.price.mul(new BN(10).pow(new BN(Math.abs(delta)))), 52 | exponent: targetExponent, 53 | }; 54 | } 55 | } 56 | 57 | // The contract uses this as a safety mechanism for stablecoin depegs 58 | export function getOraclePriceForStable(oraclePrice: OraclePrice) { 59 | const oneUsd = new BN(10).pow(new BN(Math.abs(oraclePrice.exponent))); 60 | const maxPrice = BN.max(oneUsd, oraclePrice.price); 61 | 62 | return { 63 | price: maxPrice, 64 | exponent: oraclePrice.exponent, 65 | }; 66 | } 67 | 68 | // Returns the USD value (scaled to the USDC decimals) given an oracle price and token amount 69 | export const getAssetAmountUsd = ( 70 | oracle: OraclePrice, 71 | tokenAmount: BN, 72 | tokenDecimals: number, 73 | ): BN => { 74 | if (tokenAmount.eqn(0) || oracle.price.eqn(0)) { 75 | return new BN(0); 76 | } 77 | 78 | return checkedDecimalMul( 79 | tokenAmount, 80 | -tokenDecimals, 81 | oracle.price, 82 | oracle.exponent, 83 | -USDC_DECIMALS, 84 | ); 85 | }; 86 | 87 | /* State helpers */ 88 | 89 | // Returns the amount borrowed from the custody minus interests accrued (i.e. the pure debt) 90 | export function getDebt(custody: Custody) { 91 | return divCeil( 92 | BN.max(custody.debt.sub(custody.borrowLendInterestsAccured), new BN(0)), 93 | DEBT_POWER, 94 | ); 95 | } 96 | 97 | // Returns the "true" owned token amount by the custody as the borrowed tokens are not stored in `custody.owned` 98 | export function theoreticallyOwned(custody: Custody) { 99 | return custody.assets.owned.add(getDebt(custody)); 100 | } 101 | 102 | // Returns the "true" locked token amount by the custody as the borrowed tokens are not stored in `custody.locked` 103 | export function totalLocked(custody: Custody) { 104 | return custody.assets.locked.add(getDebt(custody)); 105 | } 106 | 107 | export function getGlobalShortPnl(custody: Custody, price: BN) { 108 | const averagePrice = custody.assets.globalShortAveragePrices; 109 | const priceDelta = averagePrice.sub(price).abs(); 110 | const tradersPnlDelta = custody.assets.globalShortSizes 111 | .mul(priceDelta) 112 | .div(averagePrice); 113 | 114 | // if true, pool lost, trader profit 115 | // if false, pool profit, trader lost 116 | const tradersHasProfit = averagePrice.gt(price); 117 | 118 | return { 119 | tradersPnlDelta, 120 | tradersHasProfit, 121 | }; 122 | } 123 | 124 | /* Main */ 125 | 126 | // Returns the assets under management for a given custody in the pool 127 | export function getAssetUnderManagementUsdForCustody( 128 | custody: Custody, 129 | // An OraclePrice object can be constructed by fetching the price of the token scaled to the token's decimals, like so: 130 | // USDC OraclePrice object: 131 | // 132 | // { 133 | // price: new BN(10000000), 134 | // exponent: -6 // since USDC has 6 decimals 135 | // } 136 | // 137 | custodyPrice: OraclePrice, 138 | ) { 139 | const owned = theoreticallyOwned(custody); 140 | 141 | if (custody.isStable) { 142 | const aumUsd = getAssetAmountUsd( 143 | getOraclePriceForStable(custodyPrice as OraclePrice), 144 | owned, 145 | custody.decimals, 146 | ); 147 | 148 | return aumUsd; 149 | } else { 150 | let tradersPnlDelta = new BN(0); 151 | let tradersHasProfit = false; 152 | let aumUsd = custody.assets.guaranteedUsd; 153 | 154 | const netAssetsToken = BN.max(new BN(0), owned.sub(custody.assets.locked)); 155 | const netAssetsUsd = getAssetAmountUsd( 156 | custodyPrice, 157 | netAssetsToken, 158 | custody.decimals, 159 | ); 160 | aumUsd = aumUsd.add(netAssetsUsd); 161 | 162 | if (custody.assets.globalShortSizes.gtn(0)) { 163 | ({ tradersPnlDelta, tradersHasProfit } = getGlobalShortPnl( 164 | custody, 165 | getPrice(custodyPrice, -USDC_DECIMALS), 166 | )); 167 | 168 | if (tradersHasProfit) { 169 | aumUsd = BN.max(new BN(0), aumUsd.sub(tradersPnlDelta)); 170 | } else { 171 | aumUsd = aumUsd.add(tradersPnlDelta); 172 | } 173 | } 174 | 175 | return aumUsd; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/examples/poll-and-stream-oracle-price-updates.ts: -------------------------------------------------------------------------------- 1 | import { Connection, Keypair, PublicKey } from "@solana/web3.js"; 2 | import { AnchorProvider, BN, Program, Wallet } from "@coral-xyz/anchor"; 3 | import { IDL as DovesIDL } from "../idl/doves-idl"; 4 | import { CUSTODY_PUBKEY } from "../constants"; 5 | import { BNToUSDRepresentation } from "../utils"; 6 | 7 | /* Constants */ 8 | 9 | const connection = new Connection("https://api.mainnet-beta.solana.com"); 10 | 11 | const DOVES_PROGRAM_ID = new PublicKey( 12 | "DoVEsk76QybCEHQGzkvYPWLQu9gzNoZZZt3TPiL597e" 13 | ); 14 | 15 | const dovesProgram = new Program( 16 | DovesIDL, 17 | DOVES_PROGRAM_ID, 18 | new AnchorProvider(connection, new Wallet(Keypair.generate()), { 19 | preflightCommitment: "processed", 20 | }) 21 | ); 22 | 23 | export const CUSTODY_DETAILS = { 24 | [CUSTODY_PUBKEY.SOL]: { 25 | mint: new PublicKey("So11111111111111111111111111111111111111112"), 26 | dovesOracle: new PublicKey("39cWjvHrpHNz2SbXv6ME4NPhqBDBd4KsjUYv5JkHEAJU"), 27 | }, 28 | [CUSTODY_PUBKEY.ETH]: { 29 | mint: new PublicKey("7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs"), 30 | dovesOracle: new PublicKey("5URYohbPy32nxK1t3jAHVNfdWY2xTubHiFvLrE3VhXEp"), 31 | }, 32 | [CUSTODY_PUBKEY.BTC]: { 33 | mint: new PublicKey("3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh"), 34 | dovesOracle: new PublicKey("4HBbPx9QJdjJ7GUe6bsiJjGybvfpDhQMMPXP1UEa7VT5"), 35 | }, 36 | [CUSTODY_PUBKEY.USDC]: { 37 | mint: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), 38 | dovesOracle: new PublicKey("A28T5pKtscnhDo6C1Sz786Tup88aTjt8uyKewjVvPrGk"), 39 | }, 40 | [CUSTODY_PUBKEY.USDT]: { 41 | mint: new PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"), 42 | dovesOracle: new PublicKey("AGW7q2a3WxCzh5TB2Q6yNde1Nf41g3HLaaXdybz7cbBU"), 43 | }, 44 | }; 45 | 46 | const DOVES_ORACLES = [ 47 | { 48 | name: "SOL", 49 | publicKey: CUSTODY_DETAILS[CUSTODY_PUBKEY.SOL].dovesOracle, 50 | custody: CUSTODY_PUBKEY.SOL, 51 | }, 52 | { 53 | name: "ETH", 54 | publicKey: CUSTODY_DETAILS[CUSTODY_PUBKEY.ETH].dovesOracle, 55 | custody: CUSTODY_PUBKEY.ETH, 56 | }, 57 | { 58 | name: "BTC", 59 | publicKey: CUSTODY_DETAILS[CUSTODY_PUBKEY.BTC].dovesOracle, 60 | custody: CUSTODY_PUBKEY.BTC, 61 | }, 62 | { 63 | name: "USDC", 64 | publicKey: CUSTODY_DETAILS[CUSTODY_PUBKEY.USDC].dovesOracle, 65 | custody: CUSTODY_PUBKEY.USDC, 66 | }, 67 | { 68 | name: "USDT", 69 | publicKey: CUSTODY_DETAILS[CUSTODY_PUBKEY.USDT].dovesOracle, 70 | custody: CUSTODY_PUBKEY.USDT, 71 | }, 72 | ]; 73 | 74 | /* Types */ 75 | 76 | interface DovesOraclePrice { 77 | price: BN; 78 | priceUsd: string; 79 | timestamp: number; 80 | expo: number; 81 | } 82 | 83 | type CustodyToOraclePrice = Record; 84 | 85 | /* Functions */ 86 | 87 | export async function fetchAndUpdateOraclePriceData( 88 | cache: CustodyToOraclePrice 89 | ) { 90 | const dovesPubkey = DOVES_ORACLES.map(({ publicKey }) => publicKey); 91 | const feeds = await dovesProgram.account.priceFeed.fetchMultiple(dovesPubkey); 92 | 93 | DOVES_ORACLES.forEach(({ custody }, index) => { 94 | const feed = feeds[index]; 95 | 96 | if (!feed) { 97 | throw new Error( 98 | `Failed to fetch latest oracle price data for: ${custody.toString()}` 99 | ); 100 | } 101 | 102 | const data: DovesOraclePrice = { 103 | price: feed.price, 104 | priceUsd: BNToUSDRepresentation(feed.price, Math.abs(feed.expo)), 105 | timestamp: feed.timestamp.toNumber(), 106 | expo: feed.expo, 107 | }; 108 | 109 | cache[custody.toString()] = data; 110 | }); 111 | } 112 | 113 | export async function subscribeOraclePrices(intervalMs: number = 100) { 114 | const cache = DOVES_ORACLES.reduce((cache, entries) => { 115 | cache[entries.custody.toString()] = { 116 | price: new BN(0), 117 | priceUsd: "0", 118 | timestamp: 0, 119 | expo: 0, 120 | }; 121 | 122 | return cache; 123 | }, {} as CustodyToOraclePrice); 124 | 125 | // Initialize the oracle price cache 126 | await fetchAndUpdateOraclePriceData(cache); 127 | 128 | // Poll for price updates every `intervalMs` milliseconds 129 | const pollPriceUpdates = async () => { 130 | try { 131 | await fetchAndUpdateOraclePriceData(cache); 132 | console.log(cache); 133 | } catch (err) { 134 | console.error("Failed to fetch and update oracle price: ", err); 135 | } finally { 136 | setTimeout(pollPriceUpdates, intervalMs); 137 | } 138 | }; 139 | 140 | pollPriceUpdates(); 141 | 142 | // Stream price updates in addition to polling for price updates above. This alone is enough for most cases 143 | // but polling helps in case `onProgramAccountChange` misses price updates 144 | connection.onProgramAccountChange( 145 | DOVES_PROGRAM_ID, 146 | ({ accountId, accountInfo }) => { 147 | const oracle = DOVES_ORACLES.find((oracle) => 148 | oracle.publicKey.equals(accountId) 149 | ); 150 | 151 | if (!oracle) { 152 | throw new Error( 153 | `Cannot find custody details for account: ${accountId.toString()}` 154 | ); 155 | } 156 | 157 | const priceFeed = dovesProgram.coder.accounts.decode( 158 | "priceFeed", 159 | accountInfo.data 160 | ); 161 | 162 | const data: DovesOraclePrice = { 163 | price: priceFeed.price, 164 | priceUsd: BNToUSDRepresentation( 165 | priceFeed.price, 166 | Math.abs(priceFeed.expo) 167 | ), 168 | timestamp: priceFeed.timestamp.toNumber(), 169 | expo: priceFeed.expo, 170 | }; 171 | 172 | cache[oracle.custody.toString()] = data; 173 | }, 174 | { commitment: "confirmed" } 175 | ); 176 | 177 | return cache; 178 | } 179 | -------------------------------------------------------------------------------- /src/examples/remaining-accounts.ts: -------------------------------------------------------------------------------- 1 | // Certain instructions require extra accounts to be passed into the instruction, but they are not specified 2 | // in the instruction accounts list, but instead passed into the `remainingAccounts` array 3 | // https://ackee.xyz/trident/docs/latest/start-fuzzing/writting-fuzz-test/remaining-accounts/ 4 | // 5 | // Fortunately, for the Jupiter perps program, the remaining accounts are the same for all instructions that 6 | // require it, which is shared below 7 | import { AnchorProvider, Program, Wallet } from "@coral-xyz/anchor"; 8 | import { Connection, Keypair, PublicKey } from "@solana/web3.js"; 9 | import { IDL } from "../idl/jupiter-perpetuals-idl"; 10 | 11 | const CONNECTION = new Connection("YOUR_RPC_URL", { commitment: "confirmed" }); 12 | 13 | const PERPS_POOL_PUBLIC_KEY = new PublicKey( 14 | "5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq", 15 | ); 16 | 17 | const CUSTODY_DETAILS = { 18 | // SOL 19 | "7xS2gz2bTp3fwCC7knJvUWTEU9Tycczu6VhJYKgi1wdz": { 20 | name: "SOL", 21 | mint: new PublicKey("So11111111111111111111111111111111111111112"), 22 | pythnetOracle: new PublicKey( 23 | "7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE", 24 | ), 25 | dovesOracle: new PublicKey("39cWjvHrpHNz2SbXv6ME4NPhqBDBd4KsjUYv5JkHEAJU"), 26 | chainlinkOracle: new PublicKey( 27 | "FWLXDDgW2Qm2VuX8MdV99VYpo6X1HLEykUjfAsjz2G78", 28 | ), 29 | dovesAgOracle: new PublicKey( 30 | "FYq2BWQ1V5P1WFBqr3qB2Kb5yHVvSv7upzKodgQE5zXh", 31 | ), 32 | tokenAccount: new PublicKey("BUvduFTd2sWFagCunBPLupG8fBTJqweLw9DuhruNFSCm"), 33 | }, 34 | // ETH 35 | AQCGyheWPLeo6Qp9WpYS9m3Qj479t7R636N9ey1rEjEn: { 36 | name: "ETH", 37 | mint: new PublicKey("7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs"), 38 | pythnetOracle: new PublicKey( 39 | "42amVS4KgzR9rA28tkVYqVXjq9Qa8dcZQMbH5EYFX6XC", 40 | ), 41 | dovesOracle: new PublicKey("5URYohbPy32nxK1t3jAHVNfdWY2xTubHiFvLrE3VhXEp"), 42 | chainlinkOracle: new PublicKey( 43 | "BNQzYvnidN8vVVn78xh6wgLo5ozmV8Dx8AE8rndqeLEe", 44 | ), 45 | dovesAgOracle: new PublicKey( 46 | "AFZnHPzy4mvVCffrVwhewHbFc93uTHvDSFrVH7GtfXF1", 47 | ), 48 | tokenAccount: new PublicKey("Bgarxg65CEjN3kosjCW5Du3wEqvV3dpCGDR3a2HRQsYJ"), 49 | }, 50 | // BTC 51 | "5Pv3gM9JrFFH883SWAhvJC9RPYmo8UNxuFtv5bMMALkm": { 52 | name: "BTC", 53 | mint: new PublicKey("3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh"), 54 | pythnetOracle: new PublicKey( 55 | "4cSM2e6rvbGQUFiJbqytoVMi5GgghSMr8LwVrT9VPSPo", 56 | ), 57 | dovesOracle: new PublicKey("4HBbPx9QJdjJ7GUe6bsiJjGybvfpDhQMMPXP1UEa7VT5"), 58 | chainlinkOracle: new PublicKey( 59 | "A6F8mvoM8Qc9wTaKjrD1B5Fgpp6NhPQyJLWXeafWrbsV", 60 | ), 61 | dovesAgOracle: new PublicKey("hUqAT1KQ7eW1i6Csp9CXYtpPfSAvi835V7wKi5fRfmC"), 62 | tokenAccount: new PublicKey("FgpXg2J3TzSs7w3WGYYE7aWePdrxBVLCXSxmAKnCZNtZ"), 63 | }, 64 | // USDC 65 | G18jKKXQwBbrHeiK3C9MRXhkHsLHf7XgCSisykV46EZa: { 66 | name: "USDC", 67 | mint: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), 68 | pythnetOracle: new PublicKey( 69 | "Dpw1EAVrSB1ibxiDQyTAW6Zip3J4Btk2x4SgApQCeFbX", 70 | ), 71 | dovesOracle: new PublicKey("A28T5pKtscnhDo6C1Sz786Tup88aTjt8uyKewjVvPrGk"), 72 | chainlinkOracle: new PublicKey( 73 | "3Z4gQ5ujXZSYeVyPhkakVcrmyMxhAk6VT2NYSVV3RGGU", 74 | ), 75 | dovesAgOracle: new PublicKey( 76 | "6Jp2xZUTWdDD2ZyUPRzeMdc6AFQ5K3pFgZxk2EijfjnM", 77 | ), 78 | tokenAccount: new PublicKey("WzWUoCmtVv7eqAbU3BfKPU3fhLP6CXR8NCJH78UK9VS"), 79 | }, 80 | // USDT 81 | "4vkNeXiYEUizLdrpdPS1eC2mccyM4NUPRtERrk6ZETkk": { 82 | name: "USDT", 83 | mint: new PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"), 84 | pythnetOracle: new PublicKey("HT2PLQBcG5EiCcNSaMHAjSgd9F98ecpATbk4Sk5oYuM"), 85 | dovesOracle: new PublicKey("AGW7q2a3WxCzh5TB2Q6yNde1Nf41g3HLaaXdybz7cbBU"), 86 | chainlinkOracle: new PublicKey( 87 | "5KQxzQ4xQGPUiJGYbujLjygm6Frin9zE5h996hxxfyqe", 88 | ), 89 | dovesAgOracle: new PublicKey( 90 | "Fgc93D641F8N2d1xLjQ4jmShuD3GE3BsCXA56KBQbF5u", 91 | ), 92 | tokenAccount: new PublicKey("Gex24YznvguMad1mBzTQ7a64U1CJy59gvsStQmNnnwAd"), 93 | }, 94 | }; 95 | 96 | const PROGRAM_ID = new PublicKey("PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"); 97 | 98 | const program = new Program( 99 | IDL, 100 | PROGRAM_ID, 101 | new AnchorProvider(CONNECTION, new Wallet(Keypair.generate()), { 102 | preflightCommitment: "confirmed", 103 | }), 104 | ); 105 | 106 | const getCustodyMetas = async () => { 107 | const pool = await program.account.pool.fetch(PERPS_POOL_PUBLIC_KEY); 108 | 109 | let custodyMetas = []; 110 | for (const custody of pool.custodies) { 111 | custodyMetas.push({ 112 | isSigner: false, 113 | isWritable: false, 114 | pubkey: custody, 115 | }); 116 | } 117 | for (const custody of pool.custodies) { 118 | // @ts-ignore 119 | const custodyDetails = CUSTODY_DETAILS[custody.toString()]; 120 | 121 | if (custodyDetails) { 122 | custodyMetas.push({ 123 | isSigner: false, 124 | isWritable: false, 125 | pubkey: custodyDetails.dovesAgOracle, 126 | }); 127 | } 128 | } 129 | for (const custody of pool.custodies) { 130 | // @ts-ignore 131 | const custodyDetails = CUSTODY_DETAILS[custody.toString()]; 132 | 133 | if (custodyDetails) { 134 | custodyMetas.push({ 135 | isSigner: false, 136 | isWritable: false, 137 | pubkey: custodyDetails.pythnetOracle, 138 | }); 139 | } 140 | } 141 | return custodyMetas; 142 | }; 143 | 144 | try { 145 | // @ts-ignore 146 | // Pass this into `remainingAccounts` 147 | const custodyMetas = await getCustodyMetas(); 148 | 149 | // @ts-ignore 150 | // Example of an instruction that requires passing in remaining accounts when invoking 151 | await program.methods 152 | .removeLiquidity2({ 153 | // 154 | }) 155 | .accounts({ 156 | // 157 | }) 158 | .remainingAccounts(custodyMetas) 159 | // @ts-ignore 160 | .signers([]) 161 | .rpc(); 162 | } catch (error) { 163 | console.error(error); 164 | } 165 | -------------------------------------------------------------------------------- /src/examples/calculate-mint-burn-jlp.ts: -------------------------------------------------------------------------------- 1 | import { Custody, OraclePrice, Pool } from "../types"; 2 | import { BN } from "@coral-xyz/anchor"; 3 | import { checkedDecimalMul, getAssetAmountUsd } from "./calculate-pool-aum"; 4 | import { 5 | CUSTODY_PUBKEY, 6 | JLP_MINT_PUBKEY, 7 | JLP_POOL_ACCOUNT_PUBKEY, 8 | JUPITER_PERPETUALS_PROGRAM, 9 | RPC_CONNECTION, 10 | USDC_DECIMALS, 11 | } from "../constants"; 12 | import { subscribeOraclePrices } from "./poll-and-stream-oracle-price-updates"; 13 | import { collectSwapFees, getFeeBps } from "./calculate-swap-amount-and-fee"; 14 | import { getMint } from "@solana/spl-token"; 15 | import Decimal from "decimal.js"; 16 | 17 | export const getTokenAmount = ( 18 | oracle: OraclePrice, 19 | assetAmountUsd: BN, 20 | tokenDecimals: number 21 | ) => { 22 | if (oracle.price.eqn(0) || assetAmountUsd.eqn(0)) return new BN(0); 23 | 24 | return checkedDecimalMul( 25 | assetAmountUsd, 26 | -USDC_DECIMALS, 27 | oracle.price, 28 | oracle.exponent, 29 | -tokenDecimals 30 | ); 31 | }; 32 | 33 | export const checkedDecimalDiv = ( 34 | coefficient1: BN, 35 | exponent1: number, 36 | coefficient2: BN, 37 | exponent2: number, 38 | targetExponent: number 39 | ) => { 40 | if (coefficient2.eqn(0)) throw "MathOverflow: Division by zero"; 41 | if (coefficient1.eqn(0)) return new BN(0); 42 | 43 | let scaleFactor = 0; 44 | let targetPower = exponent1 - exponent2 - targetExponent; 45 | if (exponent1 > 0) { 46 | scaleFactor += exponent1; 47 | targetPower -= exponent1; 48 | } 49 | if (exponent2 < 0) { 50 | scaleFactor -= exponent2; 51 | targetPower += exponent2; 52 | } 53 | if (targetExponent < 0) { 54 | scaleFactor -= targetExponent; 55 | targetPower += targetExponent; 56 | } 57 | 58 | const scaledCoeff1 = 59 | scaleFactor > 0 60 | ? new Decimal(coefficient1.toString()).mul(Decimal.pow(10, scaleFactor)) 61 | : new Decimal(coefficient1.toString()); 62 | 63 | const dividend = scaledCoeff1.div(new Decimal(coefficient2.toString())); 64 | const result = 65 | targetPower >= 0 66 | ? dividend.mul(Decimal.pow(10, targetPower)) 67 | : dividend.div(Decimal.pow(10, -targetPower)); 68 | 69 | return new BN(result.toDP(0).toString()); 70 | }; 71 | 72 | // Mint JLP 73 | export function getAddLiquidityFeeBps({ 74 | pool, 75 | custody, 76 | usdDelta, 77 | tokenPrice, 78 | }: { 79 | pool: Pool; 80 | custody: Custody; 81 | usdDelta: BN; 82 | tokenPrice: OraclePrice; 83 | }) { 84 | return getFeeBps({ 85 | custody, 86 | sizeUsdDelta: usdDelta, 87 | baseFeeBps: pool.fees.addRemoveLiquidityBps, 88 | taxFeeBps: pool.fees.taxBps, 89 | multiplier: pool.fees.swapMultiplier, 90 | increment: true, 91 | pool, 92 | tokenPrice, 93 | }); 94 | } 95 | 96 | // Burn JLP 97 | export function getRemoveLiquidityFeeBps({ 98 | pool, 99 | custody, 100 | usdDelta, 101 | tokenPrice, 102 | }: { 103 | pool: Pool; 104 | custody: Custody; 105 | usdDelta: BN; 106 | tokenPrice: OraclePrice; 107 | }) { 108 | return getFeeBps({ 109 | custody, 110 | sizeUsdDelta: usdDelta, 111 | baseFeeBps: pool.fees.addRemoveLiquidityBps, 112 | taxFeeBps: pool.fees.taxBps, 113 | multiplier: pool.fees.swapMultiplier, 114 | increment: false, 115 | pool, 116 | tokenPrice, 117 | }); 118 | } 119 | 120 | // Example deposit SOL to mint JLP with fee calculations 121 | export const calculateMintJlp = async () => { 122 | const oraclePrices = await subscribeOraclePrices(); 123 | const pool = await JUPITER_PERPETUALS_PROGRAM.account.pool.fetch( 124 | JLP_POOL_ACCOUNT_PUBKEY 125 | ); 126 | 127 | // SOL as input 128 | const inputCustody = await JUPITER_PERPETUALS_PROGRAM.account.custody.fetch( 129 | CUSTODY_PUBKEY.SOL 130 | ); 131 | const inputCustodyPrice = oraclePrices[inputCustody.mint.toString()]; 132 | const inputTokenAmount = new BN(1_000_000_000); // 1 SOL 133 | const inputTokenAmountUsd = getAssetAmountUsd( 134 | inputCustodyPrice, 135 | inputTokenAmount, 136 | inputCustody.decimals 137 | ); 138 | 139 | const mintFeeBps = getAddLiquidityFeeBps({ 140 | pool, 141 | custody: inputCustody, 142 | usdDelta: inputTokenAmountUsd, 143 | tokenPrice: inputCustodyPrice, 144 | }); 145 | 146 | const depositTokenAmountAfterFee = collectSwapFees({ 147 | tokenAmount: inputTokenAmount, 148 | feeBps: mintFeeBps, 149 | }); 150 | 151 | const mintAmountUsd = getAssetAmountUsd( 152 | inputCustodyPrice, 153 | depositTokenAmountAfterFee, 154 | inputCustody.decimals 155 | ); 156 | 157 | const jlpMint = await getMint(RPC_CONNECTION, JLP_MINT_PUBKEY, "confirmed"); 158 | const mintTokenAmount = mintAmountUsd 159 | .mul(new BN(jlpMint.supply.toString())) 160 | .div(pool.aumUsd); 161 | 162 | console.log("SOL Deposit amount (lamports):", inputTokenAmount.toString()); 163 | console.log("Mint Fee (bps):", mintFeeBps.toString()); 164 | console.log("JLP mint amount USD (after fees):", mintAmountUsd.toString()); 165 | console.log( 166 | "JLP mint token amount (after fees):", 167 | mintTokenAmount.toString() 168 | ); 169 | }; 170 | 171 | // Example burn JLP to redeem JLP with fee calculations 172 | export const calculateBurnJlp = async () => { 173 | const oraclePrices = await subscribeOraclePrices(); 174 | const pool = await JUPITER_PERPETUALS_PROGRAM.account.pool.fetch( 175 | JLP_POOL_ACCOUNT_PUBKEY 176 | ); 177 | 178 | // Redeem SOL 179 | const outputCustody = await JUPITER_PERPETUALS_PROGRAM.account.custody.fetch( 180 | CUSTODY_PUBKEY.SOL 181 | ); 182 | const outputCustodyPrice = oraclePrices[outputCustody.mint.toString()]; 183 | const jlpMint = await getMint(RPC_CONNECTION, JLP_MINT_PUBKEY, "confirmed"); 184 | const inputBurnTokenAmount = new BN(1_000_000); // Burn 1 JLP 185 | const burnAmountUsd = pool.aumUsd 186 | .mul(inputBurnTokenAmount) 187 | .div(new BN(jlpMint.supply.toString())); 188 | const burnTokenAmount = getTokenAmount( 189 | outputCustodyPrice, 190 | burnAmountUsd, 191 | outputCustody.decimals 192 | ); 193 | 194 | const burnFeeBps = getRemoveLiquidityFeeBps({ 195 | pool, 196 | custody: outputCustody, 197 | usdDelta: burnAmountUsd, 198 | tokenPrice: outputCustodyPrice, 199 | }); 200 | 201 | const burnTokenAmountAfterFee = collectSwapFees({ 202 | tokenAmount: burnTokenAmount, 203 | feeBps: burnFeeBps, 204 | }); 205 | 206 | console.log("JLP Burn amount:", inputBurnTokenAmount.toString()); 207 | console.log("Burn Fee (bps):", burnFeeBps.toString()); 208 | console.log( 209 | "JLP Burn amount (after fees):", 210 | burnTokenAmountAfterFee.toString() 211 | ); 212 | }; 213 | -------------------------------------------------------------------------------- /src/examples/calculate-swap-amount-and-fee.ts: -------------------------------------------------------------------------------- 1 | import { Custody, OraclePrice, Pool } from "../types"; 2 | import { BN } from "@coral-xyz/anchor"; 3 | import { 4 | checkedDecimalMul, 5 | getAssetAmountUsd, 6 | theoreticallyOwned, 7 | totalLocked, 8 | } from "./calculate-pool-aum"; 9 | import { 10 | BPS_POWER, 11 | CUSTODY_PUBKEY, 12 | JLP_POOL_ACCOUNT_PUBKEY, 13 | JUPITER_PERPETUALS_PROGRAM, 14 | } from "../constants"; 15 | import { subscribeOraclePrices } from "./poll-and-stream-oracle-price-updates"; 16 | 17 | const ORACLE_EXPONENT_SCALE = -9; 18 | const ORACLE_PRICE_SCALE = new BN(1_000_000_000); 19 | 20 | export function getSwapFeeBps({ 21 | custodyIn, 22 | custodyOut, 23 | tokenPriceIn, 24 | tokenPriceOut, 25 | pool, 26 | swapUsdAmount, 27 | }: { 28 | custodyIn: Custody; 29 | custodyOut: Custody; 30 | tokenPriceIn: OraclePrice; 31 | tokenPriceOut: OraclePrice; 32 | pool: Pool; 33 | swapUsdAmount: BN; 34 | }) { 35 | let baseFeeBps: BN; 36 | let taxFeeBps: BN; 37 | let multiplier: BN; 38 | 39 | const isStableSwap = custodyIn.isStable && custodyOut.isStable; 40 | 41 | if (isStableSwap) { 42 | baseFeeBps = pool.fees.stableSwapBps; 43 | taxFeeBps = pool.fees.stableSwapTaxBps; 44 | multiplier = pool.fees.stableSwapMultiplier; 45 | } else { 46 | baseFeeBps = pool.fees.swapBps; 47 | taxFeeBps = pool.fees.taxBps; 48 | multiplier = pool.fees.swapMultiplier; 49 | } 50 | 51 | const inputFeeBps = getFeeBps({ 52 | custody: custodyIn, 53 | sizeUsdDelta: swapUsdAmount, 54 | baseFeeBps, 55 | taxFeeBps, 56 | multiplier, 57 | increment: true, 58 | pool, 59 | tokenPrice: tokenPriceIn, 60 | }); 61 | 62 | const outputFeeBps = getFeeBps({ 63 | custody: custodyOut, 64 | sizeUsdDelta: swapUsdAmount, 65 | baseFeeBps, 66 | taxFeeBps, 67 | multiplier, 68 | increment: false, 69 | pool, 70 | tokenPrice: tokenPriceOut, 71 | }); 72 | 73 | return BN.max(inputFeeBps, outputFeeBps); 74 | } 75 | 76 | export function getFeeBps({ 77 | custody, 78 | sizeUsdDelta, 79 | baseFeeBps, 80 | taxFeeBps, 81 | multiplier, 82 | increment, 83 | pool, 84 | tokenPrice, 85 | }: { 86 | custody: Custody; 87 | sizeUsdDelta: BN; 88 | baseFeeBps: BN; 89 | taxFeeBps: BN; 90 | multiplier: BN; 91 | increment: boolean; 92 | pool: Pool; 93 | tokenPrice: OraclePrice; 94 | }) { 95 | let initialUsd: BN; 96 | 97 | if (custody.isStable) { 98 | const currentAmount = theoreticallyOwned(custody); 99 | initialUsd = getAssetAmountUsd(tokenPrice, currentAmount, custody.decimals); 100 | } else { 101 | const currentAmount = theoreticallyOwned(custody).sub(totalLocked(custody)); 102 | initialUsd = getAssetAmountUsd( 103 | tokenPrice, 104 | currentAmount, 105 | custody.decimals 106 | ).add(custody.assets.guaranteedUsd); 107 | } 108 | 109 | const targetUsd = pool.aumUsd.mul(custody.targetRatioBps).div(BPS_POWER); 110 | 111 | if (targetUsd.eqn(0)) { 112 | return new BN(0); 113 | } 114 | 115 | const initialDiffUsd = initialUsd.sub(targetUsd).abs(); 116 | const vTradeSize = new BN(multiplier).mul(sizeUsdDelta); 117 | 118 | let nextUsd = new BN(0); 119 | if (increment) { 120 | nextUsd = initialUsd.add(vTradeSize); 121 | } else { 122 | nextUsd = BN.max(new BN(0), initialUsd.sub(vTradeSize)); 123 | } 124 | 125 | const nextDiffUsd = nextUsd.sub(targetUsd).abs(); 126 | 127 | // If the diff between target amount and current amount is less than the initial diff, that means 128 | // the swap is causing the current amount to go towards the target amount, so we discount it 129 | if (nextDiffUsd.lt(initialDiffUsd)) { 130 | const rebateBps = taxFeeBps.mul(new BN(initialDiffUsd)).div(targetUsd); 131 | return BN.max(baseFeeBps.sub(rebateBps), new BN(0)); 132 | } else { 133 | const avgDiffUsd = initialDiffUsd.add(nextDiffUsd).divn(2); 134 | const taxBps = taxFeeBps.mul(BN.min(avgDiffUsd, targetUsd)).div(targetUsd); 135 | return baseFeeBps.add(taxBps); 136 | } 137 | } 138 | 139 | export function getSwapAmount({ 140 | tokenInPrice, 141 | tokenOutPrice, 142 | custodyIn, 143 | custodyOut, 144 | amountIn, 145 | }: { 146 | tokenInPrice: OraclePrice; 147 | tokenOutPrice: OraclePrice; 148 | custodyIn: Custody; 149 | custodyOut: Custody; 150 | amountIn: BN; 151 | }) { 152 | const swapPrice = getSwapPrice({ tokenInPrice, tokenOutPrice }); 153 | 154 | return checkedDecimalMul( 155 | amountIn, 156 | -custodyIn.decimals, 157 | swapPrice.price, 158 | swapPrice.exponent, 159 | -custodyOut.decimals 160 | ); 161 | } 162 | 163 | export function getSwapPrice({ 164 | tokenInPrice, 165 | tokenOutPrice, 166 | }: { 167 | tokenInPrice: OraclePrice; 168 | tokenOutPrice: OraclePrice; 169 | }) { 170 | return { 171 | price: tokenInPrice.price.mul(ORACLE_PRICE_SCALE).div(tokenOutPrice.price), 172 | exponent: 173 | tokenInPrice.exponent + ORACLE_EXPONENT_SCALE - tokenOutPrice.exponent, 174 | }; 175 | } 176 | 177 | export function collectSwapFees({ 178 | tokenAmount, 179 | feeBps, 180 | }: { 181 | tokenAmount: BN; 182 | feeBps: BN; 183 | }) { 184 | const feeTokenAmount = tokenAmount.mul(feeBps).div(BPS_POWER); 185 | return BN.max(new BN(0), tokenAmount.sub(feeTokenAmount)); 186 | } 187 | 188 | // Example usage of swapping from 1 SOL to USDC 189 | async function main() { 190 | const oraclePrices = await subscribeOraclePrices(); 191 | const pool = await JUPITER_PERPETUALS_PROGRAM.account.pool.fetch( 192 | JLP_POOL_ACCOUNT_PUBKEY 193 | ); 194 | 195 | // SOL as input 196 | const inputCustody = await JUPITER_PERPETUALS_PROGRAM.account.custody.fetch( 197 | CUSTODY_PUBKEY.SOL 198 | ); 199 | // USDC as output 200 | const outputCustody = await JUPITER_PERPETUALS_PROGRAM.account.custody.fetch( 201 | CUSTODY_PUBKEY.USDC 202 | ); 203 | const inputCustodyPrice = oraclePrices[inputCustody.mint.toString()]; 204 | const outputCustodyPrice = oraclePrices[outputCustody.mint.toString()]; 205 | const inputTokenAmount = new BN(1_000_000_000); // 1 SOL 206 | 207 | const amountOut = getSwapAmount({ 208 | tokenInPrice: inputCustodyPrice, 209 | tokenOutPrice: outputCustodyPrice, 210 | custodyIn: inputCustody, 211 | custodyOut: outputCustody, 212 | amountIn: inputTokenAmount, 213 | }); 214 | 215 | const swapUsdAmount = getAssetAmountUsd( 216 | inputCustodyPrice, 217 | inputTokenAmount, 218 | inputCustody.decimals 219 | ); 220 | 221 | const swapFeeBps = getSwapFeeBps({ 222 | custodyIn: inputCustody, 223 | custodyOut: outputCustody, 224 | tokenPriceIn: inputCustodyPrice, 225 | tokenPriceOut: outputCustodyPrice, 226 | pool, 227 | swapUsdAmount, 228 | }); 229 | 230 | const amountOutAfterFees = collectSwapFees({ 231 | tokenAmount: amountOut, 232 | feeBps: swapFeeBps, 233 | }); 234 | 235 | console.log("SOL Swap amount in (lamports):", inputTokenAmount.toString()); 236 | console.log("Swap Fee (bps):", swapFeeBps.toString()); 237 | console.log("USDC Amount Out (before fees):", amountOut.toString()); 238 | console.log("USDC Amount Out (after fees):", amountOutAfterFees.toString()); 239 | } 240 | -------------------------------------------------------------------------------- /src/examples/create-market-trade-request.ts: -------------------------------------------------------------------------------- 1 | import { BN, Program } from "@coral-xyz/anchor"; 2 | import { 3 | Blockhash, 4 | ComputeBudgetProgram, 5 | PublicKey, 6 | SystemProgram, 7 | TransactionInstruction, 8 | TransactionMessage, 9 | VersionedTransaction, 10 | } from "@solana/web3.js"; 11 | import { CustodyAccount, Position } from "../types"; 12 | import { Perpetuals } from "../idl/jupiter-perpetuals-idl"; 13 | import { generatePositionRequestPda } from "./generate-position-and-position-request-pda"; 14 | import { 15 | createAssociatedTokenAccountIdempotentInstruction, 16 | createCloseAccountInstruction, 17 | createSyncNativeInstruction, 18 | getAssociatedTokenAddressSync, 19 | NATIVE_MINT, 20 | } from "@solana/spl-token"; 21 | import { 22 | JLP_POOL_ACCOUNT_PUBKEY, 23 | JUPITER_PERPETUALS_PROGRAM, 24 | JUPITER_PERPETUALS_PROGRAM_ID, 25 | RPC_CONNECTION, 26 | } from "../constants"; 27 | 28 | export async function constructMarketOpenPositionTrade({ 29 | custody, 30 | collateralCustody, 31 | collateralTokenDelta, 32 | inputMint, 33 | jupiterMinimumOut, 34 | owner, 35 | priceSlippage, 36 | program, 37 | recentBlockhash, 38 | side, 39 | sizeUsdDelta, 40 | positionPubkey, 41 | }: { 42 | custody: CustodyAccount; 43 | collateralCustody: CustodyAccount; 44 | collateralTokenDelta: BN; 45 | inputMint: PublicKey; 46 | jupiterMinimumOut: BN | null; 47 | owner: PublicKey; 48 | priceSlippage: BN; 49 | program: Program; 50 | recentBlockhash: Blockhash; 51 | side: Position["side"]; 52 | sizeUsdDelta: BN; 53 | positionPubkey: PublicKey; 54 | }) { 55 | // The `positionRequest` PDA holds the requests for all the perpetuals actions. Once the `positionRequest` 56 | // is submitted on chain, the keeper(s) will pick them up and execute the requests (hence the request 57 | // fulfillment model) 58 | const { positionRequest, counter } = generatePositionRequestPda({ 59 | positionPubkey, 60 | requestChange: "increase", 61 | }); 62 | 63 | // The `positionRequestAta` accounts hold the user's input mint tokens, which will be swapped (if required) 64 | // and used to fund the collateral custody's token account when the instruction is executed 65 | const positionRequestAta = getAssociatedTokenAddressSync( 66 | inputMint, 67 | positionRequest, 68 | true, 69 | ); 70 | 71 | // `fundingAccount` is the token account where we'll withdraw the `inputMint` from. Essentially, the flow of tokens will be: 72 | // `fundingAccount` -> `positionRequestAta` -> `collateralCustodyTokenAccount` 73 | const fundingAccount = getAssociatedTokenAddressSync(inputMint, owner); 74 | 75 | const preInstructions: TransactionInstruction[] = []; 76 | const postInstructions: TransactionInstruction[] = []; 77 | 78 | // Wrap to wSOL if needed so we can treat SOL as an SPL token 79 | // https://spl.solana.com/token#example-wrapping-sol-in-a-token 80 | if (inputMint.equals(NATIVE_MINT)) { 81 | const createWrappedSolAtaIx = 82 | createAssociatedTokenAccountIdempotentInstruction( 83 | owner, 84 | fundingAccount, 85 | owner, 86 | NATIVE_MINT, 87 | ); 88 | 89 | preInstructions.push(createWrappedSolAtaIx); 90 | 91 | // Transfer SOL to the wSOL associated token account and use SyncNative below to update wrapped SOL balance 92 | preInstructions.push( 93 | SystemProgram.transfer({ 94 | fromPubkey: owner, 95 | toPubkey: fundingAccount, 96 | lamports: BigInt(collateralTokenDelta.toString()), 97 | }), 98 | ); 99 | 100 | preInstructions.push(createSyncNativeInstruction(fundingAccount)); 101 | 102 | postInstructions.push( 103 | createCloseAccountInstruction(fundingAccount, owner, owner), 104 | ); 105 | } 106 | 107 | const increaseIx = await program.methods 108 | .createIncreasePositionMarketRequest({ 109 | counter, 110 | collateralTokenDelta, 111 | // jupiterMinimumOut is required for trades that require swaps 112 | // Call the Jupiter Quote API (https://station.jup.ag/api-v6/get-quote) to convert the `inputMintAmount` 113 | // to get the `jupiterMinimumOut` which is the required minimum token out amount when the swap is performed 114 | jupiterMinimumOut: 115 | jupiterMinimumOut && jupiterMinimumOut.gten(0) 116 | ? jupiterMinimumOut 117 | : null, 118 | priceSlippage, 119 | side, 120 | sizeUsdDelta, 121 | }) 122 | .accounts({ 123 | custody: custody.publicKey, 124 | collateralCustody: collateralCustody.publicKey, 125 | fundingAccount, 126 | inputMint, 127 | owner, 128 | perpetuals: PublicKey.findProgramAddressSync( 129 | [Buffer.from("perpetuals")], 130 | JUPITER_PERPETUALS_PROGRAM_ID, 131 | )[0], 132 | pool: JLP_POOL_ACCOUNT_PUBKEY, 133 | position: positionPubkey, 134 | positionRequest, 135 | positionRequestAta, 136 | referral: null, 137 | }) 138 | .instruction(); 139 | 140 | const instructions = [ 141 | ComputeBudgetProgram.setComputeUnitPrice({ 142 | microLamports: 100000, // Get the estimated compute unit price here from RPC or a provider like Triton 143 | }), 144 | ...preInstructions, 145 | increaseIx, 146 | ...postInstructions, 147 | ]; 148 | 149 | const simulateTx = new VersionedTransaction( 150 | new TransactionMessage({ 151 | instructions, 152 | // `payerKey` for simulation can be any account as long as it has enough SOL to cover the gas fees 153 | payerKey: PublicKey.default, 154 | // We don't need to pass in a real blockhash here since the `replaceRecentBlockhash` 155 | // option in `simulateTransaction` gets the latest blockhash from the RPC's internal cache 156 | // Reference: https://github.com/anza-xyz/agave/blob/master/rpc/src/rpc.rs#L3890-L3907 157 | recentBlockhash: PublicKey.default.toString(), 158 | }).compileToV0Message([]), 159 | ); 160 | 161 | const simulation = await RPC_CONNECTION.simulateTransaction(simulateTx, { 162 | replaceRecentBlockhash: true, 163 | sigVerify: false, 164 | }); 165 | 166 | instructions.unshift( 167 | ComputeBudgetProgram.setComputeUnitLimit({ 168 | units: simulation.value.unitsConsumed || 1_400_000, 169 | }), 170 | ); 171 | 172 | const txMessage = new TransactionMessage({ 173 | payerKey: owner, 174 | recentBlockhash, 175 | instructions, 176 | }).compileToV0Message(); 177 | 178 | // This transaction can be then signed and submitted onchain for the keeper to execute the trade 179 | // https://station.jup.ag/guides/perpetual-exchange/request-fulfillment-model 180 | const tx = new VersionedTransaction(txMessage); 181 | } 182 | 183 | export async function constructMarketClosePositionTrade({ 184 | desiredMint, 185 | program, 186 | recentBlockhash, 187 | positionPubkey, 188 | }: { 189 | desiredMint: PublicKey; 190 | program: Program; 191 | recentBlockhash: Blockhash; 192 | positionPubkey: PublicKey; 193 | }) { 194 | const position = 195 | await JUPITER_PERPETUALS_PROGRAM.account.position.fetch(positionPubkey); 196 | 197 | // The `positionRequest` PDA holds the requests for all the perpetuals actions. Once the `positionRequest` 198 | // is submitted on chain, the keeper(s) will pick them up and execute the requests (hence the request 199 | // fulfillment model) 200 | const { positionRequest, counter } = generatePositionRequestPda({ 201 | positionPubkey, 202 | requestChange: "decrease", 203 | }); 204 | 205 | const preInstructions: TransactionInstruction[] = []; 206 | const postInstructions: TransactionInstruction[] = []; 207 | 208 | // `desiredMint` is the mint address of the token to receive when closing the position. It can either be the 209 | // mint address of the custody token itself (BTC/ETH/SOL) or the USDC mint 210 | // 211 | // `receivingAccount` will then be the owner's ATA for the `desiredMint` 212 | const receivingAccount = getAssociatedTokenAddressSync( 213 | desiredMint, 214 | position.owner, 215 | true, 216 | ); 217 | 218 | if (desiredMint.equals(NATIVE_MINT)) { 219 | postInstructions.push( 220 | createCloseAccountInstruction( 221 | receivingAccount, 222 | position.owner, 223 | position.owner, 224 | ), 225 | ); 226 | } 227 | 228 | const decreaseIx = await program.methods 229 | .createDecreasePositionMarketRequest({ 230 | collateralUsdDelta: new BN(0), 231 | sizeUsdDelta: new BN(0), 232 | // `priceSlippage` here is scaled to 6 decimal places as per the USDC mint, so for example if the price of SOL is $100, the value would `new BN(100_000_000)` 233 | // For longs and for a lower chance of exceeding the price slippage, use a value is 5-10% lower than the current token price 234 | // For shorts and for a lower chance of exceeding the price slippage, use a value that is 5-10% higher than the current token price 235 | priceSlippage: new BN(100_000_000_000), 236 | // `jupiterMinimumOut` is not needed because we won't perform any token swaps when withdrawing collateral or reducing position size 237 | jupiterMinimumOut: null, 238 | counter, 239 | entirePosition: true, 240 | }) 241 | .accounts({ 242 | owner: position.owner, 243 | // The `receivingAccount` is the token account where we'll return the withdrawed collateral / tokens 244 | receivingAccount, 245 | perpetuals: PublicKey.findProgramAddressSync( 246 | [Buffer.from("perpetuals")], 247 | JUPITER_PERPETUALS_PROGRAM_ID, 248 | )[0], 249 | pool: JLP_POOL_ACCOUNT_PUBKEY, 250 | position: positionPubkey, 251 | positionRequest, 252 | // The `positionRequestAta` accounts hold the user's input mint tokens, which will be swapped (if required) 253 | // and used to fund the collateral custody's token account when the instruction is executed 254 | positionRequestAta: getAssociatedTokenAddressSync( 255 | desiredMint, 256 | positionRequest, 257 | true, 258 | ), 259 | custody: position.custody, 260 | collateralCustody: position.collateralCustody, 261 | desiredMint, 262 | referral: null, 263 | }) 264 | .instruction(); 265 | 266 | const instructions = [ 267 | ComputeBudgetProgram.setComputeUnitPrice({ 268 | microLamports: 100000, // Get the estimated compute unit price here from RPC or a provider like Triton 269 | }), 270 | ...preInstructions, 271 | decreaseIx, 272 | ...postInstructions, 273 | ]; 274 | 275 | const simulateTx = new VersionedTransaction( 276 | new TransactionMessage({ 277 | instructions, 278 | // `payerKey` for simulation can be any account as long as it has enough SOL to cover the gas fees 279 | payerKey: PublicKey.default, 280 | // We don't need to pass in a real blockhash here since the `replaceRecentBlockhash` 281 | // option in `simulateTransaction` gets the latest blockhash from the RPC's internal cache 282 | // Reference: https://github.com/anza-xyz/agave/blob/master/rpc/src/rpc.rs#L3890-L3907 283 | recentBlockhash: PublicKey.default.toString(), 284 | }).compileToV0Message([]), 285 | ); 286 | 287 | const simulation = await RPC_CONNECTION.simulateTransaction(simulateTx, { 288 | replaceRecentBlockhash: true, 289 | sigVerify: false, 290 | }); 291 | 292 | instructions.unshift( 293 | ComputeBudgetProgram.setComputeUnitLimit({ 294 | units: simulation.value.unitsConsumed || 1_400_000, 295 | }), 296 | ); 297 | 298 | const txMessage = new TransactionMessage({ 299 | payerKey: position.owner, 300 | recentBlockhash, 301 | instructions, 302 | }).compileToV0Message(); 303 | 304 | // This transaction can be then signed and submitted onchain for the keeper to execute the trade 305 | // https://station.jup.ag/guides/perpetual-exchange/request-fulfillment-model 306 | const tx = new VersionedTransaction(txMessage); 307 | } 308 | -------------------------------------------------------------------------------- /src/idl/doves-idl.ts: -------------------------------------------------------------------------------- 1 | export type Doves = { 2 | "version": "0.1.0", 3 | "name": "doves", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [ 8 | { 9 | "name": "admin", 10 | "isMut": true, 11 | "isSigner": true 12 | }, 13 | { 14 | "name": "feed", 15 | "isMut": true, 16 | "isSigner": false 17 | }, 18 | { 19 | "name": "systemProgram", 20 | "isMut": false, 21 | "isSigner": false 22 | } 23 | ], 24 | "args": [ 25 | { 26 | "name": "pair", 27 | "type": { 28 | "array": [ 29 | "u8", 30 | 32 31 | ] 32 | } 33 | }, 34 | { 35 | "name": "feedSigner", 36 | "type": { 37 | "array": [ 38 | "u8", 39 | 33 40 | ] 41 | } 42 | } 43 | ] 44 | }, 45 | { 46 | "name": "updateWithSigner", 47 | "accounts": [ 48 | { 49 | "name": "signer", 50 | "isMut": false, 51 | "isSigner": true 52 | }, 53 | { 54 | "name": "feed", 55 | "isMut": true, 56 | "isSigner": false 57 | }, 58 | { 59 | "name": "systemProgram", 60 | "isMut": false, 61 | "isSigner": false 62 | } 63 | ], 64 | "args": [ 65 | { 66 | "name": "update", 67 | "type": { 68 | "defined": "UpdateMessage" 69 | } 70 | }, 71 | { 72 | "name": "raise", 73 | "type": "bool" 74 | } 75 | ] 76 | }, 77 | { 78 | "name": "initializeAgPrice", 79 | "accounts": [ 80 | { 81 | "name": "signer", 82 | "isMut": true, 83 | "isSigner": true 84 | }, 85 | { 86 | "name": "agPriceFeed", 87 | "isMut": true, 88 | "isSigner": false 89 | }, 90 | { 91 | "name": "mint", 92 | "isMut": false, 93 | "isSigner": false 94 | }, 95 | { 96 | "name": "edgeFeed", 97 | "isMut": false, 98 | "isSigner": false 99 | }, 100 | { 101 | "name": "clFeed", 102 | "isMut": false, 103 | "isSigner": false 104 | }, 105 | { 106 | "name": "pythFeed", 107 | "isMut": false, 108 | "isSigner": false 109 | }, 110 | { 111 | "name": "systemProgram", 112 | "isMut": false, 113 | "isSigner": false 114 | } 115 | ], 116 | "args": [ 117 | { 118 | "name": "config", 119 | "type": { 120 | "defined": "Config" 121 | } 122 | } 123 | ] 124 | }, 125 | { 126 | "name": "updateAgPriceFeed", 127 | "accounts": [ 128 | { 129 | "name": "signer", 130 | "isMut": false, 131 | "isSigner": true 132 | }, 133 | { 134 | "name": "agPriceFeed", 135 | "isMut": true, 136 | "isSigner": false 137 | }, 138 | { 139 | "name": "edgeFeed", 140 | "isMut": false, 141 | "isSigner": false 142 | }, 143 | { 144 | "name": "clFeed", 145 | "isMut": false, 146 | "isSigner": false 147 | }, 148 | { 149 | "name": "pythFeed", 150 | "isMut": false, 151 | "isSigner": false 152 | } 153 | ], 154 | "args": [] 155 | } 156 | ], 157 | "accounts": [ 158 | { 159 | "name": "agPriceFeed", 160 | "type": { 161 | "kind": "struct", 162 | "fields": [ 163 | { 164 | "name": "mint", 165 | "type": "publicKey" 166 | }, 167 | { 168 | "name": "edgeFeed", 169 | "type": "publicKey" 170 | }, 171 | { 172 | "name": "clFeed", 173 | "type": "publicKey" 174 | }, 175 | { 176 | "name": "pythFeed", 177 | "type": "publicKey" 178 | }, 179 | { 180 | "name": "pythFeedId", 181 | "type": { 182 | "array": [ 183 | "u8", 184 | 32 185 | ] 186 | } 187 | }, 188 | { 189 | "name": "price", 190 | "type": "u64" 191 | }, 192 | { 193 | "name": "expo", 194 | "type": "i8" 195 | }, 196 | { 197 | "name": "timestamp", 198 | "type": "i64" 199 | }, 200 | { 201 | "name": "config", 202 | "type": { 203 | "defined": "Config" 204 | } 205 | }, 206 | { 207 | "name": "bump", 208 | "type": "u8" 209 | } 210 | ] 211 | } 212 | }, 213 | { 214 | "name": "priceFeed", 215 | "type": { 216 | "kind": "struct", 217 | "fields": [ 218 | { 219 | "name": "pair", 220 | "type": { 221 | "array": [ 222 | "u8", 223 | 32 224 | ] 225 | } 226 | }, 227 | { 228 | "name": "signer", 229 | "type": { 230 | "array": [ 231 | "u8", 232 | 33 233 | ] 234 | } 235 | }, 236 | { 237 | "name": "price", 238 | "type": "u64" 239 | }, 240 | { 241 | "name": "expo", 242 | "type": "i8" 243 | }, 244 | { 245 | "name": "timestamp", 246 | "type": "i64" 247 | }, 248 | { 249 | "name": "bump", 250 | "type": "u8" 251 | } 252 | ] 253 | } 254 | } 255 | ], 256 | "types": [ 257 | { 258 | "name": "Config", 259 | "type": { 260 | "kind": "struct", 261 | "fields": [ 262 | { 263 | "name": "maxAgPriceAgeSec", 264 | "type": "u32" 265 | }, 266 | { 267 | "name": "maxPriceFeedAgeSec", 268 | "type": "u32" 269 | }, 270 | { 271 | "name": "maxPriceDiffBps", 272 | "type": "u64" 273 | } 274 | ] 275 | } 276 | }, 277 | { 278 | "name": "UpdateMessage", 279 | "type": { 280 | "kind": "struct", 281 | "fields": [ 282 | { 283 | "name": "recoveryId", 284 | "type": "u8" 285 | }, 286 | { 287 | "name": "signature", 288 | "type": { 289 | "array": [ 290 | "u8", 291 | 64 292 | ] 293 | } 294 | }, 295 | { 296 | "name": "price", 297 | "type": "u64" 298 | }, 299 | { 300 | "name": "expo", 301 | "type": "i8" 302 | }, 303 | { 304 | "name": "timestamp", 305 | "type": "i64" 306 | } 307 | ] 308 | } 309 | } 310 | ], 311 | "errors": [ 312 | { 313 | "code": 6000, 314 | "name": "ThresholdUnderflow", 315 | "msg": "Threshold must be >0" 316 | }, 317 | { 318 | "code": 6001, 319 | "name": "ThresholdOverflow", 320 | "msg": "Threshold must be <= whitelist length" 321 | }, 322 | { 323 | "code": 6002, 324 | "name": "ThresholdNotMet", 325 | "msg": "Number of signers must be >= threshold" 326 | }, 327 | { 328 | "code": 6003, 329 | "name": "PairStringUnderflow", 330 | "msg": "Pair string must not be blank" 331 | }, 332 | { 333 | "code": 6004, 334 | "name": "PairStringOverflow", 335 | "msg": "Pair string must be <= 32 bytes" 336 | }, 337 | { 338 | "code": 6005, 339 | "name": "SignerIndexOverflow", 340 | "msg": "Signer out of range" 341 | }, 342 | { 343 | "code": 6006, 344 | "name": "InvalidSigner", 345 | "msg": "Signature verification failed" 346 | }, 347 | { 348 | "code": 6007, 349 | "name": "DuplicateSigner", 350 | "msg": "All signers must be unique" 351 | }, 352 | { 353 | "code": 6008, 354 | "name": "InvalidTimestamp", 355 | "msg": "New timestamp must be greater than previous one" 356 | }, 357 | { 358 | "code": 6009, 359 | "name": "Overflow", 360 | "msg": "Integer overflow" 361 | }, 362 | { 363 | "code": 6010, 364 | "name": "Underflow", 365 | "msg": "Integer underflow" 366 | }, 367 | { 368 | "code": 6011, 369 | "name": "InvalidPriceFeed", 370 | "msg": "Invalid price feed" 371 | }, 372 | { 373 | "code": 6012, 374 | "name": "InvalidExpo", 375 | "msg": "Invalid expo" 376 | } 377 | ] 378 | }; 379 | 380 | export const IDL: Doves = { 381 | "version": "0.1.0", 382 | "name": "doves", 383 | "instructions": [ 384 | { 385 | "name": "initialize", 386 | "accounts": [ 387 | { 388 | "name": "admin", 389 | "isMut": true, 390 | "isSigner": true 391 | }, 392 | { 393 | "name": "feed", 394 | "isMut": true, 395 | "isSigner": false 396 | }, 397 | { 398 | "name": "systemProgram", 399 | "isMut": false, 400 | "isSigner": false 401 | } 402 | ], 403 | "args": [ 404 | { 405 | "name": "pair", 406 | "type": { 407 | "array": [ 408 | "u8", 409 | 32 410 | ] 411 | } 412 | }, 413 | { 414 | "name": "feedSigner", 415 | "type": { 416 | "array": [ 417 | "u8", 418 | 33 419 | ] 420 | } 421 | } 422 | ] 423 | }, 424 | { 425 | "name": "updateWithSigner", 426 | "accounts": [ 427 | { 428 | "name": "signer", 429 | "isMut": false, 430 | "isSigner": true 431 | }, 432 | { 433 | "name": "feed", 434 | "isMut": true, 435 | "isSigner": false 436 | }, 437 | { 438 | "name": "systemProgram", 439 | "isMut": false, 440 | "isSigner": false 441 | } 442 | ], 443 | "args": [ 444 | { 445 | "name": "update", 446 | "type": { 447 | "defined": "UpdateMessage" 448 | } 449 | }, 450 | { 451 | "name": "raise", 452 | "type": "bool" 453 | } 454 | ] 455 | }, 456 | { 457 | "name": "initializeAgPrice", 458 | "accounts": [ 459 | { 460 | "name": "signer", 461 | "isMut": true, 462 | "isSigner": true 463 | }, 464 | { 465 | "name": "agPriceFeed", 466 | "isMut": true, 467 | "isSigner": false 468 | }, 469 | { 470 | "name": "mint", 471 | "isMut": false, 472 | "isSigner": false 473 | }, 474 | { 475 | "name": "edgeFeed", 476 | "isMut": false, 477 | "isSigner": false 478 | }, 479 | { 480 | "name": "clFeed", 481 | "isMut": false, 482 | "isSigner": false 483 | }, 484 | { 485 | "name": "pythFeed", 486 | "isMut": false, 487 | "isSigner": false 488 | }, 489 | { 490 | "name": "systemProgram", 491 | "isMut": false, 492 | "isSigner": false 493 | } 494 | ], 495 | "args": [ 496 | { 497 | "name": "config", 498 | "type": { 499 | "defined": "Config" 500 | } 501 | } 502 | ] 503 | }, 504 | { 505 | "name": "updateAgPriceFeed", 506 | "accounts": [ 507 | { 508 | "name": "signer", 509 | "isMut": false, 510 | "isSigner": true 511 | }, 512 | { 513 | "name": "agPriceFeed", 514 | "isMut": true, 515 | "isSigner": false 516 | }, 517 | { 518 | "name": "edgeFeed", 519 | "isMut": false, 520 | "isSigner": false 521 | }, 522 | { 523 | "name": "clFeed", 524 | "isMut": false, 525 | "isSigner": false 526 | }, 527 | { 528 | "name": "pythFeed", 529 | "isMut": false, 530 | "isSigner": false 531 | } 532 | ], 533 | "args": [] 534 | } 535 | ], 536 | "accounts": [ 537 | { 538 | "name": "agPriceFeed", 539 | "type": { 540 | "kind": "struct", 541 | "fields": [ 542 | { 543 | "name": "mint", 544 | "type": "publicKey" 545 | }, 546 | { 547 | "name": "edgeFeed", 548 | "type": "publicKey" 549 | }, 550 | { 551 | "name": "clFeed", 552 | "type": "publicKey" 553 | }, 554 | { 555 | "name": "pythFeed", 556 | "type": "publicKey" 557 | }, 558 | { 559 | "name": "pythFeedId", 560 | "type": { 561 | "array": [ 562 | "u8", 563 | 32 564 | ] 565 | } 566 | }, 567 | { 568 | "name": "price", 569 | "type": "u64" 570 | }, 571 | { 572 | "name": "expo", 573 | "type": "i8" 574 | }, 575 | { 576 | "name": "timestamp", 577 | "type": "i64" 578 | }, 579 | { 580 | "name": "config", 581 | "type": { 582 | "defined": "Config" 583 | } 584 | }, 585 | { 586 | "name": "bump", 587 | "type": "u8" 588 | } 589 | ] 590 | } 591 | }, 592 | { 593 | "name": "priceFeed", 594 | "type": { 595 | "kind": "struct", 596 | "fields": [ 597 | { 598 | "name": "pair", 599 | "type": { 600 | "array": [ 601 | "u8", 602 | 32 603 | ] 604 | } 605 | }, 606 | { 607 | "name": "signer", 608 | "type": { 609 | "array": [ 610 | "u8", 611 | 33 612 | ] 613 | } 614 | }, 615 | { 616 | "name": "price", 617 | "type": "u64" 618 | }, 619 | { 620 | "name": "expo", 621 | "type": "i8" 622 | }, 623 | { 624 | "name": "timestamp", 625 | "type": "i64" 626 | }, 627 | { 628 | "name": "bump", 629 | "type": "u8" 630 | } 631 | ] 632 | } 633 | } 634 | ], 635 | "types": [ 636 | { 637 | "name": "Config", 638 | "type": { 639 | "kind": "struct", 640 | "fields": [ 641 | { 642 | "name": "maxAgPriceAgeSec", 643 | "type": "u32" 644 | }, 645 | { 646 | "name": "maxPriceFeedAgeSec", 647 | "type": "u32" 648 | }, 649 | { 650 | "name": "maxPriceDiffBps", 651 | "type": "u64" 652 | } 653 | ] 654 | } 655 | }, 656 | { 657 | "name": "UpdateMessage", 658 | "type": { 659 | "kind": "struct", 660 | "fields": [ 661 | { 662 | "name": "recoveryId", 663 | "type": "u8" 664 | }, 665 | { 666 | "name": "signature", 667 | "type": { 668 | "array": [ 669 | "u8", 670 | 64 671 | ] 672 | } 673 | }, 674 | { 675 | "name": "price", 676 | "type": "u64" 677 | }, 678 | { 679 | "name": "expo", 680 | "type": "i8" 681 | }, 682 | { 683 | "name": "timestamp", 684 | "type": "i64" 685 | } 686 | ] 687 | } 688 | } 689 | ], 690 | "errors": [ 691 | { 692 | "code": 6000, 693 | "name": "ThresholdUnderflow", 694 | "msg": "Threshold must be >0" 695 | }, 696 | { 697 | "code": 6001, 698 | "name": "ThresholdOverflow", 699 | "msg": "Threshold must be <= whitelist length" 700 | }, 701 | { 702 | "code": 6002, 703 | "name": "ThresholdNotMet", 704 | "msg": "Number of signers must be >= threshold" 705 | }, 706 | { 707 | "code": 6003, 708 | "name": "PairStringUnderflow", 709 | "msg": "Pair string must not be blank" 710 | }, 711 | { 712 | "code": 6004, 713 | "name": "PairStringOverflow", 714 | "msg": "Pair string must be <= 32 bytes" 715 | }, 716 | { 717 | "code": 6005, 718 | "name": "SignerIndexOverflow", 719 | "msg": "Signer out of range" 720 | }, 721 | { 722 | "code": 6006, 723 | "name": "InvalidSigner", 724 | "msg": "Signature verification failed" 725 | }, 726 | { 727 | "code": 6007, 728 | "name": "DuplicateSigner", 729 | "msg": "All signers must be unique" 730 | }, 731 | { 732 | "code": 6008, 733 | "name": "InvalidTimestamp", 734 | "msg": "New timestamp must be greater than previous one" 735 | }, 736 | { 737 | "code": 6009, 738 | "name": "Overflow", 739 | "msg": "Integer overflow" 740 | }, 741 | { 742 | "code": 6010, 743 | "name": "Underflow", 744 | "msg": "Integer underflow" 745 | }, 746 | { 747 | "code": 6011, 748 | "name": "InvalidPriceFeed", 749 | "msg": "Invalid price feed" 750 | }, 751 | { 752 | "code": 6012, 753 | "name": "InvalidExpo", 754 | "msg": "Invalid expo" 755 | } 756 | ] 757 | }; 758 | --------------------------------------------------------------------------------