* feat(kraken): fetch and import Ledgers API for deposits, withdrawals, staking, fees
Closes#2450
Kraken TradesHistory only returns spot buy/sell trades. The Ledgers API
(/0/private/Ledgers) covers deposits, withdrawals, staking rewards, Earn
income, and standalone fees — everything that was missing from syncs.
Changes:
- Provider::Kraken#get_ledgers — new method forwarding start/type/offset params
- KrakenItem::Importer#fetch_ledgers — paginated fetch (up to 200 pages) with
graceful fallback if the API key lacks Query Ledger Entries permission
- Importer#upsert_kraken_account — stores "ledgers" alongside "trades" in
raw_transactions_payload
- KrakenAccount::LedgerProcessor — new class; maps each supported ledger type
(deposit, withdrawal, staking, earn, fee) to a Transaction entry with the
correct investment_activity_label, kind, and sign convention; skips trade/
transfer/margin types to avoid double-counting with TradesHistory
- KrakenAccount::Processor#process — calls LedgerProcessor after process_trades
- Multi-currency: fiat amounts converted via ExchangeRate (non-USD fiat bridged
through USD); crypto amounts use the spot price cached in raw_payload["assets"]
with a price_missing flag when no price is available
- Dedup guard: external_id "kraken_ledger_<id>" + source "kraken" prevents
re-importing on repeated syncs
* fix(kraken): correct sign convention, fee inclusion, and earn subtype filtering
- Sign convention: deposits/staking/earn → negative (inflow), withdrawals/fees
→ positive (outflow), matching Sure's global convention (inflow is negative)
- Fee inclusion: use (amount - fee).abs as abs_impact so withdrawal fees are
counted in the total outflow rather than discarded
- Earn subtypes: skip allocation/deallocation ledger entries (internal fund
movements); only import rewardallocation/bonusallocation as Interest income
- DebugLogEntry: replace Rails.logger.warn/error with DebugLogEntry.capture
throughout LedgerProcessor and the Ledgers permission fallback in Importer,
so support-relevant incidents surface in /settings/debug
- Importer test: stub get_ledgers in setup so existing tests do not error
on the new fetch_ledgers call
* fix(kraken): route duplicate ledger-id warning through DebugLogEntry
* perf(kraken): batch ledger idempotency check; strengthen tests
Address review feedback (jjmata):
- N+1: LedgerProcessor#process_ledger_entry ran `account.entries.exists?(...)`
per ledger entry (up to ~10k per sync). Load the existing Kraken external IDs
once into a Set and test membership in memory (newly created IDs are added so
the same run stays idempotent) — same pattern as #2452.
- Tests: the idempotency test now asserts the first pass actually creates the
entry (assert_difference) before asserting the second is a no-op; add a guard
asserting the second (all-skipped) pass issues a single bulk external_id pluck,
not one query per entry.
No behavior change to imported entries.
* perf(kraken): scope ledger idempotency pluck to kraken_ledger_ prefix
Only load existing ledger external IDs (not trade entries) into the idempotency
Set, matching the reviewed approach. No behavior change.