Skip to content

DV.net Python SDK (dv_net_client)

عميل بايثون للتكامل مع بوابة الدفع DV.net.

  • يدعم نسختي العميل المتزامن وغير المتزامن.
  • جميع الطرق تعيد كائنات DTO مُقننة النوع (dataclass)، وليس قواميس «خام».
  • توجد أدوات جاهزة للعمل مع الويبهوكات والتحقق من توقيع الطلبات.

الحزمة: dv_net_client.


التثبيت

bash
pip install dv-net-client

للعميل غير المتزامن تحتاج أيضاً إلى aiohttp:

bash
pip install aiohttp

المفاهيم الأساسية

المضيف و x-api-key

تُرسل جميع الطلبات إلى واجهة DV.net عبر مضيف أساسي، مثلاً:

python
DV_HOST = "https://cloud.dv.net"

يتم التفويض عبر ترويسة HTTP باسم x-api-key (مفتاح واجهة برمجة التطبيقات العام للمتجر):

python
DV_API_KEY = "ВАШ_X_API_KEY"

يمكن تمرير هذه المعاملات:

  • مرة واحدة في مُنشئ العميل؛
  • أو لكل طريقة (مع تجاوز قيم المُنشئ).

إذا لم يتم تحديد host أو x_api_key لا في المُنشئ ولا في الطريقة، سيتم رمي الاستثناءين:

  • DvNetUndefinedHostException
  • DvNetUndefinedXApiKeyException

العملاء

يوفر SDK نوعين من العميل:

python
from dv_net_client import MerchantClient, AsyncMerchantClient

عميل متزامن

python
from dv_net_client import MerchantClient

client = MerchantClient(
    host="https://cloud.dv.net",
    x_api_key="ВАШ_X_API_KEY",
)

يُستخدم افتراضياً UrllibHttpClient (مبني على مكتبة بايثون المعيارية urllib).

عميل غير متزامن

python
from dv_net_client import AsyncMerchantClient

client = AsyncMerchantClient(
    host="https://cloud.dv.net",
    x_api_key="ВАШ_X_API_KEY",
)

يُستخدم افتراضياً AiohttpHttpClient (مبني على aiohttp).

العملاء لا يطبّقون مدير سياق (with / async with).
يُنصح بإنشاء نسخة واحدة عند بدء التطبيق وإعادة استخدامها.


الأخطاء والاستثناءات

الاستثناءات الرئيسية في الوحدة:

python
from dv_net_client.exceptions import (
    DvNetException,
    DvNetRequestException,
    DvNetInvalidRequestException,
    DvNetServerException,
    DvNetNetworkException,
    DvNetInvalidResponseDataException,
    DvNetInvalidWebhookException,
    DvNetUndefinedHostException,
    DvNetUndefinedXApiKeyException,
)
  • DvNetInvalidRequestException — خطأ في الطلب (4xx).
  • DvNetServerException — خطأ من طرف DV.net (5xx).
  • DvNetNetworkException — مشاكل شبكة، انتهاء مهلة، إلخ.
  • DvNetInvalidResponseDataException — تعذر مطابقة الاستجابة مع DTO المتوقع.
  • DvNetInvalidWebhookException — تنسيق ويبهوك غير صحيح أو غير معروف.

يُنصح بتغليف استدعاءات العميل في try/except مع التسجيل (logging).


طرق العميل

أدناه وصف للطرق المشتركة بين MerchantClient وAsyncMerchantClient.
التواقيع متطابقة، باستثناء أن النسخة غير المتزامنة تُعلَّم بالكلمة المفتاحية async ويتم استدعاؤها عبر await.

تُؤخذ أنواع DTO من dv_net_client.dto.merchant_client.


1. get_exchange_balances

الحصول على رصيد المتجر الإجمالي عبر جميع العملات والشبكات (exchange-balance).

python
total_balance = await async_client.get_exchange_balances()
# أو
total_balance = client.get_exchange_balances()

التوقيع:

python
get_exchange_balances(
    x_api_key: Optional[str] = None,
    host: Optional[str] = None,
) -> TotalExchangeBalanceResponse

يُرجع: TotalExchangeBalanceResponse:

  • total_usd: str — الرصيد الإجمالي بالدولار الأمريكي.
  • exchange_balance: List[ExchangeBalanceDto]

ExchangeBalanceDto يحتوي على:

  • amount: str — الرصيد بالعملة.
  • amount_usd: str — ما يعادله بالدولار الأمريكي.
  • currency: str — رمز العملة.

