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. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…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. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… 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. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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 .`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.)
Python parity port of dexalot-sdk-typescript#21. 7 commits, 954 unit tests pass, 100% coverage, mypy strict + ruff clean.