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- Tạo
Config→Auth→ gọiensure_auth(auth, otp=...) ensure_authload 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- 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ằngensure_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=...)
- Tài khoản dùng Smart OTP Push: response có
- Auth service trả về
accessToken,refreshToken,expiresInvà token được lưu vàotoken_cache.json - Mọi request sau đó gắn
Authorization: Bearer <accessToken> - 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(quaconfig.py). Trong tài liệu nàyConfig(...)đượ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:
"""
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:
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
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
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.