1. Xác thực & Token Cache

Đăng nhập và lấy Access Token, cache token để tái sử dụng giữa các lần chạy

Mục tiêu

Đăng nhập vào hệ thống FastConnect và lấy token cho toàn bộ API call sau đó. Sử dụng token cache để tránh authenticate lại mỗi lần chạy script.

Luồng xử lý

Client App → Auth API → token_cache.json
  1. Tạo Config → Auth → gọi ensure_auth(auth, otp=...)
  2. ensure_auth load token từ cache; nếu token hết hạn thì refresh(), nếu refresh thất bại thì xác thực lại
  3. Nếu chưa có token hợp lệ: gọi request_otp()
    • Tài khoản dùng Smart OTP Push: response có transactionId, SDK poll bằng ensure_authenticated(transaction_id=...) cho tới khi user bấm Approve trên app SSI
    • Tài khoản dùng OTP 6 số (SMS/Email): nhập mã rồi gọi authenticate(otp=...)
  4. Auth service trả về accessToken, refreshToken, expiresIn và token được lưu vào token_cache.json
  5. Mọi request sau đó gắn Authorization: Bearer <accessToken>
  6. Nếu token hết hạn ở lần chạy sau thì tự động refresh, không cần OTP lại

Các sample trong repo đọc cấu hình từ config.json (qua config.py). Trong tài liệu này Config(...) được viết inline với placeholder cho ngắn gọn.

auth_helper — Token Cache Module

Module auth_helper.py giúp tái sử dụng token giữa các lần chạy script. clean_otp bỏ qua OTP rỗng hoặc còn là placeholder (<OTP>, YOUR_OTP_CODE) để rơi sang luồng Smart OTP / nhập tay:

python/auth_helper.py
"""
auth_helper — Token cache để tái sử dụng token giữa các lần chạy
==================================================================
Thay vì gọi authenticate() mỗi lần chạy script, module này:
  1. Load token đã lưu từ file token_cache.json (nếu có)
  2. Nếu chưa có → authenticate lần đầu và lưu xuống file
  3. Nếu token hết hạn → refresh và lưu lại
  4. Nếu token còn hạn → dùng trực tiếp, không gọi API

Cách dùng (sync):
    from auth_helper import ensure_auth
    with Auth(config) as auth:
        ensure_auth(auth)

Cách dùng (async):
    from auth_helper import ensure_auth_async
    async with AsyncAuth(config) as auth:
        await ensure_auth_async(auth)
"""

import json
import os

from ssi_sdk.models import Token

TOKEN_FILE = os.path.join(os.path.dirname(__file__), "token_cache.json")


def load_token() -> Token | None:
    """Load token từ file, trả về None nếu file không tồn tại."""
    if not os.path.exists(TOKEN_FILE):
        return None
    with open(TOKEN_FILE, "r", encoding="utf-8") as f:
        _token = json.load(f)
    return Token.from_dict(_token)


def save_token(token: Token) -> None:
    """Lưu token xuống file."""
    with open(TOKEN_FILE, "w", encoding="utf-8") as f:
        json.dump(token.to_dict(), f, indent=2)
    print(f"Token đã lưu vào {TOKEN_FILE}")


def clean_otp(otp: str | None) -> str | None:
    """Bỏ qua OTP rỗng hoặc còn là placeholder (`<OTP>`, `YOUR_OTP_CODE`) để rơi sang luồng Smart OTP / nhập tay."""
    if otp is None:
        return None
    otp = otp.strip()
    if not otp or (otp.startswith("<") and otp.endswith(">")) or otp.upper().startswith("YOUR_"):
        return None
    return otp


def ensure_auth(auth, otp: str | None = None) -> None:
    """Đảm bảo auth có token hợp lệ (sync)."""
    otp = clean_otp(otp)
    cached_token = load_token()
    if cached_token is not None:
        auth.token_manager.set_token(cached_token)
        if not auth.is_token_expired:
            print("Token còn hạn, dùng token từ file.")
            return
        print("Token đã hết hạn, đang refresh...")
        try:
            cached_token = auth.refresh()
            save_token(cached_token)
            print("Refresh token thành công.")
            return
        except Exception as e:
            print(f"Refresh token thất bại ({e}), tiến hành xác thực lại...")

    print("Không tìm thấy token hợp lệ, đang thực hiện quy trình xác thực & OTP...")
    if otp:
        cached_token = auth.authenticate(otp=otp)
    else:
        print("=== Yêu cầu OTP (Request OTP) ===")
        otp_res = auth.request_otp()
        transaction_id = None
        if isinstance(otp_res, dict):
            data_map = otp_res.get("data", {})
            if isinstance(data_map, dict):
                transaction_id = data_map.get("transactionId") or data_map.get("transaction_id")
            if not transaction_id:
                transaction_id = otp_res.get("transactionId") or otp_res.get("transaction_id")

        if transaction_id:
            print("[Smart OTP] Đã gửi yêu cầu phê duyệt.")
            print("Vui lòng mở app SSI iBoard trên điện thoại và bấm APPROVE (Xác nhận)...")
            print("SDK đang Polling chờ bạn bấm phê duyệt...")
            auth.ensure_authenticated(transaction_id=transaction_id, poll_interval_ms=5_000, poll_max_retries=6)
        else:
            user_otp = input("Vui lòng nhập mã OTP 6 số: ").strip()
            auth.authenticate(otp=user_otp)

    if auth.token:
        save_token(auth.token)
        print("Authenticate thành công, token đã lưu.")


