Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 4 additions & 28 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ on:
branches:
- main

permissions:
contents: write

jobs:
ci:
name: Typecheck, validate, and manifest freshness
Expand All @@ -19,11 +16,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0

- name: Setup Bun
uses: oven-sh/setup-bun@v2
Expand All @@ -37,29 +29,13 @@ jobs:
- name: Validate skill frontmatter
run: bun run validate

- name: Auto-regenerate manifest (same-repo PRs)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
run: |
jq '.skills' skills.json > /tmp/skills-before.json
bun run manifest
jq '.skills' skills.json > /tmp/skills-after.json
if ! diff -q /tmp/skills-before.json /tmp/skills-after.json > /dev/null 2>&1; then
echo "Manifest stale (likely from interleaved PR merges) — auto-regenerating and committing."
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add skills.json
git commit -m "chore(ci): auto-regenerate manifest after merge interleaving"
git push origin HEAD:${{ github.event.pull_request.head.ref }}
echo "Manifest regenerated and pushed. CI will re-run on the new commit."
exit 0
fi
echo "Manifest up to date."

- name: Check manifest freshness (forks and main pushes)
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
- name: Check manifest freshness
run: |
# Save the committed skills array (excluding timestamp) for comparison
jq '.skills' skills.json > /tmp/skills-before.json
# Regenerate the manifest
bun run manifest
# Compare only the skills array — timestamp changes are expected and ignored
jq '.skills' skills.json > /tmp/skills-after.json
if ! diff -q /tmp/skills-before.json /tmp/skills-after.json > /dev/null 2>&1; then
echo "skills.json is stale — run 'bun run manifest' and commit the result."
Expand Down
22 changes: 12 additions & 10 deletions hodlmm-flow/hodlmm-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const DEFAULT_SWAP_COUNT = 100;
const TX_PAGE_SIZE = 50;
const DLMM_CORE = "SP1PFR4V08H1RAZXREBGFFQ59WB739XM8VVGTFSEA.dlmm-core-v-1-1";
const LIQUIDATOR_PREFIX = "SP16B5ZKHJAK4CSHQ1WYSZE57NWMKW0KDX6YZKH4J.liquidator";
const LIQUIDATOR_ADDRESS = LIQUIDATOR_PREFIX.split(".")[0];
const CACHE_DIR = join(process.env.HOME ?? "/tmp", ".hodlmm-flow-cache");
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes

Expand Down Expand Up @@ -148,7 +149,8 @@ interface FlowAnalysis {
* Note: the denominator is all contract_call txs on this pool, not just swap-eligible txs.
*/
coverage_rate: number | null;
/** True when coverage_rate < 1.0, indicating some transactions were not recognized as swaps */
/** True when coverage_rate < 1.0. Note: denominator includes all contract_call txs on the pool
* (add-liquidity, claim-fees, rebalance, etc.), so this can fire even when all swaps are captured. */
coverage_warning: boolean;
partial?: true;
partial_reason?: string;
Expand Down Expand Up @@ -176,6 +178,11 @@ function handleError(error: unknown): void {
process.exit(1);
}

function isRateLimitError(e: unknown): boolean {
const code = (e as { statusCode?: number }).statusCode;
return code === 429 || (e instanceof Error && e.message.includes("Rate limited"));
}

// ---------------------------------------------------------------------------
// Cache
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -395,8 +402,7 @@ async function enrichSwaps(txs: HiroTx[]): Promise<SwapRecord[]> {
blockTime: tx.block_time,
blockHeight: tx.block_height,
direction,
// No liquidation function exists on dlmm-swap-router-v-1-1; metric reserved for future contracts
isLiquidation: false,
isLiquidation: tx.sender_address.startsWith(LIQUIDATOR_ADDRESS),
functionName: fn,
hops: [],
totalDx: 0n,
Expand Down Expand Up @@ -424,8 +430,7 @@ async function enrichSwaps(txs: HiroTx[]): Promise<SwapRecord[]> {
blockTime: tx.block_time,
blockHeight: tx.block_height,
direction,
// No liquidation function exists on dlmm-swap-router-v-1-1; metric reserved for future contracts
isLiquidation: false,
isLiquidation: tx.sender_address.startsWith(LIQUIDATOR_ADDRESS),
functionName: tx.contract_call!.function_name,
hops,
totalDx,
Expand Down Expand Up @@ -912,9 +917,7 @@ export async function analyzePool(
try {
fetchResult = await fetchSwapTransactions(contract, swapCount, windowSeconds);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if ((e as { statusCode?: number }).statusCode === 429 || msg.includes("Rate limited")) {
// statusCode === 429 is set by fetchJson; string fallback handles wrapped errors
if (isRateLimitError(e)) {
fetchResult = { txs: [], totalFetched: 0 };
isPartial = true;
partialReason = "hiro_rate_limited";
Expand All @@ -938,8 +941,7 @@ export async function analyzePool(
try {
swaps = await enrichSwaps(txs);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if (((e as { statusCode?: number }).statusCode === 429 || msg.includes("Rate limited")) && txs.length > 0) {
if (isRateLimitError(e) && txs.length > 0) {
// enrichSwaps uses Promise.allSettled so individual failures are handled;
// if the outer call throws it means the rate limit hit during fetchTxEvents
// outside of the batch — treat as partial
Expand Down
Loading