├── .cargo └── audit.toml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .eslintrc.js ├── .github ├── actions │ └── setup-solana │ │ ├── action.yaml │ │ └── scripts │ │ └── solana-install-init.sh └── workflows │ ├── audit.yml │ ├── main.yml │ └── security.yml ├── .gitignore ├── .gitmodules ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── .verified-build.json ├── AUDIT.md ├── Anchor.toml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── bug-bounty └── README.md ├── cli ├── .gitignore ├── README.md ├── cli.ts ├── package.json └── tsconfig.json ├── codecov.yml ├── deploy-scripts ├── build-devnet.sh ├── deploy-devnet.sh └── verified-build.sh ├── deps └── configs │ ├── pyth_lazer_storage.json │ └── usdc.json ├── package.json ├── programs ├── drift │ ├── Cargo.toml │ ├── Xargo.toml │ └── src │ │ ├── controller │ │ ├── amm.rs │ │ ├── amm │ │ │ └── tests.rs │ │ ├── funding.rs │ │ ├── insurance.rs │ │ ├── insurance │ │ │ └── tests.rs │ │ ├── liquidation.rs │ │ ├── liquidation │ │ │ └── tests.rs │ │ ├── lp.rs │ │ ├── lp │ │ │ └── tests.rs │ │ ├── mod.rs │ │ ├── orders.rs │ │ ├── orders │ │ │ ├── amm_jit_tests.rs │ │ │ ├── amm_lp_jit_tests.rs │ │ │ ├── fuel_tests.rs │ │ │ └── tests.rs │ │ ├── pda.rs │ │ ├── pnl.rs │ │ ├── pnl │ │ │ ├── delisting.rs │ │ │ └── tests.rs │ │ ├── position.rs │ │ ├── position │ │ │ └── tests.rs │ │ ├── repeg.rs │ │ ├── repeg │ │ │ └── tests.rs │ │ ├── spot_balance.rs │ │ ├── spot_balance │ │ │ └── tests.rs │ │ ├── spot_position.rs │ │ ├── spot_position │ │ │ └── tests.rs │ │ └── token.rs │ │ ├── error.rs │ │ ├── ids.rs │ │ ├── instructions │ │ ├── admin.rs │ │ ├── constraints.rs │ │ ├── if_staker.rs │ │ ├── keeper.rs │ │ ├── mod.rs │ │ ├── optional_accounts.rs │ │ ├── pyth_lazer_oracle.rs │ │ ├── pyth_pull_oracle.rs │ │ └── user.rs │ │ ├── lib.rs │ │ ├── macros.rs │ │ ├── math │ │ ├── amm.rs │ │ ├── amm │ │ │ └── tests.rs │ │ ├── amm_jit.rs │ │ ├── amm_jit │ │ │ └── tests.rs │ │ ├── amm_spread.rs │ │ ├── amm_spread │ │ │ └── tests.rs │ │ ├── auction.rs │ │ ├── auction │ │ │ └── tests.rs │ │ ├── bankruptcy.rs │ │ ├── bankruptcy │ │ │ └── tests.rs │ │ ├── bn.rs │ │ ├── casting.rs │ │ ├── ceil_div.rs │ │ ├── constants.rs │ │ ├── cp_curve.rs │ │ ├── cp_curve │ │ │ └── tests.rs │ │ ├── fees.rs │ │ ├── fees │ │ │ └── tests.rs │ │ ├── floor_div.rs │ │ ├── fuel.rs │ │ ├── fuel │ │ │ └── tests.rs │ │ ├── fulfillment.rs │ │ ├── fulfillment │ │ │ └── tests.rs │ │ ├── funding.rs │ │ ├── funding │ │ │ └── tests.rs │ │ ├── helpers.rs │ │ ├── helpers │ │ │ └── tests.rs │ │ ├── insurance.rs │ │ ├── insurance │ │ │ └── tests.rs │ │ ├── liquidation.rs │ │ ├── liquidation │ │ │ └── tests.rs │ │ ├── lp.rs │ │ ├── lp │ │ │ └── tests.rs │ │ ├── margin.rs │ │ ├── margin │ │ │ └── tests.rs │ │ ├── matching.rs │ │ ├── matching │ │ │ └── tests.rs │ │ ├── mod.rs │ │ ├── oracle.rs │ │ ├── oracle │ │ │ └── tests.rs │ │ ├── orders.rs │ │ ├── orders │ │ │ └── tests.rs │ │ ├── pnl.rs │ │ ├── position.rs │ │ ├── quote_asset.rs │ │ ├── repeg.rs │ │ ├── repeg │ │ │ └── tests.rs │ │ ├── safe_math.rs │ │ ├── safe_unwrap.rs │ │ ├── serum.rs │ │ ├── serum │ │ │ └── tests.rs │ │ ├── spot_balance.rs │ │ ├── spot_balance │ │ │ └── tests.rs │ │ ├── spot_swap.rs │ │ ├── spot_swap │ │ │ └── tests.rs │ │ ├── spot_withdraw.rs │ │ └── stats.rs │ │ ├── signer.rs │ │ ├── state │ │ ├── events.rs │ │ ├── fill_mode.rs │ │ ├── fill_mode │ │ │ └── tests.rs │ │ ├── fulfillment.rs │ │ ├── fulfillment_params │ │ │ ├── drift.rs │ │ │ ├── mod.rs │ │ │ ├── openbook_v2.rs │ │ │ ├── phoenix.rs │ │ │ └── serum.rs │ │ ├── high_leverage_mode_config.rs │ │ ├── insurance_fund_stake.rs │ │ ├── insurance_fund_stake │ │ │ └── tests.rs │ │ ├── load_ref.rs │ │ ├── margin_calculation.rs │ │ ├── mod.rs │ │ ├── oracle.rs │ │ ├── oracle │ │ │ └── tests.rs │ │ ├── oracle_map.rs │ │ ├── order_params.rs │ │ ├── order_params │ │ │ └── tests.rs │ │ ├── paused_operations.rs │ │ ├── paused_operations │ │ │ └── tests.rs │ │ ├── perp_market.rs │ │ ├── perp_market │ │ │ └── tests.rs │ │ ├── perp_market_map.rs │ │ ├── protected_maker_mode_config.rs │ │ ├── pyth_lazer_oracle.rs │ │ ├── settle_pnl_mode.rs │ │ ├── settle_pnl_mode │ │ │ └── tests.rs │ │ ├── signed_msg_user.rs │ │ ├── signed_msg_user │ │ │ └── tests.rs │ │ ├── spot_fulfillment_params.rs │ │ ├── spot_market.rs │ │ ├── spot_market_map.rs │ │ ├── state.rs │ │ ├── state │ │ │ └── tests.rs │ │ ├── traits.rs │ │ ├── traits │ │ │ └── tests.rs │ │ ├── user.rs │ │ ├── user │ │ │ └── tests.rs │ │ └── user_map.rs │ │ ├── test_utils.rs │ │ └── validation │ │ ├── fee_structure.rs │ │ ├── fee_structure │ │ └── tests.rs │ │ ├── margin.rs │ │ ├── mod.rs │ │ ├── order.rs │ │ ├── order │ │ └── test.rs │ │ ├── perp_market.rs │ │ ├── position.rs │ │ ├── sig_verification.rs │ │ ├── spot_market.rs │ │ ├── user.rs │ │ └── whitelist.rs ├── openbook_v2 │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── account.rs │ │ ├── constants.rs │ │ ├── context.rs │ │ └── lib.rs ├── pyth │ ├── Cargo.toml │ ├── Xargo.toml │ └── src │ │ ├── lib.rs │ │ └── pc.rs ├── switchboard-on-demand │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── switchboard │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── token_faucet │ ├── Cargo.toml │ ├── Xargo.toml │ └── src │ └── lib.rs ├── sdk ├── .gitignore ├── README.md ├── VERSION ├── bun.lock ├── get_events.ts ├── package.json ├── scripts │ ├── postbuild.js │ └── updateVersion.js ├── src │ ├── accounts │ │ ├── basicUserAccountSubscriber.ts │ │ ├── bulkAccountLoader.ts │ │ ├── bulkUserStatsSubscription.ts │ │ ├── bulkUserSubscription.ts │ │ ├── fetch.ts │ │ ├── grpcAccountSubscriber.ts │ │ ├── grpcDriftClientAccountSubscriber.ts │ │ ├── grpcInsuranceFundStakeAccountSubscriber.ts │ │ ├── grpcProgramAccountSubscriber.ts │ │ ├── grpcUserAccountSubscriber.ts │ │ ├── grpcUserStatsAccountSubscriber.ts │ │ ├── oneShotUserAccountSubscriber.ts │ │ ├── pollingDriftClientAccountSubscriber.ts │ │ ├── pollingHighLeverageModeConfigAccountSubscriber.ts │ │ ├── pollingInsuranceFundStakeAccountSubscriber.ts │ │ ├── pollingOracleAccountSubscriber.ts │ │ ├── pollingTokenAccountSubscriber.ts │ │ ├── pollingUserAccountSubscriber.ts │ │ ├── pollingUserStatsAccountSubscriber.ts │ │ ├── testBulkAccountLoader.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ ├── webSocketAccountSubscriber.ts │ │ ├── webSocketDriftClientAccountSubscriber.ts │ │ ├── webSocketHighLeverageModeConfigAccountSubscriber.ts │ │ ├── webSocketInsuranceFundStakeAccountSubscriber.ts │ │ ├── webSocketProgramAccountSubscriber.ts │ │ ├── webSocketUserAccountSubscriber.ts │ │ └── webSocketUserStatsAccountSubsriber.ts │ ├── addresses │ │ ├── marketAddresses.ts │ │ └── pda.ts │ ├── adminClient.ts │ ├── assert │ │ └── assert.ts │ ├── auctionSubscriber │ │ ├── auctionSubscriber.ts │ │ ├── auctionSubscriberGrpc.ts │ │ ├── index.ts │ │ └── types.ts │ ├── bankrun │ │ └── bankrunConnection.ts │ ├── blockhashSubscriber │ │ ├── BlockhashSubscriber.ts │ │ ├── index.ts │ │ └── types.ts │ ├── clock │ │ └── clockSubscriber.ts │ ├── config.ts │ ├── constants │ │ ├── numericConstants.ts │ │ ├── perpMarkets.ts │ │ ├── spotMarkets.ts │ │ └── txConstants.ts │ ├── decode │ │ ├── phoenix.ts │ │ └── user.ts │ ├── dlob │ │ ├── DLOB.ts │ │ ├── DLOBNode.ts │ │ ├── DLOBSubscriber.ts │ │ ├── NodeList.ts │ │ ├── orderBookLevels.ts │ │ └── types.ts │ ├── driftClient.ts │ ├── driftClientConfig.ts │ ├── events │ │ ├── eventList.ts │ │ ├── eventSubscriber.ts │ │ ├── eventsServerLogProvider.ts │ │ ├── fetchLogs.ts │ │ ├── parse.ts │ │ ├── pollingLogProvider.ts │ │ ├── sort.ts │ │ ├── txEventCache.ts │ │ ├── types.ts │ │ └── webSocketLogProvider.ts │ ├── factory │ │ ├── bigNum.ts │ │ └── oracleClient.ts │ ├── idl │ │ ├── drift.json │ │ ├── openbook.json │ │ ├── pyth.json │ │ ├── pyth_solana_receiver.json │ │ ├── switchboard.json │ │ ├── switchboard_on_demand_30.json │ │ └── token_faucet.json │ ├── index.ts │ ├── indicative-quotes │ │ └── indicativeQuotesSender.ts │ ├── isomorphic │ │ ├── README.md │ │ ├── grpc.browser.ts │ │ ├── grpc.node.ts │ │ └── grpc.ts │ ├── jupiter │ │ └── jupiterClient.ts │ ├── keypair.ts │ ├── marinade │ │ ├── idl │ │ │ └── idl.json │ │ ├── index.ts │ │ └── types.ts │ ├── math │ │ ├── amm.ts │ │ ├── auction.ts │ │ ├── bankruptcy.ts │ │ ├── conversion.ts │ │ ├── exchangeStatus.ts │ │ ├── fuel.ts │ │ ├── funding.ts │ │ ├── insurance.ts │ │ ├── liquidation.ts │ │ ├── margin.ts │ │ ├── market.ts │ │ ├── oracles.ts │ │ ├── orders.ts │ │ ├── position.ts │ │ ├── protectedMakerParams.ts │ │ ├── repeg.ts │ │ ├── spotBalance.ts │ │ ├── spotMarket.ts │ │ ├── spotPosition.ts │ │ ├── state.ts │ │ ├── superStake.ts │ │ ├── tiers.ts │ │ ├── trade.ts │ │ ├── userStatus.ts │ │ └── utils.ts │ ├── memcmp.ts │ ├── openbook │ │ ├── openbookV2FulfillmentConfigMap.ts │ │ └── openbookV2Subscriber.ts │ ├── oracles │ │ ├── oracleClientCache.ts │ │ ├── oracleId.ts │ │ ├── prelaunchOracleClient.ts │ │ ├── pythClient.ts │ │ ├── pythLazerClient.ts │ │ ├── pythPullClient.ts │ │ ├── quoteAssetOracleClient.ts │ │ ├── strictOraclePrice.ts │ │ ├── switchboardClient.ts │ │ ├── switchboardOnDemandClient.ts │ │ └── types.ts │ ├── orderParams.ts │ ├── orderSubscriber │ │ ├── OrderSubscriber.ts │ │ ├── PollingSubscription.ts │ │ ├── WebsocketSubscription.ts │ │ ├── grpcSubscription.ts │ │ ├── index.ts │ │ └── types.ts │ ├── phoenix │ │ ├── phoenixFulfillmentConfigMap.ts │ │ └── phoenixSubscriber.ts │ ├── priorityFee │ │ ├── averageOverSlotsStrategy.ts │ │ ├── averageStrategy.ts │ │ ├── driftPriorityFeeMethod.ts │ │ ├── ewmaStrategy.ts │ │ ├── heliusPriorityFeeMethod.ts │ │ ├── index.ts │ │ ├── maxOverSlotsStrategy.ts │ │ ├── maxStrategy.ts │ │ ├── priorityFeeSubscriber.ts │ │ ├── priorityFeeSubscriberMap.ts │ │ ├── solanaPriorityFeeMethod.ts │ │ └── types.ts │ ├── serum │ │ ├── serumFulfillmentConfigMap.ts │ │ ├── serumSubscriber.ts │ │ └── types.ts │ ├── slot │ │ ├── SlotSubscriber.ts │ │ └── SlothashSubscriber.ts │ ├── swift │ │ ├── grpcSignedMsgUserAccountSubscriber.ts │ │ ├── index.ts │ │ ├── signedMsgUserAccountSubscriber.ts │ │ └── swiftOrderSubscriber.ts │ ├── testClient.ts │ ├── token │ │ └── index.ts │ ├── tokenFaucet.ts │ ├── tx │ │ ├── baseTxSender.ts │ │ ├── blockhashFetcher │ │ │ ├── baseBlockhashFetcher.ts │ │ │ ├── cachedBlockhashFetcher.ts │ │ │ └── types.ts │ │ ├── fastSingleTxSender.ts │ │ ├── forwardOnlyTxSender.ts │ │ ├── priorityFeeCalculator.ts │ │ ├── reportTransactionError.ts │ │ ├── retryTxSender.ts │ │ ├── txHandler.ts │ │ ├── txParamProcessor.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ └── whileValidTxSender.ts │ ├── types.ts │ ├── user.ts │ ├── userConfig.ts │ ├── userMap │ │ ├── PollingSubscription.ts │ │ ├── WebsocketSubscription.ts │ │ ├── grpcSubscription.ts │ │ ├── referrerMap.ts │ │ ├── userMap.ts │ │ ├── userMapConfig.ts │ │ └── userStatsMap.ts │ ├── userName.ts │ ├── userStats.ts │ ├── userStatsConfig.ts │ ├── util │ │ ├── TransactionConfirmationManager.ts │ │ ├── chainClock.ts │ │ ├── computeUnits.ts │ │ ├── digest.ts │ │ ├── ed25519Utils.ts │ │ ├── promiseTimeout.ts │ │ ├── pythOracleUtils.ts │ │ └── tps.ts │ └── wallet.ts ├── tests │ ├── amm │ │ └── test.ts │ ├── auctions │ │ └── test.ts │ ├── bn │ │ └── test.ts │ ├── ci │ │ └── verifyConstants.ts │ ├── decode │ │ ├── phoenix.ts │ │ ├── test.ts │ │ └── userAccountBufferStrings.ts │ ├── dlob │ │ ├── helpers.ts │ │ └── test.ts │ ├── insurance │ │ └── test.ts │ ├── spot │ │ └── test.ts │ ├── subscriber │ │ └── openbook.ts │ ├── tx │ │ ├── TransactionConfirmationManager.test.ts │ │ ├── cachedBlockhashFetcher.test.ts │ │ ├── priorityFeeCalculator.ts │ │ └── priorityFeeStrategy.ts │ └── user │ │ ├── helpers.ts │ │ └── test.ts ├── tsconfig.browser.json ├── tsconfig.json └── yarn.lock ├── test-scripts ├── run-anchor-local-validator-tests.sh ├── run-anchor-tests.sh ├── run-ts-mocha └── single-anchor-test.sh ├── tests ├── admin.ts ├── adminDeposit.ts ├── assetTier.ts ├── cancelAllOrders.ts ├── cappedSymFunding.ts ├── curve.ts ├── decodeUser.ts ├── deleteInitializedSpotMarket.ts ├── delistMarket.ts ├── delistMarketLiq.ts ├── depositIntoSpotMarketVault.ts ├── driftClient.ts ├── fillSpot.ts ├── fixtures │ ├── openbook.so │ ├── phoenix_dex.so │ ├── pyth_solana_receiver.so │ └── serum_dex.so ├── forceUserDelete.ts ├── fuel.ts ├── fuelSweep.ts ├── govStakeDevnet.ts ├── highLeverageMode.ts ├── imbalancePerpPnl.ts ├── insuranceFundStake.ts ├── ksolver.ts ├── liquidateBorrowForPerpPnl.ts ├── liquidateMaxLps.ts ├── liquidatePerp.ts ├── liquidatePerpAndLp.ts ├── liquidatePerpPnlForDeposit.ts ├── liquidatePerpWithFill.ts ├── liquidateSpot.ts ├── liquidateSpotSocialLoss.ts ├── liquidateSpotWithSwap.ts ├── liquidityBook.ts ├── liquidityProvider.ts ├── marketOrder.ts ├── marketOrderBaseAssetAmount.ts ├── maxDeposit.ts ├── maxLeverageOrderParams.ts ├── modifyOrder.ts ├── multipleMakerOrders.ts ├── multipleSpotMakerOrders.ts ├── openbookHelpers.ts ├── openbookTest.ts ├── oracleDiffSources.ts ├── oracleFillPriceGuardrails.ts ├── oracleOffsetOrders.ts ├── order.ts ├── ordersWithSpread.ts ├── pauseDepositWithdraw.ts ├── pauseExchange.ts ├── perpLpJit.ts ├── perpLpRiskMitigation.ts ├── phoenixTest.ts ├── phoenixTestAccountData.ts ├── placeAndMakePerp.ts ├── placeAndMakeSignedMsg.ts ├── placeAndMakeSignedMsgBankrun.ts ├── placeAndMakeSpotOrder.ts ├── postOnly.ts ├── postOnlyAmmFulfillment.ts ├── prelisting.ts ├── prepegMarketOrderBaseAssetAmount.ts ├── pyth.ts ├── pythLazer.ts ├── pythLazerBankrun.ts ├── pythLazerData.ts ├── pythPull.ts ├── pythPullOracleData.ts ├── referrer.ts ├── repegAndSpread.ts ├── resizeSwiftUserOrderIds.ts ├── roundInFavorBaseAsset.ts ├── serumHelper.ts ├── serumTest.ts ├── signedMsgWsDelegates.ts ├── spotDepositWithdraw.ts ├── spotDepositWithdraw22.ts ├── spotMarketPoolIds.ts ├── spotSwap.ts ├── spotSwap22.ts ├── spotWithdrawUtil100.ts ├── stopLimits.ts ├── subaccounts.ts ├── surgePricing.ts ├── switchOracle.ts ├── switchboardOnDemand.ts ├── switchboardOnDemandData.ts ├── switchboardTxCus.ts ├── testHelpers.ts ├── testHelpersLocalValidator.ts ├── tokenFaucet.ts ├── tradingLP.ts ├── transferPerpPosition.ts ├── transferPools.ts ├── triggerOrders.ts ├── triggerSpotOrder.ts ├── updateAMM.ts ├── updateK.ts ├── userAccount.ts ├── userDelegate.ts ├── userOrderId.ts └── whitelist.ts ├── tsconfig.json ├── typedoc.json └── yarn.lock /.cargo/audit.toml: -------------------------------------------------------------------------------- 1 | # RUSTSEC-2022-0013 ignores as upstream dependency 2 | 3 | [advisories] 4 | ignore = [ 5 | "RUSTSEC-2022-0013", 6 | "RUSTSEC-2022-0093", # Double Public Key Signing Function Oracle Attack on `ed25519-dalek` 7 | ] 8 | informational_warnings = ["unmaintained"] # warn for categories of informational advisories 9 | severity_threshold = "high" # CVSS severity ("none", "low", "medium", "high", "critical") 10 | 11 | # Advisory Database Configuration 12 | [database] 13 | path = "~/.cargo/advisory-db" # Path where advisory git repo will be cloned 14 | url = "https://github.com/RustSec/advisory-db.git" # URL to git repo 15 | fetch = true # Perform a `git fetch` before auditing (default: true) 16 | stale = false # Allow stale advisory DB (i.e. no commits for 90 days, default: false) 17 | 18 | # Output Configuration 19 | [output] 20 | deny = [] # exit on error if unmaintained dependencies are found 21 | format = "terminal" # "terminal" (human readable report) or "json" 22 | quiet = false # Only print information on error 23 | show_tree = true # Show inverse dependency trees along with advisories (default: true) 24 | 25 | # Target Configuration 26 | [target] 27 | arch = "x86_64" # Ignore advisories for CPU architectures other than this one 28 | os = "linux" # Ignore advisories for operating systems other than this one 29 | 30 | [yanked] 31 | enabled = false # Warn for yanked crates in Cargo.lock (default: true) 32 | update_index = true # Auto-update the crates.io index (default: true) 33 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Docker image to generate deterministic, verifiable builds of Anchor programs. 3 | # This must be run *after* a given ANCHOR_CLI version is published and a git tag 4 | # is released on GitHub. 5 | # 6 | 7 | FROM rust:1.75 8 | 9 | ARG DEBIAN_FRONTEND=noninteractive 10 | 11 | ARG SOLANA_CLI="1.14.7" 12 | ARG ANCHOR_CLI="0.26.0" 13 | ARG NODE_VERSION="v18.16.0" 14 | 15 | ENV HOME="/root" 16 | ENV PATH="${HOME}/.cargo/bin:${PATH}" 17 | ENV PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}" 18 | ENV PATH="${HOME}/.nvm/versions/node/${NODE_VERSION}/bin:${PATH}" 19 | 20 | # Install base utilities. 21 | RUN mkdir -p /workdir && mkdir -p /tmp && \ 22 | apt-get update -qq && apt-get upgrade -qq && apt-get install -qq \ 23 | build-essential git curl wget jq pkg-config python3-pip \ 24 | libssl-dev libudev-dev 25 | 26 | RUN wget http://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb 27 | RUN dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb 28 | 29 | # Install rust. 30 | RUN curl "https://sh.rustup.rs" -sfo rustup.sh && \ 31 | sh rustup.sh -y && \ 32 | rustup component add rustfmt clippy 33 | 34 | # Install node / npm / yarn. 35 | RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash 36 | ENV NVM_DIR="${HOME}/.nvm" 37 | RUN . $NVM_DIR/nvm.sh && \ 38 | nvm install ${NODE_VERSION} && \ 39 | nvm use ${NODE_VERSION} && \ 40 | nvm alias default node && \ 41 | npm install -g yarn && \ 42 | yarn add ts-mocha 43 | 44 | # Install Solana tools. 45 | RUN sh -c "$(curl -sSfL https://release.solana.com/v${SOLANA_CLI}/install)" 46 | 47 | # Install anchor. 48 | RUN cargo install --git https://github.com/coral-xyz/anchor avm --locked --force 49 | RUN avm install ${ANCHOR_CLI} && avm use ${ANCHOR_CLI} 50 | 51 | RUN solana-keygen new --no-bip39-passphrase 52 | 53 | WORKDIR /workdir 54 | #be sure to add `/root/.avm/bin` to your PATH to be able to run the installed binaries 55 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { "dockerfile": "Dockerfile" }, 3 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "ignorePatterns": ["**/lib", "**/node_modules", "migrations"], 9 | "plugins": [], 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/eslint-recommended", 13 | "plugin:@typescript-eslint/recommended" 14 | ], 15 | "rules": { 16 | "@typescript-eslint/explicit-function-return-type": "off", 17 | "@typescript-eslint/ban-ts-ignore": "off", 18 | "@typescript-eslint/ban-ts-comment": "off", 19 | "@typescript-eslint/no-explicit-any": "off", 20 | "@typescript-eslint/no-unused-vars": [ 21 | 2, 22 | { 23 | "argsIgnorePattern": "^_", 24 | "varsIgnorePattern": "^_" 25 | } 26 | ], 27 | "@typescript-eslint/no-var-requires": 0, 28 | "@typescript-eslint/no-empty-function": 0, 29 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 30 | "no-prototype-builtins": "off", 31 | "semi": 2, 32 | "no-restricted-imports": [ 33 | "error", 34 | { 35 | "patterns": [ 36 | { 37 | // Restrict importing BN from bn.js 38 | "group": ["bn.js"], 39 | "message": "Import BN from @drift-labs/sdk instead", 40 | } 41 | ], 42 | }, 43 | ], 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /.github/actions/setup-solana/action.yaml: -------------------------------------------------------------------------------- 1 | name: "Setup Solana" 2 | description: "Setup Solana" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - uses: actions/cache@v3 7 | name: Cache Solana Tool Suite 8 | id: cache-solana 9 | with: 10 | path: | 11 | ~/.cache/solana/ 12 | ~/.local/share/solana/ 13 | key: solana-${{ runner.os }}-v0000-${{ env.SOLANA_VERSION }} 14 | - name: Download Solana 15 | run: | 16 | echo Downloading Solana v${{ env.SOLANA_VERSION }}... 🧬 17 | export SOLANA_RELEASE=v${{ env.SOLANA_VERSION }} 18 | export SOLANA_INSTALL_INIT_ARGS=v${{ env.SOLANA_VERSION }} 19 | ${{ github.workspace }}/.github/actions/setup-solana/scripts/solana-install-init.sh 20 | echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 21 | export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH" 22 | echo "[41,242,37,42,13,160,221,13,242,224,230,17,141,228,35,40,57,231,71,8,239,32,226,165,181,216,231,245,170,229,117,123,39,103,128,179,245,168,230,228,127,219,58,249,69,6,251,148,173,190,191,217,50,67,123,105,121,215,242,41,242,85,71,109]" > $HOME/.config/solana/id.json 23 | shell: bash -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Soteria 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | SOLANA_VERSION: "1.9.5" 14 | PROGRAM_PATH: "programs/drift/" 15 | RUST_TOOLCHAIN: nightly-2022-02-07 16 | 17 | jobs: 18 | build: 19 | name: Soteria Scan 20 | runs-on: ubicloud 21 | steps: 22 | - name: Checkout changes 23 | uses: actions/checkout@v2 24 | 25 | - name: Cache Solana Version 26 | uses: actions/cache@v2 27 | id: solana-cache 28 | with: 29 | path: | 30 | ~/.rustup 31 | ~/.cache/solana 32 | ~/.local/share/solana 33 | key: solana-v${{ env.SOLANA_VERSION }} 34 | 35 | - name: Cache Soteria Build 36 | uses: Swatinem/rust-cache@v1 37 | with: 38 | target-dir: ${{ env.PROGRAM_PATH }}.coderrect/build # Cache build files for performance 39 | 40 | - name: Install Rust nightly 41 | uses: actions-rs/toolchain@v1 42 | with: 43 | toolchain: ${{ env.RUST_TOOLCHAIN }} 44 | profile: minimal 45 | override: true 46 | components: rustfmt, clippy 47 | 48 | - name: Download Solana 49 | if: steps.solana-cache.outputs.cache-hit != 'true' # Skip this step if matched cached version is available 50 | run: | 51 | echo Downloading Solana v${{ env.SOLANA_VERSION }}... 🧬 52 | sh -c "$(curl -sSfL https://release.solana.com/v${{ env.SOLANA_VERSION }}/install)" 53 | echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 54 | export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH" 55 | echo Configuring bpf toolchain... 56 | (cd $HOME/.local/share/solana/install/active_release/bin/sdk/bpf/scripts; ./install.sh) 57 | shell: bash 58 | 59 | - name: Download Soteria # Always grab the latest version 60 | run: | 61 | echo Downloading Soteria... 🔬 62 | sh -c "$(curl -k https://supercompiler.xyz/install)" 63 | export PATH=$PWD/soteria-linux-develop/bin/:$PATH 64 | echo "$PWD/soteria-linux-develop/bin" >> $GITHUB_PATH 65 | shell: bash 66 | 67 | - name: Run Soteria 68 | run: | 69 | echo Running Soteria... 👾 70 | cd ${{ env.PROGRAM_PATH }} 71 | soteria -analyzeAll . 72 | shell: bash 73 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security Scans 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | monitor: 13 | name: Sync with snyk.io 14 | runs-on: ubicloud 15 | steps: 16 | - uses: actions/checkout@master 17 | - name: Monitor on snyk.io 18 | uses: snyk/actions/node@master 19 | env: 20 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 21 | with: 22 | command: monitor --org=${{ secrets.SNYK_ORG }} --all-projects --project-business-criticality=critical --project-lifecycle=production --project-environment=backend 23 | sast: 24 | needs: monitor 25 | name: Code Scan 26 | runs-on: ubicloud 27 | steps: 28 | - name: Checkout changes 29 | uses: actions/checkout@v2 30 | 31 | - uses: snyk/actions/setup@master 32 | - name: Snyk Code Scan 33 | run: snyk code test --org=${{ secrets.SNYK_ORG }} --severity-threshold=medium --sarif-file-output=snyk-sast.json 34 | env: 35 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 36 | 37 | - name: Upload code report 38 | if: always() 39 | uses: github/codeql-action/upload-sarif@v1 40 | with: 41 | sarif_file: snyk-sast.json 42 | sca: 43 | needs: monitor 44 | name: Dependency Scan 45 | runs-on: ubicloud 46 | steps: 47 | - name: Checkout changes 48 | uses: actions/checkout@v2 49 | 50 | - name: Snyk Dependency Scan 51 | uses: snyk/actions/node@master 52 | env: 53 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 54 | with: 55 | args: --all-projects --org=${{ secrets.SNYK_ORG }} --severity-threshold=medium --fail-on=upgradable --sarif-file-output=snyk-sca.sarif 56 | 57 | - name: Upload dependency report 58 | if: always() 59 | uses: github/codeql-action/upload-sarif@v1 60 | with: 61 | sarif_file: snyk-sca.sarif 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /**/target 2 | /**/test-ledger 3 | /**/node_modules 4 | /package-lock.json 5 | /**/output* 6 | .anchor 7 | .idea 8 | yarn-lock 9 | sdk/src/**/*.js 10 | sdk/src/**/*.js.map 11 | .DS_STORE 12 | .vscode 13 | migrations 14 | /**/*.env -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/serum-dex"] 2 | path = deps/serum-dex 3 | url = https://github.com/project-serum/serum-dex.git 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn prettify 5 | yarn lint 6 | cd sdk && yarn run test 7 | yarn build 8 | 9 | if [ -z "$TS_PRECOMMIT_ONLY" ] || [ "$TS_PRECOMMIT_ONLY" = "false" ]; then 10 | cargo fmt -- --check 11 | cargo clippy -p drift -- -D warnings -D clippy::unwrap_used -D clippy::expect_used -D clippy::panic 12 | cargo clippy -p drift --tests -- -D warnings 13 | cargo test --quiet 14 | fi -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'es5', 4 | singleQuote: true, 5 | printWidth: 80, 6 | tabWidth: 2, 7 | useTabs: true, 8 | bracketSameLine: false, 9 | endOfLine: 'auto', 10 | }; 11 | -------------------------------------------------------------------------------- /.verified-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH": ["--commit-hash", "8d2cd726afdc800f89c841ff3cf1968980719df0", "--library-name", "drift"] 3 | } 4 | -------------------------------------------------------------------------------- /AUDIT.md: -------------------------------------------------------------------------------- 1 | - [Neodyme Audit (Protocol V2)](https://github.com/drift-labs/audits/blob/master/protocol-v2/neodyme.pdf) 2 | - [Trail of Bits Audit (Protocol V2)](https://github.com/drift-labs/audits/blob/master/protocol-v2/tob.pdf) 3 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | [provider] 2 | cluster = "localnet" 3 | wallet = "~/.config/solana/id.json" 4 | 5 | [workspace] 6 | exclude = ["programs/openbook_v2"] 7 | 8 | [scripts] 9 | # to run local validator tests, use "./test-scripts/run-ts-mocha" in "test" 10 | test = "echo" # need to call anchor test to update metadata field in idl before running tests, so just do a noop 11 | lint = "yarn prettify:fix && cargo fmt" 12 | fulltest = 'cargo test && bash ./test-scripts/run-anchor-tests.sh' 13 | watch_ts = 'find ./programs/clearing_house/src/* ./tests ./sdk/src | entr -c bash ./test-scripts/single-anchor-test.sh' 14 | watch_build = 'find ./programs/clearing_house/src/* ./tests ./sdk/src | entr -c anchor build' 15 | watch_cargo = 'find ./programs/clearing_house/src/* ./tests ./sdk/src | entr -c cargo test -- --show-output' 16 | 17 | [programs.localnet] 18 | drift = "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" 19 | pyth = "FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH" 20 | token_faucet = "V4v1mQiAdLz4qwckEb45WqHYceYizoib39cDBHSWfaB" 21 | 22 | [[test.validator.account]] 23 | address = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" 24 | filename = "./deps/configs/usdc.json" 25 | 26 | [[test.validator.account]] 27 | address = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL" 28 | filename = "./deps/configs/pyth_lazer_storage.json" -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*", 4 | ] 5 | exclude = [ 6 | "deps/serum-dex" 7 | ] 8 | 9 | [profile.release] 10 | lto = "fat" 11 | codegen-units = 1 12 | 13 | [profile.release.build-override] 14 | opt-level = 3 15 | incremental = false 16 | codegen-units = 1 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Drift Protocol v2

