Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,14 @@ jobs:

- name: Run type checker (pyright)
run: uv run pyright

- name: Run tests
run: |
uv run --isolated --python=3.12 pytest
uv run --isolated --python=3.13 pytest
uv run --isolated --python=3.14 pytest

env:
V3_API_KEY: ${{ secrets.V3_API_KEY }}
V3_API_KEY_SECRET: ${{ secrets.V3_API_KEY_SECRET }}
V3_API_KEY_PASSPHRASE: ${{ secrets.V3_API_KEY_PASSPHRASE }}
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This is the Python version of the LN Markets API SDK. It provides a client-based
For public endpoints, you can just do this:

```python
from lnmarkets_sdk.client import LNMClient
from lnmarkets_sdk.http.client import LNMClient
import asyncio

async with LNMClient() as client:
Expand All @@ -21,7 +21,7 @@ Remember to sleep between requests, as the rate limit is 1 requests per second f
For endpoints that need authentication, you need to create an instance of the `LNMClient` class and provide your API credentials:

```python
from lnmarkets_sdk.client import APIAuthContext, APIClientConfig, LNMClient
from lnmarkets_sdk.http.client import APIAuthContext, APIClientConfig, LNMClient

config = APIClientConfig(
authentication=APIAuthContext(
Expand All @@ -41,7 +41,7 @@ For endpoints that requires input parameters, you can find the corresponding mod

```python

from lnmarkets_sdk.client import APIAuthContext, APIClientConfig, LNMClient
from lnmarkets_sdk.http.client import APIAuthContext, APIClientConfig, LNMClient
from lnmarkets_sdk.models.account import GetLightningDepositsParams

config = APIClientConfig(
Expand Down
162 changes: 162 additions & 0 deletions examples/basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"""
Example usage of the v3 client-based API.
"""

import asyncio
import os
from pprint import pprint

from dotenv import load_dotenv

from lnmarkets_sdk.http.client import APIAuthContext, APIClientConfig, LNMClient
from lnmarkets_sdk.models.account import GetLightningDepositsParams
from lnmarkets_sdk.models.futures_cross import (
FuturesCrossOrderLimit,
)

load_dotenv()


async def example_public_endpoints():
"""Example: Make public requests without authentication."""
print("\n" + "=" * 80)
print("PUBLIC ENDPOINTS EXAMPLE")
print("=" * 80)

# Create client without authentication for public endpoints
# The httpx.AsyncClient is created once and reuses connections
async with LNMClient(APIClientConfig(network="testnet4")) as client:
# All these requests share the same connection pool
print("\n🔄 Making multiple requests with connection reuse...")

# Get futures ticker
ticker = await client.futures.get_ticker()
print("\n--- Futures Ticker ---")
pprint(ticker.model_dump(), indent=2, width=100)

# Get leaderboard
await asyncio.sleep(1)
leaderboard = await client.futures.get_leaderboard()
print("\n--- Leaderboard (Top 3 Daily) ---")
pprint(leaderboard.daily[:3], indent=2, width=100)

# Get oracle data
await asyncio.sleep(1)
oracle_index = await client.oracle.get_index()
print("\n--- Oracle Index (Latest) ---")
pprint(oracle_index[0].model_dump() if oracle_index else "No data", indent=2)

# Get synthetic USD best price
await asyncio.sleep(1)
best_price = await client.synthetic_usd.get_best_price()
print("\n--- Synthetic USD Best Price ---")
pprint(best_price.model_dump(), indent=2)

# Ping the API
await asyncio.sleep(1)
ping_response = await client.ping()
print("\n--- Ping ---")
print(f"Response: {ping_response}")


async def example_authenticated_endpoints():
"""Example: Use authenticated endpoints with credentials."""
print("\n" + "=" * 80)
print("AUTHENTICATED ENDPOINTS EXAMPLE")
print("=" * 80)

key = os.getenv("V3_API_KEY")
secret = os.getenv("V3_API_KEY_SECRET")
passphrase = os.getenv("V3_API_KEY_PASSPHRASE")
print(f"key: {key}")
print(f"secret: {secret}")
print(f"passphrase: {passphrase}")

if not (key and secret and passphrase):
print("\n⚠️ Skipping authenticated example:")
print(" Please set V3_API_KEY, V3_API_KEY_SECRET, and V3_API_KEY_PASSPHRASE")
return

# Create config with authentication and custom timeout
config = APIClientConfig(
authentication=APIAuthContext(
key=key,
secret=secret,
passphrase=passphrase,
),
network="testnet4",
timeout=60.0, # 60 second timeout (default is 30s)
)

async with LNMClient(config) as client:
print("\n🔐 Using authenticated endpoints with connection pooling...")

# Get account info
account = await client.account.get_account()
print("\n--- Account Info ---")
print(f"Username: {account.username}")
print(f"Balance: {account.balance} sats")
print(f"Synthetic USD Balance: {account.synthetic_usd_balance}")

# Get Bitcoin address
btc_address = await client.account.get_bitcoin_address()
print("\n--- Bitcoin Deposit Address ---")
print(f"Address: {btc_address.address}")

# Get lightning deposits (last 5)
deposits = await client.account.get_lightning_deposits(
GetLightningDepositsParams(from_="2022-01-01", limit=5)
)
print(f"\n--- Recent Lightning Deposits (Last {len(deposits)}) ---")
for deposit in deposits:
print(f"Deposits {deposit.amount} sats at {deposit.created_at}")

# Get running trades
running_trades = await client.futures.isolated.get_running_trades()
print("\n--- Running Isolated Trades ---")
print(f"Count: {len(running_trades)}")
for trade in running_trades[:3]: # Show first 3
side = "LONG" if trade.side == "b" else "SHORT"
print(
f" {side} - Margin: {trade.margin} sats, Leverage: {trade.leverage}x, PL: {trade.pl} sats"
)

# Get cross margin position
try:
position = await client.futures.cross.get_position()
print("\n--- Cross Margin Position ---")
print(f"Quantity: {position.quantity}")
print(f"Margin: {position.margin} sats")
print(f"Leverage: {position.leverage}x")
print(f"Total PL: {position.total_pl} sats")
except Exception as e:
print(f"Error: {e}")

# Open a new cross order
try:
print("\n--- Try to open a new cross order ---")
order_params = FuturesCrossOrderLimit(
type="limit", price=1.5, quantity=1, side="b"
)
new_order = await client.futures.cross.new_order(order_params)
print(f"New order: {new_order}")
except Exception as e:
print(f"Error: {e}")


async def main():
"""Run all examples."""
print("\n" + "=" * 80)
print("LN MARKETS V3 CLIENT EXAMPLES")
print("=" * 80)

await example_public_endpoints()
await example_authenticated_endpoints()

print("\n" + "=" * 80)
print("EXAMPLES COMPLETE")
print("=" * 80 + "\n")


if __name__ == "__main__":
asyncio.run(main())
21 changes: 19 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "hatchling.build"
urls = { "Homepage" = "https://github.com/ln-markets/sdk-python", "Repository" = "https://github.com/ln-markets/sdk-python", "Bug Tracker" = "https://github.com/ln-markets/sdk-python/issues" }

name = "lnmarkets-sdk"
version = "0.0.3"
version = "0.0.7"
description = "LN Markets API Python SDK"
readme = "README.md"
license = { text = "MIT" }
Expand Down Expand Up @@ -44,7 +44,7 @@ dev = [
"playwright>=1.40.0",
]
lint = ["ruff>=0.12.0", "pyright>=1.1.390"]
test = ["pytest", "pytest-asyncio"]
test = ["pytest", "pytest-asyncio", "pytest-httpx>=0.35.0"]

[tool.hatch.build.metadata]
include = ["src/**", "README.md", "LICENSE"]
Expand Down Expand Up @@ -78,3 +78,20 @@ ignore = [
[tool.ruff.format]
quote-style = "double"
indent-style = "space"

[tool.pytest.ini_options]
# testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
addopts = [
"--import-mode=importlib",
]
markers = [
"asyncio: mark test as an async test",
]
filterwarnings = [
"ignore::DeprecationWarning",
]
18 changes: 8 additions & 10 deletions src/lnmarkets_sdk/_internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,32 +52,30 @@ async def request(
if not self._client:
raise RuntimeError("Client must be used within async context manager")

data = prepare_params(params)
params_dict = prepare_params(params)
headers = {}

if credentials:
if not self.auth:
raise ValueError("Authentication required but no credentials provided")

body_str = ""
if data:
data = ""
if params_dict:
if method == "GET":
path = f"{path}?{urlencode({k: str(v) for k, v in data.items()})}"
data = f"?{urlencode({k: str(v) for k, v in params_dict.items()})}"
else:
body_str = json.dumps(data, separators=(",", ":"))
data = json.dumps(params_dict, separators=(",", ":"))
headers.update({"Content-Type": "application/json"})

auth_headers = create_auth_headers(
self.auth, method, f"/v3{path}", body_str
)
auth_headers = create_auth_headers(self.auth, method, f"/v3{path}", data)
headers.update(auth_headers)

# Use httpx native parameter handling
if method == "GET":
return await self._client.request(
method, path, params=data, headers=headers if headers else None
method, path, params=params_dict, headers=headers if headers else None
)
else:
return await self._client.request(
method, path, json=data, headers=headers if headers else None
method, path, json=params_dict, headers=headers if headers else None
)
7 changes: 4 additions & 3 deletions src/lnmarkets_sdk/_internal/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pydantic.alias_generators import to_camel
from pydantic.types import UUID4

type APINetwork = Literal["mainnet", "testnet"]
type APINetwork = Literal["mainnet", "testnet4"]
type APIMethod = Literal["GET", "POST", "PUT"]
type UUID = UUID4

Expand All @@ -19,7 +19,7 @@ class BaseConfig:
str_strip_whitespace=True,
use_enum_values=True,
alias_generator=to_camel,
populate_by_name=True, # to make `from_` field becomes `from`
validate_by_name=True, # to make `from_` field becomes `from`
)


Expand Down Expand Up @@ -79,7 +79,8 @@ def __init__(self, message: str, status_code: int, response: httpx.Response):
class FromToLimitParams(BaseModel, BaseConfig):
from_: str | None = Field(
default=None,
alias="from",
serialization_alias="from",
validation_alias="from",
description="Start date as a string value in ISO format",
)
limit: int = Field(
Expand Down
8 changes: 3 additions & 5 deletions src/lnmarkets_sdk/_internal/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import hashlib
import hmac
import json
import os
from base64 import b64encode
from collections.abc import Mapping
from datetime import datetime
Expand Down Expand Up @@ -71,10 +70,9 @@ def create_auth_headers(

def get_hostname(network: APINetwork) -> str:
"""Get API hostname based on network."""
hostname = os.getenv("LNM_API_HOSTNAME")
if hostname:
return hostname
return "api.testnet.lnmarkets.com" if network == "testnet" else "api.lnmarkets.com"
return (
"api.testnet4.lnmarkets.com" if network == "testnet4" else "api.lnmarkets.com"
)


def parse_response[T](
Expand Down
Empty file.
Loading
Loading