Skip to content
Open
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Feature parity with the TypeScript SDK ([#18](https://github.com/decibeltrade/python-sdk/pull/18)):
- Write methods: `update_order`, `withdraw_non_collateral`, `admin_create_subaccount`, and `claim_campaign_reward` (with a new `campaign_package` field on `Deployment`), on both `DecibelWriteDex` and `DecibelWriteDexSync`.
- Read-client on-chain view helpers: `global_perp_engine_state`, `collateral_balance_decimals`, `usdc_decimals`, `usdc_balance`, `token_balance`, `account_balance`, `position_size`, and `get_crossed_position`.
- Read namespaces: `campaigns`, `points_leaderboard`, `streaks`, `trading_amps`, `tier`, `global_points_stats`, `referrals`, `user_fees`, and `withdraw_queue` (with WebSocket subscription, on-chain pending-withdrawals fallback, and merge helpers).
- Utilities: `calculate_liquidation_price`, `to_checksum_address` (EIP-55), and `derive_aptos_from_eth` / `derive_aptos_from_solana` (derivable accounts).

## [0.2.1] - 2026-04-08

### Fixed
Expand All @@ -19,5 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

Configurable transaction timeouts, Aptos contract ABI and registry updates for testnet and mainnet, custom submit/confirm exceptions, and CI improvements including release tagging. See the [v0.2.0](https://github.com/decibeltrade/python-sdk/releases/tag/v0.2.0) release on GitHub.

[Unreleased]: https://github.com/decibeltrade/python-sdk/compare/v0.2.1...HEAD
[0.2.1]: https://github.com/decibeltrade/python-sdk/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/decibeltrade/python-sdk/releases/tag/v0.2.0
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,41 @@ read.leaderboard.get_leaderboard()
read.portfolio_chart.get_by_addr(sub_addr=sub_addr, time_range="7d", data_type="pnl")
read.vaults.get_vaults()
read.trading_points.get_by_owner(owner_addr=addr)
read.user_fees.get_by_addr(sub_addr=sub_addr)

# Points, streaks & tiers
read.points_leaderboard.get_points_leaderboard(limit=50, sort_key="total_amps")
read.trading_amps.get_by_owner(owner_addr=addr, season="season1", days=7)
read.tier.get_by_owner(owner_addr=addr)
read.streaks.get_by_owner(owner_addr=addr)
read.global_points_stats.get()

# Campaigns & rewards
read.campaigns.get_active()
read.campaigns.get_summary(account_address=addr)

# Referrals
read.referrals.validate_code(code)
read.referrals.get_account_referral(account)
read.referrals.get_referrer_stats(account)
read.referrals.get_user_referrals(referrer_account=account)
read.referrals.get_affiliate_codes(account)
read.referrals.get_affiliate_earnings(account)
read.referrals.redeem_code(referral_code=code, account=account)

# Withdrawal queue (indexed HTTP + on-chain fallback)
read.withdraw_queue.get_by_addr(sub_addr=sub_addr, status="Queued")
read.withdraw_queue.get_pending_withdrawals(user_addr)

# On-chain view / resource helpers
read.usdc_balance(addr)
read.token_balance(addr, token_addr, token_decimals)
read.account_balance(addr)
read.position_size(addr, market_addr)
read.get_crossed_position(addr)
read.usdc_decimals()
read.collateral_balance_decimals()
read.global_perp_engine_state()

# WebSocket subscriptions
read.market_prices.subscribe_by_name(market_name, callback)
Expand All @@ -244,6 +279,7 @@ read.user_trade_history.subscribe_by_addr(sub_addr, callback)
read.user_bulk_orders.subscribe_by_addr(sub_addr, callback)
read.user_active_twaps.subscribe_by_addr(sub_addr, callback)
read.user_notifications.subscribe_by_addr(sub_addr, callback)
read.withdraw_queue.subscribe_by_addr(sub_addr, callback)
```

### Write Client
Expand All @@ -255,6 +291,7 @@ write = DecibelWriteDex(config, account, opts)

# Orders
write.place_order(market_name=..., price=..., size=..., is_buy=..., time_in_force=..., is_reduce_only=...)
write.update_order(order_id=..., market_addr=..., price=..., size=..., is_buy=..., time_in_force=..., is_reduce_only=...)
write.cancel_order(order_id=..., market_name=...)
write.cancel_client_order(client_order_id=..., market_name=...)
write.place_bulk_orders(market_name=..., sequence_number=..., bid_prices=..., bid_sizes=..., ask_prices=..., ask_sizes=...)
Expand All @@ -272,14 +309,55 @@ write.cancel_twap_order(market_addr=..., order_id=...)
# Collateral
write.deposit(amount)
write.withdraw(amount)
write.withdraw_non_collateral(asset_addr, amount)

# Vaults
write.deposit_to_vault(vault_address=..., amount=..., subaccount_addr=...)
write.withdraw_from_vault(vault_address=..., shares=...)

# Subaccounts
write.create_subaccount()
write.admin_create_subaccount(owner_address)
write.deactivate_subaccount(subaccount_addr=...)

# Rewards
write.claim_campaign_reward(campaign_id)
```

> A synchronous client, `DecibelWriteDexSync`, exposes the same methods without `async`/`await`.

### Utilities

```python
from decibel import (
calculate_liquidation_price,
LiquidationPriceInput,
LiquidationPosition,
LiquidationMarket,
LiquidationMarketContext,
to_checksum_address,
derive_aptos_from_eth,
derive_aptos_from_solana,
)

# Estimate a liquidation price (for an existing position or a simulated order).
liq_price = calculate_liquidation_price(
LiquidationPriceInput(
account_equity=1000.0,
positions=[LiquidationPosition(market_addr="0xBTC", size=0.5, entry_price=100_000)],
markets=[LiquidationMarket(market_addr="0xBTC", market_name="BTC/USD", max_leverage=10)],
market_contexts=[LiquidationMarketContext(market_name="BTC/USD", mark_price=100_000)],
target_market_addr="0xBTC",
order_size=0, # 0 = current position; non-zero simulates an order
)
)

# EIP-55 checksum an Ethereum address (zero-dependency keccak-256).
checksummed = to_checksum_address("0xd8da6bf26964af9d7eed9e03e53415d37aa96045")

# Derive an Aptos address from an EVM / Solana wallet (derivable accounts).
aptos_addr = derive_aptos_from_eth("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
aptos_addr = derive_aptos_from_solana("11111111111111111111111111111111")
```

## Development
Expand Down
23 changes: 23 additions & 0 deletions examples/read/get_campaigns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import asyncio
import os

from decibel import TESTNET_CONFIG
from decibel.read import DecibelReadDex

ACCOUNT_ADDR = "0x123..."


async def main() -> None:
read = DecibelReadDex(TESTNET_CONFIG, api_key=os.environ.get("APTOS_NODE_API_KEY"))

active = await read.campaigns.get_active()
print(f"Active campaigns: {len(active)}")
for c in active:
print(f" [{c.campaign_id}] {c.title} ({c.status})")

summary = await read.campaigns.get_summary(account_address=ACCOUNT_ADDR)
print(f"\nLifetime earned: {summary.lifetime_earned}, ready to claim: {summary.ready_to_claim}")


if __name__ == "__main__":
asyncio.run(main())
17 changes: 17 additions & 0 deletions examples/read/get_global_points_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import asyncio
import os

from decibel import TESTNET_CONFIG
from decibel.read import DecibelReadDex


async def main() -> None:
read = DecibelReadDex(TESTNET_CONFIG, api_key=os.environ.get("APTOS_NODE_API_KEY"))

stats = await read.global_points_stats.get()
print(f"Total users: {stats.total_users}")
print(f"Total amps distributed: {stats.total_amps_distributed}")


if __name__ == "__main__":
asyncio.run(main())
18 changes: 18 additions & 0 deletions examples/read/get_points_leaderboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import asyncio
import os

from decibel import TESTNET_CONFIG
from decibel.read import DecibelReadDex


async def main() -> None:
read = DecibelReadDex(TESTNET_CONFIG, api_key=os.environ.get("APTOS_NODE_API_KEY"))

page = await read.points_leaderboard.get_points_leaderboard(limit=10, sort_key="total_amps")
print(f"Points leaderboard (total {page.total_count}):")
for item in page.items:
print(f" #{item.rank} {item.owner}: {item.total_amps} amps")


if __name__ == "__main__":
asyncio.run(main())
22 changes: 22 additions & 0 deletions examples/read/get_referrals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import asyncio
import os

from decibel import TESTNET_CONFIG
from decibel.read import DecibelReadDex

ACCOUNT_ADDR = "0x123..."


async def main() -> None:
read = DecibelReadDex(TESTNET_CONFIG, api_key=os.environ.get("APTOS_NODE_API_KEY"))

stats = await read.referrals.get_referrer_stats(ACCOUNT_ADDR)
print(f"Total referrals: {stats.total_referrals}, codes: {stats.codes}")

referred = await read.referrals.get_user_referrals(referrer_account=ACCOUNT_ADDR, limit=10)
for user in referred:
print(f" {user.account} via {user.referral_code}")


if __name__ == "__main__":
asyncio.run(main())
20 changes: 20 additions & 0 deletions examples/read/get_streaks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import asyncio
import os

from decibel import TESTNET_CONFIG
from decibel.read import DecibelReadDex

OWNER_ADDR = "0x123..."


async def main() -> None:
read = DecibelReadDex(TESTNET_CONFIG, api_key=os.environ.get("APTOS_NODE_API_KEY"))

streaks = await read.streaks.get_by_owner(owner_addr=OWNER_ADDR)
print(f"Current streak: {streaks.current_streak}")
print(f"Grace days: {streaks.grace_days_used}/{streaks.grace_days_available} used")
print(f"Qualifying dates: {streaks.qualifying_dates}")


if __name__ == "__main__":
asyncio.run(main())
20 changes: 20 additions & 0 deletions examples/read/get_tier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import asyncio
import os

from decibel import TESTNET_CONFIG
from decibel.read import DecibelReadDex

OWNER_ADDR = "0x123..."


async def main() -> None:
read = DecibelReadDex(TESTNET_CONFIG, api_key=os.environ.get("APTOS_NODE_API_KEY"))

tier = await read.tier.get_by_owner(owner_addr=OWNER_ADDR)
print(f"Tier for {tier.owner}: {tier.current_tier} (rank {tier.rank})")
for t in tier.tiers:
print(f" {t.name}: threshold={t.hz_threshold}, progress={t.progress}")


if __name__ == "__main__":
asyncio.run(main())
21 changes: 21 additions & 0 deletions examples/read/get_trading_amps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import asyncio
import os

from decibel import TESTNET_CONFIG
from decibel.read import DecibelReadDex

OWNER_ADDR = "0x123..."


async def main() -> None:
read = DecibelReadDex(TESTNET_CONFIG, api_key=os.environ.get("APTOS_NODE_API_KEY"))

amps = await read.trading_amps.get_by_owner(owner_addr=OWNER_ADDR, days=7)
print(f"Trading amps for {amps.owner}: {amps.total_amps}")
if amps.breakdown:
for sub in amps.breakdown:
print(f" {sub.account}: {sub.total_amps}")


if __name__ == "__main__":
asyncio.run(main())
19 changes: 19 additions & 0 deletions examples/read/get_user_fees.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import asyncio
import os

from decibel import TESTNET_CONFIG
from decibel.read import DecibelReadDex

SUB_ADDR = "0x123..."


async def main() -> None:
read = DecibelReadDex(TESTNET_CONFIG, api_key=os.environ.get("APTOS_NODE_API_KEY"))

fees = await read.user_fees.get_by_addr(sub_addr=SUB_ADDR)
print(f"Taker rate: {fees.user_taker_rate}, maker rate: {fees.user_maker_rate}")
print(f"Fee tier: {fees.fee_tier}")


if __name__ == "__main__":
asyncio.run(main())
23 changes: 23 additions & 0 deletions examples/read/get_withdraw_queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import asyncio
import os

from decibel import TESTNET_CONFIG
from decibel.read import DecibelReadDex

SUB_ADDR = "0x123..."


async def main() -> None:
read = DecibelReadDex(TESTNET_CONFIG, api_key=os.environ.get("APTOS_NODE_API_KEY"))

page = await read.withdraw_queue.get_by_addr(sub_addr=SUB_ADDR, status="Queued")
print(f"Withdraw queue entries: {page.total_count}")
for entry in page.items:
print(f" [{entry.request_id}] {entry.status}: {entry.fungible_amount}")

pending = await read.withdraw_queue.get_pending_withdrawals(SUB_ADDR)
print(f"On-chain pending: {len(pending)}")


if __name__ == "__main__":
asyncio.run(main())
29 changes: 29 additions & 0 deletions examples/read/ws/subscribe_withdraw_queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import asyncio
import os
from typing import Any

from decibel import TESTNET_CONFIG
from decibel.read import DecibelReadDex

SUB_ADDR = "0x123..."


async def main() -> None:
read = DecibelReadDex(TESTNET_CONFIG, api_key=os.environ.get("APTOS_NODE_API_KEY"))

def on_data(msg: Any) -> None:
print(f"Withdraw queue update for {SUB_ADDR}: {len(msg.entries)} entries")
for entry in msg.entries:
print(f" [{entry.request_id}] {entry.status}: {entry.fungible_amount}")

# Subscribe first, then seed via read.withdraw_queue.get_by_addr(...) and merge
# with merge_withdraw_queue_entries to avoid missing events during the race window.
unsubscribe = read.withdraw_queue.subscribe_by_addr(SUB_ADDR, on_data)

await asyncio.sleep(30)
unsubscribe()
await read.ws.close()


if __name__ == "__main__":
asyncio.run(main())
Loading
Loading