مثال (async):

python
from dv_net_client import AsyncMerchantClient

DV_HOST = "https://cloud.dv.net"
DV_API_KEY = "ВАШ_X_API_KEY"

client = AsyncMerchantClient(host=DV_HOST, x_api_key=DV_API_KEY)

balance = await client.get_exchange_balances()
print("Total USD:", balance.total_usd)
for item in balance.exchange_balance:
    print(f"{item.currency}: {item.amount} ({item.amount_usd} USD)")

2. get_external_wallet

إنشاء (أو الحصول على الموجود) محفظة/رابط دفع لمستخدم محدد.

python
wallet = await async_client.get_external_wallet(
    store_external_id="user_123",
    amount="10.00",
    currency="USD",
)

التوقيع:

python
get_external_wallet(
    store_external_id: str,
    email: Optional[str] = None,
    ip: Optional[str] = None,
    amount: Optional[str] = None,
    currency: Optional[str] = None,
    x_api_key: Optional[str] = None,
    host: Optional[str] = None,
) -> ExternalAddressesResponse

المعاملات:

  • store_external_id — المعرّف الداخلي الخاص بك للمستخدم (إلزامي).
  • email — بريد المستخدم الإلكتروني (اختياري).
  • ip — عنوان IP للمستخدم (اختياري).
  • amount — مبلغ الشحن (كسلسلة، مثلاً "5.00").
  • currency — رمز العملة، مثلاً "USD".

يُرجع: ExternalAddressesResponse:

  • pay_url: str — رابط الدفع.
  • address: List[AddressDto] — قائمة العناوين الفردية.
  • amount_usd: str — المبلغ بالدولار الأمريكي.
  • rates: List[str] — معلومات عن أسعار الصرف.
  • حقول خدمية (id, store_id, store_external_id, created_at, updated_at إلخ).

مثال (async):

python
resp = await client.get_external_wallet(
    store_external_id="user_123",
    amount="5.00",
    currency="USD",
    email="user@example.com",
    ip="203.0.113.10",
)

print("Pay URL:", resp.pay_url)
for addr in resp.address:
    print(f"{addr.currency_id} ({addr.blockchain}): {addr.address}")

3. get_processing_wallets_balances

رصيد المحافظ «المعالجة» (حيث تتم معالجة المعاملات الواردة).

python
processing_balances = await async_client.get_processing_wallets_balances()

التوقيع:

python
get_processing_wallets_balances(
    x_api_key: Optional[str] = None,
    host: Optional[str] = None,
) -> ProcessingWalletsBalancesResponse

يُرجع: ProcessingWalletsBalancesResponse:

  • balances: List[ProcessingWalletBalanceDto]

يشمل ProcessingWalletBalanceDto على سبيل المثال:

  • balance: str
  • balance_usd: str
  • currency: CurrencyShortDto (الرمز، الاسم، البلوكشين)
  • حقول تقنية إضافية.

4. get_store_currencies

قائمة العملات المتاحة للمتجر.

python
currencies = await async_client.get_store_currencies()

التوقيع:

python
get_store_currencies(
    x_api_key: Optional[str] = None,
    host: Optional[str] = None,
) -> CurrenciesResponse

يُرجع: CurrenciesResponse:

  • currencies: List[CurrencyDto]

يحتوي CurrencyDto على:

  • id: str — معرّف العملة بالشكل "USDT.Tron".
  • code: str — الرمز المختصر، مثل "USDT".
  • name: str — اسم قابل للقراءة.
  • blockchain: str — الشبكة (مثلاً "Tron").
  • precision: int وحقول أخرى.

مثال:

python
currencies = await client.get_store_currencies()
for cur in currencies.currencies:
    print(f"{cur.code} ({cur.id}) — сеть: {cur.blockchain}")

5. get_store_currency_rate

الحصول على سعر صرف عملة محددة.

python
rate = await async_client.get_store_currency_rate("USDT.Tron")

التوقيع:

python
get_store_currency_rate(
    currency_id: str,
    x_api_key: Optional[str] = None,
    host: Optional[str] = None,
) -> CurrencyRateResponse

المعاملات:

  • currency_id — المعرّف من get_store_currencies، مثلاً "USDT.Tron".

يُرجع: CurrencyRateResponse:

  • code: str — رمز العملة.
  • rate — السعر (نوع ديناميكي، غالباً سلسلة/رقم).
  • حقول إضافية (المصدر، عملة التسعير، إلخ).

