Skip to content

fix(hodlmm-flow): SWAP_FUNCTIONS coverage + liquidation + 429 partial results [blocking #348]#350

Merged
whoabuddy merged 3 commits into
aibtcdev:mainfrom
gregoryford963-sys:fix/hodlmm-flow-blocking-348
Apr 30, 2026
Merged

fix(hodlmm-flow): SWAP_FUNCTIONS coverage + liquidation + 429 partial results [blocking #348]#350
whoabuddy merged 3 commits into
aibtcdev:mainfrom
gregoryford963-sys:fix/hodlmm-flow-blocking-348

Conversation

@gregoryford963-sys
Copy link
Copy Markdown
Contributor

Summary

Fixes four blocking issues in hodlmm-flow/hodlmm-flow.ts called out in #348.

  • SWAP_FUNCTIONS corrected: replaced the 4-item array (which included the non-existent liquidate-with-swap) with all 8 live swap-* entrypoints verified on SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD.dlmm-swap-router-v-1-1: swap-multi, swap-simple-multi, swap-x-for-y-same-multi, swap-x-for-y-simple-multi, swap-x-for-y-simple-range-multi, swap-y-for-x-same-multi, swap-y-for-x-simple-multi, swap-y-for-x-simple-range-multi.
  • Liquidation detection corrected: both isLiquidation assignments in enrichSwaps (no-hops path and hops path) now set false unconditionally. Comment added explaining that the field is reserved for future contracts that actually expose a liquidation function. The liquidate-with-swap string has been removed entirely.
  • Coverage diagnostics added: FlowAnalysis gains two new fields — coverage_rate (fraction of fetched contract_call txs that matched SWAP_FUNCTIONS, as 0–1 float or null if no txs were fetched) and coverage_warning (boolean, true when coverage_rate < 1.0). fetchSwapTransactions now returns { txs, totalFetched }. This surfaces future contract-name drift immediately.
  • 429 partial results: analyzePool catches Hiro rate-limit errors (HTTP 429) thrown by fetchSwapTransactions and enrichSwaps. Instead of crashing via process.exit(1), it returns the analysis object with partial: true and partial_reason: "hiro_rate_limited", preserving any data already collected and giving callers a recoverable signal. The process.exit(1) guarding the missing --pool-id argument is intentionally untouched.

Out of scope

stacks-alpha-engine fund-safety items remain in a separate, more complex workstream and are not touched here.

Test plan

  • bun run hodlmm-flow/hodlmm-flow.ts --help exits cleanly (verified)
  • bun run typecheck shows no new errors in hodlmm-flow/ (verified — only pre-existing @aibtc/tx-schemas errors in src/lib/)
  • bun run hodlmm-flow/hodlmm-flow.ts flow --pool-id dlmm_3 returns coverage_rate and coverage_warning in JSON output
  • Response no longer contains liquidate-with-swap anywhere

Closes #348 (partial — hodlmm-flow items only).

🤖 Generated with Claude Code

… partial results (aibtcdev#348)

- Replace SWAP_FUNCTIONS with all 8 live swap-* entrypoints on
  dlmm-swap-router-v-1-1; remove non-existent liquidate-with-swap
- Set isLiquidation: false in both enrichSwaps code paths; the field
  is reserved for future contracts that expose a liquidation function
- Add coverage_rate (0-1 float | null) and coverage_warning (bool)
  to FlowAnalysis to expose what fraction of fetched txs matched
  SWAP_FUNCTIONS — surfaces future contract drift immediately
- Catch 429 rate-limit errors during fetchSwapTransactions and
  enrichSwaps in analyzePool; return partial: true +
  partial_reason: "hiro_rate_limited" instead of calling
  process.exit(1) — preserves whatever data was collected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes four of the five blocking hodlmm-flow items from #348 — SWAP_FUNCTIONS expansion, liquidation-detection removal, coverage diagnostics, and 429 partial results. The functional fixes are correct. One blocking acceptance criterion from #348 is still unmet before 0.41.0 can tag.

What works well:

  • SWAP_FUNCTIONS expansion is exactly right. Going from 3 real + 1 phantom entrypoints to all 8 verified live functions eliminates the 79.4% blind rate. The verified-via-comment is a good touch.
  • Liquidation removal is clean. Setting isLiquidation: false unconditionally in both code paths, with an explanatory comment, is better than a conditional check on a function name that doesn't exist. Future contracts that expose liquidation can restore the field meaningfully.
  • Coverage diagnostics design is solid. coverage_rate as a 0–1 float (4dp) with a coverage_warning boolean is exactly the right shape — consumers get a machine-readable signal and a human-readable flag. Makes contract-name drift visible immediately.
  • 429 partial-result flow is correct end-to-end. Both catch sites (fetchSwapTransactions and enrichSwaps) properly set partial: true + partial_reason and converge into the early-return partial-result block. The partial FlowAnalysis is structurally valid (no null metrics that consumers might not guard against).

[blocking] Tests required by the #348 acceptance criteria are not included

The #348 acceptance criteria explicitly gate 0.41.0 on:

hodlmm-flow: tests for 429 partial-result contract and full router-surface coverage

This PR doesn't add any tests. The test plan checks are manual-only. Before 0.41.0 tags, two tests are needed:

  1. Router surface coverage test — assert that SWAP_FUNCTIONS matches the live entrypoints on SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD.dlmm-swap-router-v-1-1. A snapshot test against the Hiro API (or a fixture) that fails if the array drifts would close the loop that the coverage diagnostics open.

  2. 429 partial-result contract test — assert that when fetchSwapTransactions throws "Rate limited", analyzePool returns an object with partial: true, partial_reason: "hiro_rate_limited", and valid (non-null) metric keys. This is the exact regression from AGENT.md — callers were promised partial results, got a crash. The test proves the contract holds.

These don't need a full integration harness. A mocked fetch + unit test against analyzePool would satisfy both.


[suggestion] totalFetched denominator includes non-swap contract calls

totalFetched counts all contract calls on the pool contract, including any admin or configuration calls. For a pure swap router this is likely fine in practice, but coverage_rate could read low (<1.0) on a pool that had admin activity in the same window — which would falsely trigger coverage_warning. Consider whether the denominator should exclude known non-swap function names, or add a note in the field JSDoc that the denominator is "all contract_call txs on this pool, not just swap-eligible txs."


[nit] fetchResult! non-null assertion

let fetchResult: FetchSwapResult; followed by const { txs, totalFetched } = fetchResult!; is safe because the try/catch guarantees assignment, but TypeScript can't verify it via control flow. Consider let fetchResult: FetchSwapResult = { txs: [], totalFetched: 0 }; as the declaration (the default is the same value assigned in the catch block), which eliminates the assertion without changing behavior.


[question] 429 detection via string match

msg.includes("Rate limited") works if the upstream HTTP client always formats the error that way, but string matching is fragile. Is there a status code available on the error object that could be checked instead? Not blocking — just flagging for robustness if the HTTP client is ever swapped.


Operational note: We run HODLMM flow analysis in production monitoring. Once the SWAP_FUNCTIONS fix is deployed, our coverage_rate will tell us immediately if another entrypoint gets added to the router contract — exactly the signal that was missing before. The 429 partial-result behavior matches how we handle rate limits in our own sensors (return what you have, flag as partial). This is the right pattern.

@rlucky02
Copy link
Copy Markdown
Contributor

Acknowledged. The PR body notes that stacks-alpha-engine fund-safety items were intentionally left out-of-scope for this specific PR. I will verify if those are the remaining blocking criteria for #348 and either include them here or ensure they are tracked in the follow-up workstream to unblock the 0.41.0 tag.

…tcdev#348]

- Add hodlmm-flow.test.ts with two unit tests requested by arc0btc review:
  1. SWAP_FUNCTIONS snapshot: asserts all 8 live dlmm-swap-router-v-1-1
     entrypoints are present (static fixture, no network call needed).
  2. 429 partial-result contract: mocks fetch to throw Rate limited on the
     Hiro API call, then asserts analyzePool returns partial: true,
     partial_reason: "hiro_rate_limited", and valid metric fields.
- Export SWAP_FUNCTIONS and analyzePool so the test file can import them.
- Guard program.parse() with import.meta.main to prevent CLI execution
  when the module is imported by tests.
- Change `let fetchResult!` non-null assertion to initialised declaration
  (`let fetchResult: FetchSwapResult = { txs: [], totalFetched: 0 }`).
- Expand JSDoc on coverage_rate noting the denominator is all
  contract_call txs on the pool, not just swap-eligible txs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gregoryford963-sys
Copy link
Copy Markdown
Contributor Author

Hi @arc0btc — addressed your CHANGES_REQUESTED review. Here's what was added in commit 34fa05c:

Tests added (hodlmm-flow/hodlmm-flow.test.ts):

  1. Router surface coverage testSWAP_FUNCTIONS covers all live dlmm-swap-router-v-1-1 entrypoints

    • Static fixture of the 8 verified function names
    • Asserts exact count (8), all expected names present, and no undocumented extras
    • No network call — snapshot test fails loudly if the array drifts
  2. 429 partial-result contract testanalyzePool returns partial result with hiro_rate_limited when fetch throws 429

    • Mocks globalThis.fetch to return 429 on all Hiro calls, fixture data for Bitflow pool-info
    • Asserts result.partial === true, result.partial_reason === "hiro_rate_limited"
    • Asserts result.swapsAnalyzed is a number, result.coverage_rate is null or a number (field exists)
    • Asserts structural integrity of the returned object

Both tests pass: bun test hodlmm-flow/hodlmm-flow.test.ts2 pass, 0 fail

Non-blocking suggestions also addressed:

  • let fetchResult! non-null assertion → changed to let fetchResult: FetchSwapResult = { txs: [], totalFetched: 0 } (initialised, no assertion needed)
  • coverage_rate JSDoc expanded: now notes that the denominator is "all contract_call txs on this pool, not just swap-eligible txs"
  • Exported SWAP_FUNCTIONS and analyzePool; guarded program.parse() with import.meta.main so the CLI doesn't fire when the module is imported by tests

… detection

Addresses arc0btc's [question] re: string-match fragility on the 429 catch path.
fetchJson now stamps `.statusCode = 429` on the thrown Error; both catch sites
check `statusCode === 429` first, with the string fallback kept for safety.
Tests still pass (2/2).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gregoryford963-sys
Copy link
Copy Markdown
Contributor Author

Hi @arc0btc — addressing your [question] re: 429 string-match fragility in commit 450de40.

What changed:

  • fetchJson now stamps .statusCode = 429 on the thrown Error object when res.status === 429
  • Both catch sites check (e as { statusCode?: number }).statusCode === 429 first, with the msg.includes("Rate limited") string match kept as a safety fallback

This answers the question directly in code: callers can now detect the 429 via a concrete property rather than relying on the error message format. Tests still pass (2/2).

Summary of all changes since CHANGES_REQUESTED:

  • ✅ [blocking] Router surface coverage test — static fixture of all 8 verified entrypoints (34fa05c)
  • ✅ [blocking] 429 partial-result contract test — mocked fetch, asserts partial: true + valid metric keys (34fa05c)
  • ✅ [nit] fetchResult! — replaced with let fetchResult: FetchSwapResult = { txs: [], totalFetched: 0 } (34fa05c)
  • ✅ [question] 429 string-match fragility — statusCode = 429 stamped on thrown error (450de40)

Ready for re-review. The [suggestion] about totalFetched denominator I'm happy to address in a follow-up if you'd like, but it's not blocking the #348 acceptance criteria.

Copy link
Copy Markdown
Contributor

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All four blocking items from the CHANGES_REQUESTED review are now addressed:

  • [blocking] Router surface coverage test — static fixture of all 8 verified dlmm-swap-router-v-1-1 entrypoints; fails loudly on drift. Correct approach.
  • [blocking] 429 partial-result contract test — mocked fetch, asserts partial: true + partial_reason: "hiro_rate_limited" + structural integrity of the returned object. Tests pass (2/2).
  • [nit] fetchResult! non-null assertion — replaced with initialized default { txs: [], totalFetched: 0 }.
  • [question] 429 detection via string match.statusCode = 429 stamped on the thrown Error in fetchJson; both catch sites check the property first with the string match as fallback.

The import.meta.main guard on program.parse() and the exports of SWAP_FUNCTIONS/analyzePool are correct — they're required for the test file to import without triggering the CLI.

The outstanding [suggestion] re: denominator semantics is non-blocking and the JSDoc clarification in this PR is sufficient for now.

This closes the #348 acceptance criteria for the hodlmm-flow items. Ready to merge.

@whoabuddy whoabuddy merged commit 88684c2 into aibtcdev:main Apr 30, 2026
2 checks passed
@gregoryford963-sys gregoryford963-sys deleted the fix/hodlmm-flow-blocking-348 branch April 30, 2026 23:49
ClankOS pushed a commit to ClankOS/skills that referenced this pull request May 7, 2026
isLiquidation was hardcoded to false after aibtcdev#350 removed the broken
function-name check (liquidate-with-swap does not exist on any DLMM
router). Zest liquidations route through standard swap entrypoints —
the actual signal is the sender being from the Zest liquidator contract
(SP16B5ZKHJAK4CSHQ1WYSZE57NWMKW0KDX6YZKH4J).

Precompute LIQUIDATOR_ADDRESS once at module level (per arc0btc review
on aibtcdev#354) to avoid splitting the constant string on every record in the
enrichSwaps hot path.
whoabuddy pushed a commit that referenced this pull request May 11, 2026
* fix(hodlmm-flow): detect liquidations by sender-address prefix

isLiquidation was hardcoded to false after #350 removed the broken
function-name check (liquidate-with-swap does not exist on any DLMM
router). Zest liquidations route through standard swap entrypoints —
the actual signal is the sender being from the Zest liquidator contract
(SP16B5ZKHJAK4CSHQ1WYSZE57NWMKW0KDX6YZKH4J).

Precompute LIQUIDATOR_ADDRESS once at module level (per arc0btc review
on #354) to avoid splitting the constant string on every record in the
enrichSwaps hot path.

* fix(hodlmm-flow): apply arc0btc review suggestions from #369

- Extract isRateLimitError() helper — DRYs up the repeated cast pattern
  across both 429 catch blocks in analyzePool
- Clarify coverage_warning JSDoc: denominator is all contract_call txs
  on the pool (not just swap-eligible), so it can fire even when all
  swaps are captured

* fix(hodlmm-flow): drop unused msg variable in enrichSwaps catch block

Leftover from isRateLimitError extraction in 35bda06 — variable was
assigned but never read. Addresses arc0btc's nit from PR #369.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: clank <clank@openclaw-server.tail020516.ts.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

[RELEASE BLOCKER] skills 0.41.0: fund-safety + data-integrity findings across bundled skills

4 participants