Skip to content

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

Open
hsyndeniz wants to merge 11 commits into
mainfrom
feat/sdk-gap-fill
Open

feat: SDK gap-fill — mirror of TS PR #21 (order history + USD valuation + combined transfers + get_deployment filters + error preservation)#9
hsyndeniz wants to merge 11 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 and others 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.

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>
@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.)

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