6. get_withdrawal_processing_status

حالة عملية سحب قيد المعالجة.

python
status = await async_client.get_withdrawal_processing_status(withdrawal_id)

التوقيع:

python
get_withdrawal_processing_status(
    withdrawal_id: str,
    x_api_key: Optional[str] = None,
    host: Optional[str] = None,
) -> ProcessingWithdrawalResponse

المعاملات:

  • withdrawal_id — معرّف السحب الذي تم الحصول عليه سابقاً (مثلاً من WithdrawalWebhookResponse أو استجابة initialize_transfer).

يُرجع: ProcessingWithdrawalResponse:

  • id: str
  • status: str
  • amount: str
  • amount_usd: str
  • currency_id: str
  • created_at, updated_at وغيرها.

7. initialize_transfer

تهيئة سحب الأموال من رصيد المتجر إلى محفظة خارجية.

python
withdrawal = await async_client.initialize_transfer(
    address_to="TExm...",
    currency_id="USDT.Tron",
    amount="100.0",
    request_id="unique_id_1",
)

التوقيع:

python
initialize_transfer(
    address_to: str,
    currency_id: str,
    amount: str,
    request_id: str,
    x_api_key: Optional[str] = None,
    host: Optional[str] = None,
) -> WithdrawalResponse

المعاملات:

  • address_to — عنوان المستلم (المحفظة).
  • currency_id — معرّف العملة بالشكل "USDT.Tron".
  • amount — مبلغ السحب (كسلسلة).
  • request_id — معرّفك الفريد للطلب (لضمان عدم التكرار وربط الطلب بنظامك).

يُرجع: WithdrawalResponse:

  • id: str — معرّف السحب في DV.net.
  • address_from: str — عنوان المرسل.
  • address_to: str — عنوان المستلم.
  • amount: str, amount_usd: str.
  • currency_id: str.
  • store_id: str.
  • created_at: datetime.
  • transfer_id: Optional[str] — قد تكون None بينما العملية قيد التنفيذ.

8. get_hot_wallet_balances

رصيد المحافظ «الساخنة».

python
hot_wallets = await async_client.get_hot_wallet_balances()

التوقيع:

python
get_hot_wallet_balances(
    x_api_key: Optional[str] = None,
    host: Optional[str] = None,
) -> List[AccountDto]

يُرجع: قائمة AccountDto:

  • balance: str
  • balance_usd: str
  • count: int — عدد العناوين.
  • count_with_balance: int — عدد العناوين ذات الرصيد غير الصفري.
  • currency: CurrencyShortDto (الرمز، الاسم، البلوكشين).

9. delete_withdrawal_from_processing

إلغاء سحب قيد المعالجة.

python
await async_client.delete_withdrawal_from_processing(withdrawal_id)

التوقيع:

python
delete_withdrawal_from_processing(
    withdrawal_id: str,
    x_api_key: Optional[str] = None,
    host: Optional[str] = None,
) -> None

المعاملات:

  • withdrawal_id — معرّف السحب المطلوب إلغاؤه.

الاستثناءات:

  • عند حدوث خطأ (غير موجود، مُكتمل بالفعل، خطأ خادم، إلخ) سيتم رمي أحد: DvNetRequestException / DvNetServerException / DvNetNetworkException.

الويبهوكات

تحتوي الوحدة dv_net_client.mappers على WebhookMapper الذي يحوّل JSON الوارد من الويبهوك إلى أحد كائنات DTO من dv_net_client.dto.webhook.

python
from dv_net_client.mappers import WebhookMapper
from dv_net_client.dto.webhook import (
    ConfirmedWebhookResponse,
    UnconfirmedWebhookResponse,
    WithdrawalWebhookResponse,
)

WebhookMapper.map_webhook

التوقيع:

python
map_webhook(data: Dict[str, Any]) -> Any

الخوارزمية:

  • إذا كان في data الحقل withdrawal_id — يُرجع WithdrawalWebhookResponse.
  • إذا كان هناك الحقل type — يُرجع ConfirmedWebhookResponse.
  • إذا كان هناك الحقل unconfirmed_type — يُرجع UnconfirmedWebhookResponse.
  • خلاف ذلك يتم رمي DvNetInvalidWebhookException.

كائنات DTO للويبهوك

python
@dataclass
class TransactionDto:
    tx_id: str
    tx_hash: str
    bc_uniq_key: str
    created_at: datetime
    currency: str
    currency_id: str
    blockchain: str
    amount: str
    amount_usd: str