5 | 6 |

7 | Docs 8 | Discord Chat 9 | License 10 |

11 |
12 | 13 | # Drift Protocol v2 14 | 15 | This repository provides open source access to Drift V2's Typescript SDK, Solana Programs, and more. 16 | 17 | Integrating Drift? [Go here](./sdk/README.md) 18 | 19 | # SDK Guide 20 | 21 | SDK docs can be found [here](./sdk/README.md) 22 | 23 | # Example Bot Implementations 24 | 25 | Example bots (makers, liquidators, fillers, etc) can be found [here](https://github.com/drift-labs/keeper-bots-v2) 26 | 27 | # Building Locally 28 | 29 | Note: If you are running the build on an Apple computer with an M1 chip, please set the default rust toolchain to `stable-x86_64-apple-darwin` 30 | 31 | ```bash 32 | rustup default stable-x86_64-apple-darwin 33 | ``` 34 | 35 | ## Compiling Programs 36 | 37 | ```bash 38 | # build v2 39 | anchor build 40 | # install packages 41 | yarn 42 | # build sdk 43 | cd sdk/ && yarn && yarn build && cd .. 44 | ``` 45 | 46 | ## Running Rust Test 47 | 48 | ```bash 49 | cargo test 50 | ``` 51 | 52 | ## Running Javascript Tests 53 | 54 | ```bash 55 | bash test-scripts/run-anchor-tests.sh 56 | ``` 57 | 58 | # Bug Bounty 59 | 60 | Information about the Bug Bounty can be found [here](./bug-bounty/README.md) 61 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Bug Bounty Overview 2 | Drift offers bug bounties for Drift's on-chain program code; UI only bugs are omitted. 3 | 4 | |Severity|Description|Bug Bounty| 5 | |-----------|--------------|-------------| 6 | |Critical|Bugs that freeze user funds or drain the contract's holdings or involve theft of funds without user signatures|10% of the value of the hack up to $500,000| 7 | |High|Bugs that could temporarily freeze user funds or incorrectly assign value to user funds|$10,000 to $50,000 per bug, assessed on a case by case basis| 8 | |Medium/Low|Bugs that don't threaten user funds|$1,000 to $5,000 per bug, assessed on a case by case basis| 9 | 10 | The severity guidelines are based on [Immunefi's classification system](https://immunefi.com/severity-updated/). 11 | Note that these are simply guidelines for the severity of the bugs. Each bug bounty submission will be evaluated on a case-by-case basis. 12 | 13 | ## Submission 14 | Please email hello@drift.trade with a detailed description of the attack vector. For critical and moderate bugs, we require a proof of concept done on a privately deployed mainnet contract. We will reach back out in 1 business day with additional questions or the next steps on the bug bounty. 15 | 16 | ## Bug Bounty Payment 17 | Bug bounties will be paid in USDC. Alternative payment methods can be used on a case-by-case basis. 18 | 19 | ## Invalid Bug Bounties 20 | The following are out of scope for the bug bounty: 21 | 1. Attacks that the reporter has already exploited themselves, leading to damage. 22 | 2. Attacks requiring access to leaked keys/credentials. 23 | 3. Attacks requiring access to privileged addresses (governance, admin). 24 | 4. Incorrect data supplied by third party oracles (this does not exclude oracle manipulation/flash loan attacks). 25 | 5. Lack of liquidity. 26 | 6. Third party, off-chain bot errors (for instance bugs with an arbitrage bot running on the smart contracts). 27 | 7. Best practice critiques. 28 | 8. Sybil attacks. 29 | 9. Attempted phishing or other social engineering attacks involving Drift contributors or users 30 | 10. Denial of service, or automated testing of services that generate significant traffic. 31 | 11. Any submission violating [Immunefi's rules](https://immunefi.com/rules/) 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /bug-bounty/README.md: -------------------------------------------------------------------------------- 1 | # Bug Bounty for v2 (coming soon) 2 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | ```shell 3 | yarn && 4 | tsc && 5 | npm link && 6 | drift config init 7 | ``` -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drift-v1-cli", 3 | "version": "0.0.0", 4 | "description": "", 5 | "bin": { 6 | "drift": "./build/cli.js" 7 | }, 8 | "scripts": {}, 9 | "author": "crispheaney", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@drift-labs/sdk": "file:../sdk", 13 | "@project-serum/anchor": "0.24.2", 14 | "@solana/spl-token": "^0.1.8", 15 | "@solana/web3.js": "^1.30.2", 16 | "@types/node": "^16.7.10", 17 | "colors": "^1.4.0", 18 | "commander": "8.1.0", 19 | "loglevel": "^1.7.1", 20 | "promptly": "^3.2.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "outDir": "./build", 10 | "resolveJsonModule": true 11 | } 12 | } -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "header, diff, flags, components" 3 | 4 | component_management: 5 | individual_components: 6 | - component_id: drift 7 | name: drift 8 | paths: 9 | - programs/drift -------------------------------------------------------------------------------- /deploy-scripts/build-devnet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | anchor build -- --no-default-features -------------------------------------------------------------------------------- /deploy-scripts/deploy-devnet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | anchor upgrade --program-id dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH --provider.cluster devnet --provider.wallet $SOLANA_PATH/$DEVNET_ADMIN target/deploy/drift.so -------------------------------------------------------------------------------- /deploy-scripts/verified-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | solana-verify build --library-name drift -------------------------------------------------------------------------------- /deps/configs/pyth_lazer_storage.json: -------------------------------------------------------------------------------- 1 | { 2 | "pubkey": "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL", 3 | "account": { 4 | "lamports": 1461600, 5 | "data": [ 6 | "0XX/ucSvRAkL/td28gTUmmjn6CkzKyvYXJOMcup4pEKu3cXcP7cvDAv+13byBNSaaOfoKTMrK9hck4xy6nikQq7dxdw/ty8MAQAAAAAAAAAB9lIQvuT89bHO4eU3+rz9lQECl2U7lK8E1FT8Rz6Ug0/oNgZ6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 7 | "base64" 8 | ], 9 | "owner": "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt", 10 | "executable": false, 11 | "rentEpoch": 367 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /deps/configs/usdc.json: -------------------------------------------------------------------------------- 1 | { 2 | "pubkey": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 3 | "account": { 4 | "lamports": 1461600, 5 | "data": [ 6 | "AQAAANuZX+JRadFByrm7upK6oB+fLh7OffTLKsBRkPN/zB+dAAAAAAAAAAAGAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", 7 | "base64" 8 | ], 9 | "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 10 | "executable": false, 11 | "rentEpoch": 367 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "directories": { 3 | "test": "tests" 4 | }, 5 | "author": "", 6 | "license": "ISC", 7 | "devDependencies": { 8 | "@coral-xyz/anchor": "0.29.0", 9 | "@coral-xyz/anchor-30": "npm:@coral-xyz/anchor@0.30.1", 10 | "@project-serum/common": "0.0.1-beta.3", 11 | "@project-serum/serum": "0.13.65", 12 | "@pythnetwork/client": "2.21.0", 13 | "@solana/spl-token": "0.3.7", 14 | "@solana/web3.js": "1.73.2", 15 | "@types/bn.js": "5.1.6", 16 | "@types/chai": "5.0.0", 17 | "@types/mocha": "8.2.3", 18 | "@typescript-eslint/eslint-plugin": "4.33.0", 19 | "@typescript-eslint/parser": "4.33.0", 20 | "chai": "4.4.1", 21 | "eslint": "7.32.0", 22 | "eslint-config-prettier": "8.3.0", 23 | "eslint-plugin-prettier": "3.4.0", 24 | "husky": "7.0.4", 25 | "prettier": "3.0.1", 26 | "typedoc": "0.23.23", 27 | "typescript": "4.9.5", 28 | "@pythnetwork/price-service-client": "1.9.0" 29 | }, 30 | "dependencies": { 31 | "@ellipsis-labs/phoenix-sdk": "1.4.2", 32 | "@pythnetwork/pyth-solana-receiver": "0.8.0", 33 | "@switchboard-xyz/on-demand": "2.4.1", 34 | "@switchboard-xyz/common": "3.0.14", 35 | "anchor-bankrun": "0.3.0", 36 | "chai-bn": "0.2.2", 37 | "csvtojson": "2.0.10", 38 | "dotenv": "16.4.5", 39 | "json2csv": "5.0.7", 40 | "nanoid": "3.3.4", 41 | "rpc-websockets": "7.5.1", 42 | "solana-bankrun": "0.3.0", 43 | "zstddec": "0.1.0" 44 | }, 45 | "scripts": { 46 | "generate-docs": "typedoc --skipErrorChecking --logLevel Error", 47 | "prepare": "husky install", 48 | "prettify": "prettier --check './sdk/src/**/*.ts' './tests/**.ts' './cli/**.ts'", 49 | "prettify:fix": "prettier --write './sdk/src/**/*.ts' './tests/**.ts' './cli/**.ts'", 50 | "lint": "eslint . --ext ts --quiet", 51 | "lint:fix": "eslint . --ext ts --fix", 52 | "update-idl": "cp target/idl/drift.json sdk/src/idl/drift.json" 53 | }, 54 | "engines": { 55 | "node": ">=12" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /programs/drift/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "drift" 3 | version = "2.122.0" 4 | description = "Created with Anchor" 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "drift" 10 | path = "src/lib.rs" 11 | 12 | [features] 13 | no-entrypoint = [] 14 | cpi = ["no-entrypoint"] 15 | mainnet-beta=[] 16 | anchor-test= [] 17 | default=["mainnet-beta"] 18 | drift-rs=[] 19 | 20 | [dependencies] 21 | anchor-lang = "0.29.0" 22 | solana-program = "1.16" 23 | anchor-spl = "0.29.0" 24 | pyth-client = "0.2.2" 25 | pyth-lazer-solana-contract = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "d790d1cb4da873a949cf33ff70349b7614b232eb", features = ["no-entrypoint"]} 26 | pythnet-sdk = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "3e8a24ecd0bcf22b787313e2020f4186bb22c729"} 27 | pyth-solana-receiver-sdk = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "3e8a24ecd0bcf22b787313e2020f4186bb22c729"} 28 | bytemuck = { version = "1.4.0" } 29 | borsh = "0.10.3" 30 | hex = "0.4.3" 31 | num-traits = "0.2" 32 | uint = { version = "0.9.1", default-features = false } 33 | num-derive = "0.3" 34 | thiserror = "1.0" 35 | num-integer = "0.1.44" 36 | arrayref = "0.3.6" 37 | base64 = "0.13.0" 38 | serum_dex = { git = "https://github.com/project-serum/serum-dex", rev = "85b4f14", version = "0.5.6", features = ["no-entrypoint"] } 39 | enumflags2 = "0.6.4" 40 | phoenix-v1 = { git = "https://github.com/drift-labs/phoenix-v1", rev = "7703c5", version = "0.2.4", features = ["no-entrypoint"] } 41 | solana-security-txt = "1.1.0" 42 | static_assertions = "1.1.0" 43 | drift-macros = { git = "https://github.com/drift-labs/drift-macros.git", rev = "c57d87" } 44 | switchboard = { path = "../switchboard", features = ["no-entrypoint"] } 45 | openbook-v2-light = { path = "../openbook_v2", features = ["no-entrypoint"] } 46 | ahash = "=0.8.6" 47 | switchboard-on-demand = { path = "../switchboard-on-demand", features = ["no-entrypoint"] } 48 | byteorder = "1.4.3" 49 | 50 | [dev-dependencies] 51 | bytes = "1.2.0" 52 | pyth = { path = "../pyth", features = ["no-entrypoint"]} 53 | base64 = "0.13.0" 54 | -------------------------------------------------------------------------------- /programs/drift/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] -------------------------------------------------------------------------------- /programs/drift/src/controller/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod amm; 2 | pub mod funding; 3 | pub mod insurance; 4 | pub mod liquidation; 5 | pub mod lp; 6 | pub mod orders; 7 | pub mod pda; 8 | pub mod pnl; 9 | pub mod position; 10 | pub mod repeg; 11 | pub mod spot_balance; 12 | pub mod spot_position; 13 | pub mod token; 14 | -------------------------------------------------------------------------------- /programs/drift/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub use admin::*; 2 | pub use constraints::*; 3 | pub use if_staker::*; 4 | pub use keeper::*; 5 | pub use pyth_lazer_oracle::*; 6 | pub use pyth_pull_oracle::*; 7 | pub use user::*; 8 | 9 | mod admin; 10 | mod constraints; 11 | mod if_staker; 12 | mod keeper; 13 | pub mod optional_accounts; 14 | mod pyth_lazer_oracle; 15 | mod pyth_pull_oracle; 16 | mod user; 17 | -------------------------------------------------------------------------------- /programs/drift/src/math/amm_jit/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::math::amm_jit::*; 2 | use crate::state::perp_market::AMM; 3 | 4 | #[test] 5 | fn balanced_market_zero_jit() { 6 | let market = PerpMarket { 7 | amm: AMM { 8 | base_asset_amount_with_amm: 0, 9 | amm_jit_intensity: 100, 10 | ..AMM::default_test() 11 | }, 12 | ..PerpMarket::default() 13 | }; 14 | let jit_base_asset_amount = 100; 15 | 16 | let jit_amount = calculate_clamped_jit_base_asset_amount( 17 | &market, 18 | AMMLiquiditySplit::ProtocolOwned, 19 | jit_base_asset_amount, 20 | ) 21 | .unwrap(); 22 | assert_eq!(jit_amount, 0); 23 | } 24 | 25 | #[test] 26 | fn balanced_market_zero_intensity() { 27 | let market = PerpMarket { 28 | amm: AMM { 29 | base_asset_amount_with_amm: 100, 30 | amm_jit_intensity: 0, 31 | ..AMM::default_test() 32 | }, 33 | ..PerpMarket::default() 34 | }; 35 | let jit_base_asset_amount = 100; 36 | 37 | let jit_amount = calculate_clamped_jit_base_asset_amount( 38 | &market, 39 | AMMLiquiditySplit::ProtocolOwned, 40 | jit_base_asset_amount, 41 | ) 42 | .unwrap(); 43 | assert_eq!(jit_amount, 0); 44 | } 45 | 46 | #[test] 47 | fn balanced_market_full_intensity() { 48 | let market = PerpMarket { 49 | amm: AMM { 50 | base_asset_amount_with_amm: 100, 51 | amm_jit_intensity: 100, 52 | ..AMM::default_test() 53 | }, 54 | ..PerpMarket::default() 55 | }; 56 | let jit_base_asset_amount = 100; 57 | 58 | let jit_amount = calculate_clamped_jit_base_asset_amount( 59 | &market, 60 | AMMLiquiditySplit::ProtocolOwned, 61 | jit_base_asset_amount, 62 | ) 63 | .unwrap(); 64 | assert_eq!(jit_amount, 100); 65 | } 66 | 67 | #[test] 68 | fn balanced_market_half_intensity() { 69 | let market = PerpMarket { 70 | amm: AMM { 71 | base_asset_amount_with_amm: 100, 72 | amm_jit_intensity: 50, 73 | ..AMM::default_test() 74 | }, 75 | ..PerpMarket::default() 76 | }; 77 | let jit_base_asset_amount = 100; 78 | 79 | let jit_amount = calculate_clamped_jit_base_asset_amount( 80 | &market, 81 | AMMLiquiditySplit::ProtocolOwned, 82 | jit_base_asset_amount, 83 | ) 84 | .unwrap(); 85 | assert_eq!(jit_amount, 50); 86 | } 87 | -------------------------------------------------------------------------------- /programs/drift/src/math/bankruptcy.rs: -------------------------------------------------------------------------------- 1 | use crate::state::spot_market::SpotBalanceType; 2 | use crate::state::user::User; 3 | 4 | #[cfg(test)] 5 | mod tests; 6 | 7 | pub fn is_user_bankrupt(user: &User) -> bool { 8 | // user is bankrupt iff they have spot liabilities, no spot assets, and no perp exposure 9 | 10 | let mut has_liability = false; 11 | 12 | for spot_position in user.spot_positions.iter() { 13 | if spot_position.scaled_balance > 0 { 14 | match spot_position.balance_type { 15 | SpotBalanceType::Deposit => return false, 16 | SpotBalanceType::Borrow => has_liability = true, 17 | } 18 | } 19 | } 20 | 21 | for perp_position in user.perp_positions.iter() { 22 | if perp_position.base_asset_amount != 0 23 | || perp_position.quote_asset_amount > 0 24 | || perp_position.has_open_order() 25 | || perp_position.is_lp() 26 | { 27 | return false; 28 | } 29 | 30 | if perp_position.quote_asset_amount < 0 { 31 | has_liability = true; 32 | } 33 | } 34 | 35 | has_liability 36 | } 37 | -------------------------------------------------------------------------------- /programs/drift/src/math/bankruptcy/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::math::bankruptcy::is_user_bankrupt; 2 | use crate::state::spot_market::SpotBalanceType; 3 | use crate::state::user::{PerpPosition, SpotPosition, User}; 4 | use crate::test_utils::{get_positions, get_spot_positions}; 5 | 6 | #[test] 7 | fn user_has_position_with_base() { 8 | let user = User { 9 | perp_positions: get_positions(PerpPosition { 10 | base_asset_amount: 1, 11 | ..PerpPosition::default() 12 | }), 13 | ..User::default() 14 | }; 15 | 16 | let is_bankrupt = is_user_bankrupt(&user); 17 | assert!(!is_bankrupt); 18 | } 19 | 20 | #[test] 21 | fn user_has_position_with_positive_quote() { 22 | let user = User { 23 | perp_positions: get_positions(PerpPosition { 24 | quote_asset_amount: 1, 25 | ..PerpPosition::default() 26 | }), 27 | ..User::default() 28 | }; 29 | 30 | let is_bankrupt = is_user_bankrupt(&user); 31 | assert!(!is_bankrupt); 32 | } 33 | 34 | #[test] 35 | fn user_with_deposit() { 36 | let user = User { 37 | spot_positions: get_spot_positions(SpotPosition { 38 | balance_type: SpotBalanceType::Deposit, 39 | scaled_balance: 1, 40 | ..SpotPosition::default() 41 | }), 42 | ..User::default() 43 | }; 44 | 45 | let is_bankrupt = is_user_bankrupt(&user); 46 | assert!(!is_bankrupt); 47 | } 48 | 49 | #[test] 50 | fn user_has_position_with_negative_quote() { 51 | let user = User { 52 | perp_positions: get_positions(PerpPosition { 53 | quote_asset_amount: -1, 54 | ..PerpPosition::default() 55 | }), 56 | ..User::default() 57 | }; 58 | 59 | let is_bankrupt = is_user_bankrupt(&user); 60 | assert!(is_bankrupt); 61 | } 62 | 63 | #[test] 64 | fn user_with_borrow() { 65 | let user = User { 66 | spot_positions: get_spot_positions(SpotPosition { 67 | balance_type: SpotBalanceType::Borrow, 68 | scaled_balance: 1, 69 | ..SpotPosition::default() 70 | }), 71 | ..User::default() 72 | }; 73 | 74 | let is_bankrupt = is_user_bankrupt(&user); 75 | assert!(is_bankrupt); 76 | } 77 | 78 | #[test] 79 | fn user_with_empty_position_and_balances() { 80 | let user = User::default(); 81 | let is_bankrupt = is_user_bankrupt(&user); 82 | assert!(!is_bankrupt); 83 | } 84 | -------------------------------------------------------------------------------- /programs/drift/src/math/casting.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{DriftResult, ErrorCode}; 2 | use crate::math::bn::U192; 3 | use crate::msg; 4 | use std::convert::TryInto; 5 | use std::panic::Location; 6 | 7 | pub trait Cast: Sized { 8 | #[track_caller] 9 | #[inline(always)] 10 | fn cast>(self) -> DriftResult { 11 | match self.try_into() { 12 | Ok(result) => Ok(result), 13 | Err(_) => { 14 | let caller = Location::caller(); 15 | msg!( 16 | "Casting error thrown at {}:{}", 17 | caller.file(), 18 | caller.line() 19 | ); 20 | Err(ErrorCode::CastingFailure) 21 | } 22 | } 23 | } 24 | } 25 | 26 | impl Cast for U192 {} 27 | impl Cast for u128 {} 28 | impl Cast for u64 {} 29 | impl Cast for u32 {} 30 | impl Cast for u16 {} 31 | impl Cast for u8 {} 32 | impl Cast for usize {} 33 | impl Cast for i128 {} 34 | impl Cast for i64 {} 35 | impl Cast for i32 {} 36 | impl Cast for i16 {} 37 | impl Cast for i8 {} 38 | impl Cast for bool {} 39 | -------------------------------------------------------------------------------- /programs/drift/src/math/ceil_div.rs: -------------------------------------------------------------------------------- 1 | use crate::math::bn::{U192, U256}; 2 | use num_traits::{One, Zero}; 3 | 4 | pub trait CheckedCeilDiv: Sized { 5 | /// Perform ceiling division 6 | fn checked_ceil_div(&self, rhs: Self) -> Option; 7 | } 8 | 9 | macro_rules! checked_impl { 10 | ($t:ty) => { 11 | impl CheckedCeilDiv for $t { 12 | #[track_caller] 13 | #[inline] 14 | fn checked_ceil_div(&self, rhs: $t) -> Option<$t> { 15 | let quotient = self.checked_div(rhs)?; 16 | 17 | let remainder = self.checked_rem(rhs)?; 18 | 19 | if remainder > <$t>::zero() { 20 | quotient.checked_add(<$t>::one()) 21 | } else { 22 | Some(quotient) 23 | } 24 | } 25 | } 26 | }; 27 | } 28 | 29 | checked_impl!(U256); 30 | checked_impl!(U192); 31 | checked_impl!(u128); 32 | checked_impl!(u64); 33 | checked_impl!(u32); 34 | checked_impl!(u16); 35 | checked_impl!(u8); 36 | checked_impl!(i128); 37 | checked_impl!(i64); 38 | checked_impl!(i32); 39 | checked_impl!(i16); 40 | checked_impl!(i8); 41 | -------------------------------------------------------------------------------- /programs/drift/src/math/floor_div.rs: -------------------------------------------------------------------------------- 1 | use num_traits::{One, Zero}; 2 | 3 | pub trait CheckedFloorDiv: Sized { 4 | /// Perform floor division 5 | fn checked_floor_div(&self, rhs: Self) -> Option; 6 | } 7 | 8 | macro_rules! checked_impl { 9 | ($t:ty) => { 10 | impl CheckedFloorDiv for $t { 11 | #[track_caller] 12 | #[inline] 13 | fn checked_floor_div(&self, rhs: $t) -> Option<$t> { 14 | let quotient = self.checked_div(rhs)?; 15 | 16 | let remainder = self.checked_rem(rhs)?; 17 | 18 | if remainder != <$t>::zero() { 19 | quotient.checked_sub(<$t>::one()) 20 | } else { 21 | Some(quotient) 22 | } 23 | } 24 | } 25 | }; 26 | } 27 | 28 | checked_impl!(i128); 29 | checked_impl!(i64); 30 | checked_impl!(i32); 31 | checked_impl!(i16); 32 | checked_impl!(i8); 33 | 34 | #[cfg(test)] 35 | mod test { 36 | use crate::math::floor_div::CheckedFloorDiv; 37 | 38 | #[test] 39 | fn test() { 40 | let x = -3_i128; 41 | 42 | assert_eq!(x.checked_floor_div(2), Some(-2)); 43 | assert_eq!(x.checked_floor_div(0), None); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /programs/drift/src/math/fuel.rs: -------------------------------------------------------------------------------- 1 | use crate::error::DriftResult; 2 | use crate::math::casting::Cast; 3 | use crate::math::safe_math::SafeMath; 4 | use crate::state::perp_market::PerpMarket; 5 | use crate::state::spot_market::SpotMarket; 6 | use crate::{FUEL_WINDOW_U128, QUOTE_PRECISION, QUOTE_PRECISION_U64}; 7 | 8 | #[cfg(test)] 9 | mod tests; 10 | 11 | pub fn calculate_perp_fuel_bonus( 12 | perp_market: &PerpMarket, 13 | base_asset_value: i128, 14 | fuel_bonus_numerator: i64, 15 | ) -> DriftResult { 16 | let result: u64 = if base_asset_value.unsigned_abs() < QUOTE_PRECISION { 17 | 0_u64 18 | } else { 19 | base_asset_value 20 | .unsigned_abs() 21 | .safe_mul(fuel_bonus_numerator.cast()?)? 22 | .safe_mul(perp_market.fuel_boost_position.cast()?)? 23 | .safe_div(FUEL_WINDOW_U128)? 24 | .cast::()? 25 | / (QUOTE_PRECISION_U64 / 10) 26 | }; 27 | 28 | Ok(result) 29 | } 30 | 31 | pub fn calculate_spot_fuel_bonus( 32 | spot_market: &SpotMarket, 33 | signed_token_value: i128, 34 | fuel_bonus_numerator: i64, 35 | ) -> DriftResult { 36 | let result: u64 = if signed_token_value.unsigned_abs() < QUOTE_PRECISION { 37 | 0_u64 38 | } else if signed_token_value > 0 { 39 | signed_token_value 40 | .unsigned_abs() 41 | .safe_mul(fuel_bonus_numerator.cast()?)? 42 | .safe_mul(spot_market.fuel_boost_deposits.cast()?)? 43 | .safe_div(FUEL_WINDOW_U128)? 44 | .cast::()? 45 | / (QUOTE_PRECISION_U64 / 10) 46 | } else { 47 | signed_token_value 48 | .unsigned_abs() 49 | .safe_mul(fuel_bonus_numerator.cast()?)? 50 | .safe_mul(spot_market.fuel_boost_borrows.cast()?)? 51 | .safe_div(FUEL_WINDOW_U128)? 52 | .cast::()? 53 | / (QUOTE_PRECISION_U64 / 10) 54 | }; 55 | 56 | Ok(result) 57 | } 58 | 59 | pub fn calculate_insurance_fuel_bonus( 60 | spot_market: &SpotMarket, 61 | stake_amount: u64, 62 | stake_amount_delta: i64, 63 | fuel_bonus_numerator: u32, 64 | ) -> DriftResult { 65 | Ok(stake_amount 66 | .saturating_sub(stake_amount_delta.unsigned_abs()) 67 | .cast::()? 68 | .safe_mul(fuel_bonus_numerator.cast()?)? 69 | .safe_mul(spot_market.fuel_boost_insurance.cast()?)? 70 | .safe_div(FUEL_WINDOW_U128)? 71 | .cast::()? 72 | / (QUOTE_PRECISION_U64 / 10)) 73 | } 74 | -------------------------------------------------------------------------------- /programs/drift/src/math/fuel/tests.rs: -------------------------------------------------------------------------------- 1 | mod calculate_perp_fuel_bonus { 2 | use crate::math::fuel::calculate_perp_fuel_bonus; 3 | use crate::state::perp_market::PerpMarket; 4 | use crate::{FUEL_WINDOW_U128, QUOTE_PRECISION_I128}; 5 | 6 | #[test] 7 | fn test() { 8 | let perp_market = PerpMarket { 9 | fuel_boost_position: 1, 10 | ..PerpMarket::default() 11 | }; 12 | let bonus = 13 | calculate_perp_fuel_bonus(&perp_market, QUOTE_PRECISION_I128, FUEL_WINDOW_U128 as i64) 14 | .unwrap(); 15 | assert_eq!(bonus, 10); 16 | } 17 | } 18 | 19 | mod calculate_spot_fuel_bonus { 20 | use crate::math::fuel::calculate_spot_fuel_bonus; 21 | 22 | use crate::state::spot_market::SpotMarket; 23 | use crate::{FUEL_WINDOW_U128, QUOTE_PRECISION_I128}; 24 | 25 | #[test] 26 | fn test() { 27 | let mut spot_market = SpotMarket { 28 | fuel_boost_deposits: 1, 29 | ..SpotMarket::default() 30 | }; 31 | let bonus = 32 | calculate_spot_fuel_bonus(&spot_market, QUOTE_PRECISION_I128, FUEL_WINDOW_U128 as i64) 33 | .unwrap(); 34 | assert_eq!(bonus, 10); 35 | 36 | spot_market.fuel_boost_borrows = 1; 37 | 38 | let bonus = 39 | calculate_spot_fuel_bonus(&spot_market, -QUOTE_PRECISION_I128, FUEL_WINDOW_U128 as i64) 40 | .unwrap(); 41 | assert_eq!(bonus, 10); 42 | } 43 | } 44 | 45 | mod calculate_insurance_fuel_bonus { 46 | use crate::math::fuel::calculate_insurance_fuel_bonus; 47 | 48 | use crate::state::spot_market::SpotMarket; 49 | use crate::{FUEL_WINDOW_U128, QUOTE_PRECISION_U64}; 50 | 51 | #[test] 52 | fn test() { 53 | let spot_market = SpotMarket { 54 | fuel_boost_insurance: 1, 55 | ..SpotMarket::default() 56 | }; 57 | let bonus = calculate_insurance_fuel_bonus( 58 | &spot_market, 59 | QUOTE_PRECISION_U64, 60 | 0, 61 | FUEL_WINDOW_U128 as u32, 62 | ) 63 | .unwrap(); 64 | assert_eq!(bonus, 10); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /programs/drift/src/math/helpers/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::math::helpers::*; 2 | 3 | #[test] 4 | pub fn log_test() { 5 | assert_eq!(log10_iter(0), 0); 6 | assert_eq!(log10(0), 0); 7 | 8 | assert_eq!(log10_iter(9), 0); 9 | assert_eq!(log10(9), 0); 10 | 11 | assert_eq!(log10(19), 1); 12 | assert_eq!(log10_iter(19), 1); 13 | 14 | assert_eq!(log10_iter(13432429), 7); 15 | 16 | assert_eq!(log10(100), 2); 17 | assert_eq!(log10_iter(100), 2); 18 | 19 | // no modify check 20 | let n = 1005325523; 21 | assert_eq!(log10_iter(n), 9); 22 | assert_eq!(log10_iter(n), 9); 23 | assert_eq!(log10(n), 9); 24 | assert_eq!(log10_iter(n), 9); 25 | } 26 | 27 | #[test] 28 | fn proportion_tests() { 29 | let result = get_proportion_i128(999999999369, 1000000036297, 1000000042597).unwrap(); 30 | assert_eq!(result, 999999993069); 31 | let result = get_proportion_u128(999999999369, 1000000036297, 1000000042597).unwrap(); 32 | assert_eq!(result, 999999993069); 33 | let result = get_proportion_u128(1000000036297, 999999999369, 1000000042597).unwrap(); 34 | assert_eq!(result, 999999993069); 35 | 36 | let result = get_proportion_u128(999999999369, 1000000042597, 1000000036297).unwrap(); 37 | assert_eq!(result, 1000000005668); 38 | let result = get_proportion_u128(1000000042597, 999999999369, 1000000036297).unwrap(); 39 | assert_eq!(result, 1000000005668); 40 | } 41 | -------------------------------------------------------------------------------- /programs/drift/src/math/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod amm; 2 | pub mod amm_jit; 3 | pub mod amm_spread; 4 | pub mod auction; 5 | pub mod bankruptcy; 6 | pub mod bn; 7 | pub mod casting; 8 | pub mod ceil_div; 9 | pub mod constants; 10 | pub mod cp_curve; 11 | pub mod fees; 12 | mod floor_div; 13 | pub mod fuel; 14 | pub mod fulfillment; 15 | pub mod funding; 16 | pub mod helpers; 17 | pub mod insurance; 18 | pub mod liquidation; 19 | pub mod lp; 20 | pub mod margin; 21 | pub mod matching; 22 | pub mod oracle; 23 | pub mod orders; 24 | pub mod pnl; 25 | pub mod position; 26 | pub mod quote_asset; 27 | pub mod repeg; 28 | pub mod safe_math; 29 | pub mod safe_unwrap; 30 | pub mod serum; 31 | pub mod spot_balance; 32 | pub mod spot_swap; 33 | pub mod spot_withdraw; 34 | pub mod stats; 35 | -------------------------------------------------------------------------------- /programs/drift/src/math/pnl.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::amm::SwapDirection; 2 | use crate::error::DriftResult; 3 | use crate::math::casting::Cast; 4 | use crate::math::safe_math::SafeMath; 5 | 6 | pub fn calculate_pnl( 7 | exit_value: u128, 8 | entry_value: u128, 9 | swap_direction_to_close: SwapDirection, 10 | ) -> DriftResult { 11 | match swap_direction_to_close { 12 | SwapDirection::Add => exit_value.cast::()?.safe_sub(entry_value.cast()?), 13 | SwapDirection::Remove => entry_value.cast::()?.safe_sub(exit_value.cast()?), 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /programs/drift/src/math/quote_asset.rs: -------------------------------------------------------------------------------- 1 | use crate::error::DriftResult; 2 | use crate::math::constants::AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO; 3 | use crate::math::safe_math::SafeMath; 4 | 5 | use std::ops::Div; 6 | 7 | pub fn reserve_to_asset_amount( 8 | quote_asset_reserve: u128, 9 | peg_multiplier: u128, 10 | ) -> DriftResult { 11 | Ok(quote_asset_reserve 12 | .safe_mul(peg_multiplier)? 13 | .div(AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO)) 14 | } 15 | 16 | pub fn asset_to_reserve_amount( 17 | quote_asset_amount: u128, 18 | peg_multiplier: u128, 19 | ) -> DriftResult { 20 | Ok(quote_asset_amount 21 | .safe_mul(AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO)? 22 | .div(peg_multiplier)) 23 | } 24 | -------------------------------------------------------------------------------- /programs/drift/src/math/safe_unwrap.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{DriftResult, ErrorCode}; 2 | use crate::msg; 3 | use std::panic::Location; 4 | 5 | pub trait SafeUnwrap { 6 | type Item; 7 | 8 | fn safe_unwrap(self) -> DriftResult; 9 | } 10 | 11 | impl SafeUnwrap for Option { 12 | type Item = T; 13 | 14 | #[track_caller] 15 | #[inline(always)] 16 | fn safe_unwrap(self) -> DriftResult { 17 | match self { 18 | Some(v) => Ok(v), 19 | None => { 20 | let caller = Location::caller(); 21 | msg!("Unwrap error thrown at {}:{}", caller.file(), caller.line()); 22 | Err(ErrorCode::FailedUnwrap) 23 | } 24 | } 25 | } 26 | } 27 | 28 | impl SafeUnwrap for Result { 29 | type Item = T; 30 | 31 | #[track_caller] 32 | #[inline(always)] 33 | fn safe_unwrap(self) -> DriftResult { 34 | match self { 35 | Ok(v) => Ok(v), 36 | Err(_) => { 37 | let caller = Location::caller(); 38 | msg!("Unwrap error thrown at {}:{}", caller.file(), caller.line()); 39 | Err(ErrorCode::FailedUnwrap) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /programs/drift/src/math/serum.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests; 3 | 4 | use crate::controller::position::PositionDirection; 5 | use crate::error::DriftResult; 6 | use crate::math::casting::Cast; 7 | use crate::math::constants::PRICE_TO_QUOTE_PRECISION_RATIO; 8 | use crate::math::safe_math::SafeMath; 9 | 10 | // Max amount of base to put deposit into serum 11 | pub fn calculate_serum_max_coin_qty( 12 | base_asset_amount: u64, 13 | coin_lot_size: u64, 14 | ) -> DriftResult { 15 | base_asset_amount.safe_div(coin_lot_size) 16 | } 17 | 18 | // calculate limit price in serum lot sizes 19 | pub fn calculate_serum_limit_price( 20 | limit_price: u64, 21 | pc_lot_size: u64, 22 | coin_decimals: u32, 23 | coin_lot_size: u64, 24 | direction: PositionDirection, 25 | ) -> DriftResult { 26 | let coin_precision = 10_u128.pow(coin_decimals); 27 | 28 | match direction { 29 | PositionDirection::Long => limit_price 30 | .cast::()? 31 | .safe_div(PRICE_TO_QUOTE_PRECISION_RATIO)? 32 | .safe_mul(coin_lot_size.cast()?)? 33 | .safe_div(pc_lot_size.cast::()?.safe_mul(coin_precision)?)? 34 | .cast(), 35 | PositionDirection::Short => limit_price 36 | .cast::()? 37 | .safe_div(PRICE_TO_QUOTE_PRECISION_RATIO)? 38 | .safe_mul(coin_lot_size.cast()?)? 39 | .safe_div_ceil(pc_lot_size.cast::()?.safe_mul(coin_precision)?)? 40 | .cast(), 41 | } 42 | } 43 | 44 | // Max amount of quote to put deposit into serum 45 | pub fn calculate_serum_max_native_pc_quantity( 46 | serum_limit_price: u64, 47 | serum_coin_qty: u64, 48 | pc_lot_size: u64, 49 | ) -> DriftResult { 50 | pc_lot_size 51 | .safe_add(pc_lot_size / 2500)? // max 4bps 52 | .safe_mul(serum_limit_price)? 53 | .safe_mul(serum_coin_qty)? 54 | .safe_mul(10004)? 55 | .safe_div(10000) 56 | } 57 | 58 | pub fn calculate_price_from_serum_limit_price( 59 | limit_price: u64, 60 | pc_lot_size: u64, 61 | coin_decimals: u32, 62 | coin_lot_size: u64, 63 | ) -> DriftResult { 64 | let coin_precision = 10_u128.pow(coin_decimals); 65 | 66 | limit_price 67 | .cast::()? 68 | .safe_mul(pc_lot_size.cast::()?.safe_mul(coin_precision)?)? 69 | .safe_mul(PRICE_TO_QUOTE_PRECISION_RATIO)? 70 | .safe_div(coin_lot_size.cast()?)? 71 | .cast() 72 | } 73 | -------------------------------------------------------------------------------- /programs/drift/src/math/serum/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::position::PositionDirection; 2 | use crate::math::constants::{LAMPORTS_PER_SOL_U64, PRICE_PRECISION_U64}; 3 | use crate::math::serum::{ 4 | calculate_price_from_serum_limit_price, calculate_serum_limit_price, 5 | calculate_serum_max_coin_qty, calculate_serum_max_native_pc_quantity, 6 | }; 7 | 8 | #[test] 9 | fn test_calculate_serum_max_coin_qty() { 10 | let base_asset_amount = LAMPORTS_PER_SOL_U64; 11 | let coin_lot_size = 100000000; 12 | let max_coin_qty = calculate_serum_max_coin_qty(base_asset_amount, coin_lot_size).unwrap(); 13 | assert_eq!(max_coin_qty, 10) 14 | } 15 | 16 | #[test] 17 | fn test_calculate_serum_limit_price_bid() { 18 | let limit_price = 21359900; 19 | let pc_lot_size = 1_u64; 20 | let coin_lot_size = 1000000; 21 | let coin_decimals = 9; 22 | 23 | let direction = PositionDirection::Long; 24 | let serum_limit_price = calculate_serum_limit_price( 25 | limit_price, 26 | pc_lot_size, 27 | coin_decimals, 28 | coin_lot_size, 29 | direction, 30 | ) 31 | .unwrap(); 32 | 33 | assert_eq!(serum_limit_price, 21359); 34 | } 35 | 36 | #[test] 37 | fn test_calculate_serum_limit_price_ask() { 38 | let limit_price = 21359900; 39 | let pc_lot_size = 1_u64; 40 | let coin_lot_size = 1000000; 41 | let coin_decimals = 9; 42 | 43 | let direction = PositionDirection::Short; 44 | let serum_limit_price = calculate_serum_limit_price( 45 | limit_price, 46 | pc_lot_size, 47 | coin_decimals, 48 | coin_lot_size, 49 | direction, 50 | ) 51 | .unwrap(); 52 | 53 | assert_eq!(serum_limit_price, 21360); 54 | } 55 | 56 | #[test] 57 | fn test_calculate_serum_max_native_pc_quantity() { 58 | let serum_limit_price = 100000_u64; 59 | let serum_coin_qty = 10; 60 | let pc_lot_size = 100_u64; 61 | 62 | let max_native_pc_quantity = 63 | calculate_serum_max_native_pc_quantity(serum_limit_price, serum_coin_qty, pc_lot_size) 64 | .unwrap(); 65 | 66 | assert_eq!(max_native_pc_quantity, 100040000); // $100.04 67 | } 68 | 69 | #[test] 70 | fn test_calculate_price_from_serum_limit_price() { 71 | let serum_limit_price = 100000_u64; 72 | let pc_lot_size = 100_u64; 73 | let coin_lot_size = 100000000; 74 | let coin_decimals = 9; 75 | 76 | let price = calculate_price_from_serum_limit_price( 77 | serum_limit_price, 78 | pc_lot_size, 79 | coin_decimals, 80 | coin_lot_size, 81 | ) 82 | .unwrap(); 83 | 84 | assert_eq!(price, 100 * PRICE_PRECISION_U64); 85 | } 86 | -------------------------------------------------------------------------------- /programs/drift/src/math/spot_balance/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod test { 3 | use crate::math::spot_balance::{get_spot_balance, get_token_amount}; 4 | use crate::state::spot_market::{SpotBalanceType, SpotMarket}; 5 | use crate::SPOT_CUMULATIVE_INTEREST_PRECISION; 6 | 7 | #[test] 8 | fn bonk() { 9 | let spot_market = SpotMarket { 10 | cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, 11 | decimals: 5, 12 | ..SpotMarket::default_quote_market() 13 | }; 14 | 15 | let one_bonk = 10_u128.pow(spot_market.decimals); 16 | 17 | let balance = 18 | get_spot_balance(one_bonk, &spot_market, &SpotBalanceType::Deposit, false).unwrap(); 19 | 20 | let token_amount = 21 | get_token_amount(balance, &spot_market, &SpotBalanceType::Deposit).unwrap(); 22 | assert_eq!(token_amount, one_bonk); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /programs/drift/src/math/stats.rs: -------------------------------------------------------------------------------- 1 | use crate::error::DriftResult; 2 | use crate::math::casting::Cast; 3 | use crate::math::safe_math::SafeMath; 4 | use std::cmp::max; 5 | 6 | pub fn calculate_rolling_sum( 7 | data1: u64, 8 | data2: u64, 9 | weight1_numer: i64, 10 | weight1_denom: i64, 11 | ) -> DriftResult { 12 | // assumes that missing times are zeros (e.g. handle NaN as 0) 13 | let prev_twap_99 = data1 14 | .cast::()? 15 | .safe_mul(max(0, weight1_denom.safe_sub(weight1_numer)?).cast::()?)? 16 | .safe_div(weight1_denom.cast::()?)?; 17 | 18 | prev_twap_99.cast::()?.safe_add(data2) 19 | } 20 | 21 | pub fn calculate_weighted_average( 22 | data1: i64, 23 | data2: i64, 24 | weight1: i64, 25 | weight2: i64, 26 | ) -> DriftResult { 27 | let denominator = weight1.safe_add(weight2)?.cast::()?; 28 | let prev_twap_99 = data1.cast::()?.safe_mul(weight1.cast()?)?; 29 | let latest_price_01 = data2.cast::()?.safe_mul(weight2.cast()?)?; 30 | 31 | if weight1 == 0 { 32 | return Ok(data2); 33 | } 34 | 35 | if weight2 == 0 { 36 | return Ok(data1); 37 | } 38 | 39 | let bias: i64 = if weight2 > 1 { 40 | if latest_price_01 < prev_twap_99 { 41 | -1 42 | } else if latest_price_01 > prev_twap_99 { 43 | 1 44 | } else { 45 | 0 46 | } 47 | } else { 48 | 0 49 | }; 50 | 51 | let twap = prev_twap_99 52 | .safe_add(latest_price_01)? 53 | .safe_div(denominator)? 54 | .cast::()?; 55 | 56 | if twap == 0 && bias < 0 { 57 | return Ok(twap); 58 | } 59 | 60 | twap.safe_add(bias) 61 | } 62 | 63 | pub fn calculate_new_twap( 64 | current_price: i64, 65 | current_ts: i64, 66 | last_twap: i64, 67 | last_ts: i64, 68 | period: i64, 69 | ) -> DriftResult { 70 | let since_last = max(0_i64, current_ts.safe_sub(last_ts)?); 71 | let from_start = max(1_i64, period.safe_sub(since_last)?); 72 | 73 | calculate_weighted_average(current_price, last_twap, since_last, from_start) 74 | } 75 | -------------------------------------------------------------------------------- /programs/drift/src/signer.rs: -------------------------------------------------------------------------------- 1 | pub fn get_signer_seeds(nonce: &u8) -> [&[u8]; 2] { 2 | [b"drift_signer".as_ref(), bytemuck::bytes_of(nonce)] 3 | } 4 | -------------------------------------------------------------------------------- /programs/drift/src/state/fill_mode.rs: -------------------------------------------------------------------------------- 1 | use crate::error::DriftResult; 2 | use crate::math::auction::calculate_auction_price; 3 | use crate::math::casting::Cast; 4 | use crate::math::safe_math::SafeMath; 5 | use crate::state::user::Order; 6 | 7 | #[cfg(test)] 8 | mod tests; 9 | 10 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 11 | pub enum FillMode { 12 | Fill, 13 | PlaceAndMake, 14 | PlaceAndTake(bool, u8), 15 | Liquidation, 16 | } 17 | 18 | impl FillMode { 19 | pub fn get_limit_price( 20 | &self, 21 | order: &Order, 22 | valid_oracle_price: Option, 23 | slot: u64, 24 | tick_size: u64, 25 | is_prediction_market: bool, 26 | ) -> DriftResult> { 27 | match self { 28 | FillMode::Fill | FillMode::PlaceAndMake | FillMode::Liquidation => order 29 | .get_limit_price( 30 | valid_oracle_price, 31 | None, 32 | slot, 33 | tick_size, 34 | is_prediction_market, 35 | None, 36 | ), 37 | FillMode::PlaceAndTake(_, auction_duration_percentage) => { 38 | let auction_duration = order 39 | .auction_duration 40 | .cast::()? 41 | .safe_mul(auction_duration_percentage.min(&100).cast()?)? 42 | .safe_div(100)? 43 | .cast::()?; 44 | 45 | if order.has_auction() { 46 | calculate_auction_price( 47 | order, 48 | order.slot.safe_add(auction_duration)?, 49 | tick_size, 50 | valid_oracle_price, 51 | is_prediction_market, 52 | ) 53 | .map(Some) 54 | } else { 55 | order.get_limit_price( 56 | valid_oracle_price, 57 | None, 58 | slot, 59 | tick_size, 60 | is_prediction_market, 61 | None, 62 | ) 63 | } 64 | } 65 | } 66 | } 67 | 68 | pub fn is_liquidation(&self) -> bool { 69 | self == &FillMode::Liquidation 70 | } 71 | 72 | pub fn is_ioc(&self) -> bool { 73 | matches!(self, FillMode::PlaceAndTake(true, _)) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /programs/drift/src/state/fill_mode/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::state::fill_mode::FillMode; 2 | use crate::state::user::{Order, OrderType}; 3 | use crate::{PositionDirection, PRICE_PRECISION_I64, PRICE_PRECISION_U64}; 4 | 5 | #[test] 6 | fn test() { 7 | let market_order = Order { 8 | order_type: OrderType::Market, 9 | direction: PositionDirection::Long, 10 | auction_start_price: 100 * PRICE_PRECISION_I64, 11 | auction_end_price: 110 * PRICE_PRECISION_I64, 12 | price: 120 * PRICE_PRECISION_U64, 13 | slot: 0, 14 | auction_duration: 10, 15 | ..Order::default() 16 | }; 17 | 18 | let fill_mode = FillMode::Fill; 19 | 20 | let slot = 0; 21 | let oracle_price = Some(100 * PRICE_PRECISION_I64); 22 | let tick_size = 1; 23 | 24 | let limit_price = fill_mode 25 | .get_limit_price(&market_order, oracle_price, slot, tick_size, false) 26 | .unwrap(); 27 | 28 | assert_eq!(limit_price, Some(100 * PRICE_PRECISION_U64)); 29 | 30 | let place_and_take_mode = FillMode::PlaceAndTake(false, 100); 31 | 32 | let limit_price = place_and_take_mode 33 | .get_limit_price(&market_order, oracle_price, slot, tick_size, false) 34 | .unwrap(); 35 | 36 | assert_eq!(limit_price, Some(110 * PRICE_PRECISION_U64)); 37 | 38 | let limit_order = Order { 39 | order_type: OrderType::Limit, 40 | direction: PositionDirection::Long, 41 | price: 120 * PRICE_PRECISION_U64, 42 | slot: 0, 43 | ..Order::default() 44 | }; 45 | 46 | let limit_price = place_and_take_mode 47 | .get_limit_price(&limit_order, oracle_price, slot, tick_size, false) 48 | .unwrap(); 49 | 50 | assert_eq!(limit_price, Some(120 * PRICE_PRECISION_U64)); 51 | } 52 | -------------------------------------------------------------------------------- /programs/drift/src/state/fulfillment.rs: -------------------------------------------------------------------------------- 1 | use solana_program::pubkey::Pubkey; 2 | 3 | #[derive(Debug, PartialEq, Eq)] 4 | pub enum PerpFulfillmentMethod { 5 | AMM(Option), 6 | Match(Pubkey, u16, u64), 7 | } 8 | 9 | #[derive(Debug)] 10 | pub enum SpotFulfillmentMethod { 11 | ExternalMarket, 12 | Match(Pubkey, u16), 13 | } 14 | -------------------------------------------------------------------------------- /programs/drift/src/state/fulfillment_params/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod drift; 2 | pub mod openbook_v2; 3 | pub mod phoenix; 4 | pub mod serum; 5 | -------------------------------------------------------------------------------- /programs/drift/src/state/high_leverage_mode_config.rs: -------------------------------------------------------------------------------- 1 | use crate::error::DriftResult; 2 | use crate::error::ErrorCode; 3 | use crate::math::safe_math::SafeMath; 4 | use crate::state::traits::Size; 5 | use crate::validate; 6 | use anchor_lang::prelude::*; 7 | 8 | use super::user::MarginMode; 9 | use super::user::User; 10 | 11 | #[account(zero_copy(unsafe))] 12 | #[derive(Default, Eq, PartialEq, Debug)] 13 | #[repr(C)] 14 | pub struct HighLeverageModeConfig { 15 | pub max_users: u32, 16 | pub current_users: u32, 17 | pub reduce_only: u8, 18 | pub padding: [u8; 31], 19 | } 20 | 21 | // implement SIZE const for ProtocolIfSharesTransferConfig 22 | impl Size for HighLeverageModeConfig { 23 | const SIZE: usize = 48; 24 | } 25 | 26 | impl HighLeverageModeConfig { 27 | pub fn validate(&self) -> DriftResult { 28 | validate!( 29 | self.current_users <= self.max_users, 30 | ErrorCode::InvalidHighLeverageModeConfig, 31 | "current users ({}) > max users ({})", 32 | self.current_users, 33 | self.max_users 34 | )?; 35 | 36 | Ok(()) 37 | } 38 | 39 | pub fn is_reduce_only(&self) -> bool { 40 | self.reduce_only > 0 41 | } 42 | 43 | pub fn is_full(&self) -> bool { 44 | (self.current_users >= self.max_users) || self.is_reduce_only() 45 | } 46 | 47 | pub fn update_user(&mut self, user: &mut User) -> DriftResult { 48 | if user.margin_mode == MarginMode::HighLeverage { 49 | return Ok(()); 50 | } 51 | 52 | user.margin_mode = MarginMode::HighLeverage; 53 | 54 | validate!( 55 | !self.is_reduce_only(), 56 | ErrorCode::DefaultError, 57 | "high leverage mode config reduce only" 58 | )?; 59 | 60 | self.current_users = self.current_users.safe_add(1)?; 61 | 62 | self.validate()?; 63 | 64 | Ok(()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /programs/drift/src/state/insurance_fund_stake/tests.rs: -------------------------------------------------------------------------------- 1 | mod transfer_config { 2 | use crate::state::insurance_fund_stake::ProtocolIfSharesTransferConfig; 3 | use solana_program::pubkey::Pubkey; 4 | 5 | #[test] 6 | fn validate_signer() { 7 | let mut config = ProtocolIfSharesTransferConfig::default(); 8 | 9 | let signer = Pubkey::new_unique(); 10 | 11 | assert!(config.validate_signer(&signer).is_err()); 12 | 13 | let signer = Pubkey::default(); 14 | 15 | assert!(config.validate_signer(&signer).is_err()); 16 | 17 | let signer = Pubkey::new_unique(); 18 | config.whitelisted_signers[0] = signer; 19 | 20 | assert!(config.validate_signer(&signer).is_ok()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /programs/drift/src/state/load_ref.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use anchor_lang::ZeroCopy; 3 | use arrayref::array_ref; 4 | use std::cell::{Ref, RefMut}; 5 | use std::mem; 6 | 7 | pub fn load_ref<'a, T: ZeroCopy + Owner>(account_info: &'a AccountInfo) -> Result> { 8 | let data = account_info.try_borrow_data()?; 9 | if data.len() < T::discriminator().len() { 10 | return Err(ErrorCode::AccountDiscriminatorNotFound.into()); 11 | } 12 | 13 | let disc_bytes = array_ref![data, 0, 8]; 14 | if disc_bytes != &T::discriminator() { 15 | return Err(ErrorCode::AccountDiscriminatorMismatch.into()); 16 | } 17 | 18 | Ok(Ref::map(data, |data| { 19 | bytemuck::from_bytes(&data[8..mem::size_of::() + 8]) 20 | })) 21 | } 22 | 23 | pub fn load_ref_mut<'a, T: ZeroCopy + Owner>( 24 | account_info: &'a AccountInfo, 25 | ) -> Result> { 26 | let data = account_info.try_borrow_mut_data()?; 27 | if data.len() < T::discriminator().len() { 28 | return Err(ErrorCode::AccountDiscriminatorNotFound.into()); 29 | } 30 | 31 | let disc_bytes = array_ref![data, 0, 8]; 32 | if disc_bytes != &T::discriminator() { 33 | return Err(ErrorCode::AccountDiscriminatorMismatch.into()); 34 | } 35 | 36 | Ok(RefMut::map(data, |data| { 37 | bytemuck::from_bytes_mut(&mut data[8..mem::size_of::() + 8]) 38 | })) 39 | } 40 | -------------------------------------------------------------------------------- /programs/drift/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod events; 2 | pub mod fill_mode; 3 | pub mod fulfillment; 4 | pub mod fulfillment_params; 5 | pub mod high_leverage_mode_config; 6 | pub mod insurance_fund_stake; 7 | pub mod load_ref; 8 | pub mod margin_calculation; 9 | pub mod oracle; 10 | pub mod oracle_map; 11 | pub mod order_params; 12 | pub mod paused_operations; 13 | pub mod perp_market; 14 | pub mod perp_market_map; 15 | pub mod protected_maker_mode_config; 16 | pub mod pyth_lazer_oracle; 17 | pub mod settle_pnl_mode; 18 | pub mod signed_msg_user; 19 | pub mod spot_fulfillment_params; 20 | pub mod spot_market; 21 | pub mod spot_market_map; 22 | #[allow(clippy::module_inception)] 23 | pub mod state; 24 | pub mod traits; 25 | pub mod user; 26 | pub mod user_map; 27 | -------------------------------------------------------------------------------- /programs/drift/src/state/protected_maker_mode_config.rs: -------------------------------------------------------------------------------- 1 | use crate::error::DriftResult; 2 | use crate::error::ErrorCode; 3 | use crate::state::traits::Size; 4 | use crate::validate; 5 | use anchor_lang::prelude::*; 6 | 7 | #[account(zero_copy(unsafe))] 8 | #[derive(Default, Eq, PartialEq, Debug)] 9 | #[repr(C)] 10 | pub struct ProtectedMakerModeConfig { 11 | pub max_users: u32, 12 | pub current_users: u32, 13 | pub reduce_only: u8, 14 | pub padding: [u8; 31], 15 | } 16 | 17 | impl Size for ProtectedMakerModeConfig { 18 | const SIZE: usize = 48; 19 | } 20 | 21 | impl ProtectedMakerModeConfig { 22 | pub fn validate(&self) -> DriftResult { 23 | validate!( 24 | self.current_users <= self.max_users, 25 | ErrorCode::InvalidProtectedMakerModeConfig, 26 | "current users ({}) > max users ({})", 27 | self.current_users, 28 | self.max_users 29 | )?; 30 | 31 | Ok(()) 32 | } 33 | 34 | pub fn is_reduce_only(&self) -> bool { 35 | self.reduce_only > 0 36 | } 37 | } 38 | 39 | #[derive(Clone, Copy, Default)] 40 | pub struct ProtectedMakerParams { 41 | pub limit_price_divisor: u8, 42 | pub dynamic_offset: u64, 43 | pub tick_size: u64, 44 | } 45 | -------------------------------------------------------------------------------- /programs/drift/src/state/pyth_lazer_oracle.rs: -------------------------------------------------------------------------------- 1 | use crate::state::traits::Size; 2 | use anchor_lang::prelude::*; 3 | use solana_program::pubkey; 4 | 5 | pub const PYTH_LAZER_ORACLE_SEED: &[u8] = b"pyth_lazer"; 6 | pub const PYTH_LAZER_STORAGE_ID: Pubkey = pubkey!("3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL"); 7 | 8 | impl Size for PythLazerOracle { 9 | const SIZE: usize = 48; 10 | } 11 | 12 | #[account(zero_copy(unsafe))] 13 | #[derive(Default, Eq, PartialEq, Debug)] 14 | #[repr(C)] 15 | pub struct PythLazerOracle { 16 | pub price: i64, 17 | pub publish_time: u64, 18 | pub posted_slot: u64, 19 | pub exponent: i32, 20 | pub _padding: [u8; 4], 21 | pub conf: u64, 22 | } 23 | -------------------------------------------------------------------------------- /programs/drift/src/state/settle_pnl_mode.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{DriftResult, ErrorCode}; 2 | use crate::msg; 3 | use borsh::{BorshDeserialize, BorshSerialize}; 4 | use std::panic::Location; 5 | 6 | #[cfg(test)] 7 | mod tests; 8 | 9 | #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] 10 | pub enum SettlePnlMode { 11 | MustSettle, 12 | TrySettle, 13 | } 14 | 15 | impl SettlePnlMode { 16 | #[track_caller] 17 | #[inline(always)] 18 | pub fn result(self, error_code: ErrorCode, market_index: u16, msg: &str) -> DriftResult { 19 | let caller = Location::caller(); 20 | msg!(msg); 21 | msg!( 22 | "Error {:?} for market {} at {}:{}", 23 | error_code, 24 | market_index, 25 | caller.file(), 26 | caller.line() 27 | ); 28 | match self { 29 | SettlePnlMode::MustSettle => Err(error_code), 30 | SettlePnlMode::TrySettle => Ok(()), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /programs/drift/src/state/settle_pnl_mode/tests.rs: -------------------------------------------------------------------------------- 1 | mod test { 2 | use crate::error::ErrorCode; 3 | use crate::SettlePnlMode; 4 | 5 | #[test] 6 | fn test_must_settle_returns_err() { 7 | let mode = SettlePnlMode::MustSettle; 8 | let result = mode.result(ErrorCode::DefaultError, 0, "Must settle error"); 9 | assert_eq!(result, Err(ErrorCode::DefaultError)); 10 | } 11 | 12 | #[test] 13 | fn test_try_settle_returns_ok() { 14 | let mode = SettlePnlMode::TrySettle; 15 | let result = mode.result(ErrorCode::DefaultError, 0, "Try settle error"); 16 | assert_eq!(result, Ok(())); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /programs/drift/src/state/state/tests.rs: -------------------------------------------------------------------------------- 1 | mod get_init_user_fee { 2 | use crate::State; 3 | 4 | #[test] 5 | fn it_works() { 6 | let state = State::default(); 7 | let init_user_fee = state.get_init_user_fee().unwrap(); 8 | assert_eq!(init_user_fee, 0); 9 | 10 | let state = State { 11 | max_initialize_user_fee: 1, 12 | max_number_of_sub_accounts: 10, 13 | number_of_sub_accounts: 800, 14 | ..State::default() 15 | }; 16 | 17 | let max_number_of_sub_accounts = state.max_number_of_sub_accounts(); 18 | assert_eq!(max_number_of_sub_accounts, 1000); 19 | 20 | let init_user_fee = state.get_init_user_fee().unwrap(); 21 | assert_eq!(init_user_fee, 0); 22 | 23 | let state = State { 24 | max_initialize_user_fee: 1, 25 | max_number_of_sub_accounts: 10, 26 | number_of_sub_accounts: 900, 27 | ..State::default() 28 | }; 29 | 30 | let init_user_fee = state.get_init_user_fee().unwrap(); 31 | assert_eq!(init_user_fee, 5000000); 32 | 33 | let state = State { 34 | max_initialize_user_fee: 1, 35 | max_number_of_sub_accounts: 10, 36 | number_of_sub_accounts: 1000, 37 | ..State::default() 38 | }; 39 | 40 | let init_user_fee = state.get_init_user_fee().unwrap(); 41 | assert_eq!(init_user_fee, 10000000); 42 | 43 | let state = State { 44 | max_initialize_user_fee: 100, 45 | max_number_of_sub_accounts: 10, 46 | number_of_sub_accounts: 1000, 47 | ..State::default() 48 | }; 49 | 50 | let init_user_fee = state.get_init_user_fee().unwrap(); 51 | assert_eq!(init_user_fee, 1000000000); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /programs/drift/src/state/traits.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests; 3 | 4 | pub trait Size { 5 | const SIZE: usize; 6 | } 7 | 8 | pub trait MarketIndexOffset { 9 | const MARKET_INDEX_OFFSET: usize; 10 | } 11 | -------------------------------------------------------------------------------- /programs/drift/src/validation/fee_structure/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::state::state::FeeStructure; 2 | use crate::validation::fee_structure::validate_fee_structure; 3 | 4 | #[test] 5 | fn default_fee_structures() { 6 | let mut default_perp_fees = FeeStructure::perps_default(); 7 | default_perp_fees.flat_filler_fee = 3333; 8 | validate_fee_structure(&default_perp_fees).unwrap(); 9 | 10 | let mut default_spot_fees = FeeStructure::spot_default(); 11 | default_spot_fees.flat_filler_fee = 3333; 12 | validate_fee_structure(&default_spot_fees).unwrap(); 13 | } 14 | -------------------------------------------------------------------------------- /programs/drift/src/validation/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fee_structure; 2 | pub mod margin; 3 | pub mod order; 4 | pub mod perp_market; 5 | pub mod position; 6 | pub mod sig_verification; 7 | pub mod spot_market; 8 | pub mod user; 9 | pub mod whitelist; 10 | -------------------------------------------------------------------------------- /programs/drift/src/validation/position.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{DriftResult, ErrorCode}; 2 | use crate::math::casting::Cast; 3 | use crate::math::constants::MAX_OPEN_ORDERS; 4 | use crate::math::orders::is_multiple_of_step_size; 5 | use crate::msg; 6 | use crate::state::perp_market::PerpMarket; 7 | use crate::state::user::{PerpPosition, SpotPosition}; 8 | use crate::validate; 9 | 10 | pub fn validate_perp_position_with_perp_market( 11 | position: &PerpPosition, 12 | market: &PerpMarket, 13 | ) -> DriftResult { 14 | if position.lp_shares != 0 { 15 | validate!( 16 | position.per_lp_base == market.amm.per_lp_base, 17 | ErrorCode::InvalidPerpPositionDetected, 18 | "position/market per_lp_base unequal" 19 | )?; 20 | } 21 | 22 | validate!( 23 | position.market_index == market.market_index, 24 | ErrorCode::InvalidPerpPositionDetected, 25 | "position/market market_index unequal" 26 | )?; 27 | 28 | validate!( 29 | is_multiple_of_step_size( 30 | position.base_asset_amount.unsigned_abs().cast()?, 31 | market.amm.order_step_size 32 | )?, 33 | ErrorCode::InvalidPerpPositionDetected, 34 | "position not multiple of stepsize" 35 | )?; 36 | 37 | Ok(()) 38 | } 39 | 40 | pub fn validate_spot_position(position: &SpotPosition) -> DriftResult { 41 | validate!( 42 | position.open_orders <= MAX_OPEN_ORDERS, 43 | ErrorCode::InvalidSpotPositionDetected, 44 | "user spot={} position.open_orders={} is greater than MAX_OPEN_ORDERS={}", 45 | position.market_index, 46 | position.open_orders, 47 | MAX_OPEN_ORDERS, 48 | )?; 49 | 50 | validate!( 51 | position.open_bids >= 0, 52 | ErrorCode::InvalidSpotPositionDetected, 53 | "user spot={} position.open_bids={} is less than 0", 54 | position.market_index, 55 | position.open_bids, 56 | )?; 57 | 58 | validate!( 59 | position.open_asks <= 0, 60 | ErrorCode::InvalidSpotPositionDetected, 61 | "user spot={} position.open_asks={} is greater than 0", 62 | position.market_index, 63 | position.open_asks, 64 | )?; 65 | 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /programs/drift/src/validation/spot_market.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{DriftResult, ErrorCode}; 2 | use crate::math::constants::SPOT_UTILIZATION_PRECISION_U32; 3 | use crate::msg; 4 | use crate::validate; 5 | 6 | pub fn validate_borrow_rate( 7 | optimal_utilization: u32, 8 | optimal_borrow_rate: u32, 9 | max_borrow_rate: u32, 10 | min_borrow_rate: u32, 11 | ) -> DriftResult { 12 | validate!( 13 | optimal_utilization <= SPOT_UTILIZATION_PRECISION_U32, 14 | ErrorCode::InvalidSpotMarketInitialization, 15 | "For spot market, optimal_utilization must be < {}", 16 | SPOT_UTILIZATION_PRECISION_U32 17 | )?; 18 | 19 | validate!( 20 | optimal_borrow_rate <= max_borrow_rate, 21 | ErrorCode::InvalidSpotMarketInitialization, 22 | "For spot market, optimal borrow rate ({}) must be <= max borrow rate ({})", 23 | optimal_borrow_rate, 24 | max_borrow_rate 25 | )?; 26 | 27 | validate!( 28 | optimal_borrow_rate >= min_borrow_rate, 29 | ErrorCode::InvalidSpotMarketInitialization, 30 | "For spot market, optimal borrow rate ({}) must be >= min borrow rate ({})", 31 | optimal_borrow_rate, 32 | min_borrow_rate 33 | )?; 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /programs/drift/src/validation/whitelist.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{DriftResult, ErrorCode}; 2 | use crate::msg; 3 | use crate::validate; 4 | use anchor_lang::prelude::{Account, Pubkey}; 5 | use anchor_spl::token::TokenAccount; 6 | 7 | pub fn validate_whitelist_token( 8 | whitelist_token: Account, 9 | whitelist_mint: &Pubkey, 10 | authority: &Pubkey, 11 | ) -> DriftResult { 12 | validate!( 13 | &whitelist_token.owner == authority, 14 | ErrorCode::InvalidWhitelistToken, 15 | "Whitelist token owner ({:?}) does not match authority ({:?})", 16 | whitelist_token.owner, 17 | authority 18 | )?; 19 | 20 | validate!( 21 | &whitelist_token.mint == whitelist_mint, 22 | ErrorCode::InvalidWhitelistToken, 23 | "Token mint ({:?}) does not whitelist mint ({:?})", 24 | whitelist_token.mint, 25 | whitelist_mint 26 | )?; 27 | 28 | validate!( 29 | whitelist_token.amount > 0, 30 | ErrorCode::InvalidWhitelistToken, 31 | "Whitelist token amount must be > 0", 32 | )?; 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /programs/openbook_v2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openbook-v2-light" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "openbook_v2_light" 10 | path = "src/lib.rs" 11 | 12 | [features] 13 | default = ["cpi"] 14 | no-entrypoint = [] 15 | no-idl = [] 16 | no-log-ix-name = [] 17 | cpi = ["no-entrypoint"] 18 | 19 | 20 | [dependencies] 21 | anchor-lang = { version = "0.29.0", features = ["derive"] } 22 | borsh = {version = "0.10.3", features = ["const-generics", "default"]} 23 | bytemuck = { version = "1.4.0" , features = ["derive", "min_const_generics"]} -------------------------------------------------------------------------------- /programs/openbook_v2/README.md: -------------------------------------------------------------------------------- 1 | 2 | Openbook V2 integration to add ability to fulfill via openbook v2 liquidity 3 | 4 | to run integration test - integration.rs: 5 | ``` 6 | anchor build 7 | cargo test-sbf --package openbook-v2-light --test integration 8 | ``` 9 | 10 | #### Please note integration test will not run on Apple M x chips - will throw error during deserializing of SpotMarket data -------------------------------------------------------------------------------- /programs/openbook_v2/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const LEAF_NODE_TAG: u8 = 2; 2 | pub const OPEN_ORDERS_ACCOUNT_DISCRIMINATOR: [u8; 8] = [255, 194, 78, 123, 16, 105, 208, 165]; 3 | -------------------------------------------------------------------------------- /programs/openbook_v2/src/context.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Accounts)] 4 | pub struct PlaceTakeOrder<'info> { 5 | pub dummy_authority: Signer<'info>, 6 | } 7 | 8 | #[derive(Accounts)] 9 | pub struct CreateMarket<'info> { 10 | pub dummy_authority: Signer<'info>, 11 | } 12 | 13 | #[derive(Accounts)] 14 | pub struct CreateOpenOrdersIndexer<'info> { 15 | pub dummy_authority: Signer<'info>, 16 | } 17 | 18 | #[derive(Accounts)] 19 | pub struct CreateOpenOrdersAccount<'info> { 20 | pub dummy_authority: Signer<'info>, 21 | } 22 | 23 | #[derive(Accounts)] 24 | pub struct PlaceOrder<'info> { 25 | pub dummy_authority: Signer<'info>, 26 | } 27 | 28 | #[derive(Accounts)] 29 | pub struct ConsumeEvents<'info> { 30 | pub dummy_authority: Signer<'info>, 31 | } 32 | -------------------------------------------------------------------------------- /programs/openbook_v2/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::too_many_arguments)] 2 | 3 | use anchor_lang::prelude::{ 4 | borsh::{BorshDeserialize, BorshSerialize}, 5 | *, 6 | }; 7 | 8 | declare_id!("opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb"); 9 | 10 | pub mod account; 11 | // pub mod order; 12 | pub mod constants; 13 | pub mod context; 14 | 15 | pub use crate::account::*; 16 | pub use crate::constants::*; 17 | pub use crate::context::*; 18 | 19 | #[program] 20 | mod openbook_v2 { 21 | #![allow(dead_code)] 22 | #![allow(unused_variables)] 23 | 24 | use super::*; 25 | 26 | pub(crate) fn place_take_order( 27 | ctx: Context, 28 | side: Side, 29 | price_lots: i64, 30 | max_base_lots: i64, 31 | max_quote_lots_including_fees: i64, 32 | order_type: PlaceOrderType, 33 | limit: u8, 34 | ) -> Result<()> { 35 | Ok(()) 36 | } 37 | 38 | pub(crate) fn create_market( 39 | ctx: Context, 40 | name: String, 41 | oracle_config: OracleConfigParams, 42 | quote_lot_size: i64, 43 | base_lot_size: i64, 44 | maker_fee: i64, 45 | taker_fee: i64, 46 | time_expiry: i64, 47 | ) -> Result<()> { 48 | Ok(()) 49 | } 50 | 51 | pub(crate) fn create_open_orders_indexer(ctx: Context) -> Result<()> { 52 | Ok(()) 53 | } 54 | 55 | pub(crate) fn create_open_orders_account( 56 | ctx: Context, 57 | name: String, 58 | ) -> Result<()> { 59 | Ok(()) 60 | } 61 | 62 | pub(crate) fn place_order( 63 | ctx: Context, 64 | side: Side, 65 | price_lots: i64, 66 | max_base_lots: i64, 67 | max_quote_lots_including_fees: i64, 68 | client_order_id: u64, 69 | order_type: PlaceOrderType, 70 | expiry_timestamp: u64, 71 | self_trade_behavior: SelfTradeBehavior, 72 | limit: u8, 73 | ) -> Result<()> { 74 | Ok(()) 75 | } 76 | 77 | pub(crate) fn consume_events(ctx: Context, limit: u64) -> Result<()> { 78 | Ok(()) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /programs/pyth/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyth" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "pyth" 10 | 11 | [features] 12 | no-entrypoint = [] 13 | no-idl = [] 14 | cpi = ["no-entrypoint"] 15 | default = ["mainnet-beta"] 16 | mainnet-beta=[] 17 | anchor-test= [] 18 | 19 | [dependencies] 20 | anchor-lang = "0.29.0" 21 | arrayref = "0.3.6" 22 | bytemuck = { version = "1.4.0" } 23 | -------------------------------------------------------------------------------- /programs/pyth/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] -------------------------------------------------------------------------------- /programs/switchboard-on-demand/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "switchboard-on-demand" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "lib"] 8 | name = "switchboard_on_demand" 9 | 10 | [features] 11 | no-entrypoint = [] 12 | no-idl = [] 13 | cpi = ["no-entrypoint"] 14 | default = ["mainnet-beta"] 15 | mainnet-beta=[] 16 | anchor-test= [] 17 | 18 | [dependencies] 19 | anchor-lang = "0.29.0" 20 | bytemuck = { version = "1.4.0" } 21 | solana-program = "1.16" 22 | 23 | [dev-dependencies] 24 | base64 = "0.13.0" 25 | 26 | -------------------------------------------------------------------------------- /programs/switchboard/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "switchboard" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "lib"] 8 | name = "switchboard" 9 | 10 | [features] 11 | no-entrypoint = [] 12 | no-idl = [] 13 | cpi = ["no-entrypoint"] 14 | default = ["mainnet-beta"] 15 | mainnet-beta=[] 16 | anchor-test= [] 17 | 18 | [dependencies] 19 | anchor-lang = "0.29.0" 20 | 21 | [dev-dependencies] 22 | base64 = "0.13.0" 23 | 24 | -------------------------------------------------------------------------------- /programs/token_faucet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "token-faucet" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "token_faucet" 10 | 11 | [features] 12 | no-entrypoint = [] 13 | cpi = ["no-entrypoint"] 14 | mainnet-beta=[] 15 | anchor-test= [] 16 | 17 | [dependencies] 18 | anchor-lang = "0.29.0" 19 | solana-program = "1.16" 20 | anchor-spl = "0.29.0" 21 | bytemuck = { version = "1.4.0" } -------------------------------------------------------------------------------- /programs/token_faucet/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] -------------------------------------------------------------------------------- /sdk/.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | package-lock.json 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /sdk/VERSION: -------------------------------------------------------------------------------- 1 | 2.123.0-beta.0 -------------------------------------------------------------------------------- /sdk/get_events.ts: -------------------------------------------------------------------------------- 1 | import { Connection, Keypair, PublicKey } from '@solana/web3.js'; 2 | import { 3 | configs, 4 | DriftClient, 5 | Wallet, 6 | } from "@drift-labs/sdk"; 7 | 8 | 9 | async function main() { 10 | 11 | const driftConfig = configs['mainnet-beta']; 12 | const connection = new Connection('https://api.mainnet-beta.solana.com'); 13 | 14 | const driftClient = new DriftClient({ 15 | connection: connection, 16 | wallet: new Wallet(new Keypair()), 17 | programID: new PublicKey(driftConfig.DRIFT_PROGRAM_ID), 18 | userStats: true, 19 | env: 'mainnet-beta', 20 | }); 21 | console.log(`driftClientSubscribed: ${await driftClient.subscribe()}`); 22 | 23 | const txHash = "3gvGQufckXGHrFDv4dNWEXuXKRMy3NZkKHMyFrAhLoYScaXXTGCp9vq58kWkfyJ8oDYZrz4bTyGayjUy9PKigeLS"; 24 | 25 | const tx = await driftClient.connection.getParsedTransaction(txHash, { 26 | commitment: "confirmed", 27 | maxSupportedTransactionVersion: 0, 28 | }); 29 | 30 | let logIdx = 0; 31 | // @ts-ignore 32 | for (const event of driftClient.program._events._eventParser.parseLogs(tx!.meta!.logMessages)) { 33 | console.log("----------------------------------------"); 34 | console.log(`Log ${logIdx++}`); 35 | console.log("----------------------------------------"); 36 | console.log(`${JSON.stringify(event, null, 2)}`); 37 | } 38 | 39 | console.log("========================================"); 40 | console.log("Raw transaction logs"); 41 | console.log("========================================"); 42 | console.log(JSON.stringify(tx!.meta!.logMessages, null, 2)); 43 | 44 | process.exit(0); 45 | } 46 | 47 | main().catch(console.error); 48 | -------------------------------------------------------------------------------- /sdk/scripts/postbuild.js: -------------------------------------------------------------------------------- 1 | // scripts/postbuild.js 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const yargs = require('yargs/yargs'); 5 | const { hideBin } = require('yargs/helpers'); 6 | 7 | const forceEnv = yargs(hideBin(process.argv)) 8 | .option('force-env', { 9 | type: 'string', 10 | description: 'Specify environment to force (node or browser)', 11 | choices: ['node', 'browser'] 12 | }) 13 | .argv?.forceEnv; 14 | 15 | const isomorphicPackages = ['grpc']; 16 | const environments = ['node', 'browser']; 17 | 18 | environments.forEach((environment) => { 19 | console.log(`Running ${environment} environment postbuild script`); 20 | console.log(``); 21 | 22 | isomorphicPackages.forEach((package) => { 23 | 24 | // We want to overwrite the base isomorphic files (the "target" files) with the concrete implementation code and definition files (the "source" files). 25 | 26 | const isomorphicFolderPath = path.join( 27 | __dirname, 28 | '..', 29 | 'lib', 30 | environment, 31 | 'isomorphic' 32 | ); 33 | 34 | const targetEnv = forceEnv ? forceEnv : environment; 35 | 36 | const filesToSwap = [ 37 | { 38 | source: `${package}.${targetEnv}.js`, 39 | target: `${package}.js`, 40 | }, 41 | { 42 | source: `${package}.${targetEnv}.d.ts`, 43 | target: `${package}.d.ts`, 44 | }, 45 | ]; 46 | 47 | for (const file of filesToSwap) { 48 | const sourcePath = path.join( 49 | isomorphicFolderPath, 50 | file.source 51 | ); 52 | 53 | const targetPath = path.join( 54 | isomorphicFolderPath, 55 | file.target 56 | ); 57 | 58 | try { 59 | const sourceContent = fs.readFileSync(sourcePath, 'utf8'); 60 | fs.writeFileSync(targetPath, sourceContent); 61 | } catch (error) { 62 | console.error( 63 | `Error processing isomophic package : ${package} :: ${error.message}` 64 | ); 65 | } 66 | } 67 | 68 | // Delete other environment files for safety 69 | environments.forEach((otherEnvironment) => { 70 | if (otherEnvironment === targetEnv) { 71 | return; 72 | } 73 | 74 | const otherTargetFiles = [ 75 | `${package}.${otherEnvironment}.js`, 76 | `${package}.${otherEnvironment}.d.ts`, 77 | ]; 78 | 79 | for (const otherTargetFile of otherTargetFiles) { 80 | const otherTargetPath = path.join( 81 | __dirname, 82 | '..', 83 | 'lib', 84 | environment, 85 | 'isomorphic', 86 | otherTargetFile 87 | ); 88 | 89 | if (fs.existsSync(otherTargetPath)) { 90 | fs.unlinkSync(otherTargetPath); 91 | } 92 | } 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /sdk/scripts/updateVersion.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const packageJson = require('../package.json'); 4 | 5 | const versionFilePath = path.join(__dirname, '..', 'VERSION'); 6 | 7 | let version = fs.readFileSync(versionFilePath, 'utf8'); 8 | version = version.replace(/\n/g, ''); 9 | 10 | const filesToReplace = [ 11 | // sdk/src/idl/drift.json 12 | path.join(__dirname, '..', 'src', 'idl', 'drift.json'), 13 | // programs/drift/Cargo.toml 14 | path.join(__dirname, '..', '..', 'programs', 'drift', 'Cargo.toml'), 15 | // Cargo.lock 16 | path.join(__dirname, '..', '..', 'Cargo.lock'), 17 | ] 18 | 19 | console.log(`Updating versions from ${version} to ${packageJson.version} in:`); 20 | for (const file of filesToReplace) { 21 | console.log(`* ${file}`); 22 | const fileContents = fs.readFileSync(file, 'utf8'); 23 | const newFileContents = fileContents.replace(version, packageJson.version); 24 | fs.writeFileSync(file, newFileContents); 25 | } 26 | 27 | fs.writeFileSync(versionFilePath, packageJson.version); 28 | console.log(""); -------------------------------------------------------------------------------- /sdk/src/accounts/basicUserAccountSubscriber.ts: -------------------------------------------------------------------------------- 1 | import { DataAndSlot, UserAccountEvents, UserAccountSubscriber } from './types'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | import StrictEventEmitter from 'strict-event-emitter-types'; 4 | import { EventEmitter } from 'events'; 5 | import { UserAccount } from '../types'; 6 | 7 | /** 8 | * Basic implementation of UserAccountSubscriber. It will only take in UserAccount 9 | * data during initialization and will not fetch or subscribe to updates. 10 | */ 11 | export class BasicUserAccountSubscriber implements UserAccountSubscriber { 12 | isSubscribed: boolean; 13 | eventEmitter: StrictEventEmitter; 14 | userAccountPublicKey: PublicKey; 15 | 16 | callbackId?: string; 17 | errorCallbackId?: string; 18 | 19 | user: DataAndSlot; 20 | 21 | public constructor( 22 | userAccountPublicKey: PublicKey, 23 | data?: UserAccount, 24 | slot?: number 25 | ) { 26 | this.isSubscribed = true; 27 | this.eventEmitter = new EventEmitter(); 28 | this.userAccountPublicKey = userAccountPublicKey; 29 | this.user = { data, slot }; 30 | } 31 | 32 | async subscribe(_userAccount?: UserAccount): Promise { 33 | return true; 34 | } 35 | 36 | async addToAccountLoader(): Promise {} 37 | 38 | async fetch(): Promise {} 39 | 40 | doesAccountExist(): boolean { 41 | return this.user !== undefined; 42 | } 43 | 44 | async unsubscribe(): Promise {} 45 | 46 | assertIsSubscribed(): void {} 47 | 48 | public getUserAccountAndSlot(): DataAndSlot { 49 | return this.user; 50 | } 51 | 52 | public updateData(userAccount: UserAccount, slot: number): void { 53 | if (!this.user || slot >= (this.user.slot ?? 0)) { 54 | this.user = { data: userAccount, slot }; 55 | this.eventEmitter.emit('userAccountUpdate', userAccount); 56 | this.eventEmitter.emit('update'); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /sdk/src/accounts/bulkUserStatsSubscription.ts: -------------------------------------------------------------------------------- 1 | import { UserStats } from '../userStats'; 2 | import { BulkAccountLoader } from './bulkAccountLoader'; 3 | import { PollingUserStatsAccountSubscriber } from './pollingUserStatsAccountSubscriber'; 4 | 5 | /** 6 | * @param userStats 7 | * @param accountLoader 8 | */ 9 | export async function bulkPollingUserStatsSubscribe( 10 | userStats: UserStats[], 11 | accountLoader: BulkAccountLoader 12 | ): Promise { 13 | if (userStats.length === 0) { 14 | await accountLoader.load(); 15 | return; 16 | } 17 | 18 | await Promise.all( 19 | userStats.map((userStat) => { 20 | return ( 21 | userStat.accountSubscriber as PollingUserStatsAccountSubscriber 22 | ).addToAccountLoader(); 23 | }) 24 | ); 25 | 26 | await accountLoader.load(); 27 | 28 | await Promise.all( 29 | userStats.map(async (userStat) => { 30 | return userStat.subscribe(); 31 | }) 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /sdk/src/accounts/bulkUserSubscription.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../user'; 2 | import { BulkAccountLoader } from './bulkAccountLoader'; 3 | import { PollingUserAccountSubscriber } from './pollingUserAccountSubscriber'; 4 | 5 | /** 6 | * @param users 7 | * @param accountLoader 8 | */ 9 | export async function bulkPollingUserSubscribe( 10 | users: User[], 11 | accountLoader: BulkAccountLoader 12 | ): Promise { 13 | if (users.length === 0) { 14 | await accountLoader.load(); 15 | return; 16 | } 17 | 18 | await Promise.all( 19 | users.map((user) => { 20 | return ( 21 | user.accountSubscriber as PollingUserAccountSubscriber 22 | ).addToAccountLoader(); 23 | }) 24 | ); 25 | 26 | await accountLoader.load(); 27 | 28 | await Promise.all( 29 | users.map(async (user) => { 30 | return user.subscribe(); 31 | }) 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /sdk/src/accounts/fetch.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { UserAccount, UserStatsAccount } from '../types'; 3 | import { 4 | getUserAccountPublicKey, 5 | getUserStatsAccountPublicKey, 6 | } from '../addresses/pda'; 7 | import { Program } from '@coral-xyz/anchor'; 8 | 9 | export async function fetchUserAccounts( 10 | connection: Connection, 11 | program: Program, 12 | authority: PublicKey, 13 | limit = 8 14 | ): Promise<(UserAccount | undefined)[]> { 15 | const userAccountPublicKeys = new Array(); 16 | for (let i = 0; i < limit; i++) { 17 | userAccountPublicKeys.push( 18 | await getUserAccountPublicKey(program.programId, authority, i) 19 | ); 20 | } 21 | 22 | return fetchUserAccountsUsingKeys(connection, program, userAccountPublicKeys); 23 | } 24 | 25 | export async function fetchUserAccountsUsingKeys( 26 | connection: Connection, 27 | program: Program, 28 | userAccountPublicKeys: PublicKey[] 29 | ): Promise<(UserAccount | undefined)[]> { 30 | const accountInfos = await connection.getMultipleAccountsInfo( 31 | userAccountPublicKeys, 32 | 'confirmed' 33 | ); 34 | 35 | return accountInfos.map((accountInfo) => { 36 | if (!accountInfo) { 37 | return undefined; 38 | } 39 | return program.account.user.coder.accounts.decodeUnchecked( 40 | 'User', 41 | accountInfo.data 42 | ) as UserAccount; 43 | }); 44 | } 45 | 46 | export async function fetchUserStatsAccount( 47 | connection: Connection, 48 | program: Program, 49 | authority: PublicKey 50 | ): Promise { 51 | const userStatsPublicKey = getUserStatsAccountPublicKey( 52 | program.programId, 53 | authority 54 | ); 55 | const accountInfo = await connection.getAccountInfo( 56 | userStatsPublicKey, 57 | 'confirmed' 58 | ); 59 | 60 | return accountInfo 61 | ? (program.account.user.coder.accounts.decodeUnchecked( 62 | 'UserStats', 63 | accountInfo.data 64 | ) as UserStatsAccount) 65 | : undefined; 66 | } 67 | -------------------------------------------------------------------------------- /sdk/src/accounts/grpcInsuranceFundStakeAccountSubscriber.ts: -------------------------------------------------------------------------------- 1 | import { GrpcConfigs } from './types'; 2 | import { Program } from '@coral-xyz/anchor'; 3 | import { PublicKey } from '@solana/web3.js'; 4 | import { InsuranceFundStake } from '../types'; 5 | import { WebSocketInsuranceFundStakeAccountSubscriber } from './webSocketInsuranceFundStakeAccountSubscriber'; 6 | import { grpcAccountSubscriber } from './grpcAccountSubscriber'; 7 | 8 | export class grpcInsuranceFundStakeAccountSubscriber extends WebSocketInsuranceFundStakeAccountSubscriber { 9 | private grpcConfigs: GrpcConfigs; 10 | 11 | public constructor( 12 | grpcConfigs: GrpcConfigs, 13 | program: Program, 14 | insuranceFundStakeAccountPublicKey: PublicKey, 15 | resubTimeoutMs?: number 16 | ) { 17 | super(program, insuranceFundStakeAccountPublicKey, resubTimeoutMs); 18 | this.grpcConfigs = grpcConfigs; 19 | } 20 | 21 | async subscribe( 22 | insuranceFundStakeAccount?: InsuranceFundStake 23 | ): Promise { 24 | if (this.isSubscribed) { 25 | return true; 26 | } 27 | 28 | this.insuranceFundStakeDataAccountSubscriber = 29 | await grpcAccountSubscriber.create( 30 | this.grpcConfigs, 31 | 'insuranceFundStake', 32 | this.program, 33 | this.insuranceFundStakeAccountPublicKey, 34 | undefined, 35 | { 36 | resubTimeoutMs: this.resubTimeoutMs, 37 | } 38 | ); 39 | 40 | if (insuranceFundStakeAccount) { 41 | this.insuranceFundStakeDataAccountSubscriber.setData( 42 | insuranceFundStakeAccount 43 | ); 44 | } 45 | 46 | await this.insuranceFundStakeDataAccountSubscriber.subscribe( 47 | (data: InsuranceFundStake) => { 48 | this.eventEmitter.emit('insuranceFundStakeAccountUpdate', data); 49 | this.eventEmitter.emit('update'); 50 | } 51 | ); 52 | 53 | this.eventEmitter.emit('update'); 54 | this.isSubscribed = true; 55 | return true; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /sdk/src/accounts/grpcUserAccountSubscriber.ts: -------------------------------------------------------------------------------- 1 | import { ResubOpts, GrpcConfigs } from './types'; 2 | import { Program } from '@coral-xyz/anchor'; 3 | import { PublicKey } from '@solana/web3.js'; 4 | import { UserAccount } from '../types'; 5 | import { WebSocketUserAccountSubscriber } from './webSocketUserAccountSubscriber'; 6 | import { grpcAccountSubscriber } from './grpcAccountSubscriber'; 7 | 8 | export class grpcUserAccountSubscriber extends WebSocketUserAccountSubscriber { 9 | private grpcConfigs: GrpcConfigs; 10 | 11 | public constructor( 12 | grpcConfigs: GrpcConfigs, 13 | program: Program, 14 | userAccountPublicKey: PublicKey, 15 | resubOpts?: ResubOpts 16 | ) { 17 | super(program, userAccountPublicKey, resubOpts); 18 | this.grpcConfigs = grpcConfigs; 19 | } 20 | 21 | async subscribe(userAccount?: UserAccount): Promise { 22 | if (this.isSubscribed) { 23 | return true; 24 | } 25 | 26 | this.userDataAccountSubscriber = await grpcAccountSubscriber.create( 27 | this.grpcConfigs, 28 | 'user', 29 | this.program, 30 | this.userAccountPublicKey, 31 | undefined, 32 | this.resubOpts 33 | ); 34 | 35 | if (userAccount) { 36 | this.userDataAccountSubscriber.setData(userAccount); 37 | } 38 | 39 | await this.userDataAccountSubscriber.subscribe((data: UserAccount) => { 40 | this.eventEmitter.emit('userAccountUpdate', data); 41 | this.eventEmitter.emit('update'); 42 | }); 43 | 44 | this.eventEmitter.emit('update'); 45 | this.isSubscribed = true; 46 | return true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sdk/src/accounts/grpcUserStatsAccountSubscriber.ts: -------------------------------------------------------------------------------- 1 | import { ResubOpts, GrpcConfigs } from './types'; 2 | import { Program } from '@coral-xyz/anchor'; 3 | import { PublicKey } from '@solana/web3.js'; 4 | import { UserStatsAccount } from '../types'; 5 | import { WebSocketUserStatsAccountSubscriber } from './webSocketUserStatsAccountSubsriber'; 6 | import { grpcAccountSubscriber } from './grpcAccountSubscriber'; 7 | 8 | export class grpcUserStatsAccountSubscriber extends WebSocketUserStatsAccountSubscriber { 9 | private grpcConfigs: GrpcConfigs; 10 | 11 | public constructor( 12 | grpcConfigs: GrpcConfigs, 13 | program: Program, 14 | userStatsAccountPublicKey: PublicKey, 15 | resubOpts?: ResubOpts 16 | ) { 17 | super(program, userStatsAccountPublicKey, resubOpts); 18 | this.grpcConfigs = grpcConfigs; 19 | } 20 | 21 | async subscribe(userStatsAccount?: UserStatsAccount): Promise { 22 | if (this.isSubscribed) { 23 | return true; 24 | } 25 | 26 | this.userStatsAccountSubscriber = await grpcAccountSubscriber.create( 27 | this.grpcConfigs, 28 | 'userStats', 29 | this.program, 30 | this.userStatsAccountPublicKey, 31 | undefined, 32 | this.resubOpts 33 | ); 34 | 35 | if (userStatsAccount) { 36 | this.userStatsAccountSubscriber.setData(userStatsAccount); 37 | } 38 | 39 | await this.userStatsAccountSubscriber.subscribe( 40 | (data: UserStatsAccount) => { 41 | this.eventEmitter.emit('userStatsAccountUpdate', data); 42 | this.eventEmitter.emit('update'); 43 | } 44 | ); 45 | 46 | this.eventEmitter.emit('update'); 47 | this.isSubscribed = true; 48 | return true; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sdk/src/accounts/oneShotUserAccountSubscriber.ts: -------------------------------------------------------------------------------- 1 | import { Commitment, PublicKey } from '@solana/web3.js'; 2 | import { UserAccount } from '../types'; 3 | import { BasicUserAccountSubscriber } from './basicUserAccountSubscriber'; 4 | import { Program } from '@coral-xyz/anchor'; 5 | import { UserAccountSubscriber } from './types'; 6 | 7 | /** 8 | * Simple implementation of UserAccountSubscriber. It will fetch the UserAccount 9 | * date on subscribe (or call to fetch) if no account data is provided on init. 10 | * Expect to use only 1 RPC call unless you call fetch repeatedly. 11 | */ 12 | export class OneShotUserAccountSubscriber 13 | extends BasicUserAccountSubscriber 14 | implements UserAccountSubscriber 15 | { 16 | program: Program; 17 | commitment: Commitment; 18 | 19 | public constructor( 20 | program: Program, 21 | userAccountPublicKey: PublicKey, 22 | data?: UserAccount, 23 | slot?: number, 24 | commitment?: Commitment 25 | ) { 26 | super(userAccountPublicKey, data, slot); 27 | this.program = program; 28 | this.commitment = commitment ?? 'confirmed'; 29 | } 30 | 31 | async subscribe(userAccount?: UserAccount): Promise { 32 | if (userAccount) { 33 | this.user = { data: userAccount, slot: this.user.slot }; 34 | return true; 35 | } 36 | 37 | await this.fetchIfUnloaded(); 38 | if (this.doesAccountExist()) { 39 | this.eventEmitter.emit('update'); 40 | } 41 | return true; 42 | } 43 | 44 | async fetchIfUnloaded(): Promise { 45 | if (this.user.data === undefined) { 46 | await this.fetch(); 47 | } 48 | } 49 | 50 | async fetch(): Promise { 51 | try { 52 | const dataAndContext = await this.program.account.user.fetchAndContext( 53 | this.userAccountPublicKey, 54 | this.commitment 55 | ); 56 | if (dataAndContext.context.slot > (this.user?.slot ?? 0)) { 57 | this.user = { 58 | data: dataAndContext.data as UserAccount, 59 | slot: dataAndContext.context.slot, 60 | }; 61 | } 62 | } catch (e) { 63 | console.error( 64 | `OneShotUserAccountSubscriber.fetch() UserAccount does not exist: ${e.message}` 65 | ); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /sdk/src/accounts/testBulkAccountLoader.ts: -------------------------------------------------------------------------------- 1 | import { AccountToLoad, BulkAccountLoader } from './bulkAccountLoader'; 2 | 3 | export class TestBulkAccountLoader extends BulkAccountLoader { 4 | async loadChunk(accountsToLoadChunks: AccountToLoad[][]): Promise { 5 | if (accountsToLoadChunks.length === 0) { 6 | return; 7 | } 8 | 9 | const accounts = []; 10 | for (const accountsToLoadChunk of accountsToLoadChunks) { 11 | for (const accountToLoad of accountsToLoadChunk) { 12 | const account = await this.connection.getAccountInfoAndContext( 13 | accountToLoad.publicKey, 14 | this.commitment 15 | ); 16 | accounts.push(account); 17 | const newSlot = account.context.slot; 18 | if (newSlot > this.mostRecentSlot) { 19 | this.mostRecentSlot = newSlot; 20 | } 21 | 22 | if (accountToLoad.callbacks.size === 0) { 23 | return; 24 | } 25 | 26 | const key = accountToLoad.publicKey.toBase58(); 27 | const prev = this.bufferAndSlotMap.get(key); 28 | 29 | if (prev && newSlot < prev.slot) { 30 | return; 31 | } 32 | 33 | let newBuffer: Buffer | undefined = undefined; 34 | 35 | if (account.value) { 36 | newBuffer = account.value.data; 37 | } 38 | 39 | if (!prev) { 40 | this.bufferAndSlotMap.set(key, { slot: newSlot, buffer: newBuffer }); 41 | this.handleAccountCallbacks(accountToLoad, newBuffer, newSlot); 42 | return; 43 | } 44 | 45 | const oldBuffer = prev.buffer; 46 | if (newBuffer && (!oldBuffer || !newBuffer.equals(oldBuffer))) { 47 | this.bufferAndSlotMap.set(key, { slot: newSlot, buffer: newBuffer }); 48 | this.handleAccountCallbacks(accountToLoad, newBuffer, newSlot); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /sdk/src/accounts/utils.ts: -------------------------------------------------------------------------------- 1 | import { DataAndSlot } from './types'; 2 | import { isVariant, PerpMarketAccount, SpotMarketAccount } from '../types'; 3 | import { OracleInfo } from '../oracles/types'; 4 | import { getOracleId } from '../oracles/oracleId'; 5 | 6 | export function capitalize(value: string): string { 7 | return value[0].toUpperCase() + value.slice(1); 8 | } 9 | 10 | export function findDelistedPerpMarketsAndOracles( 11 | perpMarkets: DataAndSlot[], 12 | spotMarkets: DataAndSlot[] 13 | ): { perpMarketIndexes: number[]; oracles: OracleInfo[] } { 14 | const delistedPerpMarketIndexes = []; 15 | const delistedOracles: OracleInfo[] = []; 16 | for (const perpMarket of perpMarkets) { 17 | if (!perpMarket.data) { 18 | continue; 19 | } 20 | 21 | if (isVariant(perpMarket.data.status, 'delisted')) { 22 | delistedPerpMarketIndexes.push(perpMarket.data.marketIndex); 23 | delistedOracles.push({ 24 | publicKey: perpMarket.data.amm.oracle, 25 | source: perpMarket.data.amm.oracleSource, 26 | }); 27 | } 28 | } 29 | 30 | // make sure oracle isn't used by spot market 31 | const filteredDelistedOracles = []; 32 | for (const delistedOracle of delistedOracles) { 33 | let isUsedBySpotMarket = false; 34 | for (const spotMarket of spotMarkets) { 35 | if (!spotMarket.data) { 36 | continue; 37 | } 38 | 39 | const delistedOracleId = getOracleId( 40 | delistedOracle.publicKey, 41 | delistedOracle.source 42 | ); 43 | const spotMarketOracleId = getOracleId( 44 | spotMarket.data.oracle, 45 | spotMarket.data.oracleSource 46 | ); 47 | if (spotMarketOracleId === delistedOracleId) { 48 | isUsedBySpotMarket = true; 49 | break; 50 | } 51 | } 52 | 53 | if (!isUsedBySpotMarket) { 54 | filteredDelistedOracles.push(delistedOracle); 55 | } 56 | } 57 | 58 | return { 59 | perpMarketIndexes: delistedPerpMarketIndexes, 60 | oracles: filteredDelistedOracles, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /sdk/src/addresses/marketAddresses.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { getPerpMarketPublicKey } from './pda'; 3 | 4 | const CACHE = new Map(); 5 | export async function getMarketAddress( 6 | programId: PublicKey, 7 | marketIndex: number 8 | ): Promise { 9 | const cacheKey = `${programId.toString()}-${marketIndex.toString()}`; 10 | if (CACHE.has(cacheKey)) { 11 | return CACHE.get(cacheKey); 12 | } 13 | 14 | const publicKey = await getPerpMarketPublicKey(programId, marketIndex); 15 | CACHE.set(cacheKey, publicKey); 16 | return publicKey; 17 | } 18 | -------------------------------------------------------------------------------- /sdk/src/assert/assert.ts: -------------------------------------------------------------------------------- 1 | export function assert(condition: boolean, error?: string): void { 2 | if (!condition) { 3 | throw new Error(error || 'Unspecified AssertionError'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /sdk/src/auctionSubscriber/auctionSubscriber.ts: -------------------------------------------------------------------------------- 1 | import { AuctionSubscriberConfig, AuctionSubscriberEvents } from './types'; 2 | import { DriftClient } from '../driftClient'; 3 | import { getUserFilter, getUserWithAuctionFilter } from '../memcmp'; 4 | import StrictEventEmitter from 'strict-event-emitter-types'; 5 | import { EventEmitter } from 'events'; 6 | import { UserAccount } from '../types'; 7 | import { ConfirmOptions, Context, PublicKey } from '@solana/web3.js'; 8 | import { WebSocketProgramAccountSubscriber } from '../accounts/webSocketProgramAccountSubscriber'; 9 | import { ResubOpts } from '../accounts/types'; 10 | 11 | export class AuctionSubscriber { 12 | private driftClient: DriftClient; 13 | private opts: ConfirmOptions; 14 | private resubOpts?: ResubOpts; 15 | 16 | eventEmitter: StrictEventEmitter; 17 | private subscriber: WebSocketProgramAccountSubscriber; 18 | 19 | constructor({ 20 | driftClient, 21 | opts, 22 | resubTimeoutMs, 23 | logResubMessages, 24 | }: AuctionSubscriberConfig) { 25 | this.driftClient = driftClient; 26 | this.opts = opts || this.driftClient.opts; 27 | this.eventEmitter = new EventEmitter(); 28 | this.resubOpts = { resubTimeoutMs, logResubMessages }; 29 | } 30 | 31 | public async subscribe() { 32 | if (!this.subscriber) { 33 | this.subscriber = new WebSocketProgramAccountSubscriber( 34 | 'AuctionSubscriber', 35 | 'User', 36 | this.driftClient.program, 37 | this.driftClient.program.account.user.coder.accounts.decode.bind( 38 | this.driftClient.program.account.user.coder.accounts 39 | ), 40 | { 41 | filters: [getUserFilter(), getUserWithAuctionFilter()], 42 | commitment: this.opts.commitment, 43 | }, 44 | this.resubOpts 45 | ); 46 | } 47 | 48 | await this.subscriber.subscribe( 49 | (accountId: PublicKey, data: UserAccount, context: Context) => { 50 | this.eventEmitter.emit( 51 | 'onAccountUpdate', 52 | data, 53 | accountId, 54 | context.slot 55 | ); 56 | } 57 | ); 58 | } 59 | 60 | public async unsubscribe() { 61 | if (!this.subscriber) { 62 | return; 63 | } 64 | this.subscriber.unsubscribe(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /sdk/src/auctionSubscriber/auctionSubscriberGrpc.ts: -------------------------------------------------------------------------------- 1 | import { AuctionSubscriberConfig, AuctionSubscriberEvents } from './types'; 2 | import { DriftClient } from '../driftClient'; 3 | import { getUserFilter, getUserWithAuctionFilter } from '../memcmp'; 4 | import StrictEventEmitter from 'strict-event-emitter-types'; 5 | import { EventEmitter } from 'events'; 6 | import { UserAccount } from '../types'; 7 | import { ConfirmOptions, Context, PublicKey } from '@solana/web3.js'; 8 | import { WebSocketProgramAccountSubscriber } from '../accounts/webSocketProgramAccountSubscriber'; 9 | import { GrpcConfigs, ResubOpts } from '../accounts/types'; 10 | import { grpcProgramAccountSubscriber } from '../accounts/grpcProgramAccountSubscriber'; 11 | 12 | export class AuctionSubscriberGrpc { 13 | private driftClient: DriftClient; 14 | private opts: ConfirmOptions; 15 | private resubOpts?: ResubOpts; 16 | private grpcConfigs?: GrpcConfigs; 17 | 18 | eventEmitter: StrictEventEmitter; 19 | private subscriber: WebSocketProgramAccountSubscriber; 20 | 21 | constructor({ 22 | driftClient, 23 | opts, 24 | grpcConfigs, 25 | resubTimeoutMs, 26 | logResubMessages, 27 | }: AuctionSubscriberConfig) { 28 | this.driftClient = driftClient; 29 | this.opts = opts || this.driftClient.opts; 30 | this.eventEmitter = new EventEmitter(); 31 | this.resubOpts = { resubTimeoutMs, logResubMessages }; 32 | this.grpcConfigs = grpcConfigs; 33 | } 34 | 35 | public async subscribe() { 36 | if (!this.subscriber) { 37 | this.subscriber = await grpcProgramAccountSubscriber.create( 38 | this.grpcConfigs, 39 | 'AuctionSubscriber', 40 | 'User', 41 | this.driftClient.program, 42 | this.driftClient.program.account.user.coder.accounts.decode.bind( 43 | this.driftClient.program.account.user.coder.accounts 44 | ), 45 | { 46 | filters: [getUserFilter(), getUserWithAuctionFilter()], 47 | }, 48 | this.resubOpts 49 | ); 50 | } 51 | 52 | await this.subscriber.subscribe( 53 | (accountId: PublicKey, data: UserAccount, context: Context) => { 54 | this.eventEmitter.emit( 55 | 'onAccountUpdate', 56 | data, 57 | accountId, 58 | context.slot 59 | ); 60 | } 61 | ); 62 | } 63 | 64 | public async unsubscribe() { 65 | if (!this.subscriber) { 66 | return; 67 | } 68 | this.subscriber.unsubscribe(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sdk/src/auctionSubscriber/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './auctionSubscriber'; 3 | export * from './auctionSubscriberGrpc'; 4 | -------------------------------------------------------------------------------- /sdk/src/auctionSubscriber/types.ts: -------------------------------------------------------------------------------- 1 | import { GrpcConfigs } from '../accounts/types'; 2 | import { DriftClient } from '../driftClient'; 3 | import { UserAccount } from '../types'; 4 | import { ConfirmOptions, PublicKey } from '@solana/web3.js'; 5 | 6 | export type AuctionSubscriberConfig = { 7 | driftClient: DriftClient; 8 | opts?: ConfirmOptions; 9 | resubTimeoutMs?: number; 10 | logResubMessages?: boolean; 11 | grpcConfigs?: GrpcConfigs; 12 | }; 13 | 14 | export interface AuctionSubscriberEvents { 15 | onAccountUpdate: ( 16 | account: UserAccount, 17 | pubkey: PublicKey, 18 | slot: number 19 | ) => void; 20 | } 21 | -------------------------------------------------------------------------------- /sdk/src/blockhashSubscriber/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BlockhashSubscriber'; 2 | -------------------------------------------------------------------------------- /sdk/src/blockhashSubscriber/types.ts: -------------------------------------------------------------------------------- 1 | import { Commitment, Connection } from '@solana/web3.js'; 2 | 3 | export type BlockhashSubscriberConfig = { 4 | /// rpcUrl to poll block hashes from, one of rpcUrl or Connection must provided 5 | rpcUrl?: string; 6 | /// connection to poll block hashes from, one of rpcUrl or Connection must provided 7 | connection?: Connection; 8 | /// commitment to poll block hashes with, default is 'confirmed' 9 | commitment?: Commitment; 10 | /// interval to poll block hashes, default is 1000 ms 11 | updateIntervalMs?: number; 12 | }; 13 | -------------------------------------------------------------------------------- /sdk/src/constants/txConstants.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | 3 | export const NOT_CONFIRMED_ERROR_CODE = -1001; 4 | export const FUEL_RESET_LOG_ACCOUNT = new PublicKey( 5 | 'FuE1gqp2fzw2sDNLrbZqKsqpphJcoSW6HPaSJjGd4RZ4' 6 | ); 7 | -------------------------------------------------------------------------------- /sdk/src/dlob/types.ts: -------------------------------------------------------------------------------- 1 | import { DLOB } from './DLOB'; 2 | import { DriftClient } from '../driftClient'; 3 | import { ProtectedMakerParams } from '../types'; 4 | import { MarketTypeStr } from '../types'; 5 | 6 | export type DLOBSubscriptionConfig = { 7 | driftClient: DriftClient; 8 | dlobSource: DLOBSource; 9 | slotSource: SlotSource; 10 | updateFrequency: number; 11 | protectedMakerView?: boolean; 12 | }; 13 | 14 | export interface DLOBSubscriberEvents { 15 | update: (dlob: DLOB) => void; 16 | error: (e: Error) => void; 17 | } 18 | 19 | export interface DLOBSource { 20 | getDLOB( 21 | slot: number, 22 | protectedMakerParamsMap?: ProtectMakerParamsMap 23 | ): Promise; 24 | } 25 | 26 | export interface SlotSource { 27 | getSlot(): number; 28 | } 29 | 30 | export type ProtectMakerParamsMap = { 31 | [marketType in MarketTypeStr]: Map; 32 | }; 33 | -------------------------------------------------------------------------------- /sdk/src/driftClientConfig.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Commitment, 3 | ConfirmOptions, 4 | Connection, 5 | PublicKey, 6 | TransactionVersion, 7 | } from '@solana/web3.js'; 8 | import { IWallet, TxParams } from './types'; 9 | import { OracleInfo } from './oracles/types'; 10 | import { BulkAccountLoader } from './accounts/bulkAccountLoader'; 11 | import { DriftEnv } from './config'; 12 | import { TxSender } from './tx/types'; 13 | import { TxHandler, TxHandlerConfig } from './tx/txHandler'; 14 | import { DelistedMarketSetting, GrpcConfigs } from './accounts/types'; 15 | 16 | export type DriftClientConfig = { 17 | connection: Connection; 18 | wallet: IWallet; 19 | env?: DriftEnv; 20 | programID?: PublicKey; 21 | accountSubscription?: DriftClientSubscriptionConfig; 22 | opts?: ConfirmOptions; 23 | txSender?: TxSender; 24 | txHandler?: TxHandler; 25 | subAccountIds?: number[]; 26 | activeSubAccountId?: number; 27 | perpMarketIndexes?: number[]; 28 | spotMarketIndexes?: number[]; 29 | /** @deprecated use marketLookupTables */ 30 | marketLookupTable?: PublicKey; 31 | marketLookupTables?: PublicKey[]; 32 | oracleInfos?: OracleInfo[]; 33 | userStats?: boolean; 34 | authority?: PublicKey; // explicitly pass an authority if signer is delegate 35 | includeDelegates?: boolean; // flag for whether to load delegate accounts as well 36 | authoritySubAccountMap?: Map; // if passed this will override subAccountIds and includeDelegates 37 | skipLoadUsers?: boolean; // if passed to constructor, no user accounts will be loaded. they will load if updateWallet is called afterwards. 38 | txVersion?: TransactionVersion; // which tx version to use 39 | txParams?: TxParams; // default tx params to use 40 | enableMetricsEvents?: boolean; 41 | txHandlerConfig?: TxHandlerConfig; 42 | delistedMarketSetting?: DelistedMarketSetting; 43 | useHotWalletAdmin?: boolean; 44 | }; 45 | 46 | export type DriftClientSubscriptionConfig = 47 | | { 48 | type: 'grpc'; 49 | grpcConfigs: GrpcConfigs; 50 | resubTimeoutMs?: number; 51 | logResubMessages?: boolean; 52 | } 53 | | { 54 | type: 'websocket'; 55 | resubTimeoutMs?: number; 56 | logResubMessages?: boolean; 57 | commitment?: Commitment; 58 | } 59 | | { 60 | type: 'polling'; 61 | accountLoader: BulkAccountLoader; 62 | }; 63 | -------------------------------------------------------------------------------- /sdk/src/events/eventList.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EventType, 3 | EventMap, 4 | EventSubscriptionOrderDirection, 5 | SortFn, 6 | } from './types'; 7 | 8 | class Node { 9 | constructor( 10 | public event: Event, 11 | public next?: Node, 12 | public prev?: Node 13 | ) {} 14 | } 15 | 16 | export class EventList { 17 | size = 0; 18 | head?: Node; 19 | tail?: Node; 20 | 21 | public constructor( 22 | public eventType: Type, 23 | public maxSize: number, 24 | private sortFn: SortFn, 25 | private orderDirection: EventSubscriptionOrderDirection 26 | ) {} 27 | 28 | public insert(event: EventMap[Type]): void { 29 | this.size++; 30 | const newNode = new Node(event); 31 | if (this.head === undefined) { 32 | this.head = this.tail = newNode; 33 | return; 34 | } 35 | 36 | if ( 37 | this.sortFn(this.head.event, newNode.event) === 38 | (this.orderDirection === 'asc' ? 'less than' : 'greater than') 39 | ) { 40 | this.head.prev = newNode; 41 | newNode.next = this.head; 42 | this.head = newNode; 43 | } else { 44 | let currentNode = this.head; 45 | while ( 46 | currentNode.next !== undefined && 47 | this.sortFn(currentNode.next.event, newNode.event) !== 48 | (this.orderDirection === 'asc' ? 'less than' : 'greater than') 49 | ) { 50 | currentNode = currentNode.next; 51 | } 52 | 53 | newNode.next = currentNode.next; 54 | if (currentNode.next !== undefined) { 55 | newNode.next.prev = newNode; 56 | } else { 57 | this.tail = newNode; 58 | } 59 | 60 | currentNode.next = newNode; 61 | newNode.prev = currentNode; 62 | } 63 | 64 | if (this.size > this.maxSize) { 65 | this.detach(); 66 | } 67 | } 68 | 69 | detach(): void { 70 | const node = this.tail; 71 | if (node.prev !== undefined) { 72 | node.prev.next = node.next; 73 | } else { 74 | this.head = node.next; 75 | } 76 | 77 | if (node.next !== undefined) { 78 | node.next.prev = node.prev; 79 | } else { 80 | this.tail = node.prev; 81 | } 82 | 83 | this.size--; 84 | } 85 | 86 | toArray(): EventMap[Type][] { 87 | return Array.from(this); 88 | } 89 | 90 | *[Symbol.iterator]() { 91 | let node = this.head; 92 | while (node) { 93 | yield node.event; 94 | node = node.next; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /sdk/src/events/pollingLogProvider.ts: -------------------------------------------------------------------------------- 1 | import { LogProvider, logProviderCallback } from './types'; 2 | import { 3 | Commitment, 4 | Connection, 5 | Finality, 6 | PublicKey, 7 | TransactionSignature, 8 | } from '@solana/web3.js'; 9 | import { fetchLogs } from './fetchLogs'; 10 | 11 | export class PollingLogProvider implements LogProvider { 12 | private finality: Finality; 13 | private intervalId: ReturnType; 14 | private mostRecentSeenTx?: TransactionSignature; 15 | private mutex: number; 16 | private firstFetch = true; 17 | 18 | public constructor( 19 | private connection: Connection, 20 | private address: PublicKey, 21 | commitment: Commitment, 22 | private frequency = 15 * 1000, 23 | private batchSize?: number 24 | ) { 25 | this.finality = commitment === 'finalized' ? 'finalized' : 'confirmed'; 26 | } 27 | 28 | public async subscribe( 29 | callback: logProviderCallback, 30 | skipHistory?: boolean 31 | ): Promise { 32 | if (this.intervalId) { 33 | return true; 34 | } 35 | 36 | this.intervalId = setInterval(async () => { 37 | if (this.mutex === 1) { 38 | return; 39 | } 40 | this.mutex = 1; 41 | 42 | try { 43 | const response = await fetchLogs( 44 | this.connection, 45 | this.address, 46 | this.finality, 47 | undefined, 48 | this.mostRecentSeenTx, 49 | // If skipping history, only fetch one log back, not the maximum amount available 50 | skipHistory && this.firstFetch ? 1 : undefined, 51 | this.batchSize 52 | ); 53 | 54 | if (response === undefined) { 55 | return; 56 | } 57 | 58 | this.firstFetch = false; 59 | 60 | const { mostRecentTx, transactionLogs } = response; 61 | 62 | for (const { txSig, slot, logs } of transactionLogs) { 63 | callback(txSig, slot, logs, response.mostRecentBlockTime, undefined); 64 | } 65 | 66 | this.mostRecentSeenTx = mostRecentTx; 67 | } catch (e) { 68 | console.error('PollingLogProvider threw an Error'); 69 | console.error(e); 70 | } finally { 71 | this.mutex = 0; 72 | } 73 | }, this.frequency); 74 | 75 | return true; 76 | } 77 | 78 | public isSubscribed(): boolean { 79 | return this.intervalId !== undefined; 80 | } 81 | 82 | public async unsubscribe(): Promise { 83 | if (this.intervalId !== undefined) { 84 | clearInterval(this.intervalId); 85 | this.intervalId = undefined; 86 | } 87 | return true; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /sdk/src/events/sort.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EventMap, 3 | EventSubscriptionOrderBy, 4 | EventSubscriptionOrderDirection, 5 | EventType, 6 | SortFn, 7 | } from './types'; 8 | 9 | function clientSortAscFn(): 'less than' { 10 | return 'less than'; 11 | } 12 | 13 | function clientSortDescFn(): 'greater than' { 14 | return 'greater than'; 15 | } 16 | 17 | function blockchainSortFn( 18 | currentEvent: EventMap[EventType], 19 | newEvent: EventMap[EventType] 20 | ): 'less than' | 'greater than' { 21 | if (currentEvent.slot == newEvent.slot) { 22 | return currentEvent.txSigIndex < newEvent.txSigIndex 23 | ? 'less than' 24 | : 'greater than'; 25 | } 26 | 27 | return currentEvent.slot < newEvent.slot ? 'less than' : 'greater than'; 28 | } 29 | 30 | export function getSortFn( 31 | orderBy: EventSubscriptionOrderBy, 32 | orderDir: EventSubscriptionOrderDirection 33 | ): SortFn { 34 | if (orderBy === 'client') { 35 | return orderDir === 'asc' ? clientSortAscFn : clientSortDescFn; 36 | } 37 | 38 | return blockchainSortFn; 39 | } 40 | -------------------------------------------------------------------------------- /sdk/src/events/txEventCache.ts: -------------------------------------------------------------------------------- 1 | import { WrappedEvent, EventType } from './types'; 2 | 3 | class Node { 4 | constructor( 5 | public key: string, 6 | public value: WrappedEvent[], 7 | public next?: Node, 8 | public prev?: Node 9 | ) {} 10 | } 11 | 12 | // lru cache 13 | export class TxEventCache { 14 | size = 0; 15 | head?: Node; 16 | tail?: Node; 17 | cacheMap: { [key: string]: Node } = {}; 18 | 19 | constructor(public maxTx = 1024) {} 20 | 21 | public add(key: string, events: WrappedEvent[]): void { 22 | const existingNode = this.cacheMap[key]; 23 | if (existingNode) { 24 | this.detach(existingNode); 25 | this.size--; 26 | } else if (this.size === this.maxTx) { 27 | delete this.cacheMap[this.tail.key]; 28 | this.detach(this.tail); 29 | this.size--; 30 | } 31 | 32 | // Write to head of LinkedList 33 | if (!this.head) { 34 | this.head = this.tail = new Node(key, events); 35 | } else { 36 | const node = new Node(key, events, this.head); 37 | this.head.prev = node; 38 | this.head = node; 39 | } 40 | 41 | // update cacheMap with LinkedList key and Node reference 42 | this.cacheMap[key] = this.head; 43 | this.size++; 44 | } 45 | 46 | public has(key: string): boolean { 47 | return this.cacheMap.hasOwnProperty(key); 48 | } 49 | 50 | public get(key: string): WrappedEvent[] | undefined { 51 | return this.cacheMap[key]?.value; 52 | } 53 | 54 | detach(node: Node): void { 55 | if (node.prev !== undefined) { 56 | node.prev.next = node.next; 57 | } else { 58 | this.head = node.next; 59 | } 60 | 61 | if (node.next !== undefined) { 62 | node.next.prev = node.prev; 63 | } else { 64 | this.tail = node.prev; 65 | } 66 | } 67 | 68 | public clear(): void { 69 | this.head = undefined; 70 | this.tail = undefined; 71 | this.size = 0; 72 | this.cacheMap = {}; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /sdk/src/isomorphic/grpc.browser.ts: -------------------------------------------------------------------------------- 1 | // Export a function to create a new Client instance 2 | export function createClient(..._args: any) { 3 | throw new Error('Only available in node context'); 4 | } 5 | -------------------------------------------------------------------------------- /sdk/src/isomorphic/grpc.node.ts: -------------------------------------------------------------------------------- 1 | import type Client from '@triton-one/yellowstone-grpc'; 2 | import type { 3 | SubscribeRequest, 4 | SubscribeUpdate, 5 | CommitmentLevel, 6 | } from '@triton-one/yellowstone-grpc'; 7 | import { ClientDuplexStream, ChannelOptions } from '@grpc/grpc-js'; 8 | 9 | export { 10 | ClientDuplexStream, 11 | ChannelOptions, 12 | SubscribeRequest, 13 | SubscribeUpdate, 14 | CommitmentLevel, 15 | Client, 16 | }; 17 | 18 | // Export a function to create a new Client instance 19 | export async function createClient( 20 | ...args: ConstructorParameters 21 | ): Promise { 22 | const { default: Client_ } = await import('@triton-one/yellowstone-grpc'); 23 | return new Client_(...args); 24 | } 25 | -------------------------------------------------------------------------------- /sdk/src/isomorphic/grpc.ts: -------------------------------------------------------------------------------- 1 | export * from './grpc.node'; 2 | -------------------------------------------------------------------------------- /sdk/src/keypair.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import bs58 from 'bs58'; 3 | import { Keypair } from '@solana/web3.js'; 4 | 5 | export function loadKeypair(privateKey: string): Keypair { 6 | // try to load privateKey as a filepath 7 | let loadedKey: Uint8Array; 8 | if (fs.existsSync(privateKey)) { 9 | privateKey = fs.readFileSync(privateKey).toString(); 10 | } 11 | 12 | if (privateKey.includes('[') && privateKey.includes(']')) { 13 | loadedKey = Uint8Array.from(JSON.parse(privateKey)); 14 | } else if (privateKey.includes(',')) { 15 | loadedKey = Uint8Array.from( 16 | privateKey.split(',').map((val) => Number(val)) 17 | ); 18 | } else { 19 | privateKey = privateKey.replace(/\s/g, ''); 20 | loadedKey = new Uint8Array(bs58.decode(privateKey)); 21 | } 22 | 23 | return Keypair.fromSecretKey(Uint8Array.from(loadedKey)); 24 | } 25 | -------------------------------------------------------------------------------- /sdk/src/marinade/index.ts: -------------------------------------------------------------------------------- 1 | import { AnchorProvider, BN, Program } from '@coral-xyz/anchor'; 2 | import { MarinadeFinance, IDL } from './types'; 3 | import { 4 | PublicKey, 5 | SystemProgram, 6 | TransactionInstruction, 7 | } from '@solana/web3.js'; 8 | import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; 9 | 10 | const marinadeFinanceProgramId = new PublicKey( 11 | 'MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD' 12 | ); 13 | 14 | export function getMarinadeFinanceProgram( 15 | provider: AnchorProvider 16 | ): Program { 17 | return new Program(IDL, marinadeFinanceProgramId, provider); 18 | } 19 | 20 | export function getMarinadeDepositIx({ 21 | program, 22 | amount, 23 | mSOLAccount, 24 | transferFrom, 25 | }: { 26 | amount: BN; 27 | mSOLAccount: PublicKey; 28 | transferFrom: PublicKey; 29 | program: Program; 30 | }): Promise { 31 | return program.methods 32 | .deposit(amount) 33 | .accountsStrict({ 34 | reservePda: new PublicKey('Du3Ysj1wKbxPKkuPPnvzQLQh8oMSVifs3jGZjJWXFmHN'), 35 | state: new PublicKey('8szGkuLTAux9XMgZ2vtY39jVSowEcpBfFfD8hXSEqdGC'), 36 | msolMint: new PublicKey('mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So'), 37 | msolMintAuthority: new PublicKey( 38 | '3JLPCS1qM2zRw3Dp6V4hZnYHd4toMNPkNesXdX9tg6KM' 39 | ), 40 | liqPoolMsolLegAuthority: new PublicKey( 41 | 'EyaSjUtSgo9aRD1f8LWXwdvkpDTmXAW54yoSHZRF14WL' 42 | ), 43 | liqPoolMsolLeg: new PublicKey( 44 | '7GgPYjS5Dza89wV6FpZ23kUJRG5vbQ1GM25ezspYFSoE' 45 | ), 46 | liqPoolSolLegPda: new PublicKey( 47 | 'UefNb6z6yvArqe4cJHTXCqStRsKmWhGxnZzuHbikP5Q' 48 | ), 49 | mintTo: mSOLAccount, 50 | transferFrom, 51 | systemProgram: SystemProgram.programId, 52 | tokenProgram: TOKEN_PROGRAM_ID, 53 | }) 54 | .instruction(); 55 | } 56 | 57 | export async function getMarinadeMSolPrice( 58 | program: Program 59 | ): Promise { 60 | const state = await program.account.state.fetch( 61 | new PublicKey('8szGkuLTAux9XMgZ2vtY39jVSowEcpBfFfD8hXSEqdGC') 62 | ); 63 | return state.msolPrice.toNumber() / 0x1_0000_0000; 64 | } 65 | -------------------------------------------------------------------------------- /sdk/src/math/bankruptcy.ts: -------------------------------------------------------------------------------- 1 | import { ZERO, hasOpenOrders, isVariant } from '..'; 2 | import { User } from '../user'; 3 | 4 | export function isUserBankrupt(user: User): boolean { 5 | const userAccount = user.getUserAccount(); 6 | let hasLiability = false; 7 | for (const position of userAccount.spotPositions) { 8 | if (position.scaledBalance.gt(ZERO)) { 9 | if (isVariant(position.balanceType, 'deposit')) { 10 | return false; 11 | } 12 | if (isVariant(position.balanceType, 'borrow')) { 13 | hasLiability = true; 14 | } 15 | } 16 | } 17 | 18 | for (const position of userAccount.perpPositions) { 19 | if ( 20 | !position.baseAssetAmount.eq(ZERO) || 21 | position.quoteAssetAmount.gt(ZERO) || 22 | hasOpenOrders(position) || 23 | position.lpShares.gt(ZERO) 24 | ) { 25 | return false; 26 | } 27 | 28 | if (position.quoteAssetAmount.lt(ZERO)) { 29 | hasLiability = true; 30 | } 31 | } 32 | 33 | return hasLiability; 34 | } 35 | -------------------------------------------------------------------------------- /sdk/src/math/conversion.ts: -------------------------------------------------------------------------------- 1 | import { BN } from '../'; 2 | import { PRICE_PRECISION } from '../constants/numericConstants'; 3 | 4 | export const convertToNumber = ( 5 | bigNumber: BN, 6 | precision: BN = PRICE_PRECISION 7 | ) => { 8 | if (!bigNumber) return 0; 9 | return ( 10 | bigNumber.div(precision).toNumber() + 11 | bigNumber.mod(precision).toNumber() / precision.toNumber() 12 | ); 13 | }; 14 | 15 | export function convertToBN(value: number, precision: BN): BN { 16 | // Get the whole part using Math.floor 17 | const wholePart = Math.floor(value); 18 | 19 | // Get decimal part by subtracting whole part and multiplying by precision 20 | const decimalPart = Math.round((value - wholePart) * precision.toNumber()); 21 | 22 | // Combine: wholePart * PRECISION + decimalPart 23 | return new BN(wholePart).mul(precision).add(new BN(decimalPart)); 24 | } 25 | -------------------------------------------------------------------------------- /sdk/src/math/fuel.ts: -------------------------------------------------------------------------------- 1 | import { BN } from '@coral-xyz/anchor'; 2 | import { SpotMarketAccount, PerpMarketAccount } from '..'; 3 | import { 4 | QUOTE_PRECISION, 5 | ZERO, 6 | FUEL_WINDOW, 7 | } from '../constants/numericConstants'; 8 | 9 | export function calculateInsuranceFuelBonus( 10 | spotMarket: SpotMarketAccount, 11 | tokenStakeAmount: BN, 12 | fuelBonusNumerator: BN 13 | ): BN { 14 | const result = tokenStakeAmount 15 | .abs() 16 | .mul(fuelBonusNumerator) 17 | .mul(new BN(spotMarket.fuelBoostInsurance)) 18 | .div(FUEL_WINDOW) 19 | .div(QUOTE_PRECISION.div(new BN(10))); 20 | return result; 21 | } 22 | 23 | export function calculateSpotFuelBonus( 24 | spotMarket: SpotMarketAccount, 25 | signedTokenValue: BN, 26 | fuelBonusNumerator: BN 27 | ): BN { 28 | let result: BN; 29 | 30 | if (signedTokenValue.abs().lte(QUOTE_PRECISION)) { 31 | result = ZERO; 32 | } else if (signedTokenValue.gt(new BN(0))) { 33 | result = signedTokenValue 34 | .abs() 35 | .mul(fuelBonusNumerator) 36 | .mul(new BN(spotMarket.fuelBoostDeposits)) 37 | .div(FUEL_WINDOW) 38 | .div(QUOTE_PRECISION.div(new BN(10))); 39 | } else { 40 | result = signedTokenValue 41 | .abs() 42 | .mul(fuelBonusNumerator) 43 | .mul(new BN(spotMarket.fuelBoostBorrows)) 44 | .div(FUEL_WINDOW) 45 | .div(QUOTE_PRECISION.div(new BN(10))); 46 | } 47 | 48 | return result; 49 | } 50 | 51 | export function calculatePerpFuelBonus( 52 | perpMarket: PerpMarketAccount, 53 | baseAssetValue: BN, 54 | fuelBonusNumerator: BN 55 | ): BN { 56 | let result: BN; 57 | 58 | if (baseAssetValue.abs().lte(QUOTE_PRECISION)) { 59 | result = new BN(0); 60 | } else { 61 | result = baseAssetValue 62 | .abs() 63 | .mul(fuelBonusNumerator) 64 | .mul(new BN(perpMarket.fuelBoostPosition)) 65 | .div(FUEL_WINDOW) 66 | .div(QUOTE_PRECISION.div(new BN(10))); 67 | } 68 | 69 | return result; 70 | } 71 | -------------------------------------------------------------------------------- /sdk/src/math/protectedMakerParams.ts: -------------------------------------------------------------------------------- 1 | import { BN, ProtectMakerParamsMap } from '..'; 2 | import { PerpMarketAccount, ProtectedMakerParams } from '../types'; 3 | 4 | export function getProtectedMakerParams( 5 | perpMarket: PerpMarketAccount 6 | ): ProtectedMakerParams { 7 | let dynamicOffset; 8 | if (perpMarket.protectedMakerDynamicDivisor > 0) { 9 | dynamicOffset = BN.max( 10 | perpMarket.amm.oracleStd, 11 | perpMarket.amm.markStd 12 | ).divn(perpMarket.protectedMakerDynamicDivisor); 13 | } else { 14 | dynamicOffset = 0; 15 | } 16 | 17 | return { 18 | tickSize: perpMarket.amm.orderTickSize, 19 | limitPriceDivisor: perpMarket.protectedMakerLimitPriceDivisor, 20 | dynamicOffset: dynamicOffset, 21 | }; 22 | } 23 | 24 | export function getProtectedMakerParamsMap( 25 | perpMarkets: PerpMarketAccount[] 26 | ): ProtectMakerParamsMap { 27 | const map = { 28 | perp: new Map(), 29 | spot: new Map(), 30 | }; 31 | for (const perpMarket of perpMarkets) { 32 | const marketIndex = perpMarket.marketIndex; 33 | const protectedMakerParams = getProtectedMakerParams(perpMarket); 34 | map.perp.set(marketIndex, protectedMakerParams); 35 | } 36 | return map; 37 | } 38 | -------------------------------------------------------------------------------- /sdk/src/math/spotMarket.ts: -------------------------------------------------------------------------------- 1 | import { BN } from '@coral-xyz/anchor'; 2 | import { 3 | isVariant, 4 | MarginCategory, 5 | SpotBalanceType, 6 | SpotMarketAccount, 7 | } from '../types'; 8 | import { 9 | calculateAssetWeight, 10 | calculateLiabilityWeight, 11 | getTokenAmount, 12 | } from './spotBalance'; 13 | import { MARGIN_PRECISION, ZERO } from '../constants/numericConstants'; 14 | import { numberToSafeBN } from './utils'; 15 | 16 | export function castNumberToSpotPrecision( 17 | value: number | BN, 18 | spotMarket: SpotMarketAccount 19 | ): BN { 20 | if (typeof value === 'number') { 21 | return numberToSafeBN(value, new BN(Math.pow(10, spotMarket.decimals))); 22 | } else { 23 | return value.mul(new BN(Math.pow(10, spotMarket.decimals))); 24 | } 25 | } 26 | 27 | export function calculateSpotMarketMarginRatio( 28 | market: SpotMarketAccount, 29 | oraclePrice: BN, 30 | marginCategory: MarginCategory, 31 | size: BN, 32 | balanceType: SpotBalanceType, 33 | customMarginRatio = 0 34 | ): number { 35 | let marginRatio; 36 | 37 | if (isVariant(balanceType, 'deposit')) { 38 | const assetWeight = calculateAssetWeight( 39 | size, 40 | oraclePrice, 41 | market, 42 | marginCategory 43 | ); 44 | marginRatio = MARGIN_PRECISION.sub(assetWeight).toNumber(); 45 | } else { 46 | const liabilityWeight = calculateLiabilityWeight( 47 | size, 48 | market, 49 | marginCategory 50 | ); 51 | marginRatio = liabilityWeight.sub(MARGIN_PRECISION).toNumber(); 52 | } 53 | 54 | if (marginCategory === 'Initial') { 55 | // use lowest leverage between max allowed and optional user custom max 56 | return Math.max(marginRatio, customMarginRatio); 57 | } 58 | 59 | return marginRatio; 60 | } 61 | 62 | /** 63 | * Returns the maximum remaining deposit that can be made to the spot market. If the maxTokenDeposits on the market is zero then there is no limit and this function will also return zero. (so that needs to be checked) 64 | * @param market 65 | * @returns 66 | */ 67 | export function calculateMaxRemainingDeposit(market: SpotMarketAccount) { 68 | const marketMaxTokenDeposits = market.maxTokenDeposits; 69 | 70 | if (marketMaxTokenDeposits.eq(ZERO)) { 71 | // If the maxTokenDeposits is set to zero then that means there is no limit. Return the largest number we can to represent infinite available deposit. 72 | return ZERO; 73 | } 74 | 75 | const totalDepositsTokenAmount = getTokenAmount( 76 | market.depositBalance, 77 | market, 78 | SpotBalanceType.DEPOSIT 79 | ); 80 | 81 | return marketMaxTokenDeposits.sub(totalDepositsTokenAmount); 82 | } 83 | -------------------------------------------------------------------------------- /sdk/src/math/state.ts: -------------------------------------------------------------------------------- 1 | import { StateAccount } from '../types'; 2 | import { BN, LAMPORTS_PRECISION, PERCENTAGE_PRECISION, ZERO } from '../'; 3 | 4 | export function calculateInitUserFee(stateAccount: StateAccount): BN { 5 | const maxInitFee = new BN(stateAccount.maxInitializeUserFee) 6 | .mul(LAMPORTS_PRECISION) 7 | .divn(100); 8 | const targetUtilization = PERCENTAGE_PRECISION.muln(8).divn(10); 9 | 10 | const accountSpaceUtilization = stateAccount.numberOfSubAccounts 11 | .addn(1) 12 | .mul(PERCENTAGE_PRECISION) 13 | .div(getMaxNumberOfSubAccounts(stateAccount)); 14 | 15 | if (accountSpaceUtilization.gt(targetUtilization)) { 16 | return maxInitFee 17 | .mul(accountSpaceUtilization.sub(targetUtilization)) 18 | .div(PERCENTAGE_PRECISION.sub(targetUtilization)); 19 | } else { 20 | return ZERO; 21 | } 22 | } 23 | 24 | export function getMaxNumberOfSubAccounts(stateAccount: StateAccount): BN { 25 | if (stateAccount.maxNumberOfSubAccounts <= 5) { 26 | return new BN(stateAccount.maxNumberOfSubAccounts); 27 | } 28 | return new BN(stateAccount.maxNumberOfSubAccounts).muln(100); 29 | } 30 | -------------------------------------------------------------------------------- /sdk/src/math/tiers.ts: -------------------------------------------------------------------------------- 1 | import { isVariant, PerpMarketAccount, SpotMarketAccount } from '../types'; 2 | 3 | export function getPerpMarketTierNumber(perpMarket: PerpMarketAccount): number { 4 | if (isVariant(perpMarket.contractTier, 'a')) { 5 | return 0; 6 | } else if (isVariant(perpMarket.contractTier, 'b')) { 7 | return 1; 8 | } else if (isVariant(perpMarket.contractTier, 'c')) { 9 | return 2; 10 | } else if (isVariant(perpMarket.contractTier, 'speculative')) { 11 | return 3; 12 | } else if (isVariant(perpMarket.contractTier, 'highlySpeculative')) { 13 | return 4; 14 | } else { 15 | return 5; 16 | } 17 | } 18 | 19 | export function getSpotMarketTierNumber(spotMarket: SpotMarketAccount): number { 20 | if (isVariant(spotMarket.assetTier, 'collateral')) { 21 | return 0; 22 | } else if (isVariant(spotMarket.assetTier, 'protected')) { 23 | return 1; 24 | } else if (isVariant(spotMarket.assetTier, 'cross')) { 25 | return 2; 26 | } else if (isVariant(spotMarket.assetTier, 'isolated')) { 27 | return 3; 28 | } else if (isVariant(spotMarket.assetTier, 'unlisted')) { 29 | return 4; 30 | } else { 31 | return 5; 32 | } 33 | } 34 | 35 | export function perpTierIsAsSafeAs( 36 | perpTier: number, 37 | otherPerpTier: number, 38 | otherSpotTier: number 39 | ): boolean { 40 | const asSafeAsPerp = perpTier <= otherPerpTier; 41 | const asSafeAsSpot = 42 | otherSpotTier === 4 || (otherSpotTier >= 2 && perpTier <= 2); 43 | return asSafeAsSpot && asSafeAsPerp; 44 | } 45 | -------------------------------------------------------------------------------- /sdk/src/math/userStatus.ts: -------------------------------------------------------------------------------- 1 | import { UserAccount, UserStatus } from '..'; 2 | 3 | export function isUserProtectedMaker(userAccount: UserAccount): boolean { 4 | return (userAccount.status & UserStatus.PROTECTED_MAKER) > 0; 5 | } 6 | -------------------------------------------------------------------------------- /sdk/src/openbook/openbookV2FulfillmentConfigMap.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { OpenbookV2FulfillmentConfigAccount } from '../types'; 3 | import { DriftClient } from '../driftClient'; 4 | 5 | export class OpenbookV2FulfillmentConfigMap { 6 | driftClient: DriftClient; 7 | map = new Map(); 8 | 9 | public constructor(driftClient: DriftClient) { 10 | this.driftClient = driftClient; 11 | } 12 | 13 | public async add( 14 | marketIndex: number, 15 | openbookV2MarketAddress: PublicKey 16 | ): Promise { 17 | const account = await this.driftClient.getOpenbookV2FulfillmentConfig( 18 | openbookV2MarketAddress 19 | ); 20 | 21 | this.map.set(marketIndex, account); 22 | } 23 | 24 | public get( 25 | marketIndex: number 26 | ): OpenbookV2FulfillmentConfigAccount | undefined { 27 | return this.map.get(marketIndex); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sdk/src/oracles/oracleClientCache.ts: -------------------------------------------------------------------------------- 1 | import { OracleClient } from './types'; 2 | import { OracleSource } from '../types'; 3 | import { getOracleClient } from '../factory/oracleClient'; 4 | import { Connection } from '@solana/web3.js'; 5 | import { Program } from '@coral-xyz/anchor'; 6 | 7 | export class OracleClientCache { 8 | cache = new Map(); 9 | public constructor() {} 10 | 11 | public get( 12 | oracleSource: OracleSource, 13 | connection: Connection, 14 | program: Program 15 | ) { 16 | const key = Object.keys(oracleSource)[0]; 17 | if (this.cache.has(key)) { 18 | return this.cache.get(key); 19 | } 20 | 21 | const client = getOracleClient(oracleSource, connection, program); 22 | this.cache.set(key, client); 23 | return client; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sdk/src/oracles/oracleId.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { OracleSource, OracleSourceNum } from '../types'; 3 | 4 | export function getOracleSourceNum(source: OracleSource): number { 5 | if ('pyth' in source) return OracleSourceNum.PYTH; 6 | if ('pyth1K' in source) return OracleSourceNum.PYTH_1K; 7 | if ('pyth1M' in source) return OracleSourceNum.PYTH_1M; 8 | if ('pythPull' in source) return OracleSourceNum.PYTH_PULL; 9 | if ('pyth1KPull' in source) return OracleSourceNum.PYTH_1K_PULL; 10 | if ('pyth1MPull' in source) return OracleSourceNum.PYTH_1M_PULL; 11 | if ('switchboard' in source) return OracleSourceNum.SWITCHBOARD; 12 | if ('quoteAsset' in source) return OracleSourceNum.QUOTE_ASSET; 13 | if ('pythStableCoin' in source) return OracleSourceNum.PYTH_STABLE_COIN; 14 | if ('pythStableCoinPull' in source) 15 | return OracleSourceNum.PYTH_STABLE_COIN_PULL; 16 | if ('prelaunch' in source) return OracleSourceNum.PRELAUNCH; 17 | if ('switchboardOnDemand' in source) 18 | return OracleSourceNum.SWITCHBOARD_ON_DEMAND; 19 | if ('pythLazer' in source) return OracleSourceNum.PYTH_LAZER; 20 | if ('pythLazer1K' in source) return OracleSourceNum.PYTH_LAZER_1K; 21 | if ('pythLazer1M' in source) return OracleSourceNum.PYTH_LAZER_1M; 22 | if ('pythLazerStableCoin' in source) 23 | return OracleSourceNum.PYTH_LAZER_STABLE_COIN; 24 | throw new Error('Invalid oracle source'); 25 | } 26 | 27 | export function getOracleId( 28 | publicKey: PublicKey, 29 | source: OracleSource 30 | ): string { 31 | return `${publicKey.toBase58()}-${getOracleSourceNum(source)}`; 32 | } 33 | -------------------------------------------------------------------------------- /sdk/src/oracles/prelaunchOracleClient.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { OracleClient, OraclePriceData } from './types'; 3 | import { Program } from '@coral-xyz/anchor'; 4 | import { PrelaunchOracle } from '../types'; 5 | 6 | export class PrelaunchOracleClient implements OracleClient { 7 | private connection: Connection; 8 | private program: Program; 9 | 10 | public constructor(connection: Connection, program: Program) { 11 | this.connection = connection; 12 | this.program = program; 13 | } 14 | 15 | public async getOraclePriceData( 16 | pricePublicKey: PublicKey 17 | ): Promise { 18 | const accountInfo = await this.connection.getAccountInfo(pricePublicKey); 19 | return this.getOraclePriceDataFromBuffer(accountInfo.data); 20 | } 21 | 22 | public getOraclePriceDataFromBuffer(buffer: Buffer): OraclePriceData { 23 | const prelaunchOracle = 24 | this.program.account.prelaunchOracle.coder.accounts.decodeUnchecked( 25 | 'PrelaunchOracle', 26 | buffer 27 | ) as PrelaunchOracle; 28 | 29 | return { 30 | price: prelaunchOracle.price, 31 | slot: prelaunchOracle.ammLastUpdateSlot, 32 | confidence: prelaunchOracle.confidence, 33 | hasSufficientNumberOfDataPoints: true, 34 | maxPrice: prelaunchOracle.maxPrice, 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sdk/src/oracles/pythClient.ts: -------------------------------------------------------------------------------- 1 | import { parsePriceData } from '@pythnetwork/client'; 2 | import { Connection, PublicKey } from '@solana/web3.js'; 3 | import { OracleClient, OraclePriceData } from './types'; 4 | import { BN } from '@coral-xyz/anchor'; 5 | import { 6 | ONE, 7 | PRICE_PRECISION, 8 | QUOTE_PRECISION, 9 | TEN, 10 | } from '../constants/numericConstants'; 11 | 12 | export class PythClient implements OracleClient { 13 | private connection: Connection; 14 | private multiple: BN; 15 | private stableCoin: boolean; 16 | 17 | public constructor( 18 | connection: Connection, 19 | multiple = ONE, 20 | stableCoin = false 21 | ) { 22 | this.connection = connection; 23 | this.multiple = multiple; 24 | this.stableCoin = stableCoin; 25 | } 26 | 27 | public async getOraclePriceData( 28 | pricePublicKey: PublicKey 29 | ): Promise { 30 | const accountInfo = await this.connection.getAccountInfo(pricePublicKey); 31 | return this.getOraclePriceDataFromBuffer(accountInfo.data); 32 | } 33 | 34 | public getOraclePriceDataFromBuffer(buffer: Buffer): OraclePriceData { 35 | const priceData = parsePriceData(buffer); 36 | const confidence = convertPythPrice( 37 | priceData.confidence, 38 | priceData.exponent, 39 | this.multiple 40 | ); 41 | const minPublishers = Math.min(priceData.numComponentPrices, 3); 42 | let price = convertPythPrice( 43 | priceData.aggregate.price, 44 | priceData.exponent, 45 | this.multiple 46 | ); 47 | if (this.stableCoin) { 48 | price = getStableCoinPrice(price, confidence); 49 | } 50 | 51 | return { 52 | price, 53 | slot: new BN(priceData.lastSlot.toString()), 54 | confidence, 55 | twap: convertPythPrice( 56 | priceData.twap.value, 57 | priceData.exponent, 58 | this.multiple 59 | ), 60 | twapConfidence: convertPythPrice( 61 | priceData.twac.value, 62 | priceData.exponent, 63 | this.multiple 64 | ), 65 | hasSufficientNumberOfDataPoints: priceData.numQuoters >= minPublishers, 66 | }; 67 | } 68 | } 69 | 70 | function convertPythPrice(price: number, exponent: number, multiple: BN): BN { 71 | exponent = Math.abs(exponent); 72 | const pythPrecision = TEN.pow(new BN(exponent).abs()).div(multiple); 73 | return new BN(price * Math.pow(10, exponent)) 74 | .mul(PRICE_PRECISION) 75 | .div(pythPrecision); 76 | } 77 | 78 | const fiveBPS = new BN(500); 79 | function getStableCoinPrice(price: BN, confidence: BN): BN { 80 | if (price.sub(QUOTE_PRECISION).abs().lt(BN.min(confidence, fiveBPS))) { 81 | return QUOTE_PRECISION; 82 | } else { 83 | return price; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /sdk/src/oracles/quoteAssetOracleClient.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { OracleClient, OraclePriceData } from './types'; 3 | import { BN } from '@coral-xyz/anchor'; 4 | import { PRICE_PRECISION } from '../constants/numericConstants'; 5 | 6 | export const QUOTE_ORACLE_PRICE_DATA: OraclePriceData = { 7 | price: PRICE_PRECISION, 8 | slot: new BN(0), 9 | confidence: new BN(1), 10 | hasSufficientNumberOfDataPoints: true, 11 | }; 12 | 13 | export class QuoteAssetOracleClient implements OracleClient { 14 | public constructor() {} 15 | 16 | public async getOraclePriceData( 17 | _pricePublicKey: PublicKey 18 | ): Promise { 19 | return Promise.resolve(QUOTE_ORACLE_PRICE_DATA); 20 | } 21 | 22 | public getOraclePriceDataFromBuffer(_buffer: Buffer): OraclePriceData { 23 | return QUOTE_ORACLE_PRICE_DATA; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sdk/src/oracles/strictOraclePrice.ts: -------------------------------------------------------------------------------- 1 | import { BN } from '@coral-xyz/anchor'; 2 | 3 | export class StrictOraclePrice { 4 | current: BN; 5 | twap?: BN; 6 | 7 | constructor(current: BN, twap?: BN) { 8 | this.current = current; 9 | this.twap = twap; 10 | } 11 | 12 | public max(): BN { 13 | return this.twap ? BN.max(this.twap, this.current) : this.current; 14 | } 15 | 16 | public min(): BN { 17 | return this.twap ? BN.min(this.twap, this.current) : this.current; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sdk/src/oracles/switchboardClient.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { PRICE_PRECISION, TEN } from '../constants/numericConstants'; 3 | import { OracleClient, OraclePriceData } from './types'; 4 | import switchboardV2Idl from '../idl/switchboard.json'; 5 | import { BorshAccountsCoder, BN, Idl } from '@coral-xyz/anchor'; 6 | 7 | type SwitchboardDecimal = { 8 | scale: number; 9 | mantissa: BN; 10 | }; 11 | 12 | type AggregatorAccountData = { 13 | latestConfirmedRound: { 14 | result: SwitchboardDecimal; 15 | stdDeviation: SwitchboardDecimal; 16 | numSuccess: number; 17 | roundOpenSlot: BN; 18 | }; 19 | minOracleResults: number; 20 | }; 21 | 22 | export class SwitchboardClient implements OracleClient { 23 | connection: Connection; 24 | coder: BorshAccountsCoder; 25 | 26 | public constructor(connection: Connection) { 27 | this.connection = connection; 28 | this.coder = new BorshAccountsCoder(switchboardV2Idl as Idl); 29 | } 30 | 31 | public async getOraclePriceData( 32 | pricePublicKey: PublicKey 33 | ): Promise { 34 | const accountInfo = await this.connection.getAccountInfo(pricePublicKey); 35 | return this.getOraclePriceDataFromBuffer(accountInfo.data); 36 | } 37 | 38 | public getOraclePriceDataFromBuffer(buffer: Buffer): OraclePriceData { 39 | const aggregatorAccountData = this.coder.decodeUnchecked( 40 | 'AggregatorAccountData', 41 | buffer 42 | ) as AggregatorAccountData; 43 | 44 | const price = convertSwitchboardDecimal( 45 | aggregatorAccountData.latestConfirmedRound.result 46 | ); 47 | 48 | const confidence = BN.max( 49 | convertSwitchboardDecimal( 50 | aggregatorAccountData.latestConfirmedRound.stdDeviation 51 | ), 52 | price.divn(1000) 53 | ); 54 | 55 | const hasSufficientNumberOfDataPoints = 56 | aggregatorAccountData.latestConfirmedRound.numSuccess >= 57 | aggregatorAccountData.minOracleResults; 58 | 59 | const slot: BN = aggregatorAccountData.latestConfirmedRound.roundOpenSlot; 60 | return { 61 | price, 62 | slot, 63 | confidence, 64 | hasSufficientNumberOfDataPoints, 65 | }; 66 | } 67 | } 68 | 69 | function convertSwitchboardDecimal(switchboardDecimal: { 70 | scale: number; 71 | mantissa: BN; 72 | }): BN { 73 | const switchboardPrecision = TEN.pow(new BN(switchboardDecimal.scale)); 74 | return switchboardDecimal.mantissa 75 | .mul(PRICE_PRECISION) 76 | .div(switchboardPrecision); 77 | } 78 | -------------------------------------------------------------------------------- /sdk/src/oracles/switchboardOnDemandClient.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { OracleClient, OraclePriceData } from './types'; 3 | import { BN } from '@coral-xyz/anchor'; 4 | import switchboardOnDemandIdl from '../idl/switchboard_on_demand_30.json'; 5 | import { PRICE_PRECISION_EXP } from '../constants/numericConstants'; 6 | import { 7 | BorshAccountsCoder as BorshAccountsCoder30, 8 | Idl as Idl30, 9 | } from '@coral-xyz/anchor-30'; 10 | 11 | const SB_PRECISION_EXP = new BN(18); 12 | const SB_PRECISION = new BN(10).pow(SB_PRECISION_EXP.sub(PRICE_PRECISION_EXP)); 13 | 14 | type PullFeedAccountData = { 15 | submissions: { 16 | landed_at: BN; 17 | value: BN; 18 | }[]; 19 | result: { 20 | value: BN; 21 | std_dev: BN; 22 | mean: BN; 23 | slot: BN; 24 | range: BN; 25 | }; 26 | last_update_timestamp: BN; 27 | max_variance: BN; 28 | min_responses: BN; 29 | }; 30 | 31 | export class SwitchboardOnDemandClient implements OracleClient { 32 | connection: Connection; 33 | coder: BorshAccountsCoder30; 34 | 35 | public constructor(connection: Connection) { 36 | this.connection = connection; 37 | this.coder = new BorshAccountsCoder30(switchboardOnDemandIdl as Idl30); 38 | } 39 | 40 | public async getOraclePriceData( 41 | pricePublicKey: PublicKey 42 | ): Promise { 43 | const accountInfo = await this.connection.getAccountInfo(pricePublicKey); 44 | return this.getOraclePriceDataFromBuffer(accountInfo.data); 45 | } 46 | 47 | public getOraclePriceDataFromBuffer(buffer: Buffer): OraclePriceData { 48 | const pullFeedAccountData = this.coder.decodeUnchecked( 49 | 'PullFeedAccountData', 50 | buffer 51 | ) as PullFeedAccountData; 52 | 53 | const landedAt = pullFeedAccountData.submissions.reduce( 54 | (max, s) => BN.max(max, s.landed_at), 55 | new BN(0) 56 | ); 57 | return { 58 | price: pullFeedAccountData.result.value.div(SB_PRECISION), 59 | slot: landedAt, 60 | confidence: pullFeedAccountData.result.range.div(SB_PRECISION), 61 | hasSufficientNumberOfDataPoints: true, 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /sdk/src/oracles/types.ts: -------------------------------------------------------------------------------- 1 | import { BN } from '@coral-xyz/anchor'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | import { OracleSource } from '../types'; 4 | 5 | export type OraclePriceData = { 6 | price: BN; 7 | slot: BN; 8 | confidence: BN; 9 | hasSufficientNumberOfDataPoints: boolean; 10 | twap?: BN; 11 | twapConfidence?: BN; 12 | maxPrice?: BN; // pre-launch markets only 13 | }; 14 | 15 | export type OracleInfo = { 16 | publicKey: PublicKey; 17 | source: OracleSource; 18 | }; 19 | 20 | export interface OracleClient { 21 | getOraclePriceDataFromBuffer(buffer: Buffer): OraclePriceData; 22 | getOraclePriceData(publicKey: PublicKey): Promise; 23 | } 24 | -------------------------------------------------------------------------------- /sdk/src/orderParams.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultOrderParams, 3 | OptionalOrderParams, 4 | OrderParams, 5 | OrderParamsBitFlag, 6 | OrderTriggerCondition, 7 | OrderType, 8 | } from './types'; 9 | import { BN } from '@coral-xyz/anchor'; 10 | 11 | export function getLimitOrderParams( 12 | params: Omit & { price: BN } 13 | ): OptionalOrderParams { 14 | return getOrderParams( 15 | Object.assign({}, params, { 16 | orderType: OrderType.LIMIT, 17 | }) 18 | ); 19 | } 20 | 21 | export function getTriggerMarketOrderParams( 22 | params: Omit & { 23 | triggerCondition: OrderTriggerCondition; 24 | triggerPrice: BN; 25 | } 26 | ): OptionalOrderParams { 27 | return getOrderParams( 28 | Object.assign({}, params, { 29 | orderType: OrderType.TRIGGER_MARKET, 30 | }) 31 | ); 32 | } 33 | 34 | export function getTriggerLimitOrderParams( 35 | params: Omit & { 36 | triggerCondition: OrderTriggerCondition; 37 | triggerPrice: BN; 38 | price: BN; 39 | } 40 | ): OptionalOrderParams { 41 | return getOrderParams( 42 | Object.assign({}, params, { 43 | orderType: OrderType.TRIGGER_LIMIT, 44 | }) 45 | ); 46 | } 47 | 48 | export function getMarketOrderParams( 49 | params: Omit 50 | ): OptionalOrderParams { 51 | return getOrderParams( 52 | Object.assign({}, params, { 53 | orderType: OrderType.MARKET, 54 | }) 55 | ); 56 | } 57 | 58 | /** 59 | * Creates an OrderParams object with the given OptionalOrderParams and any params to override. 60 | * 61 | * example: 62 | * ``` 63 | * const orderParams = getOrderParams(optionalOrderParams, { marketType: MarketType.PERP }); 64 | * ``` 65 | * 66 | * @param optionalOrderParams 67 | * @param overridingParams 68 | * @returns 69 | */ 70 | export function getOrderParams( 71 | optionalOrderParams: OptionalOrderParams, 72 | overridingParams: Record = {} 73 | ): OrderParams { 74 | return Object.assign( 75 | {}, 76 | DefaultOrderParams, 77 | optionalOrderParams, 78 | overridingParams 79 | ); 80 | } 81 | 82 | export function isUpdateHighLeverageMode(bitFlags: number): boolean { 83 | return (bitFlags & OrderParamsBitFlag.UpdateHighLeverageMode) !== 0; 84 | } 85 | -------------------------------------------------------------------------------- /sdk/src/orderSubscriber/PollingSubscription.ts: -------------------------------------------------------------------------------- 1 | import { OrderSubscriber } from './OrderSubscriber'; 2 | 3 | export class PollingSubscription { 4 | private orderSubscriber: OrderSubscriber; 5 | private frequency: number; 6 | 7 | intervalId?: ReturnType; 8 | 9 | constructor({ 10 | orderSubscriber, 11 | frequency, 12 | }: { 13 | orderSubscriber: OrderSubscriber; 14 | frequency: number; 15 | }) { 16 | this.orderSubscriber = orderSubscriber; 17 | this.frequency = frequency; 18 | } 19 | 20 | public async subscribe(): Promise { 21 | if (this.intervalId) { 22 | return; 23 | } 24 | 25 | this.intervalId = setInterval( 26 | this.orderSubscriber.fetch.bind(this.orderSubscriber), 27 | this.frequency 28 | ); 29 | 30 | await this.orderSubscriber.fetch(); 31 | } 32 | 33 | public async unsubscribe(): Promise { 34 | if (this.intervalId) { 35 | clearInterval(this.intervalId); 36 | this.intervalId = undefined; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sdk/src/orderSubscriber/index.ts: -------------------------------------------------------------------------------- 1 | export * from './OrderSubscriber'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /sdk/src/orderSubscriber/types.ts: -------------------------------------------------------------------------------- 1 | import { Commitment, PublicKey } from '@solana/web3.js'; 2 | import { Order, UserAccount } from '../types'; 3 | import { DriftClient } from '../driftClient'; 4 | import { GrpcConfigs } from '../accounts/types'; 5 | 6 | export type OrderSubscriberConfig = { 7 | driftClient: DriftClient; 8 | subscriptionConfig: 9 | | { 10 | type: 'polling'; 11 | frequency: number; 12 | commitment?: Commitment; 13 | } 14 | | { 15 | type: 'websocket'; 16 | skipInitialLoad?: boolean; 17 | resubTimeoutMs?: number; 18 | logResubMessages?: boolean; 19 | resyncIntervalMs?: number; 20 | commitment?: Commitment; 21 | } 22 | | { 23 | type: 'grpc'; 24 | grpcConfigs: GrpcConfigs; 25 | skipInitialLoad?: boolean; 26 | resubTimeoutMs?: number; 27 | logResubMessages?: boolean; 28 | resyncIntervalMs?: number; 29 | commitment?: Commitment; 30 | }; 31 | fastDecode?: boolean; 32 | decodeData?: boolean; 33 | fetchAllNonIdleUsers?: boolean; 34 | }; 35 | 36 | export interface OrderSubscriberEvents { 37 | orderCreated: ( 38 | account: UserAccount, 39 | updatedOrders: Order[], 40 | pubkey: PublicKey, 41 | slot: number, 42 | dataType: 'raw' | 'decoded' | 'buffer' 43 | ) => void; 44 | userUpdated: ( 45 | account: UserAccount, 46 | pubkey: PublicKey, 47 | slot: number, 48 | dataType: 'raw' | 'decoded' | 'buffer' 49 | ) => void; 50 | updateReceived: ( 51 | pubkey: PublicKey, 52 | slot: number, 53 | dataType: 'raw' | 'decoded' | 'buffer' 54 | ) => void; 55 | } 56 | -------------------------------------------------------------------------------- /sdk/src/phoenix/phoenixFulfillmentConfigMap.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { PhoenixV1FulfillmentConfigAccount } from '../types'; 3 | import { DriftClient } from '../driftClient'; 4 | 5 | export class PhoenixFulfillmentConfigMap { 6 | driftClient: DriftClient; 7 | map = new Map(); 8 | 9 | public constructor(driftClient: DriftClient) { 10 | this.driftClient = driftClient; 11 | } 12 | 13 | public async add( 14 | marketIndex: number, 15 | phoenixMarketAddress: PublicKey 16 | ): Promise { 17 | const account = await this.driftClient.getPhoenixV1FulfillmentConfig( 18 | phoenixMarketAddress 19 | ); 20 | this.map.set(marketIndex, account); 21 | } 22 | 23 | public get(marketIndex: number): PhoenixV1FulfillmentConfigAccount { 24 | return this.map.get(marketIndex); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sdk/src/priorityFee/averageOverSlotsStrategy.ts: -------------------------------------------------------------------------------- 1 | import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod'; 2 | import { PriorityFeeStrategy } from './types'; 3 | 4 | export class AverageOverSlotsStrategy implements PriorityFeeStrategy { 5 | calculate(samples: SolanaPriorityFeeResponse[]): number { 6 | if (samples.length === 0) { 7 | return 0; 8 | } 9 | let runningSumFees = 0; 10 | 11 | for (let i = 0; i < samples.length; i++) { 12 | runningSumFees += samples[i].prioritizationFee; 13 | } 14 | return runningSumFees / samples.length; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sdk/src/priorityFee/averageStrategy.ts: -------------------------------------------------------------------------------- 1 | import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod'; 2 | import { PriorityFeeStrategy } from './types'; 3 | 4 | export class AverageStrategy implements PriorityFeeStrategy { 5 | calculate(samples: SolanaPriorityFeeResponse[]): number { 6 | return ( 7 | samples.reduce((a, b) => { 8 | return a + b.prioritizationFee; 9 | }, 0) / samples.length 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sdk/src/priorityFee/driftPriorityFeeMethod.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { HeliusPriorityLevel } from './heliusPriorityFeeMethod'; 3 | 4 | export type DriftMarketInfo = { 5 | marketType: string; 6 | marketIndex: number; 7 | }; 8 | 9 | export type DriftPriorityFeeLevels = { 10 | [key in HeliusPriorityLevel]: number; 11 | } & { 12 | marketType: 'perp' | 'spot'; 13 | marketIndex: number; 14 | }; 15 | 16 | export type DriftPriorityFeeResponse = DriftPriorityFeeLevels[]; 17 | 18 | export async function fetchDriftPriorityFee( 19 | url: string, 20 | marketTypes: string[], 21 | marketIndexes: number[] 22 | ): Promise { 23 | try { 24 | const response = await fetch( 25 | `${url}/batchPriorityFees?marketType=${marketTypes.join( 26 | ',' 27 | )}&marketIndex=${marketIndexes.join(',')}` 28 | ); 29 | if (!response.ok) { 30 | throw new Error(`HTTP error! status: ${response.status}`); 31 | } 32 | return await response.json(); 33 | } catch (err) { 34 | if (err instanceof Error) { 35 | console.error('Error fetching priority fees:', err.message); 36 | } else { 37 | console.error('Unknown error fetching priority fees:', err); 38 | } 39 | } 40 | 41 | return []; 42 | } 43 | -------------------------------------------------------------------------------- /sdk/src/priorityFee/ewmaStrategy.ts: -------------------------------------------------------------------------------- 1 | import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod'; 2 | import { PriorityFeeStrategy } from './types'; 3 | 4 | class EwmaStrategy implements PriorityFeeStrategy { 5 | private halfLife: number; 6 | 7 | /** 8 | * @param halfLife The half life of the EWMA in slots. Default is 25 slots, approx 10 seconds. 9 | */ 10 | constructor(halfLife = 25) { 11 | this.halfLife = halfLife; 12 | } 13 | 14 | // samples provided in desc slot order 15 | calculate(samples: SolanaPriorityFeeResponse[]): number { 16 | if (samples.length === 0) { 17 | return 0; 18 | } 19 | if (samples.length === 1) { 20 | return samples[0].prioritizationFee; 21 | } 22 | 23 | let ewma = 0; 24 | 25 | const samplesReversed = samples.slice().reverse(); 26 | for (let i = 0; i < samplesReversed.length; i++) { 27 | if (i === 0) { 28 | ewma = samplesReversed[i].prioritizationFee; 29 | continue; 30 | } 31 | const gap = samplesReversed[i].slot - samplesReversed[i - 1].slot; 32 | const alpha = 1 - Math.exp((Math.log(0.5) / this.halfLife) * gap); 33 | 34 | ewma = alpha * samplesReversed[i].prioritizationFee + (1 - alpha) * ewma; 35 | } 36 | 37 | return ewma; 38 | } 39 | } 40 | 41 | export { EwmaStrategy }; 42 | -------------------------------------------------------------------------------- /sdk/src/priorityFee/heliusPriorityFeeMethod.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | export enum HeliusPriorityLevel { 4 | MIN = 'min', // 25th percentile 5 | LOW = 'low', // 25th percentile 6 | MEDIUM = 'medium', // 50th percentile 7 | HIGH = 'high', // 75th percentile 8 | VERY_HIGH = 'veryHigh', // 95th percentile 9 | UNSAFE_MAX = 'unsafeMax', // 100th percentile 10 | } 11 | 12 | export type HeliusPriorityFeeLevels = { 13 | [key in HeliusPriorityLevel]: number; 14 | }; 15 | 16 | export type HeliusPriorityFeeResponse = { 17 | jsonrpc: string; 18 | result: { 19 | priorityFeeEstimate?: number; 20 | priorityFeeLevels?: HeliusPriorityFeeLevels; 21 | }; 22 | id: string; 23 | }; 24 | 25 | /// Fetches the priority fee from the Helius API 26 | /// https://docs.helius.dev/solana-rpc-nodes/alpha-priority-fee-api 27 | export async function fetchHeliusPriorityFee( 28 | heliusRpcUrl: string, 29 | lookbackDistance: number, 30 | addresses: string[] 31 | ): Promise { 32 | try { 33 | const response = await fetch(heliusRpcUrl, { 34 | method: 'POST', 35 | headers: { 'Content-Type': 'application/json' }, 36 | body: JSON.stringify({ 37 | jsonrpc: '2.0', 38 | id: '1', 39 | method: 'getPriorityFeeEstimate', 40 | params: [ 41 | { 42 | accountKeys: addresses, 43 | options: { 44 | includeAllPriorityFeeLevels: true, 45 | lookbackSlots: lookbackDistance, 46 | }, 47 | }, 48 | ], 49 | }), 50 | }); 51 | return await response.json(); 52 | } catch (err) { 53 | console.error(err); 54 | } 55 | 56 | return undefined; 57 | } 58 | -------------------------------------------------------------------------------- /sdk/src/priorityFee/index.ts: -------------------------------------------------------------------------------- 1 | export * from './averageOverSlotsStrategy'; 2 | export * from './averageStrategy'; 3 | export * from './ewmaStrategy'; 4 | export * from './maxOverSlotsStrategy'; 5 | export * from './maxStrategy'; 6 | export * from './priorityFeeSubscriber'; 7 | export * from './priorityFeeSubscriberMap'; 8 | export * from './solanaPriorityFeeMethod'; 9 | export * from './heliusPriorityFeeMethod'; 10 | export * from './driftPriorityFeeMethod'; 11 | export * from './types'; 12 | -------------------------------------------------------------------------------- /sdk/src/priorityFee/maxOverSlotsStrategy.ts: -------------------------------------------------------------------------------- 1 | import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod'; 2 | import { PriorityFeeStrategy } from './types'; 3 | 4 | export class MaxOverSlotsStrategy implements PriorityFeeStrategy { 5 | calculate(samples: SolanaPriorityFeeResponse[]): number { 6 | if (samples.length === 0) { 7 | return 0; 8 | } 9 | // Assuming samples are sorted in descending order of slot. 10 | let currMaxFee = samples[0].prioritizationFee; 11 | 12 | for (let i = 0; i < samples.length; i++) { 13 | currMaxFee = Math.max(samples[i].prioritizationFee, currMaxFee); 14 | } 15 | return currMaxFee; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sdk/src/priorityFee/maxStrategy.ts: -------------------------------------------------------------------------------- 1 | import { PriorityFeeStrategy } from './types'; 2 | 3 | export class MaxStrategy implements PriorityFeeStrategy { 4 | calculate(samples: { slot: number; prioritizationFee: number }[]): number { 5 | return Math.max(...samples.map((result) => result.prioritizationFee)); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sdk/src/priorityFee/solanaPriorityFeeMethod.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | 3 | export type SolanaPriorityFeeResponse = { 4 | slot: number; 5 | prioritizationFee: number; 6 | }; 7 | 8 | export async function fetchSolanaPriorityFee( 9 | connection: Connection, 10 | lookbackDistance: number, 11 | addresses: string[] 12 | ): Promise { 13 | try { 14 | // @ts-ignore 15 | const rpcJSONResponse: any = await connection._rpcRequest( 16 | 'getRecentPrioritizationFees', 17 | [addresses] 18 | ); 19 | 20 | const results: SolanaPriorityFeeResponse[] = rpcJSONResponse?.result; 21 | 22 | if (!results.length) return; 23 | 24 | // Sort and filter results based on the slot lookback setting 25 | const descResults = results.sort((a, b) => b.slot - a.slot); 26 | const cutoffSlot = descResults[0].slot - lookbackDistance; 27 | 28 | return descResults.filter((result) => result.slot >= cutoffSlot); 29 | } catch (err) { 30 | console.error(err); 31 | } 32 | 33 | return []; 34 | } 35 | -------------------------------------------------------------------------------- /sdk/src/priorityFee/types.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod'; 3 | import { HeliusPriorityFeeResponse } from './heliusPriorityFeeMethod'; 4 | import { 5 | DriftMarketInfo, 6 | DriftPriorityFeeResponse, 7 | } from './driftPriorityFeeMethod'; 8 | 9 | export const DEFAULT_PRIORITY_FEE_MAP_FREQUENCY_MS = 10_000; 10 | 11 | export interface PriorityFeeStrategy { 12 | // calculate the priority fee for a given set of samples. 13 | // expect samples to be sorted in descending order (by slot) 14 | calculate( 15 | samples: 16 | | SolanaPriorityFeeResponse[] 17 | | HeliusPriorityFeeResponse 18 | | DriftPriorityFeeResponse 19 | ): number; 20 | } 21 | 22 | export enum PriorityFeeMethod { 23 | SOLANA = 'solana', 24 | HELIUS = 'helius', 25 | DRIFT = 'drift', 26 | } 27 | 28 | export type PriorityFeeSubscriberConfig = { 29 | /// rpc connection, optional if using priorityFeeMethod.HELIUS 30 | connection?: Connection; 31 | /// frequency to make RPC calls to update priority fee samples, in milliseconds 32 | frequencyMs?: number; 33 | /// addresses you plan to write lock, used to determine priority fees 34 | addresses?: PublicKey[]; 35 | /// drift market type and index, optionally provide at initialization time if using priorityFeeMethod.DRIFT 36 | driftMarkets?: DriftMarketInfo[]; 37 | /// custom strategy to calculate priority fees, defaults to AVERAGE 38 | customStrategy?: PriorityFeeStrategy; 39 | /// method for fetching priority fee samples 40 | priorityFeeMethod?: PriorityFeeMethod; 41 | /// lookback window to determine priority fees, in slots. 42 | slotsToCheck?: number; 43 | /// url for helius rpc, required if using priorityFeeMethod.HELIUS 44 | heliusRpcUrl?: string; 45 | /// url for drift cached priority fee endpoint, required if using priorityFeeMethod.DRIFT 46 | driftPriorityFeeEndpoint?: string; 47 | /// clamp any returned priority fee value to this value. 48 | maxFeeMicroLamports?: number; 49 | /// multiplier applied to priority fee before maxFeeMicroLamports, defaults to 1.0 50 | priorityFeeMultiplier?: number; 51 | }; 52 | 53 | export type PriorityFeeSubscriberMapConfig = { 54 | /// frequency to make RPC calls to update priority fee samples, in milliseconds 55 | frequencyMs?: number; 56 | /// drift market type and associated market index to query 57 | driftMarkets?: DriftMarketInfo[]; 58 | /// url for drift cached priority fee endpoint 59 | driftPriorityFeeEndpoint: string; 60 | }; 61 | -------------------------------------------------------------------------------- /sdk/src/serum/serumFulfillmentConfigMap.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { SerumV3FulfillmentConfigAccount } from '../types'; 3 | import { DriftClient } from '../driftClient'; 4 | 5 | export class SerumFulfillmentConfigMap { 6 | driftClient: DriftClient; 7 | map = new Map(); 8 | 9 | public constructor(driftClient: DriftClient) { 10 | this.driftClient = driftClient; 11 | } 12 | 13 | public async add( 14 | marketIndex: number, 15 | serumMarketAddress: PublicKey 16 | ): Promise { 17 | const account = await this.driftClient.getSerumV3FulfillmentConfig( 18 | serumMarketAddress 19 | ); 20 | this.map.set(marketIndex, account); 21 | } 22 | 23 | public get(marketIndex: number): SerumV3FulfillmentConfigAccount { 24 | return this.map.get(marketIndex); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sdk/src/serum/types.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { BulkAccountLoader } from '../accounts/bulkAccountLoader'; 3 | 4 | export type SerumMarketSubscriberConfig = { 5 | connection: Connection; 6 | programId: PublicKey; 7 | marketAddress: PublicKey; 8 | accountSubscription: 9 | | { 10 | // enables use to add web sockets in the future 11 | type: 'polling'; 12 | accountLoader: BulkAccountLoader; 13 | } 14 | | { 15 | type: 'websocket'; 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /sdk/src/swift/index.ts: -------------------------------------------------------------------------------- 1 | export * from './swiftOrderSubscriber'; 2 | export * from './signedMsgUserAccountSubscriber'; 3 | export * from './grpcSignedMsgUserAccountSubscriber'; 4 | -------------------------------------------------------------------------------- /sdk/src/testClient.ts: -------------------------------------------------------------------------------- 1 | import { AdminClient } from './adminClient'; 2 | import { ConfirmOptions, Signer, Transaction } from '@solana/web3.js'; 3 | import { TxSigAndSlot } from './tx/types'; 4 | import { PollingDriftClientAccountSubscriber } from './accounts/pollingDriftClientAccountSubscriber'; 5 | import { DriftClientConfig } from './driftClientConfig'; 6 | 7 | export class TestClient extends AdminClient { 8 | public constructor(config: DriftClientConfig) { 9 | config.txVersion = 'legacy'; 10 | if (config.accountSubscription.type !== 'polling') { 11 | throw new Error('Test client must be polling'); 12 | } 13 | super(config); 14 | } 15 | 16 | async sendTransaction( 17 | tx: Transaction, 18 | additionalSigners?: Array, 19 | opts?: ConfirmOptions, 20 | preSigned?: boolean 21 | ): Promise { 22 | const { txSig, slot } = await super.sendTransaction( 23 | tx, 24 | additionalSigners, 25 | opts, 26 | preSigned 27 | ); 28 | 29 | let lastFetchedSlot = ( 30 | this.accountSubscriber as PollingDriftClientAccountSubscriber 31 | ).accountLoader.mostRecentSlot; 32 | await this.fetchAccounts(); 33 | while (lastFetchedSlot < slot) { 34 | await this.fetchAccounts(); 35 | lastFetchedSlot = ( 36 | this.accountSubscriber as PollingDriftClientAccountSubscriber 37 | ).accountLoader.mostRecentSlot; 38 | } 39 | 40 | return { txSig, slot }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /sdk/src/token/index.ts: -------------------------------------------------------------------------------- 1 | import { Account, TOKEN_PROGRAM_ID, unpackAccount } from '@solana/spl-token'; 2 | import { PublicKey, AccountInfo } from '@solana/web3.js'; 3 | 4 | export function parseTokenAccount(data: Buffer, pubkey: PublicKey): Account { 5 | // mock AccountInfo so unpackAccount can be used 6 | const accountInfo: AccountInfo = { 7 | data, 8 | owner: TOKEN_PROGRAM_ID, 9 | executable: false, 10 | lamports: 0, 11 | }; 12 | return unpackAccount(pubkey, accountInfo, TOKEN_PROGRAM_ID); 13 | } 14 | -------------------------------------------------------------------------------- /sdk/src/tx/blockhashFetcher/baseBlockhashFetcher.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BlockhashWithExpiryBlockHeight, 3 | Commitment, 4 | Connection, 5 | } from '@solana/web3.js'; 6 | import { BlockhashFetcher } from './types'; 7 | 8 | export class BaseBlockhashFetcher implements BlockhashFetcher { 9 | constructor( 10 | private connection: Connection, 11 | private blockhashCommitment: Commitment 12 | ) {} 13 | 14 | public async getLatestBlockhash(): Promise< 15 | BlockhashWithExpiryBlockHeight | undefined 16 | > { 17 | return this.connection.getLatestBlockhash(this.blockhashCommitment); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sdk/src/tx/blockhashFetcher/types.ts: -------------------------------------------------------------------------------- 1 | import { BlockhashWithExpiryBlockHeight } from '@solana/web3.js'; 2 | 3 | export interface BlockhashFetcher { 4 | getLatestBlockhash(): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /sdk/src/tx/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AddressLookupTableAccount, 3 | BlockhashWithExpiryBlockHeight, 4 | ConfirmOptions, 5 | Signer, 6 | Transaction, 7 | TransactionInstruction, 8 | TransactionSignature, 9 | VersionedTransaction, 10 | } from '@solana/web3.js'; 11 | import { IWallet } from '../types'; 12 | 13 | export enum ConfirmationStrategy { 14 | WebSocket = 'websocket', 15 | Polling = 'polling', 16 | Combo = 'combo', 17 | } 18 | 19 | export type TxSigAndSlot = { 20 | txSig: TransactionSignature; 21 | slot: number; 22 | }; 23 | 24 | export interface TxSender { 25 | wallet: IWallet; 26 | 27 | send( 28 | tx: Transaction, 29 | additionalSigners?: Array, 30 | opts?: ConfirmOptions, 31 | preSigned?: boolean 32 | ): Promise; 33 | 34 | sendVersionedTransaction( 35 | tx: VersionedTransaction, 36 | additionalSigners?: Array, 37 | opts?: ConfirmOptions, 38 | preSigned?: boolean 39 | ): Promise; 40 | 41 | getVersionedTransaction( 42 | ixs: TransactionInstruction[], 43 | lookupTableAccounts: AddressLookupTableAccount[], 44 | additionalSigners?: Array, 45 | opts?: ConfirmOptions, 46 | blockhash?: BlockhashWithExpiryBlockHeight 47 | ): Promise; 48 | 49 | sendRawTransaction( 50 | rawTransaction: Buffer | Uint8Array, 51 | opts: ConfirmOptions 52 | ): Promise; 53 | 54 | simulateTransaction(tx: VersionedTransaction): Promise; 55 | 56 | getTimeoutCount(): number; 57 | getSuggestedPriorityFeeMultiplier(): number; 58 | getTxLandRate(): number; 59 | } 60 | 61 | export class TxSendError extends Error { 62 | constructor( 63 | public message: string, 64 | public code: number 65 | ) { 66 | super(message); 67 | if (Error.captureStackTrace) { 68 | Error.captureStackTrace(this, TxSendError); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /sdk/src/userConfig.ts: -------------------------------------------------------------------------------- 1 | import { DriftClient } from './driftClient'; 2 | import { Commitment, PublicKey } from '@solana/web3.js'; 3 | import { BulkAccountLoader } from './accounts/bulkAccountLoader'; 4 | import { GrpcConfigs, UserAccountSubscriber } from './accounts/types'; 5 | 6 | export type UserConfig = { 7 | accountSubscription?: UserSubscriptionConfig; 8 | driftClient: DriftClient; 9 | userAccountPublicKey: PublicKey; 10 | }; 11 | 12 | export type UserSubscriptionConfig = 13 | | { 14 | type: 'grpc'; 15 | resubTimeoutMs?: number; 16 | logResubMessages?: boolean; 17 | grpcConfigs: GrpcConfigs; 18 | } 19 | | { 20 | type: 'websocket'; 21 | resubTimeoutMs?: number; 22 | logResubMessages?: boolean; 23 | commitment?: Commitment; 24 | } 25 | | { 26 | type: 'polling'; 27 | accountLoader: BulkAccountLoader; 28 | } 29 | | { 30 | type: 'custom'; 31 | userAccountSubscriber: UserAccountSubscriber; 32 | }; 33 | -------------------------------------------------------------------------------- /sdk/src/userMap/PollingSubscription.ts: -------------------------------------------------------------------------------- 1 | import { UserMap } from './userMap'; 2 | 3 | export class PollingSubscription { 4 | private userMap: UserMap; 5 | private frequency: number; 6 | private skipInitialLoad: boolean; 7 | 8 | intervalId?: ReturnType; 9 | 10 | constructor({ 11 | userMap, 12 | frequency, 13 | skipInitialLoad = false, 14 | }: { 15 | userMap: UserMap; 16 | frequency: number; 17 | skipInitialLoad?: boolean; 18 | includeIdle?: boolean; 19 | }) { 20 | this.userMap = userMap; 21 | this.frequency = frequency; 22 | this.skipInitialLoad = skipInitialLoad; 23 | } 24 | 25 | public async subscribe(): Promise { 26 | if (this.intervalId || this.frequency <= 0) { 27 | return; 28 | } 29 | 30 | const executeSync = async () => { 31 | await this.userMap.sync(); 32 | this.intervalId = setTimeout(executeSync, this.frequency); 33 | }; 34 | 35 | if (!this.skipInitialLoad) { 36 | await this.userMap.sync(); 37 | } 38 | executeSync(); 39 | } 40 | 41 | public async unsubscribe(): Promise { 42 | if (this.intervalId) { 43 | clearInterval(this.intervalId); 44 | this.intervalId = undefined; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sdk/src/userMap/WebsocketSubscription.ts: -------------------------------------------------------------------------------- 1 | import { UserMap } from './userMap'; 2 | import { getNonIdleUserFilter, getUserFilter } from '../memcmp'; 3 | import { WebSocketProgramAccountSubscriber } from '../accounts/webSocketProgramAccountSubscriber'; 4 | import { UserAccount } from '../types'; 5 | import { Commitment, Context, MemcmpFilter, PublicKey } from '@solana/web3.js'; 6 | import { ResubOpts } from '../accounts/types'; 7 | 8 | export class WebsocketSubscription { 9 | private userMap: UserMap; 10 | private commitment: Commitment; 11 | private skipInitialLoad: boolean; 12 | private resubOpts?: ResubOpts; 13 | private includeIdle?: boolean; 14 | private additionalFilters?: MemcmpFilter[]; 15 | private decodeFn: (name: string, data: Buffer) => UserAccount; 16 | 17 | private subscriber: WebSocketProgramAccountSubscriber; 18 | 19 | constructor({ 20 | userMap, 21 | commitment, 22 | skipInitialLoad = false, 23 | resubOpts, 24 | includeIdle = false, 25 | decodeFn, 26 | additionalFilters = undefined, 27 | }: { 28 | userMap: UserMap; 29 | commitment: Commitment; 30 | skipInitialLoad?: boolean; 31 | resubOpts?: ResubOpts; 32 | includeIdle?: boolean; 33 | decodeFn: (name: string, data: Buffer) => UserAccount; 34 | additionalFilters?: MemcmpFilter[]; 35 | }) { 36 | this.userMap = userMap; 37 | this.commitment = commitment; 38 | this.skipInitialLoad = skipInitialLoad; 39 | this.resubOpts = resubOpts; 40 | this.includeIdle = includeIdle || false; 41 | this.decodeFn = decodeFn; 42 | this.additionalFilters = additionalFilters; 43 | } 44 | 45 | public async subscribe(): Promise { 46 | if (!this.subscriber) { 47 | const filters = [getUserFilter()]; 48 | if (!this.includeIdle) { 49 | filters.push(getNonIdleUserFilter()); 50 | } 51 | if (this.additionalFilters) { 52 | filters.push(...this.additionalFilters); 53 | } 54 | this.subscriber = new WebSocketProgramAccountSubscriber( 55 | 'UserMap', 56 | 'User', 57 | this.userMap.driftClient.program, 58 | this.decodeFn, 59 | { 60 | filters, 61 | commitment: this.commitment, 62 | }, 63 | this.resubOpts 64 | ); 65 | } 66 | 67 | await this.subscriber.subscribe( 68 | (accountId: PublicKey, account: UserAccount, context: Context) => { 69 | const userKey = accountId.toBase58(); 70 | this.userMap.updateUserAccount(userKey, account, context.slot); 71 | } 72 | ); 73 | 74 | if (!this.skipInitialLoad) { 75 | await this.userMap.sync(); 76 | } 77 | } 78 | 79 | public async unsubscribe(): Promise { 80 | if (!this.subscriber) return; 81 | await this.subscriber.unsubscribe(); 82 | this.subscriber = undefined; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /sdk/src/userMap/userMapConfig.ts: -------------------------------------------------------------------------------- 1 | import { Commitment, Connection, MemcmpFilter } from '@solana/web3.js'; 2 | import { DriftClient } from '../driftClient'; 3 | import { GrpcConfigs } from '../accounts/types'; 4 | 5 | // passed into UserMap.getUniqueAuthorities to filter users 6 | export type UserAccountFilterCriteria = { 7 | // only return users that have open orders 8 | hasOpenOrders: boolean; 9 | }; 10 | 11 | export type SyncConfig = 12 | | { 13 | type: 'default'; 14 | } 15 | | { 16 | type: 'paginated'; 17 | chunkSize?: number; 18 | concurrencyLimit?: number; 19 | }; 20 | 21 | export type UserMapConfig = { 22 | driftClient: DriftClient; 23 | // connection object to use specifically for the UserMap. If undefined, will use the driftClient's connection 24 | connection?: Connection; 25 | subscriptionConfig: 26 | | { 27 | type: 'polling'; 28 | frequency: number; 29 | commitment?: Commitment; 30 | } 31 | | { 32 | type: 'grpc'; 33 | grpcConfigs: GrpcConfigs; 34 | resubTimeoutMs?: number; 35 | logResubMessages?: boolean; 36 | } 37 | | { 38 | type: 'websocket'; 39 | resubTimeoutMs?: number; 40 | logResubMessages?: boolean; 41 | commitment?: Commitment; 42 | }; 43 | 44 | // True to skip the initial load of userAccounts via getProgramAccounts 45 | skipInitialLoad?: boolean; 46 | 47 | // True to include idle users when loading. Defaults to false to decrease # of accounts subscribed to. 48 | includeIdle?: boolean; 49 | 50 | // Whether to skip loading available perp/spot positions and open orders 51 | fastDecode?: boolean; 52 | 53 | // If true, will not do a full sync whenever StateAccount.numberOfSubAccounts changes. 54 | // default behavior is to do a full sync on changes. 55 | disableSyncOnTotalAccountsChange?: boolean; 56 | 57 | syncConfig?: SyncConfig; 58 | 59 | // Whether to throw an error if the userMap fails to sync. Defaults to true. 60 | throwOnFailedSync?: boolean; 61 | 62 | // Whether to filter users by poolId. Defaults to false (all users). 63 | filterByPoolId?: number; 64 | 65 | additionalFilters?: MemcmpFilter[]; 66 | }; 67 | -------------------------------------------------------------------------------- /sdk/src/userName.ts: -------------------------------------------------------------------------------- 1 | export const MAX_NAME_LENGTH = 32; 2 | 3 | export const DEFAULT_USER_NAME = 'Main Account'; 4 | export const DEFAULT_MARKET_NAME = 'Default Market Name'; 5 | 6 | export function encodeName(name: string): number[] { 7 | if (name.length > MAX_NAME_LENGTH) { 8 | throw Error(`Name (${name}) longer than 32 characters`); 9 | } 10 | 11 | const buffer = Buffer.alloc(32); 12 | buffer.fill(name); 13 | buffer.fill(' ', name.length); 14 | 15 | return Array(...buffer); 16 | } 17 | 18 | export function decodeName(bytes: number[]): string { 19 | const buffer = Buffer.from(bytes); 20 | return buffer.toString('utf8').trim(); 21 | } 22 | -------------------------------------------------------------------------------- /sdk/src/userStatsConfig.ts: -------------------------------------------------------------------------------- 1 | import { DriftClient } from './driftClient'; 2 | import { Commitment, PublicKey } from '@solana/web3.js'; 3 | import { BulkAccountLoader } from './accounts/bulkAccountLoader'; 4 | import { GrpcConfigs } from './accounts/types'; 5 | 6 | export type UserStatsConfig = { 7 | accountSubscription?: UserStatsSubscriptionConfig; 8 | driftClient: DriftClient; 9 | userStatsAccountPublicKey: PublicKey; 10 | }; 11 | 12 | export type UserStatsSubscriptionConfig = 13 | | { 14 | type: 'websocket'; 15 | resubTimeoutMs?: number; 16 | logResubMessages?: boolean; 17 | commitment?: Commitment; 18 | } 19 | | { 20 | type: 'polling'; 21 | accountLoader: BulkAccountLoader; 22 | } 23 | | { 24 | type: 'custom'; 25 | } 26 | | { 27 | type: 'grpc'; 28 | resubTimeoutMs?: number; 29 | logResubMessages?: boolean; 30 | grpcConfigs: GrpcConfigs; 31 | }; 32 | -------------------------------------------------------------------------------- /sdk/src/util/chainClock.ts: -------------------------------------------------------------------------------- 1 | import { Commitment } from '@solana/web3.js'; 2 | 3 | export type ChainClockProgress = { 4 | blockHeight?: number; 5 | slot?: number; 6 | ts?: number; 7 | }; 8 | 9 | export type ChainClockUpdateProps = { 10 | commitment: Commitment; 11 | } & ChainClockProgress; 12 | 13 | export type ChainClockState = Map; 14 | 15 | export type ChainClickInitialisationProps = ChainClockUpdateProps[]; 16 | 17 | export class ChainClock { 18 | private _state: ChainClockState; 19 | 20 | constructor(props: ChainClickInitialisationProps) { 21 | this._state = new Map(); 22 | props.forEach((prop) => { 23 | this._state.set(prop.commitment, prop); 24 | }); 25 | } 26 | 27 | update(props: ChainClockUpdateProps): void { 28 | const state = this._state.get(props.commitment); 29 | if (state) { 30 | if (props.blockHeight) state.blockHeight = props.blockHeight; 31 | if (props.slot) state.slot = props.slot; 32 | if (props.ts) state.ts = props.ts; 33 | } else { 34 | this._state.set(props.commitment, props); 35 | } 36 | } 37 | 38 | public getState(commitment: Commitment): ChainClockProgress { 39 | return this._state.get(commitment); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sdk/src/util/computeUnits.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComputeBudgetProgram, 3 | Connection, 4 | Finality, 5 | PublicKey, 6 | TransactionInstruction, 7 | } from '@solana/web3.js'; 8 | 9 | export async function findComputeUnitConsumption( 10 | programId: PublicKey, 11 | connection: Connection, 12 | txSignature: string, 13 | commitment: Finality = 'confirmed' 14 | ): Promise { 15 | const tx = await connection.getTransaction(txSignature, { commitment }); 16 | const computeUnits = []; 17 | const regex = new RegExp( 18 | `Program ${programId.toString()} consumed ([0-9]{0,6}) of ([0-9]{0,7}) compute units` 19 | ); 20 | tx.meta.logMessages.forEach((logMessage) => { 21 | const match = logMessage.match(regex); 22 | if (match && match[1]) { 23 | computeUnits.push(match[1]); 24 | } 25 | }); 26 | return computeUnits; 27 | } 28 | 29 | export function isSetComputeUnitsIx(ix: TransactionInstruction): boolean { 30 | // Compute budget program discriminator is first byte 31 | // 2: set compute unit limit 32 | // 3: set compute unit price 33 | if ( 34 | ix.programId.equals(ComputeBudgetProgram.programId) && 35 | // @ts-ignore 36 | ix.data.at(0) === 2 37 | ) { 38 | return true; 39 | } 40 | return false; 41 | } 42 | 43 | export function isSetComputeUnitPriceIx(ix: TransactionInstruction): boolean { 44 | // Compute budget program discriminator is first byte 45 | // 2: set compute unit limit 46 | // 3: set compute unit price 47 | if ( 48 | ix.programId.equals(ComputeBudgetProgram.programId) && 49 | // @ts-ignore 50 | ix.data.at(0) === 3 51 | ) { 52 | return true; 53 | } 54 | return false; 55 | } 56 | 57 | export function containsComputeUnitIxs(ixs: TransactionInstruction[]): { 58 | hasSetComputeUnitLimitIx: boolean; 59 | hasSetComputeUnitPriceIx: boolean; 60 | } { 61 | return { 62 | hasSetComputeUnitLimitIx: ixs.some(isSetComputeUnitsIx), 63 | hasSetComputeUnitPriceIx: ixs.some(isSetComputeUnitPriceIx), 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /sdk/src/util/digest.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto'; 2 | import { nanoid } from 'nanoid'; 3 | 4 | export function digest(data: Buffer): Buffer { 5 | const hash = createHash('sha256'); 6 | hash.update(data); 7 | return hash.digest(); 8 | } 9 | 10 | export function digestSignature(signature: Uint8Array): string { 11 | return createHash('sha256').update(signature).digest('base64'); 12 | } 13 | 14 | export function generateSignedMsgUuid(): Uint8Array { 15 | return Uint8Array.from(Buffer.from(nanoid(8))); 16 | } 17 | -------------------------------------------------------------------------------- /sdk/src/util/promiseTimeout.ts: -------------------------------------------------------------------------------- 1 | export function promiseTimeout( 2 | promise: Promise, 3 | timeoutMs: number 4 | ): Promise { 5 | let timeoutId: ReturnType; 6 | const timeoutPromise: Promise = new Promise((resolve) => { 7 | timeoutId = setTimeout(() => resolve(null), timeoutMs); 8 | }); 9 | 10 | return Promise.race([promise, timeoutPromise]).then((result: T | null) => { 11 | clearTimeout(timeoutId); 12 | return result; 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /sdk/src/util/pythOracleUtils.ts: -------------------------------------------------------------------------------- 1 | export function trimFeedId(feedId: string): string { 2 | if (feedId.startsWith('0x')) { 3 | return feedId.slice(2); 4 | } 5 | return feedId; 6 | } 7 | 8 | export function getFeedIdUint8Array(feedId: string): Uint8Array { 9 | const trimmedFeedId = trimFeedId(feedId); 10 | return Uint8Array.from(Buffer.from(trimmedFeedId, 'hex')); 11 | } 12 | -------------------------------------------------------------------------------- /sdk/src/util/tps.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | 3 | export async function estimateTps( 4 | programId: PublicKey, 5 | connection: Connection, 6 | failed: boolean 7 | ): Promise { 8 | let signatures = await connection.getSignaturesForAddress( 9 | programId, 10 | undefined, 11 | 'finalized' 12 | ); 13 | if (failed) { 14 | signatures = signatures.filter((signature) => signature.err); 15 | } 16 | 17 | const numberOfSignatures = signatures.length; 18 | 19 | if (numberOfSignatures === 0) { 20 | return 0; 21 | } 22 | 23 | return ( 24 | numberOfSignatures / 25 | (signatures[0].blockTime - signatures[numberOfSignatures - 1].blockTime) 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /sdk/src/wallet.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Keypair, 3 | PublicKey, 4 | Transaction, 5 | VersionedTransaction, 6 | } from '@solana/web3.js'; 7 | import { IWallet, IVersionedWallet } from './types'; 8 | 9 | export class Wallet implements IWallet, IVersionedWallet { 10 | constructor(readonly payer: Keypair) {} 11 | 12 | async signTransaction(tx: Transaction): Promise { 13 | tx.partialSign(this.payer); 14 | return tx; 15 | } 16 | 17 | async signVersionedTransaction( 18 | tx: VersionedTransaction 19 | ): Promise { 20 | tx.sign([this.payer]); 21 | return tx; 22 | } 23 | 24 | async signAllTransactions(txs: Transaction[]): Promise { 25 | return txs.map((t) => { 26 | t.partialSign(this.payer); 27 | return t; 28 | }); 29 | } 30 | 31 | async signAllVersionedTransactions( 32 | txs: VersionedTransaction[] 33 | ): Promise { 34 | return txs.map((t) => { 35 | t.sign([this.payer]); 36 | return t; 37 | }); 38 | } 39 | 40 | get publicKey(): PublicKey { 41 | return this.payer.publicKey; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sdk/tests/auctions/test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PRICE_PRECISION, 3 | BN, 4 | deriveOracleAuctionParams, 5 | PositionDirection, 6 | } from '../../src'; 7 | import { assert } from 'chai'; 8 | 9 | describe('Auction Tests', () => { 10 | it('deriveOracleAuctionParams', async () => { 11 | let oraclePrice = new BN(100).mul(PRICE_PRECISION); 12 | let auctionStartPrice = new BN(90).mul(PRICE_PRECISION); 13 | let auctionEndPrice = new BN(110).mul(PRICE_PRECISION); 14 | let limitPrice = new BN(120).mul(PRICE_PRECISION); 15 | 16 | let oracleOrderParams = deriveOracleAuctionParams({ 17 | direction: PositionDirection.LONG, 18 | oraclePrice, 19 | auctionStartPrice, 20 | auctionEndPrice, 21 | limitPrice, 22 | }); 23 | 24 | assert( 25 | oracleOrderParams.auctionStartPrice.eq(new BN(-10).mul(PRICE_PRECISION)) 26 | ); 27 | assert( 28 | oracleOrderParams.auctionEndPrice.eq(new BN(10).mul(PRICE_PRECISION)) 29 | ); 30 | assert( 31 | oracleOrderParams.oraclePriceOffset === 20 * PRICE_PRECISION.toNumber() 32 | ); 33 | 34 | oracleOrderParams = deriveOracleAuctionParams({ 35 | direction: PositionDirection.LONG, 36 | oraclePrice, 37 | auctionStartPrice: oraclePrice, 38 | auctionEndPrice: oraclePrice, 39 | limitPrice: oraclePrice, 40 | }); 41 | 42 | assert(oracleOrderParams.auctionStartPrice.eq(new BN(0))); 43 | assert(oracleOrderParams.auctionEndPrice.eq(new BN(0))); 44 | assert(oracleOrderParams.oraclePriceOffset === 1); 45 | 46 | oraclePrice = new BN(100).mul(PRICE_PRECISION); 47 | auctionStartPrice = new BN(110).mul(PRICE_PRECISION); 48 | auctionEndPrice = new BN(90).mul(PRICE_PRECISION); 49 | limitPrice = new BN(80).mul(PRICE_PRECISION); 50 | 51 | oracleOrderParams = deriveOracleAuctionParams({ 52 | direction: PositionDirection.SHORT, 53 | oraclePrice, 54 | auctionStartPrice, 55 | auctionEndPrice, 56 | limitPrice, 57 | }); 58 | 59 | assert( 60 | oracleOrderParams.auctionStartPrice.eq(new BN(10).mul(PRICE_PRECISION)) 61 | ); 62 | assert( 63 | oracleOrderParams.auctionEndPrice.eq(new BN(-10).mul(PRICE_PRECISION)) 64 | ); 65 | assert( 66 | oracleOrderParams.oraclePriceOffset === -20 * PRICE_PRECISION.toNumber() 67 | ); 68 | 69 | oracleOrderParams = deriveOracleAuctionParams({ 70 | direction: PositionDirection.SHORT, 71 | oraclePrice, 72 | auctionStartPrice: oraclePrice, 73 | auctionEndPrice: oraclePrice, 74 | limitPrice: oraclePrice, 75 | }); 76 | 77 | assert(oracleOrderParams.auctionStartPrice.eq(new BN(0))); 78 | assert(oracleOrderParams.auctionEndPrice.eq(new BN(0))); 79 | assert(oracleOrderParams.oraclePriceOffset === -1); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /sdk/tests/decode/phoenix.ts: -------------------------------------------------------------------------------- 1 | import { MarketData, deserializeMarketData } from '@ellipsis-labs/phoenix-sdk'; 2 | import { fastDecode } from '../../src/decode/phoenix'; 3 | import { Connection, PublicKey } from '@solana/web3.js'; 4 | import assert from 'assert'; 5 | 6 | describe('custom phoenix decode', function () { 7 | this.timeout(100_000); 8 | 9 | it('decodes quickly', async function () { 10 | const connection = new Connection('https://api.mainnet-beta.solana.com'); 11 | 12 | const val = await connection.getAccountInfo( 13 | new PublicKey('4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg') 14 | ); 15 | 16 | const numIterations = 100; 17 | 18 | let regularDecoded: MarketData; 19 | const regularStart = performance.now(); 20 | for (let i = 0; i < numIterations; i++) { 21 | regularDecoded = deserializeMarketData(val!.data); 22 | } 23 | const regularEnd = performance.now(); 24 | 25 | let fastDecoded: MarketData; 26 | const fastStart = performance.now(); 27 | for (let i = 0; i < numIterations; i++) { 28 | fastDecoded = fastDecode(val!.data); 29 | } 30 | const fastEnd = performance.now(); 31 | 32 | console.log(`Regular: ${regularEnd - regularStart} ms`); 33 | console.log( 34 | `Regular avg: ${(regularEnd - regularStart) / numIterations} ms` 35 | ); 36 | 37 | console.log(`Fast: ${fastEnd - fastStart} ms`); 38 | console.log(`Fast avg: ${(fastEnd - fastStart) / numIterations} ms`); 39 | 40 | // @ts-ignore 41 | assert(deepEqual(fastDecoded.bids, regularDecoded.bids)); 42 | // @ts-ignore 43 | assert(deepEqual(regularDecoded.asks, fastDecoded.asks)); 44 | }); 45 | }); 46 | 47 | function deepEqual(obj1: any, obj2: any) { 48 | if (obj1 === obj2) return true; 49 | 50 | if ( 51 | obj1 == null || 52 | obj2 == null || 53 | typeof obj1 !== 'object' || 54 | typeof obj2 !== 'object' 55 | ) { 56 | return false; 57 | } 58 | 59 | const keys1 = Object.keys(obj1); 60 | const keys2 = Object.keys(obj2); 61 | 62 | if (keys1.length !== keys2.length) return false; 63 | 64 | for (const key of keys1) { 65 | if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) { 66 | return false; 67 | } 68 | } 69 | 70 | return true; 71 | } 72 | -------------------------------------------------------------------------------- /sdk/tests/insurance/test.ts: -------------------------------------------------------------------------------- 1 | import { BN, ZERO, timeRemainingUntilUpdate, ONE } from '../../src'; 2 | // import { mockPerpMarkets } from '../dlob/helpers'; 3 | 4 | import { assert } from '../../src/assert/assert'; 5 | 6 | describe('Insurance Tests', () => { 7 | it('time remaining updates', () => { 8 | const now = new BN(1683576852); 9 | const lastUpdate = new BN(1683576000); 10 | const period = new BN(3600); //hourly 11 | 12 | let tr; 13 | // console.log(now.sub(lastUpdate).toString()); 14 | 15 | tr = timeRemainingUntilUpdate(now, lastUpdate, period); 16 | // console.log(tr.toString()); 17 | assert(tr.eq(new BN('2748'))); 18 | 19 | tr = timeRemainingUntilUpdate(now, lastUpdate.sub(period), period); 20 | // console.log(tr.toString()); 21 | assert(tr.eq(ZERO)); 22 | 23 | const tooLateUpdate = lastUpdate.sub(period.div(new BN(3)).add(ONE)); 24 | tr = timeRemainingUntilUpdate( 25 | tooLateUpdate.add(ONE), 26 | tooLateUpdate, 27 | period 28 | ); 29 | // console.log(tr.toString()); 30 | assert(tr.eq(new BN('4800'))); 31 | 32 | tr = timeRemainingUntilUpdate(now, lastUpdate.add(ONE), period); 33 | // console.log(tr.toString()); 34 | assert(tr.eq(new BN('2748'))); 35 | 36 | tr = timeRemainingUntilUpdate(now, lastUpdate.sub(ONE), period); 37 | // console.log(tr.toString()); 38 | assert(tr.eq(new BN('2748'))); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /sdk/tests/subscriber/openbook.ts: -------------------------------------------------------------------------------- 1 | import { OpenbookV2Subscriber, PRICE_PRECISION } from '../../src'; 2 | import { Connection, PublicKey } from '@solana/web3.js'; 3 | 4 | describe('openbook v2 subscriber', function () { 5 | this.timeout(100_000); 6 | 7 | it('works', async function () { 8 | if (!process.env.MAINNET_RPC_ENDPOINT) { 9 | return; 10 | } 11 | 12 | const connection = new Connection( 13 | process.env.MAINNET_RPC_ENDPOINT as string 14 | ); 15 | const solUsdc = new PublicKey( 16 | 'AFgkED1FUVfBe2trPUDqSqK9QKd4stJrfzq5q1RwAFTa' 17 | ); 18 | const openbook = new PublicKey( 19 | 'opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb' 20 | ); 21 | 22 | const openbookV2Subscriber = new OpenbookV2Subscriber({ 23 | connection, 24 | programId: openbook, 25 | marketAddress: solUsdc, 26 | accountSubscription: { 27 | type: 'websocket', 28 | }, 29 | }); 30 | 31 | await openbookV2Subscriber.subscribe(); 32 | 33 | // wait for updates 34 | await new Promise((resolve) => setTimeout(resolve, 5_000)); 35 | 36 | const basePrecision = Math.ceil( 37 | 1 / openbookV2Subscriber.market.baseNativeFactor.toNumber() 38 | ); 39 | 40 | console.log('Bids'); 41 | for (const bid of openbookV2Subscriber.getL2Bids()) { 42 | console.log('Price: ', bid.price.toNumber() / PRICE_PRECISION.toNumber()); 43 | console.log('Size: ', bid.size.toNumber() / basePrecision); 44 | console.log('Source: ', bid.sources); 45 | } 46 | 47 | console.log('Asks'); 48 | for (const ask of openbookV2Subscriber.getL2Asks()) { 49 | console.log('Price: ', ask.price.toNumber() / PRICE_PRECISION.toNumber()); 50 | console.log('Size: ', ask.size.toNumber() / basePrecision); 51 | console.log('Source: ', ask.sources); 52 | } 53 | 54 | const bestBid = await openbookV2Subscriber.getBestBid(); 55 | console.log('Best bid:', bestBid.toNumber()); 56 | 57 | const bestAsk = await openbookV2Subscriber.getBestAsk(); 58 | console.log('Best ask:', bestAsk.toNumber()); 59 | 60 | await openbookV2Subscriber.unsubscribe(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /sdk/tests/tx/priorityFeeCalculator.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { PriorityFeeCalculator } from '../../src/tx/priorityFeeCalculator'; 3 | 4 | describe('PriorityFeeCalculator', () => { 5 | let priorityFeeCalculator: PriorityFeeCalculator; 6 | 7 | const startTime = 1000000; 8 | const latch_duration = 10_000; 9 | 10 | beforeEach(() => { 11 | priorityFeeCalculator = new PriorityFeeCalculator( 12 | startTime, 13 | latch_duration 14 | ); 15 | }); 16 | 17 | it('should trigger priority fee when timeout count increases', () => { 18 | const timeoutCount = 1; 19 | expect(priorityFeeCalculator.updatePriorityFee(startTime, timeoutCount)).to 20 | .be.true; 21 | expect( 22 | priorityFeeCalculator.updatePriorityFee( 23 | startTime + latch_duration, 24 | timeoutCount + 1 25 | ) 26 | ).to.be.true; 27 | expect( 28 | priorityFeeCalculator.updatePriorityFee( 29 | startTime + latch_duration, 30 | timeoutCount + 2 31 | ) 32 | ).to.be.true; 33 | }); 34 | 35 | it('should trigger priority fee when timeout count increases, and stay latched until latch duration', () => { 36 | const timeoutCount = 1; 37 | expect(priorityFeeCalculator.updatePriorityFee(startTime, timeoutCount)).to 38 | .be.true; 39 | expect( 40 | priorityFeeCalculator.updatePriorityFee( 41 | startTime + latch_duration / 2, 42 | timeoutCount 43 | ) 44 | ).to.be.true; 45 | expect( 46 | priorityFeeCalculator.updatePriorityFee( 47 | startTime + latch_duration - 1, 48 | timeoutCount 49 | ) 50 | ).to.be.true; 51 | expect( 52 | priorityFeeCalculator.updatePriorityFee( 53 | startTime + latch_duration * 2, 54 | timeoutCount 55 | ) 56 | ).to.be.false; 57 | }); 58 | 59 | it('should not trigger priority fee when timeout count does not increase', () => { 60 | const timeoutCount = 0; 61 | expect(priorityFeeCalculator.updatePriorityFee(startTime, timeoutCount)).to 62 | .be.false; 63 | }); 64 | 65 | it('should correctly calculate compute unit price', () => { 66 | const computeUnitLimit = 1_000_000; 67 | const additionalFeeMicroLamports = 1_000_000_000; // 1000 lamports 68 | const actualComputeUnitPrice = 69 | priorityFeeCalculator.calculateComputeUnitPrice( 70 | computeUnitLimit, 71 | additionalFeeMicroLamports 72 | ); 73 | expect(actualComputeUnitPrice * computeUnitLimit).to.equal( 74 | additionalFeeMicroLamports 75 | ); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /sdk/tests/user/helpers.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | 3 | import { 4 | SpotPosition, 5 | SpotBalanceType, 6 | Order, 7 | OrderStatus, 8 | MarketType, 9 | OrderType, 10 | PositionDirection, 11 | OrderTriggerCondition, 12 | UserAccount, 13 | ZERO, 14 | MarginMode, 15 | } from '../../src'; 16 | 17 | import { mockPerpPosition } from '../dlob/helpers'; 18 | 19 | export const mockOrder: Order = { 20 | status: OrderStatus.INIT, 21 | orderType: OrderType.MARKET, 22 | marketType: MarketType.PERP, 23 | slot: ZERO, 24 | orderId: 0, 25 | userOrderId: 0, 26 | marketIndex: 0, 27 | price: ZERO, 28 | baseAssetAmount: ZERO, 29 | baseAssetAmountFilled: ZERO, 30 | quoteAssetAmount: ZERO, 31 | quoteAssetAmountFilled: ZERO, 32 | direction: PositionDirection.LONG, 33 | reduceOnly: false, 34 | triggerPrice: ZERO, 35 | triggerCondition: OrderTriggerCondition.ABOVE, 36 | existingPositionDirection: PositionDirection.LONG, 37 | postOnly: false, 38 | immediateOrCancel: false, 39 | oraclePriceOffset: 0, 40 | auctionDuration: 0, 41 | auctionStartPrice: ZERO, 42 | auctionEndPrice: ZERO, 43 | maxTs: ZERO, 44 | bitFlags: 0, 45 | postedSlotTail: 0, 46 | }; 47 | 48 | export const mockSpotPosition: SpotPosition = { 49 | marketIndex: 0, 50 | balanceType: SpotBalanceType.DEPOSIT, 51 | scaledBalance: ZERO, 52 | openOrders: 0, 53 | openBids: ZERO, 54 | openAsks: ZERO, 55 | cumulativeDeposits: ZERO, 56 | }; 57 | 58 | export const mockUserAccount: UserAccount = { 59 | authority: PublicKey.default, 60 | delegate: PublicKey.default, 61 | name: [1], 62 | subAccountId: 0, 63 | spotPositions: Array.from({ length: 8 }, function () { 64 | return Object.assign({}, mockSpotPosition); 65 | }), 66 | perpPositions: Array.from({ length: 8 }, function () { 67 | return Object.assign({}, mockPerpPosition); 68 | }), 69 | orders: Array.from({ length: 8 }, function () { 70 | return Object.assign({}, mockOrder); 71 | }), 72 | status: 0, 73 | nextLiquidationId: 0, 74 | nextOrderId: 0, 75 | maxMarginRatio: 0, 76 | lastAddPerpLpSharesTs: ZERO, 77 | settledPerpPnl: ZERO, 78 | totalDeposits: ZERO, 79 | totalWithdraws: ZERO, 80 | totalSocialLoss: ZERO, 81 | cumulativePerpFunding: ZERO, 82 | cumulativeSpotFees: ZERO, 83 | liquidationMarginFreed: ZERO, 84 | lastActiveSlot: ZERO, 85 | isMarginTradingEnabled: true, 86 | idle: false, 87 | openOrders: 0, 88 | hasOpenOrder: false, 89 | openAuctions: 0, 90 | hasOpenAuction: false, 91 | lastFuelBonusUpdateTs: 0, 92 | marginMode: MarginMode.DEFAULT, 93 | poolId: 0, 94 | }; 95 | -------------------------------------------------------------------------------- /sdk/tsconfig.browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2019", 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "outDir": "./lib/browser", 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["src"], 12 | "exclude": ["node_modules", "tests"] 13 | } 14 | -------------------------------------------------------------------------------- /sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2019", 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "outDir": "./lib/node", 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "declarationMap": true 11 | }, 12 | "include": ["src"], 13 | "exclude": ["node_modules", "tests"] 14 | } 15 | -------------------------------------------------------------------------------- /test-scripts/run-anchor-local-validator-tests.sh: -------------------------------------------------------------------------------- 1 | if [ "$1" != "--skip-build" ]; then 2 | anchor build -- --features anchor-test && 3 | cp target/idl/drift.json sdk/src/idl/ 4 | fi 5 | 6 | export ANCHOR_WALLET=~/.config/solana/id.json 7 | 8 | test_files=( 9 | # pythLazer.ts 10 | placeAndMakeSignedMsg.ts 11 | ) 12 | 13 | 14 | for test_file in ${test_files[@]}; do 15 | export ANCHOR_TEST_FILE=${test_file} && anchor test --skip-build || exit 1 16 | done -------------------------------------------------------------------------------- /test-scripts/run-anchor-tests.sh: -------------------------------------------------------------------------------- 1 | if [ "$1" != "--skip-build" ]; then 2 | anchor build -- --features anchor-test && anchor test --skip-build && 3 | cp target/idl/drift.json sdk/src/idl/ 4 | fi 5 | 6 | export ANCHOR_WALLET=~/.config/solana/id.json 7 | 8 | test_files=( 9 | # cappedSymFunding.ts 10 | # delistMarket.ts 11 | # delistMarketLiq.ts 12 | # imbalancePerpPnl.ts 13 | # ksolver.ts 14 | # repegAndSpread.ts 15 | # spotWithdrawUtil100.ts 16 | # updateAMM.ts 17 | # updateK.ts 18 | # postOnlyAmmFulfillment.ts 19 | # TODO BROKEN ^^ 20 | decodeUser.ts 21 | fuel.ts 22 | fuelSweep.ts 23 | admin.ts 24 | assetTier.ts 25 | cancelAllOrders.ts 26 | curve.ts 27 | deleteInitializedSpotMarket.ts 28 | depositIntoSpotMarketVault.ts 29 | driftClient.ts 30 | fillSpot.ts 31 | highLeverageMode.ts 32 | insuranceFundStake.ts 33 | liquidateBorrowForPerpPnl.ts 34 | liquidateMaxLps.ts 35 | liquidatePerp.ts 36 | liquidatePerpAndLp.ts 37 | liquidatePerpWithFill.ts 38 | liquidatePerpPnlForDeposit.ts 39 | liquidateSpot.ts 40 | liquidateSpotSocialLoss.ts 41 | liquidityProvider.ts 42 | marketOrder.ts 43 | marketOrderBaseAssetAmount.ts 44 | maxDeposit.ts 45 | maxLeverageOrderParams.ts 46 | modifyOrder.ts 47 | multipleMakerOrders.ts 48 | multipleSpotMakerOrders.ts 49 | openbookTest.ts 50 | oracleDiffSources.ts 51 | oracleFillPriceGuardrails.ts 52 | oracleOffsetOrders.ts 53 | order.ts 54 | ordersWithSpread.ts 55 | pauseExchange.ts 56 | pauseDepositWithdraw.ts 57 | perpLpJit.ts 58 | perpLpRiskMitigation.ts 59 | phoenixTest.ts 60 | placeAndMakePerp.ts 61 | placeAndMakeSignedMsgBankrun.ts 62 | placeAndMakeSpotOrder.ts 63 | postOnly.ts 64 | prelisting.ts 65 | pyth.ts 66 | pythPull.ts 67 | pythLazerBankrun.ts 68 | referrer.ts 69 | roundInFavorBaseAsset.ts 70 | serumTest.ts 71 | spotDepositWithdraw.ts 72 | spotDepositWithdraw22.ts 73 | spotMarketPoolIds.ts 74 | spotSwap.ts 75 | spotSwap22.ts 76 | stopLimits.ts 77 | subaccounts.ts 78 | surgePricing.ts 79 | switchboardTxCus.ts 80 | switchOracle.ts 81 | tradingLP.ts 82 | triggerOrders.ts 83 | triggerSpotOrder.ts 84 | transferPerpPosition.ts 85 | userAccount.ts 86 | userDelegate.ts 87 | userOrderId.ts 88 | whitelist.ts 89 | ) 90 | 91 | 92 | for test_file in ${test_files[@]}; do 93 | ts-mocha -t 300000 ./tests/${test_file} || exit 1 94 | done -------------------------------------------------------------------------------- /test-scripts/run-ts-mocha: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ts-mocha -t 1000000 ./tests/${ANCHOR_TEST_FILE} -------------------------------------------------------------------------------- /test-scripts/single-anchor-test.sh: -------------------------------------------------------------------------------- 1 | if [ "$1" != "--skip-build" ] 2 | then 3 | anchor build -- --features anchor-test && anchor test --skip-build && 4 | cp target/idl/drift.json sdk/src/idl/ 5 | fi 6 | 7 | export ANCHOR_WALLET=~/.config/solana/id.json 8 | 9 | test_files=(adminDeposit.ts) 10 | 11 | for test_file in ${test_files[@]}; do 12 | ts-mocha -t 300000 ./tests/${test_file} 13 | done 14 | -------------------------------------------------------------------------------- /tests/fixtures/openbook.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drift-labs/protocol-v2/acf51a0f57a467334948ad54b3799ac3014fdb9e/tests/fixtures/openbook.so -------------------------------------------------------------------------------- /tests/fixtures/phoenix_dex.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drift-labs/protocol-v2/acf51a0f57a467334948ad54b3799ac3014fdb9e/tests/fixtures/phoenix_dex.so -------------------------------------------------------------------------------- /tests/fixtures/pyth_solana_receiver.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drift-labs/protocol-v2/acf51a0f57a467334948ad54b3799ac3014fdb9e/tests/fixtures/pyth_solana_receiver.so -------------------------------------------------------------------------------- /tests/fixtures/serum_dex.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drift-labs/protocol-v2/acf51a0f57a467334948ad54b3799ac3014fdb9e/tests/fixtures/serum_dex.so -------------------------------------------------------------------------------- /tests/liquidityBook.ts: -------------------------------------------------------------------------------- 1 | import { BN } from '@coral-xyz/anchor'; 2 | import { 3 | PRICE_PRECISION, 4 | PerpMarketAccount, 5 | calculateReservePrice, 6 | calculateTargetPriceTrade, 7 | ZERO, 8 | } from '../sdk/src'; 9 | 10 | /** 11 | * liquidityBook 12 | * show snapshot of liquidity, similar to traditional orderbook 13 | * @param market 14 | * @param N number of bids/asks 15 | * @param incrementSize grouping of liquidity by pct price move 16 | * @returns 17 | */ 18 | export function liquidityBook( 19 | market: PerpMarketAccount, 20 | N = 5, 21 | incrementSize = 0.1 22 | ) { 23 | const defaultSlippageBN = new BN(incrementSize * PRICE_PRECISION.toNumber()); 24 | const baseAssetPriceWithMantissa = calculateReservePrice(market); 25 | const bidsPrice = []; 26 | const bidsCumSize = []; 27 | const asksPrice = []; 28 | const asksCumSize = []; 29 | 30 | for (let i = 1; i <= N; i++) { 31 | const targetPriceDefaultSlippage = baseAssetPriceWithMantissa 32 | .mul(PRICE_PRECISION.add(defaultSlippageBN.mul(new BN(i)))) 33 | .div(PRICE_PRECISION); 34 | const [_direction, liquidity, entryPrice] = calculateTargetPriceTrade( 35 | market, 36 | BN.max(targetPriceDefaultSlippage, new BN(1)) 37 | ); 38 | 39 | console.log(liquidity.toString()); 40 | if (liquidity.gt(ZERO)) { 41 | asksPrice.push(entryPrice); 42 | asksCumSize.push(liquidity); 43 | } 44 | 45 | const targetPriceDefaultSlippageBid = baseAssetPriceWithMantissa 46 | .mul(PRICE_PRECISION.sub(defaultSlippageBN.mul(new BN(i)))) 47 | .div(PRICE_PRECISION); 48 | const [_directionBid, liquidityBid, entryPriceBid] = 49 | calculateTargetPriceTrade( 50 | market, 51 | BN.max(targetPriceDefaultSlippageBid, new BN(1)) 52 | ); 53 | 54 | if (liquidityBid.gt(ZERO)) { 55 | bidsPrice.push(entryPriceBid); 56 | bidsCumSize.push(liquidityBid); 57 | } 58 | } 59 | 60 | return [bidsPrice, bidsCumSize, asksPrice, asksCumSize]; 61 | } 62 | -------------------------------------------------------------------------------- /tests/pythLazerData.ts: -------------------------------------------------------------------------------- 1 | export const PYTH_STORAGE_DATA = 2 | '0XX/ucSvRAkL/td28gTUmmjn6CkzKyvYXJOMcup4pEKu3cXcP7cvDAv+13byBNSaaOfoKTMrK9hck4xy6nikQq7dxdw/ty8MAQAAAAAAAAAB9lIQvuT89bHO4eU3+rz9lQECl2U7lK8E1FT8Rz6Ug0/oNgZ6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; 3 | export const PYTH_LAZER_HEX_STRING_BTC = 4 | 'b9011a82724cc202a99fd13b7b7c86db3139d69fc32894a31bdaa07beebc4a3e5371599c471d78b2ddf84b64a6b210b14de936316dfa03c8d28644331fa8ca6f20ea0904f65210bee4fcf5b1cee1e537fabcfd95010297653b94af04d454fc473e94834f310075d3c79380943b82452d06000301010000000400a11ac87c3b09000002c53b4ff43d0900000110bffd9c3809000004f8ff'; 5 | 6 | export const PYTH_LAZER_HEX_STRING_SOL = 7 | 'b9011a82ddf32394c899c6df8af8948db062f7795dc46c07206253b00c402af40c830cbda12ece76dbd1b90e0b44a94399a86fbf2c55815e3c48f580092ed098ce11f302f65210bee4fcf5b1cee1e537fabcfd95010297653b94af04d454fc473e94834f310075d3c79300a1e56e452d06000301060000000400023ca7090500000002c214050b0500000001e68210080500000004f8ff'; 8 | 9 | export const PYTH_LAZER_HEX_STRING_SOL_LATER = 10 | 'b9011a8262ed9fd4ecebc7c161d5d21c5313514fba286b89f903aa65a9daa69ed26154340570921e6fd8ea272856cf3f4959114f9e76c5ebe906c75820e28cef2bb7d909f65210bee4fcf5b1cee1e537fabcfd95010297653b94af04d454fc473e94834f310075d3c79380bbeb6e452d06000301060000000400980bab090500000002c214050b0500000001bc2118080500000004f8ff'; 11 | 12 | export const PYTH_LAZER_HEX_STRING_MULTI = 13 | 'b9011a82c7ca01a7763210edc0895200419a37632e6898f9e43714868d902326a9aff45dd7ea09052b5c9b026fdfec3398c8bba013decf7ba3d46d811684b73d9e1aa00df65210bee4fcf5b1cee1e537fabcfd95010297653b94af04d454fc473e94834f770075d3c79380190b83452d06000303010000000400ed7287213c0900000224ab12883e09000001d3f9b0523909000004f8ff020000000400730f58f64200000002f88d560843000000016c2ddce04200000004f8ff06000000040023a60b0805000000024d7c69090500000001cb0175060500000004f8ff'; 14 | -------------------------------------------------------------------------------- /tests/pythPullOracleData.ts: -------------------------------------------------------------------------------- 1 | export const WORMHOLE_DATA = 2 | 'eE1KYiJTYH0EAAAAEwAAAFiTtadsP3OWRWSIhb3MwGzXCjzT/2y5Ulib3oYsJe9DkhMvudSkIVcRTehGAZO986L8+B+GoJdl9HYv0RB6AIazLXoJd5JqIFEx2HMdOcvrjIKy/YL67ScR1Zrw8kmdFucm9rIRs5dWwEJEG+bYZQtptU6+cV4jQ1TOW000j7dLlY6JZuLsPb1JWKfNFefK8HxOPcjnxGn5LIzYj7gAWiB0o7+ROVPWlSYNiLwaolpO7jY+8AAKwAdnJ7NfvqLawo/uXMsP6naOr0XO0Ta52eJJA0ZK6In1yKcj/BT5MSS3xziEPLuJ6GTIYsOM3czPldLMN6TcA2qNIytI9izdRzFBL0iQ2nmPaJajMx9ktIwS0dV/2cvnCBFxqhvh02yv44Z5EPmcCeNHiZwZw4GStuc4fM12gnfBfasbelAnwLPPF44hrS53rgZxFUnPux+cep2AluheFIfzVRXQKpJ1NQSo11RxufSe22++vImPQD5Hc+lf6xXoDJqZyDSNeJkeZgAAAAA='; 3 | 4 | export const PYTH_ORACLE_TWO_DATA = 5 | 'IvEjY51+9M1gMUcENA3t3zcf1CRyFI8kjp0abRpesqw6zYt/1dayQwHvDYtv2izrpB2hXUCV0do5Kg0vjtDGx7wPTPrIwoC1bdpHvSIDAAAATo3GAAAAAAD4////ALVxZgAAAAAAtXFmAAAAAMRoiicDAAAAi6PEAAAAAAB9pz8QAAAAAAA='; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "resolveJsonModule": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tsconfig": "sdk/tsconfig.json", 3 | "entryPoints": ["sdk/src/index.ts"], 4 | "out": "docs/sdk/" 5 | } --------------------------------------------------------------------------------