async def ensure_auth_async(auth, otp: str | None = None) -> None:
    """Đảm bảo auth có token hợp lệ (async). Cùng luồng với ensure_auth, mọi lời gọi SDK đều có await."""
    otp = clean_otp(otp)
    cached_token = load_token()
    if cached_token is not None:
        await auth.token_manager.set_token(cached_token)
        if not auth.is_token_expired:
            print("Token còn hạn, dùng token từ file.")
            return
        print("Token đã hết hạn, đang refresh...")
        try:
            cached_token = await auth.refresh()
            save_token(cached_token)
            print("Refresh token thành công.")
            return
        except Exception as e:
            print(f"Refresh token thất bại ({e}), tiến hành xác thực lại...")

    print("Không tìm thấy token hợp lệ, đang thực hiện quy trình xác thực & OTP...")
    if otp:
        cached_token = await auth.authenticate(otp=otp)
    else:
        otp_res = await auth.request_otp()
        transaction_id = None
        if isinstance(otp_res, dict):
            data_map = otp_res.get("data", {})
            if isinstance(data_map, dict):
                transaction_id = data_map.get("transactionId") or data_map.get("transaction_id")
            if not transaction_id:
                transaction_id = otp_res.get("transactionId") or otp_res.get("transaction_id")

        if transaction_id:
            print("Vui lòng mở app SSI iBoard trên điện thoại và bấm APPROVE (Xác nhận)...")
            await auth.ensure_authenticated(transaction_id=transaction_id, poll_interval_ms=5_000, poll_max_retries=6)
        else:
            user_otp = input("Vui lòng nhập mã OTP 6 số: ").strip()
            await auth.authenticate(otp=user_otp)

    if auth.token:
        save_token(auth.token)
        print("Authenticate thành công, token đã lưu.")

helpers — Tiện ích dùng chung

Từ SDK 3.3.0, các trường enum (Account.account_type, Order.side/order_type/status...) có thể là enum, str (giá trị server mới mà SDK chưa biết) hoặc None. Các sample dùng helpers.py để in an toàn và che số tài khoản:

python/helpers.py
def enum_value(value) -> str:
    """Trả về .value nếu là enum, giữ nguyên nếu là str, '-' nếu None."""
    if value is None:
        return "-"
    return str(getattr(value, "value", value))


def mask_account(account_no: str) -> str:
    """Che số tài khoản khi in ra màn hình (chỉ giữ 3 ký tự cuối)."""
    if not account_no:
        return "-"
    return "*" * max(len(account_no) - 3, 0) + account_no[-3:]

Repo còn có require_account(account_no) (dừng ngay nếu config.json thiếu equity_account) và confirm_real_orders(what) (cảnh báo lệnh THẬT, bắt gõ YES mới chạy tiếp).

Sample Code — Sync

python/sample_01_auth.py
from ssi_sdk import Auth, Trading, Config
from auth_helper import ensure_auth
from helpers import enum_value, mask_account

config = Config(
    client_id="<your_client_id>",
    api_key="<your_api_key>",
    api_secret="<your_api_secret>",
    private_key="<your_private_key>",
)

with Auth(config) as auth:
    # Bước 1-3: Xác thực + Yêu cầu & Xác thực OTP / Smart OTP
    ensure_auth(auth)
    token = auth.token
    print("Access Token :", token.access_token[:40], "...")
    print("Token Type   :", token.token_type)
    print("Expires At   :", token.expires_at)
    print("Refresh Token:", token.refresh_token[:40] if token.refresh_token else "N/A", "...")

    # Bước 4: Kiểm tra token hết hạn & refresh
    if auth.is_token_expired:
        new_token = auth.refresh()
        print("Token mới:", new_token.access_token[:40], "...")

    # Bước 5: Xác nhận token hoạt động bằng cách gọi API
    with Trading(auth) as trading:
        accounts = trading.account.get_account_info()
        print(f"\nXác thực thành công! Tìm thấy {len(accounts)} tài khoản:")
        for acc in accounts:
            print(f"  - {mask_account(acc.account_no)} ({enum_value(acc.account_type)})")