python
@dataclass
class WalletDto:
    id: str
    store_external_id: str
python
@dataclass
class ConfirmedWebhookResponse:
    type: str
    status: str
    created_at: datetime
    paid_at: datetime
    amount: str
    transactions: TransactionDto
    wallet: WalletDto
python
@dataclass
class UnconfirmedWebhookResponse:
    type: str
    status: str
    created_at: datetime
    paid_at: datetime
    amount: str
    transactions: TransactionDto
    wallet: WalletDto
python
@dataclass
class WithdrawalWebhookResponse:
    type: str
    created_at: datetime
    paid_at: datetime
    amount: str
    transactions: TransactionDto
    withdrawal_id: str

مثال على معالجة الويبهوك

python
from dv_net_client.mappers import WebhookMapper
from dv_net_client.dto.webhook import ConfirmedWebhookResponse

mapper = WebhookMapper()

def handle_webhook(data: dict):
    webhook = mapper.map_webhook(data)

    if isinstance(webhook, ConfirmedWebhookResponse) and webhook.status == "completed":
        user_id = webhook.wallet.store_external_id
        amount_usd = webhook.transactions.amount_usd
        tx_id = webhook.transactions.tx_id

        # бизнес-логика пополнения
        print(f"Платёж от {user_id}: {amount_usd} USD, tx_id={tx_id}")

الأدوات (MerchantUtilsManager)

تحتوي الفئة MerchantUtilsManager من dv_net_client.utils على طرق مساعدة.

python
from dv_net_client.utils import MerchantUtilsManager

utils = MerchantUtilsManager()

check_sign

التحقق من توقيع طلبات DV.net (الويبهوكات).

التوقيع:

python
check_sign(
    client_signature: str,
    client_key: str,
    request_body: Union[Dict[str, Any], str, bytes],
) -> bool

الخوارزمية:

  1. يتم تحويل جسم الطلب إلى سلسلة:
    • dictjson.dumps(..., sort_keys=True, separators=(',', ':'))
    • bytes.decode('utf-8')
    • str — دون تغيير.
  2. يتم ربط string_body + client_key.
  3. يُحسب SHA-256 بصيغة hex.
  4. تتم مقارنة الناتج مع client_signature عبر hmac.compare_digest.

مثال (aiohttp):

python
from aiohttp import web
from dv_net_client.utils import MerchantUtilsManager

utils = MerchantUtilsManager()
SECRET_KEY = "ВАШ_SECRET_KEY"

async def handle_webhook(request: web.Request):
    raw_body = await request.text()
    signature = request.headers.get("X-Signature", "")

    if not utils.check_sign(signature, SECRET_KEY, raw_body):
        return web.Response(status=403, text="invalid signature")

    data = await request.json()
    ...

طريقة مساعدة لإنشاء رابط وفق النمط host/store_uuid/client_id?email=....

التوقيع:

python
generate_link(host: str, store_uuid: str, client_id: str, email: str) -> str

مثال دمج الدفع في بوت تيليغرام

فيما يلي مثال مبسّط لدمج الدفع عبر DV.net في بوت تيليغرام باستخدام python-telegram-bot (v20+) مع AsyncMerchantClient وخادم aiohttp للويبهوكات.

المهمة

  • الأمر /pay يعطي المستخدم رابط دفع من DV.net.
  • مسار HTTP منفصل يستقبل ويبهوكات DV.net:
    • يتحقق من التوقيع؛
    • يحوّل الويبهوك إلى DTO؛
    • عند الدفع الناجح يرسل رسالة للمستخدم في تيليغرام.

التبعيات

bash
pip install python-telegram-bot aiohttp dv-net-client

الإعداد

python
# config.py (пример)

TG_TOKEN = "ВАШ_TELEGRAM_TOKEN"
DV_HOST = "https://cloud.dv.net"
DV_API_KEY = "ВАШ_X_API_KEY"
DV_SECRET_KEY = "ВАШ_SECRET_KEY_ИЗ_КАБИНЕТА"  # для проверки подписи вебхуков

WEBHOOK_PORT = 7623
WEBHOOK_PATH = "/dv/webhook"

كود البوت + الويبهوك

python
import logging
from aiohttp import web
from telegram import Update
from telegram.constants import ParseMode
from telegram.ext import (
    Application,
    CommandHandler,
    ContextTypes,
)

