Skip to content

feat: SDK gap-fill — mirror of TS PR #21 (order history + USD valuation + combined transfers + get_deployment filters + error preservation)#9

Merged
hsyndeniz merged 15 commits into
mainfrom
feat/sdk-gap-fill
Jun 16, 2026
Merged

feat: SDK gap-fill — mirror of TS PR #21 (order history + USD valuation + combined transfers + get_deployment filters + error preservation)#9
hsyndeniz merged 15 commits into
mainfrom
feat/sdk-gap-fill

Conversation

@hsyndeniz

Copy link
Copy Markdown
Collaborator

Python parity port of dexalot-sdk-typescript#21. 7 commits, 954 unit tests pass, 100% coverage, mypy strict + ruff clean.

hsyndeniz added 11 commits June 2, 2026 20:47
…_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 .`.
@hsyndeniz hsyndeniz requested a review from ngurmen June 4, 2026 15:27
@ngurmen ngurmen requested a review from ilkerulutas June 12, 2026 15:08
@ngurmen ngurmen self-requested a review June 12, 2026 15:40
@ngurmen

ngurmen commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Implementation spec: reconcile feat/sdk-gap-fill (Python ↔ TypeScript)

Background & canonical decisions

The 5 gap-fill methods are net-new on both feat/sdk-gap-fill branches and absent
from both mains. The two SDKs implemented the same endpoints with different public
shapes. Align both to the canonical shapes below. Do this before either branch
merges/releases
— post-release these become breaking changes.

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?.fromTs and
    params.periodto = opts?.toTs (only when defined).
  • Build the cache key from the translated values (address +
    itemsperpage/pageno/symbol/periodfrom/periodto) so equivalent
    limit/offset combinations that map to the same page share a slot
    (matches Python).
  • Update the method's JSDoc: document symbol/fromTs/toTs/limit/offset,
    and that limit/offset are translated to itemsperpage/pageno internally
    and fromTs/toTs to periodfrom/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; (was string) — "Source-leg time as unix seconds (UTC)."
  • targetTs: number | null; (was string | 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/targetTx are 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)

  • getCombinedTransfers tests: switch any { itemsperpage, pageno, periodfrom, periodto } call sites to { limit, offset, fromTs, toTs, symbol }; add a test
    asserting the limit/offsetitemsperpage/pageno translation (e.g.
    limit: 50, offset: 100itemsperpage=50, pageno=3) and that fromTs/toTs
    go out as periodfrom/periodto.
  • Transfer normalization tests: assert sourceTs/targetTs are numbers (unix
    seconds) and that a row with no target leg yields targetTs === null (alongside
    the already-null targetEnv/targetChainId/targetTx).
  • getOrderHistory: add a test asserting a lowercase/alias pair is normalized in
    the outgoing params.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 kindsymbol (src/dexalot_sdk/core/transfer.py, get_combined_transfers, ~line 2160)

  • Signature: kind: str | None = Nonesymbol: str | None = None.
  • Body (~line 2212): if symbol is not None: normalized_symbol = self._normalize_user_token(symbol).
  • Docstring: rename the kind: arg to symbol: 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 | None

Add 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 None
  • target_chain_id = ... else None (keep the bool-exclusion guard)
  • target_tx = target_tx_raw if isinstance(target_tx_raw, str) else None
  • target_ts = self._coerce_timestamp_seconds(raw.get("target_ts")) (drop the or 0)
  • Leave source_ts = self._coerce_timestamp_seconds(raw.get("source_ts")) or 0 unchanged (source is non-null).

B3. Python tests (tests/unit/core/test_transfer.py)

  • Update the three kind= call sites (lines ~3013, ~3063, ~3086) to symbol=.
  • Add a test for a row with omitted/null target_* fields asserting target_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 (kindsymbol) 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).
@hsyndeniz hsyndeniz force-pushed the feat/sdk-gap-fill branch from 1a469ab to e5dab09 Compare June 15, 2026 13:55
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.
@hsyndeniz hsyndeniz merged commit 5492e4f into main Jun 16, 2026
3 checks passed
@hsyndeniz hsyndeniz deleted the feat/sdk-gap-fill branch June 16, 2026 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants