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