Sample Code — Async

python/sample_01_auth_async.py
import asyncio
from ssi_sdk import AsyncAuth, AsyncTrading, Config
from auth_helper import ensure_auth_async
from helpers import enum_value, mask_account

config = Config(
    client_id="<your_client_id>",
    api_key="<your_api_key>",
    api_secret="<your_api_secret>",
    private_key="<your_private_key>",
)

async def main():
    async with AsyncAuth(config) as auth:
        await ensure_auth_async(auth)
        token = auth.token
        print("Access Token :", token.access_token[:40], "...")

        if auth.is_token_expired:
            new_token = await auth.refresh()
            print("Token mới:", new_token.access_token[:40], "...")

        async with AsyncTrading(auth) as trading:
            accounts = await trading.account.get_account_info()
            print(f"\nXác thực thành công! Tìm thấy {len(accounts)} tài khoản:")
            for acc in accounts:
                print(f"  - {mask_account(acc.account_no)} ({enum_value(acc.account_type)})")

asyncio.run(main())

Smart OTP — Approve trên app (Sync)

Với tài khoản đã kích hoạt Smart OTP, thay vì nhập mã số, người dùng bấm "Duyệt" trực tiếp trên app. Trong repo, luồng này nằm trong ensure_auth (xem auth_helper.py ở trên) chứ không có file sample riêng. SDK tự poll cho đến khi user duyệt xong hoặc hết số lần thử. Từ SDK 3.3.0 khoảng cách giữa các lần poll tính bằng mili-giây (poll_interval_ms); tên cũ poll_interval (giây) đã bị xóa và báo TypeError.

from ssi_sdk import Auth, Config

config = Config(
    client_id="<your_client_id>",
    api_key="<your_api_key>",
    api_secret="<your_api_secret>",
    private_key="<your_private_key>",
)

with Auth(config) as auth:
    # Bước 1: gửi yêu cầu duyệt tới app Smart OTP, nhận transaction_id
    # (transaction_id chỉ có với Smart OTP; OTP SMS/Email thì là None)
    otp_result = auth.request_otp_typed()
    transaction_id = otp_result.transaction_id
    print("Đã gửi yêu cầu duyệt. Vui lòng mở app Smart OTP và bấm Duyệt.")

    # Bước 2: SDK tự poll cho đến khi user bấm duyệt (hoặc hết poll_max_retries)
    token = auth.ensure_authenticated(
        transaction_id=transaction_id,
        poll_interval_ms=5_000,
        poll_max_retries=6,
    )
    print("Duyệt thành công! Access Token:", token.access_token[:40], "...")

Smart OTP — Approve trên app (Async)

import asyncio
from ssi_sdk import AsyncAuth, Config

config = Config(
    client_id="<your_client_id>",
    api_key="<your_api_key>",
    api_secret="<your_api_secret>",
    private_key="<your_private_key>",
)

async def main():
    async with AsyncAuth(config) as auth:
        # Bước 1: gửi yêu cầu duyệt tới app Smart OTP, nhận transaction_id
        otp_result = await auth.request_otp_typed()
        transaction_id = otp_result.transaction_id
        print("Đã gửi yêu cầu duyệt. Vui lòng mở app Smart OTP và bấm Duyệt.")

        # Bước 2: SDK tự poll cho đến khi user bấm duyệt (hoặc hết poll_max_retries)
        token = await auth.ensure_authenticated(
            transaction_id=transaction_id,
            poll_interval_ms=5_000,
            poll_max_retries=6,
        )
        print("Duyệt thành công! Access Token:", token.access_token[:40], "...")

asyncio.run(main())

Nếu poll hết số lần mà giao dịch còn hiệu lực, SDK raise SmartOTPPendingError: gọi lại ensure_authenticated với cùng transaction_id. Nếu user từ chối, hoặc giao dịch hết hạn/không hợp lệ, SDK raise SmartOTPRejectedError. Cách này chỉ áp dụng cho tài khoản đã kích hoạt Smart OTP. Tài khoản dùng OTP SMS/email thông thường vẫn dùng ensure_auth(auth, otp=...) như phần Sample Code ở trên.

Lưu ý quan trọng

  • Không hard-code credentials trong mã nguồn. Sử dụng biến môi trường hoặc file cấu hình.
  • OTP chỉ cần ở lần authenticate đầu tiên. Các lần sau dùng refresh token.
  • Token cache (token_cache.json) nên được thêm vào .gitignore.
  • SDK tự động gắn Authorization: Bearer <accessToken> cho mọi request.

Trên trang này