from dv_net_client import AsyncMerchantClient
from dv_net_client.mappers import WebhookMapper
from dv_net_client.dto.webhook import ConfirmedWebhookResponse
from dv_net_client.utils import MerchantUtilsManager

from config import (
    TG_TOKEN, DV_HOST, DV_API_KEY, DV_SECRET_KEY,
    WEBHOOK_PORT, WEBHOOK_PATH,
)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Инициализация SDK
dv_client = AsyncMerchantClient(host=DV_HOST, x_api_key=DV_API_KEY)
webhook_mapper = WebhookMapper()
utils_manager = MerchantUtilsManager()


# 1. Команда /pay — создание платёжной ссылки
async def cmd_pay(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    msg = await update.message.reply_text("Генерирую платёжную ссылку...")

    try:
        resp = await dv_client.get_external_wallet(
            store_external_id=str(user_id),
            amount="5.00",     # сумма платежа
            currency="USD",    # валюта
        )

        if resp and resp.pay_url:
            text = (
                "<b>Счёт на оплату</b>

"
                "Сумма: 5.00 USD
"
                f"Ссылка на оплату: {resp.pay_url}"
            )
            await msg.edit_text(text, parse_mode=ParseMode.HTML)
        else:
            await msg.edit_text("Не удалось получить платёжную ссылку.")
    except Exception as e:
        logger.exception("DV API error")
        await msg.edit_text(f"Ошибка DV API: {e}")


# 2. HTTP-обработчик вебхуков DV.net
async def dv_webhook_handler(request: web.Request):
    # Проверка подписи
    raw_body = await request.text()
    signature = request.headers.get("X-Signature", "")

    if not utils_manager.check_sign(signature, DV_SECRET_KEY, raw_body):
        logger.warning("Invalid DV webhook signature")
        return web.Response(status=403, text="invalid signature")

    # Разбор JSON
    try:
        data = await request.json()
    except Exception:
        return web.Response(status=400, text="invalid json")

    # Маппинг вебхука в DTO
    try:
        webhook = webhook_mapper.map_webhook(data)
    except Exception as e:
        logger.error(f"Webhook mapping failed: {e}")
        return web.json_response({"success": True})

    # Обработка подтверждённого платежа
    if isinstance(webhook, ConfirmedWebhookResponse) and webhook.status == "completed":
        try:
            user_id = int(webhook.wallet.store_external_id)
        except ValueError:
            logger.error("Invalid store_external_id in webhook")
            return web.json_response({"success": True})

        amount_usd = webhook.transactions.amount_usd

        app: Application = request.app["bot_app"]
        await app.bot.send_message(
            chat_id=user_id,
            text=f"Оплата получена. Начислено: {amount_usd} USD.",
            parse_mode=ParseMode.HTML,
        )

    return web.json_response({"success": True})


# 3. Настройка запуска Telegram-бота и aiohttp-сервера
async def on_startup(application: Application):
    # aiohttp-приложение для приёма вебхуков DV.net
    web_app = web.Application()
    web_app["bot_app"] = application
    web_app.router.add_post(WEBHOOK_PATH, dv_webhook_handler)

    runner = web.AppRunner(web_app)
    await runner.setup()
    site = web.TCPSite(runner, "0.0.0.0", WEBHOOK_PORT)
    await site.start()

    logger.info("DV webhook server started on port %s", WEBHOOK_PORT)


def main():
    app = Application.builder().token(TG_TOKEN).build()

    # Телеграм-хендлеры
    app.add_handler(CommandHandler("pay", cmd_pay))

    # При инициализации бота поднимаем и HTTP-сервер для вебхуков
    app.post_init = on_startup

    app.run_polling()


if __name__ == "__main__":
    main()

مخطط تبادل مختصر

  1. يرسل المستخدم الأمر /pay في تيليغرام.
  2. يستدعي البوت AsyncMerchantClient.get_external_wallet(...) ويرسل للمستخدم pay_url.
  3. يُتم المستخدم الدفع عبر الرابط.
  4. يرسل DV.net ويبهوك إلى http://<your-server>:7623/dv/webhook:
    • يتم التحقق من التوقيع عبر MerchantUtilsManager.check_sign;
    • يُحوَّل JSON عبر WebhookMapper.map_webhook;
    • عند ConfirmedWebhookResponse بالحالة "completed" يحصل البوت على store_external_id والمبلغ، ينفّذ منطق العمل (مثلاً زيادة الرصيد في قاعدة البيانات) ويرسل رسالة للمستخدم.