diff --git a/README.md b/README.md index 05c5827a..42e79365 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ -DriftPy is the Python client for the [Drift](https://www.drift.trade/) protocol. It allows you to trade and fetch data from Drift using Python. +DriftPy is the Python client for the [Drift](https://www.drift.trade/) protocol. +It allows you to trade and fetch data from Drift using Python. **[Read the full SDK documentation here!](https://drift-labs.github.io/v2-teacher/)** @@ -16,7 +17,13 @@ pip install driftpy Note: requires Python >= 3.10. -## ⚠️ IMPORTANT ⚠️ + +## SDK Examples + +- `examples/` folder includes more examples of how to use the SDK including how to provide liquidity/become an lp, stake in the insurance fund, etc. + + +## Note on using QuickNode If you are using QuickNode free plan, you *must* use `AccountSubscriptionConfig("demo")`, and you can only subscribe to 1 perp market and 1 spot market at a time. @@ -24,7 +31,7 @@ Non-QuickNode free RPCs (including the public mainnet-beta url) can use `cached` Example setup for `AccountSubscriptionConfig("demo")`: -``` +```python # This example will listen to perp markets 0 & 1 and spot market 0 # If you are listening to any perp markets, you must listen to spot market 0 or the SDK will break @@ -48,31 +55,30 @@ If you intend to use `AccountSubscriptionConfig("demo)`, you *must* call `get_ma `get_markets_and_oracles` will return all the necessary `OracleInfo`s and `market_indexes` in order to use the SDK. -## SDK Examples - -- `examples/` folder includes more examples of how to use the SDK including how to provide liquidity/become an lp, stake in the insurance fund, etc. +# Development ## Setting Up Dev Env `bash setup.sh` -# Development Ensure correct python version (using pyenv is recommended): -``` +```bash pyenv install 3.10.11 pyenv global 3.10.11 poetry env use $(pyenv which python) ``` Install dependencies: -``` +```bash poetry install ``` -Run tests: -``` -poetry run bash test-scripts/integration_test.sh -poetry run bash test-scripts/math_tests.sh -``` +To run tests, first ensure you have set up the RPC url, then run `pytest`: +```bash +export MAINNET_RPC_ENDPOINT="" +export DEVNET_RPC_ENDPOINT="https://api.devnet.solana.com" # or your own RPC +poetry run pytest -v -s -x tests/ci/*.py +poetry run pytest -v -s tests/math/*.py +``` diff --git a/setup.sh b/setup.sh old mode 100644 new mode 100755 index ceda1269..1a8c50d8 --- a/setup.sh +++ b/setup.sh @@ -1,8 +1,3 @@ git submodule update --init --recursive -# build v2 cd protocol-v2 -yarn && anchor build -# build dependencies for v2 -cd deps/serum-dex/dex && anchor build && cd ../../ -# go back to top-level -cd ../ \ No newline at end of file +yarn && anchor build \ No newline at end of file diff --git a/src/driftpy/constants/perp_markets.py b/src/driftpy/constants/perp_markets.py index ce5b10de..0d7a02dd 100644 --- a/src/driftpy/constants/perp_markets.py +++ b/src/driftpy/constants/perp_markets.py @@ -521,4 +521,11 @@ class PerpMarketConfig: oracle=Pubkey.from_string("DpJz7rjTJLxxnuqrqZTUjMWtnaMFAEfZUv5ATdb9HTh1"), oracle_source=OracleSource.Prelaunch(), ), + PerpMarketConfig( + symbol="MOTHER-PERP", + base_asset_symbol="MOTHER", + market_index=44, + oracle=Pubkey.from_string("56ap2coZG7FPWUigVm9XrpQs3xuCwnwQaWtjWZcffEUG"), + oracle_source=OracleSource.PythPull(), + ), ] diff --git a/src/driftpy/drift_client.py b/src/driftpy/drift_client.py index 3e3eb466..c59284ab 100644 --- a/src/driftpy/drift_client.py +++ b/src/driftpy/drift_client.py @@ -1,57 +1,69 @@ import json import os -import anchorpy -from deprecated import deprecated -import requests -from solders.pubkey import Pubkey -from solders.keypair import Keypair -from solders.transaction import TransactionVersion, Legacy -from solders.instruction import Instruction -from solders.system_program import ID -from solders.sysvar import RENT -from solders.signature import Signature -from solders.address_lookup_table_account import AddressLookupTableAccount -from solana.rpc.async_api import AsyncClient -from solana.rpc.types import TxOpts -from solana.rpc.commitment import Processed -from solana.transaction import AccountMeta -from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price -from spl.token.constants import TOKEN_PROGRAM_ID -from spl.token.instructions import get_associated_token_address -from anchorpy import Program, Context, Idl, Provider, Wallet from pathlib import Path +import random +import string +from typing import List, Optional, Tuple, Union +import anchorpy +from anchorpy import Context +from anchorpy import Idl +from anchorpy import Program +from anchorpy import Provider +from anchorpy import Wallet +from deprecated import deprecated import driftpy from driftpy.account_subscription_config import AccountSubscriptionConfig +from driftpy.accounts import * from driftpy.address_lookup_table import get_address_lookup_table -from driftpy.constants import BASE_PRECISION, PRICE_PRECISION -from driftpy.constants.numeric_constants import ( - QUOTE_SPOT_MARKET_INDEX, -) +from driftpy.addresses import get_sequencer_public_key_and_bump +from driftpy.constants import BASE_PRECISION +from driftpy.constants import PRICE_PRECISION +from driftpy.constants.config import configs +from driftpy.constants.config import decode_account +from driftpy.constants.config import DEVNET_SEQUENCER_PROGRAM_ID +from driftpy.constants.config import DRIFT_PROGRAM_ID +from driftpy.constants.config import DriftEnv +from driftpy.constants.config import SEQUENCER_PROGRAM_ID +from driftpy.constants.numeric_constants import QUOTE_SPOT_MARKET_INDEX +from driftpy.constants.spot_markets import WRAPPED_SOL_MINT from driftpy.decode.utils import decode_name from driftpy.drift_user import DriftUser -from driftpy.accounts import * - -from driftpy.constants.config import ( - DriftEnv, - DRIFT_PROGRAM_ID, - configs, - SEQUENCER_PROGRAM_ID, - DEVNET_SEQUENCER_PROGRAM_ID, - decode_account, -) - -from typing import Tuple, Union, Optional, List -from driftpy.drift_user_stats import DriftUserStats, UserStatsSubscriptionConfig +from driftpy.drift_user_stats import DriftUserStats +from driftpy.drift_user_stats import UserStatsSubscriptionConfig from driftpy.math.perp_position import is_available -from driftpy.math.spot_position import is_spot_position_available from driftpy.math.spot_market import cast_to_spot_precision +from driftpy.math.spot_position import is_spot_position_available from driftpy.name import encode_name from driftpy.tx.standard_tx_sender import StandardTxSender -from driftpy.tx.types import TxSender, TxSigAndSlot -from driftpy.addresses import get_sequencer_public_key_and_bump -from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID +from driftpy.tx.types import TxSender +from driftpy.tx.types import TxSigAndSlot +import requests +from solana.rpc.async_api import AsyncClient +from solana.rpc.commitment import Processed +from solana.rpc.types import TxOpts +from solana.transaction import AccountMeta +from solders import system_program +from solders.address_lookup_table_account import AddressLookupTableAccount +from solders.compute_budget import set_compute_unit_limit +from solders.compute_budget import set_compute_unit_price +from solders.instruction import Instruction +from solders.keypair import Keypair +from solders.pubkey import Pubkey +from solders.signature import Signature +from solders.system_program import ID from solders.system_program import ID as SYS_PROGRAM_ID +from solders.sysvar import RENT +from solders.transaction import Legacy +from solders.transaction import TransactionVersion +from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID +from spl.token.constants import TOKEN_PROGRAM_ID +from spl.token.instructions import close_account +from spl.token.instructions import CloseAccountParams +from spl.token.instructions import get_associated_token_address +from spl.token.instructions import initialize_account +from spl.token.instructions import InitializeAccountParams + DEFAULT_USER_NAME = "Main Account" @@ -863,12 +875,52 @@ def get_initialize_user_instructions( ) return initialize_user_account_ix + def random_string(self, length: int) -> str: + return "".join(random.choices(string.ascii_letters + string.digits, k=length)) + + async def get_wrapped_sol_account_creation_ixs( + self, amount: int, include_rent: bool = True + ) -> (List[Instruction], Pubkey): + wallet_pubkey = self.wallet.public_key + seed = self.random_string(32) + wrapped_sol_account = Pubkey.create_with_seed( + wallet_pubkey, seed, TOKEN_PROGRAM_ID + ) + result = {"ixs": [], "pubkey": wrapped_sol_account} + + LAMPORTS_PER_SOL: int = 1_000_000_000 + rent_space_lamports = int(LAMPORTS_PER_SOL / 100) + lamports = amount + rent_space_lamports if include_rent else rent_space_lamports + + create_params = system_program.CreateAccountWithSeedParams( + from_pubkey=wallet_pubkey, + to_pubkey=wrapped_sol_account, + base=wallet_pubkey, + seed=seed, + lamports=lamports, + space=165, + owner=TOKEN_PROGRAM_ID, + ) + + result["ixs"].append(system_program.create_account_with_seed(create_params)) + + initialize_params = InitializeAccountParams( + program_id=TOKEN_PROGRAM_ID, + account=wrapped_sol_account, + mint=WRAPPED_SOL_MINT, + owner=wallet_pubkey, + ) + + result["ixs"].append(initialize_account(initialize_params)) + + return result["ixs"], result["pubkey"] + async def deposit( self, amount: int, spot_market_index: int, - user_token_account: Pubkey = None, - sub_account_id: int = None, + user_token_account: Pubkey, + sub_account_id: Optional[int] = None, reduce_only=False, user_initialized=True, ): @@ -886,30 +938,39 @@ async def deposit( str: sig """ tx_sig_and_slot = await self.send_ixs( - [ - self.get_deposit_collateral_ix( - amount, - spot_market_index, - user_token_account, - sub_account_id, - reduce_only, - user_initialized, - ) - ] + await self.get_deposit_collateral_ix( + amount, + spot_market_index, + user_token_account, + sub_account_id, + reduce_only, + user_initialized, + ) ) self.last_spot_market_seen_cache[spot_market_index] = tx_sig_and_slot.slot return tx_sig_and_slot - def get_deposit_collateral_ix( + async def get_deposit_collateral_ix( self, amount: int, spot_market_index: int, - user_token_account: Pubkey = None, - sub_account_id: int = None, - reduce_only=False, - user_initialized=True, - ) -> Instruction: + user_token_account: Pubkey, + sub_account_id: Optional[int] = None, + reduce_only: Optional[bool] = False, + user_initialized: Optional[bool] = True, + ) -> List[Instruction]: + sub_account_id = self.get_sub_account_id_for_ix(sub_account_id) + spot_market_account = self.get_spot_market_account(spot_market_index) + if not spot_market_account: + raise Exception("Spot market account not found") + + is_sol_market = spot_market_account.mint == WRAPPED_SOL_MINT + signer_authority = self.wallet.public_key + + create_WSOL_token_account = ( + is_sol_market and user_token_account == signer_authority + ) if user_initialized: remaining_accounts = self.get_remaining_accounts( @@ -919,6 +980,13 @@ def get_deposit_collateral_ix( else: raise Exception("not implemented...") + instructions = [] + + if create_WSOL_token_account: + ixs, ata_pubkey = await self.get_wrapped_sol_account_creation_ixs(amount) + instructions.extend(ixs) + user_token_account = ata_pubkey + user_token_account = ( user_token_account if user_token_account is not None @@ -932,7 +1000,7 @@ def get_deposit_collateral_ix( user_account_public_key = get_user_account_public_key( self.program_id, self.authority, sub_account_id ) - return self.program.instruction["deposit"]( + deposit_ix = self.program.instruction["deposit"]( spot_market_index, amount, reduce_only, @@ -950,6 +1018,16 @@ def get_deposit_collateral_ix( remaining_accounts=remaining_accounts, ), ) + instructions.append(deposit_ix) + close_account_params = CloseAccountParams( + program_id=TOKEN_PROGRAM_ID, + account=ata_pubkey, + dest=signer_authority, + owner=signer_authority, + ) + close_account_ix = close_account(close_account_params) + instructions.append(close_account_ix) + return instructions async def withdraw( self, diff --git a/src/driftpy/drift_user.py b/src/driftpy/drift_user.py index 9a84a0b0..59673da1 100644 --- a/src/driftpy/drift_user.py +++ b/src/driftpy/drift_user.py @@ -1,29 +1,23 @@ +import copy import math import time -import copy - from typing import Tuple from driftpy.account_subscription_config import AccountSubscriptionConfig +from driftpy.accounts.oracle import * from driftpy.math.amm import calculate_market_open_bid_ask from driftpy.math.conversion import convert_to_number +from driftpy.math.fuel import calculate_insurance_fuel_bonus +from driftpy.math.fuel import calculate_perp_fuel_bonus +from driftpy.math.fuel import calculate_spot_fuel_bonus +from driftpy.math.margin import * from driftpy.math.oracles import calculate_live_oracle_twap from driftpy.math.perp_position import * -from driftpy.math.margin import * from driftpy.math.spot_balance import get_strict_token_value from driftpy.math.spot_market import * -from driftpy.math.fuel import ( - calculate_spot_fuel_bonus, - calculate_perp_fuel_bonus, - calculate_insurance_fuel_bonus, -) -from driftpy.accounts.oracle import * -from driftpy.math.spot_position import ( - calculate_weighted_token_value, - get_worst_case_token_amounts, - is_spot_position_available, -) -from driftpy.math.amm import calculate_market_open_bid_ask +from driftpy.math.spot_position import calculate_weighted_token_value +from driftpy.math.spot_position import get_worst_case_token_amounts +from driftpy.math.spot_position import is_spot_position_available from driftpy.oracles.strict_oracle_price import StrictOraclePrice from driftpy.types import OraclePriceData @@ -141,7 +135,7 @@ def get_spot_position(self, market_index: int) -> Optional[SpotPosition]: def get_perp_market_liability( self, - market_index: int = None, + market_index: int, margin_category: Optional[MarginCategory] = None, liquidation_buffer: Optional[int] = 0, include_open_orders: bool = False, @@ -768,27 +762,8 @@ def get_spot_market_liability_value( return total_liability_value def get_leverage(self, include_open_orders: bool = True) -> int: - perp_liability = self.get_perp_market_liability( - include_open_orders=include_open_orders - ) - perp_pnl = self.get_unrealized_pnl(True) - - ( - spot_asset_value, - spot_liability_value, - ) = self.get_spot_market_asset_and_liability_value( - include_open_orders=include_open_orders - ) - - total_asset_value = spot_asset_value + perp_pnl - total_liability_value = spot_liability_value + perp_liability - - net_asset_value = total_asset_value - spot_liability_value - - if net_asset_value == 0: - return 0 - - return (total_liability_value * 10_000) // net_asset_value + leverage_components = self.get_leverage_components(include_open_orders) + return self.calculate_leverage_from_components(leverage_components) def get_leverage_components( self, @@ -810,6 +785,18 @@ def get_leverage_components( return perp_liability, perp_pnl, spot_asset_value, spot_liability_value + def calculate_leverage_from_components(self, components: Tuple[int, int, int, int]): + perp_liability, perp_pnl, spot_asset_value, spot_liability_value = components + + total_liabs = perp_liability + spot_liability_value + total_assets = spot_asset_value + perp_pnl + net_assets = total_assets - spot_liability_value + + if net_assets == 0: + return 0 + + return (total_liabs * 10_000) // net_assets + def get_max_leverage_for_perp( self, perp_market_index: int, @@ -1467,7 +1454,7 @@ def get_total_perp_position_liability( liquidation_buffer: int = 0, include_open_orders: bool = False, strict: bool = False, - ) -> int: + ): total_perp_value = 0 for perp_position in self.get_active_perp_positions(): base_asset_value = self.calculate_weighted_perp_position_liability( diff --git a/src/driftpy/idl/drift.json b/src/driftpy/idl/drift.json index c7ac9b38..98bf266a 100644 --- a/src/driftpy/idl/drift.json +++ b/src/driftpy/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.89.0", + "version": "2.95.0", "name": "drift", "instructions": [ { @@ -1606,15 +1606,20 @@ { "name": "settleExpiredMarket", "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, { "name": "state", "isMut": false, "isSigner": false }, { - "name": "authority", - "isMut": false, - "isSigner": true + "name": "perpMarket", + "isMut": true, + "isSigner": false } ], "args": [ @@ -1881,6 +1886,27 @@ } ] }, + { + "name": "setUserStatusToBeingLiquidated", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, { "name": "resolvePerpPnlDeficit", "accounts": [ @@ -2259,7 +2285,7 @@ }, { "name": "spotMarket", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -2273,7 +2299,7 @@ "isSigner": false }, { - "name": "authority", + "name": "signer", "isMut": false, "isSigner": true }, @@ -2295,7 +2321,7 @@ }, { "name": "spotMarket", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -2309,7 +2335,7 @@ "isSigner": false }, { - "name": "authority", + "name": "signer", "isMut": false, "isSigner": true }, @@ -3403,6 +3429,27 @@ } ] }, + { + "name": "initializePredictionMarket", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, { "name": "deleteInitializedPerpMarket", "accounts": [ @@ -12906,6 +12953,11 @@ "code": 6283, "name": "LiquidationOrderFailedToFill", "msg": "Liquidation order failed to fill" + }, + { + "code": 6284, + "name": "InvalidPredictionMarketOrder", + "msg": "Invalid prediction market order" } ] } \ No newline at end of file diff --git a/tests/ci/mainnet.py b/tests/ci/mainnet.py index ed6e03db..756719be 100644 --- a/tests/ci/mainnet.py +++ b/tests/ci/mainnet.py @@ -1,17 +1,14 @@ -import os -import pytest import asyncio - -from pytest import mark - -from solana.rpc.async_api import AsyncClient +import os from anchorpy import Wallet - -from driftpy.drift_client import DriftClient from driftpy.account_subscription_config import AccountSubscriptionConfig from driftpy.constants.perp_markets import mainnet_perp_market_configs from driftpy.constants.spot_markets import mainnet_spot_market_configs +from driftpy.drift_client import DriftClient +import pytest +from pytest import mark +from solana.rpc.async_api import AsyncClient @pytest.fixture(scope="session") diff --git a/update_idl.sh b/update_idl.sh old mode 100644 new mode 100755