feat: SDK gap-fill — mirror of TS PR #21 (order history + USD valuation + combined transfers + get_deployment filters + error preservation)#9
Conversation
…_call Backend reason codes (FQ-, P-, T-, RF-) now surface in Result.fail messages instead of being swallowed as generic HTTP status errors. Mirrors TypeScript SDK commit bbabf19.
…ilters Backward-compatible. No-args call still resolves with defaults. Cache key includes filter params so variants don't collide. Mirrors TypeScript SDK commit 14265ad.
Returns token-symbol → USD-price map from the public /info/usd-prices
endpoint. Cached at Semi-Static tier (15m). No auth required.
The backend currently emits a flat ``dict[str, str]`` map (string prices,
including scientific notation for very small values); the SDK coerces to
``float`` and silently drops entries it cannot interpret. An array-of-
objects fallback (``[{"symbol", "price"}, ...]``) is also accepted in
case the backend shape ever changes. Cache key is namespaced by ``env``
so testnet and mainnet clients in the same process never collide.
The ``env`` query parameter is forwarded for parity with the TypeScript
SDK; the backend determines the network from the API host today and
ignores it, but the SDK is forward-compatible.
Also fixes a latent cache-instance-identity bug in ``_configure_caches``:
the function was reassigning the module-level cache globals when a
custom TTL was supplied, but subscriber modules (``transfer.py`` /
``swap.py`` / ``clob.py``) capture those globals by reference at module
load time. Reassigning would leave the cache the decorator writes to
disconnected from the cache test fixtures clear, producing
order-dependent test failures. ``_configure_caches`` now mutates the
existing ``MemoryCache.ttl`` in place.
Mirrors TypeScript SDK commit ea9f4a4.
get_token_price_history and get_token_hourly_price_history return
ascending-time-ordered ``list[PricePoint]`` from the public
``/api/info/token-usd-price-history`` and
``/api/info/token-usd-price-history-hourly`` endpoints. Static-tier
cached (1 h TTL) with path-namespaced keys so daily and hourly never
collide; past prices don't change.
Both methods delegate to a shared ``_fetch_price_history`` helper that
normalizes the raw ``{date: ISO-8601-string, price: stringified-decimal}``
rows (descending by date on the wire) into ascending unix-seconds +
numeric ``PricePoint`` list, tolerates ``ts``/``timestamp``/``time``
numeric aliases (values ``>= 1e12`` treated as milliseconds), and
silently drops malformed rows. Optional ``from_ts``/``to_ts`` window
is forwarded to the backend (ignored today, forward-compat) and
additionally applied client-side so the caller's range contract holds.
The Python signature uses keyword-only ``from_ts``/``to_ts`` instead
of the TypeScript ``opts: {from, to}`` shape to avoid the ``from``
keyword conflict in Python.
New ``PricePoint`` frozen dataclass exported from
``dexalot_sdk.core.transfer`` (kept in the module that produces it,
matching the existing ``ResolvedChain`` convention in ``base.py``).
Mirrors TypeScript SDK commit 63b2e61.
Signed GET returning canonical ``list[Transfer]`` (snake_case fields,
status/action_type/bridge lifted from numeric enums) with
kind/from_ts/to_ts/limit/offset filters from the signed REST endpoint
``/api/trading/signed/transferscombined`` (NOT under ``/privapi/`` —
empirically confirmed via OPTIONS preflight: 404 vs 204). Balance-tier
cached (10 s TTL) per ``(address, kind, from_ts, to_ts, limit, offset)``.
Backend pagination uses ``itemsperpage`` / ``pageno``; the SDK exposes
the more conventional ``limit`` / ``offset`` signature and translates
internally (``pageno = (offset // limit) + 1``). ``kind`` is forwarded
to the backend as ``symbol``; ``from_ts`` / ``to_ts`` as
``periodfrom`` / ``periodto``. Response shape is ``{count, rows}``
with a bare-list fallback for forward-compat.
``quantity`` / ``fee`` arrive as already-display-decimal numeric strings
from the backend (the official frontend reads them straight through
Big.js — no decimals divide); the SDK coerces to ``float`` via the
shared ``_coerce_usd_price`` helper. Unknown ``action_type`` / ``status``
enums and rows missing required fields are silently dropped so a
single bad row does not poison the page. Unknown ``bridge`` enum falls
back to ``"NATIVE"`` to match the frontend's display-only treatment.
To support a signed call outside the CLOB surface, ``_get_auth_headers``
is lifted from ``CLOBClient`` to ``DexalotBaseClient``. The CLOB call
sites are unchanged; the duplicate body is removed and a pointer
comment kept. The lift also matches the TypeScript SDK structure where
``_getAuthHeaders`` lives on a shared base.
New ``Transfer`` / ``TransferStatus`` / ``TransferActionType`` /
``TransferBridge`` types exported from ``dexalot_sdk.core.transfer``.
Mirrors TypeScript SDK commit f7a9945.
Signed GET; returns canonical Order list (same shape as get_open_orders). Accepts pair/status/limit/offset filters and optional explicit account argument. Balance-tier cached (10s). Reuses _transform_order_from_api and _get_auth_headers. Mirrors TypeScript SDK commit ece5dcd.
… preservation Adds new methods to Cached Methods lists, type-normalization entries for PricePoint + Transfer, an Error Handling note on preserved backend reason codes, and a get_order_history usage snippet. Mirrors TypeScript SDK commit fa4959c.
Six new features in this PR (order history, USD valuation x3, combined transfers, get_deployment filters) plus reason_code/reason error preservation. Local was at 0.5.14; PyPI has 0.5.15 already, so skip to 0.5.16 to avoid version collision.
Resolve conflicts: - VERSION / pyproject.toml / src/dexalot_sdk/__init__.py: keep 0.5.16 (main released 0.5.15; this PR ships 0.5.16) - src/dexalot_sdk/core/clob.py: keep both sides — main added 'import logging' + 'import time' for its display-decimals helpers, this branch's get_order_history is preserved as auto-merged - uv.lock: regenerated via 'uv lock' against the merged pyproject Also drop a now-unused [no-untyped-def] ignore that mypy flagged post-merge (BrokenAccount.address gets an explicit return type).
CI's `ruff format --check` caught formatting drift in:
- src/dexalot_sdk/core/transfer.py
- tests/unit/core/{test_base,test_clob,test_transfer}.py
My local Makefile target `make lint` only runs `ruff check` (lint),
not `ruff format --check`. The CI runs both. Auto-applied with
`ruff format .`.
Implementation spec: reconcile
|
| Decision | Canonical |
|---|---|
getCombinedTransfers params |
symbol, fromTs/from_ts & toTs/to_ts (unix-seconds int), limit/offset; translate to backend itemsperpage/pageno/periodfrom/periodto internally |
Transfer timestamps |
sourceTs/source_ts: unix-seconds int (non-null); targetTs/target_ts: unix-seconds int or null |
Transfer target legs |
targetEnv/targetChainId/targetTx (+ target_ts) are null when the transfer never crosses chains — never 0/"" sentinels |
getOrderHistory pair filter |
validate and normalize before sending |
Rationale for the coder: unix-int timestamps match PricePoint (which is already
unix-int on both SDKs) and fix a TS internal inconsistency (PricePoint.timestamp: number vs Transfer.sourceTs: string). Nullability comes from TS — null for a
non-crossing leg is real signal that Python's 0/"" sentinels destroy.
Repo A — TypeScript (dexalot-sdk-typescript) — most of the work is here
A1. getCombinedTransfers → ergonomic params (src/core/transfer.ts)
Current signature exposes raw backend params. Change to:
public async getCombinedTransfers(opts?: {
symbol?: string;
fromTs?: number; // unix seconds
toTs?: number; // unix seconds
limit?: number;
offset?: number;
}): Promise<Result<Transfer[]>>In the body, after resolving address:
const itemsperpage = Math.max(1, opts?.limit ?? 100);const pageno = Math.floor((opts?.offset ?? 0) / itemsperpage) + 1;const symbol = opts?.symbol ? this.normalizeToken(opts.symbol) : undefined;- Forward to the backend as
params.periodfrom = opts?.fromTsand
params.periodto = opts?.toTs(only when defined). - Build the cache key from the translated values (
address+
itemsperpage/pageno/symbol/periodfrom/periodto) so equivalent
limit/offsetcombinations that map to the same page share a slot
(matches Python). - Update the method's JSDoc: document
symbol/fromTs/toTs/limit/offset,
and thatlimit/offsetare translated toitemsperpage/pagenointernally
andfromTs/toTstoperiodfrom/periodto.
A2. Transfer timestamps → unix seconds (src/types/index.ts + src/core/transfer.ts)
In src/types/index.ts, change the Transfer interface:
sourceTs: number;(wasstring) — "Source-leg time as unix seconds (UTC)."targetTs: number | null;(wasstring | null)- Update the surrounding doc comment that currently says the timestamps are ISO strings.
In src/core/transfer.ts _normalizeTransfer, add a small private helper and use it:
/** ISO-8601 string OR numeric/numeric-string → unix seconds (UTC), or null. */
private _coerceTransferTs(raw: unknown): number | null {
if (typeof raw === 'string' && raw.trim() !== '') {
const parsed = Date.parse(raw);
if (Number.isFinite(parsed)) return Math.floor(parsed / 1000);
}
return this._coerceTimestampSeconds(raw);
}Then:
const sourceTs = this._coerceTransferTs(r.source_ts) ?? 0;(source always present; 0 fallback keeps it non-null)const targetTs = this._coerceTransferTs(r.target_ts);(null when absent —targetEnv/targetChainId/targetTxare already nullable in TS, leave them)
A3. getOrderHistory pair normalization (src/core/clob.ts)
After the existing validatePairFormat(opts.pair, 'pair') check, normalize before
sending. BaseClient.normalizePair() exists (src/core/base.ts:1137). Set
params.pair = this.normalizePair(opts.pair) instead of forwarding the raw
opts.pair.
A4. TS tests (tests/unit/transfer.test.ts, tests/unit/clob.test.ts)
getCombinedTransferstests: switch any{ itemsperpage, pageno, periodfrom, periodto }call sites to{ limit, offset, fromTs, toTs, symbol }; add a test
asserting thelimit/offset→itemsperpage/pagenotranslation (e.g.
limit: 50, offset: 100⇒itemsperpage=50, pageno=3) and thatfromTs/toTs
go out asperiodfrom/periodto.Transfernormalization tests: assertsourceTs/targetTsare numbers (unix
seconds) and that a row with no target leg yieldstargetTs === null(alongside
the already-nulltargetEnv/targetChainId/targetTx).getOrderHistory: add a test asserting a lowercase/alias pair is normalized in
the outgoingparams.pair.
A5. TS docs (README.md)
Update the getCombinedTransfers signature/param table and the Transfer field
descriptions (timestamps now unix-seconds numbers; target legs nullable).
Repo B — Python (dexalot-sdk-python) — small changes
B1. Rename kind → symbol (src/dexalot_sdk/core/transfer.py, get_combined_transfers, ~line 2160)
- Signature:
kind: str | None = None→symbol: str | None = None. - Body (~line 2212):
if symbol is not None: normalized_symbol = self._normalize_user_token(symbol). - Docstring: rename the
kind:arg tosymbol:and update the cache-key tuple line
(address, kind, ...)→(address, symbol, ...).
B2. Nullable target legs (src/dexalot_sdk/core/transfer.py)
Transfer dataclass (~lines 98-101): change field types to
target_env: str | None
target_chain_id: int | None
target_tx: str | None
target_ts: int | NoneAdd a one-line doc note that target_* is None for non-crossing transfers.
In _normalize_transfer (~lines 2124-2137): emit None instead of sentinels —
target_env = target_env_raw if isinstance(target_env_raw, str) else Nonetarget_chain_id = ... else None(keep thebool-exclusion guard)target_tx = target_tx_raw if isinstance(target_tx_raw, str) else Nonetarget_ts = self._coerce_timestamp_seconds(raw.get("target_ts"))(drop theor 0)- Leave
source_ts = self._coerce_timestamp_seconds(raw.get("source_ts")) or 0unchanged (source is non-null).
B3. Python tests (tests/unit/core/test_transfer.py)
- Update the three
kind=call sites (lines ~3013, ~3063, ~3086) tosymbol=. - Add a test for a row with omitted/null
target_*fields assertingtarget_ts is None,target_env is None,target_chain_id is None,target_tx is None. The
existing full-row test (lines ~2780-2818) still passes unchanged.
B4. Python docs (README.md, CLAUDE.md if it documents the field shape)
Update the get_combined_transfers param name (kind→symbol) and the Transfer
field nullability note.
Gates (run in each repo before requesting review)
Python: make test · make lint · make mypy (all three must pass — mypy
strict will catch any missed | None propagation).
TypeScript: pnpm exec tsc --noEmit -p tsconfig.build.json · pnpm exec jest --ci tests/unit · pnpm audit --audit-level=high. (Ignore the whole-project pnpm typecheck — it has 48 pre-existing test-file errors on main and is not a CI gate.)
…B1-B4)
Breaking changes that must land before merge to match the canonical
cross-SDK spec for the gap-fill surface.
B1: rename get_combined_transfers `kind` kwarg -> `symbol`. Body now
normalizes `symbol` via _normalize_user_token; docstring arg doc and
cache-key tuple doc updated to `symbol`. The cached layer already
keys/forwards on the symbol value.
B2: Transfer.target_* legs are now nullable (str|None / int|None) and
_normalize_transfer emits None (not "" / 0) for non-crossing rows.
target_ts drops the trailing `or 0`; source_ts stays non-null.
B3: tests updated to symbol= call sites; added a null-target-legs test
asserting target_env/target_chain_id/target_tx/target_ts are None.
Coverage held at 100%.
B4: README get_combined_transfers param renamed and Transfer nullability
note added. (CLAUDE.md does not document this field shape -> untouched.)
get_order_history already validates + normalizes the pair (no gap).
… for the backend The reconciliation forwarded the ergonomic unix-seconds from_ts/to_ts straight through as the backend's periodfrom/periodto, but that endpoint rejects raw unix integers with "Malformed Request! ISO Date format problem". Convert to ISO-8601 (matching the TS SDK's new Date(ts*1000).toISOString() output byte-for-byte) before sending. Public API is unchanged — from_ts/to_ts stay unix-seconds ints; only the internal backend translation is corrected. Found via live devnet smoke through the trade-kit consumer (unit tests mock the SDK so couldn't catch the malformed query).
1a469ab to
e5dab09
Compare
Live devnet testing showed the combined-transfers backend returns source_ts/target_ts as ISO-8601 strings, but _coerce_timestamp_seconds is numeric-only — so it yielded source_ts=0 / target_ts=None for real data, violating the canonical "source_ts: unix-seconds int" shape (the TS SDK, which uses Date.parse, returned the correct seconds). Add _coerce_transfer_ts (ISO-8601 + numeric -> unix seconds, mirroring the TS _coerceTransferTs) and use it in _normalize_transfer. Verified live on devnet: source_ts/target_ts now match TS exactly (1779731190 / 1779731274). Unit tests previously mocked numeric timestamps, which is why they missed it — added a coercer branch test.
pip-audit (CI security gate) began failing on newly-published advisories: - aiohttp 3.14.0 -> 3.14.1 (8 CVEs: CVE-2026-54273..54280) - cryptography 47.0.0 -> 48.0.1 (GHSA-537c-gmf6-5ccf) cryptography's constraint was <48, which excluded the fix, so widened to >=48.0.1,<49; aiohttp floor raised to >=3.14.1. Relocked uv.lock. Tests (1045), 100% coverage, mypy, ruff, bandit, pip-audit all clean; live devnet smoke confirms the bumped HTTP/crypto libs work.
Python parity port of dexalot-sdk-typescript#21. 7 commits, 954 unit tests pass, 100% coverage, mypy strict + ruff clean.