Skip to content

Bump @alphafi/alphalend-sdk to ^4.1.0 - #98

Merged
11felix merged 3 commits into
mainfrom
chore/bump-alphalend-sdk-4-1-0
Aug 12, 2026
Merged

Bump @alphafi/alphalend-sdk to ^4.1.0#98
11felix merged 3 commits into
mainfrom
chore/bump-alphalend-sdk-4-1-0

Conversation

@11felix

@11felix 11felix commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Picks up the alphalend-sdk release that replaced 7k swap with Cetus (alphalend-sdk-js#172). Removes @7kprotocol/sdk-ts, @flowx-finance/sdk, and @bluefin-exchange/bluefin7k-aggregator-sdk from this package's dependency tree entirely.

Only Network, getConstants, getUserPositionCapId, and AlphalendClient are consumed from the SDK, and their type signatures are unchanged across 3.0.2→4.1.0. Two of them change behaviour, though:

  • AlphalendClient.updatePrices() switches oracle mechanism, Pyth Core → Pyth Lazer. It is now a delegate to updatePricesLazer(), which fetches signed update bytes from LAZER_PROXY_URL (https://api.alphalend.xyz/lazer/update) and appends pyth_lazer::parse_and_verify_le_ecdsa_update_v2 + oracle::ingest_lazer_update to the PTB, in place of the old @pythnetwork/pyth-sui-js path. This changes the PTB shape of every updatePrices(...) call site in src/strategies/** and src/admin/.
  • getConstants() changes values, not just shape: ALPHAFI_LATEST_ORACLE_PACKAGE_ID 0x7671937b…0x869b2d44…, plus new LAZER_* / STSUI_* keys.

Chain-side readiness is in place: alphafi-sdk-rust main already runs the Lazer path (f0c2456).

Also folded in (see review discussion): the reward-collection paths now issue one oracle update per PTB covering the union of coin types, instead of one per reward, and every updatePrices call is awaited so the oracle moveCalls are ordered before the collect_reward_and_swap_* calls that consume the price.

Build, lint and type-check are clean.

Unblocks removing the v1 @mysten/sui compat shims in alphafi-fe (companion PR there).

@11felix
11felix requested a review from jangid as a code owner August 10, 2026 11:45
@11felix
11felix requested a review from Zorag44 August 10, 2026 11:58
Zorag44
Zorag44 previously approved these changes Aug 10, 2026
@jangid

jangid commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

operate.md review — head 2cd63de, CI green (lint/test/build). I diffed the published 3.0.2 and 4.1.0 tarballs rather than trusting the changelog, because a lockfile-only diff hides everything that matters here.

1. 🟡 The PR body's central claim does not hold. "Only Network, getConstants, getUserPositionCapId, and AlphalendClient are consumed — none changed across 3.0.2→4.1.0" is true at the type level and wrong at the behavioural level:

  • AlphalendClient.updatePrices() in 3.0.2 built Pyth updates via @pythnetwork/pyth-sui-js (SuiPythClient/SuiPriceServiceConnection, both dropped from the 4.1.0 surface). In 4.1.0 it is a one-line delegate to the new updatePricesLazer(), which fetches signed update bytes over HTTP from LAZER_PROXY_URL (https://api.alphalend.xyz/lazer/update, 3 attempts × 4 s timeout) and appends pyth_lazer::parse_and_verify_le_ecdsa_update_v2 + oracle::ingest_lazer_update to the PTB.
  • getConstants() changed values, not just shape: ALPHAFI_LATEST_ORACLE_PACKAGE_ID moves 0x7671937b…0x869b2d44…, plus new LAZER_* / STSUI_* keys.

So the bump rewrites the PTB shape of all 21 updatePrices(...) call sites in src/strategies/** and src/admin/. Chain-side readiness looks fine — alphafi-sdk-rust main already carries the same migration (f0c2456 "migrate AlphaLend price updates from Pyth Core to Lazer") — but the body should say what this actually is, since the existing approval was given against "none changed".

2. 🟡 No call site passes updateBytes, so multi-update PTBs now carry N oracle updates. updatePrices(tx, coins, updateBytes?) accepts pre-fetched bytes; nothing here uses it. Every call re-fetches from the proxy and appends another verify + ingest_lazer_update pair to the same transaction. Call sites that fire more than once per PTB: slushSingleAssetLooping.ts:485,526,556 (inside the per-reward loop — so it scales with reward count), slushLending.ts:281, slushLooping.ts:321, looping.ts:726,943. The Rust SDK hit exactly this and fixed it in dd6f56d ("skip the redundant emergency price update when autocompound already refreshed the same coins in the PTB"). Fetch once per transaction and thread the bytes through, or dedup by coin set. Cost as written: redundant proxy round-trips, duplicate mutable borrows of the shared oracle object, extra gas, and extra PTB commands on the reward-collection paths.

3. 🟡 Five updatePrices calls are not awaitedslushSingleAssetLooping.ts:485,526,556, slushLending.ts:281, slushLooping.ts:321. The oracle moveCalls are appended whenever the promise settles, so they can land after the collect_reward_and_swap_* calls that consume the price, or after tx.build() (never appended). Pre-existing — 3.0.2's Pyth path was async too — but the retry path here (3 attempts + exponential backoff) widens the window materially, and the fix is one keyword per site.

4. 🟢 New runtime dependency on api.alphalend.xyz in the transaction-build path. A wedged proxy now fails every strategy deposit/withdraw/compound build after ~13 s of retries, in a code path that previously depended only on Hermes. Worth confirming that endpoint is monitored and alerting like the other production endpoints.

5. 🟢 @naviprotocol/lending is now duplicated in the tree — root ^2.0.7 plus a nested exact 2.0.3 under @alphafi/alphalend-sdk (which pins it exactly). Two copies ship; aligning the root to 2.0.3, or relaxing the pin upstream, collapses them.

Holding approval on item 2 — small, contained change, and it keeps the reward-collect PTBs from carrying one oracle update per reward. Items 1 and 4–5 are advisory; item 3 is pre-existing but cheap to fix in the same pass.

@jangid

jangid commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

@11felix — one change requested before this can be approved (full detail in the operate.md comment above): fetch the Lazer update bytes once per transaction and pass them into every updatePrices call.

Why it matters: in 4.1.0 updatePrices() is a delegate to updatePricesLazer(), which — when updateBytes is omitted — fetches from LAZER_PROXY_URL and appends a fresh pyth_lazer::parse_and_verify_le_ecdsa_update_v2 + oracle::ingest_lazer_update pair to the PTB on every call. Several of our paths call it more than once per transaction, and one of them scales with reward count:

  • src/strategies/slushSingleAssetLooping.ts:485,526,556 — 485 and 526 are inside the per-reward loop
  • src/strategies/slushLending.ts:281
  • src/strategies/slushLooping.ts:321
  • src/strategies/looping.ts:726,943

So a pool with several claimable rewards builds a PTB carrying one proxy round-trip and one duplicate shared-oracle mutation per reward — extra gas, extra commands, and needless contention on the oracle object. alphafi-sdk-rust hit the same thing and fixed it in dd6f56d ("skip the redundant emergency price update when autocompound already refreshed the same coins in the PTB"), so there's a precedent to mirror.

Shape of the fix: hoist one fetchLazerUpdateBytes(...) (or a first updatePrices call whose bytes you keep) to the top of each tx-building function and thread the Uint8Array into subsequent calls, or dedup per coin set so the verify+ingest pair is appended at most once per PTB.

Two smaller things worth folding into the same pass:

  1. Five of those call sites are not awaited (slushSingleAssetLooping.ts:485,526,556, slushLending.ts:281, slushLooping.ts:321), so the oracle moveCalls can be appended after the collect_reward_and_swap_* calls that consume the price — or after tx.build(), i.e. never. Pre-existing, but the Lazer retry path (3 attempts × 4 s + backoff) makes it much easier to hit. One keyword per site.
  2. The PR body says only Network, getConstants, getUserPositionCapId and AlphalendClient are consumed and "none changed across 3.0.2→4.1.0". That's accurate for the type signatures, but updatePrices changed oracle mechanism and getConstants() changed values (ALPHAFI_LATEST_ORACLE_PACKAGE_ID 0x7671937b…0x869b2d44…). Worth correcting so the description matches the blast radius — the existing approval was given against that sentence.

Chain-side readiness isn't a concern: alphafi-sdk-rust main already runs the Lazer path (f0c2456). This is purely about not stacking N oracle updates into one transaction.

@11felix

11felix commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — items 1, 2 and 3 were real. Fixed in 8415956 (rebased on the latest branch head). One correction on the proposed mechanism for item 2, detail below.

2 — multi-update PTBs: fixed, but not by threading updateBytes

Confirmed the problem: updatePricesLazer appends a verify + ingest pair on every call, and slushSingleAssetLooping/slushLooping call it inside the per-reward loop, so the pair scaled with reward count.

Threading updateBytes doesn't actually fix it, though. Look at the 4.1.0 body:

async updatePricesLazer(tx, coinTypes, updateBytes) {
    const [bytes, oracleInitialSharedVersion] = await Promise.all([
        updateBytes ?? fetchLazerUpdateBytes(this.constants.LAZER_PROXY_URL),
        ...
    ]);
    appendLazerUpdate(tx, ..., bytes);   // ← unconditional
    for (const coinType of new Set(coinTypes)) appendOracleToLendingBridge(tx, coinType, ...);
}

appendLazerUpdate runs regardless of whether the bytes were passed in. So passing pre-fetched bytes saves the HTTP round-trip and nothing else — the duplicate PTB commands, the extra gas, and the repeated mutable borrow of the shared oracle all remain. That's the part that actually costs us.

Two further blockers on that shape: fetchLazerUpdateBytes isn't re-exported from the SDK's public index.d.ts (only AlphalendClient, Market, Position, the position helpers, the caches, flashRepay and coinHelpers are), and updatePrices returns Promise<void>, so "keep the bytes from the first call" isn't reachable either.

What I did instead: one updatePrices per reward-collection path, with the union of the coin types those swaps need, hoisted above the swap moveCalls.

  • slushSingleAssetLooping.ts — pre-pass over rewardsToClaim builds a Set of exactly the coin types the taken branches need (ALPHA→stSUI→SUI, reward→SUI, and the USDSUI base-asset conversion), then a single awaited call. 3 call sites → 1.
  • slushLooping.ts — filter to rewardsToSwap first, single awaited call over [...new Set(rewardCoinTypes), SUI], then the loop. N call sites → 1.
  • slushLending.ts — already a single call, just needed the await.

Union rather than the full 10-coin list on purpose: appendOracleToLendingBridge is per coin type, so passing everything would trade N verify+ingest pairs for ~11 needless bridge calls on every deposit.

3 — unawaited calls: fixed

All five are gone. Three disappeared with the hoist above; slushLending.ts:281 and the slushLooping loop call are now awaited. grep -rn 'updatePrices(' src/ | grep -v await is empty.

One thing I did not fully close

The deposit/withdraw entrypoints still each carry their own updatePrices and then call collectAndSwapRewards, which does its own — so those PTBs land on 2 oracle updates, not 1. Same in singleAssetLooping.ts (487/512 + 330), which was already awaited and already 2.

I left that alone deliberately. Collapsing it to 1 means the caller has to know the reward coin types before it can build the union, which means splitting the portfolio fetch out of collectAndSwapRewards and threading a coin set through four files — and in slushLooping the helper early-returns for new pools and for the no-rewards case, where the caller's own price update is still required, so a naive "move the call inside" silently breaks deposit. That's more surgery than a dependency bump should carry.

The real fix is upstream: dedup the verify+ingest inside updatePricesLazer (e.g. a WeakMap<Transaction, Set<string>> so the pair is appended once per tx and only new coin types get a bridge call). That makes every consumer correct without any of them coordinating, and it's where alphafi-sdk-rust's dd6f56d equivalent belongs. Happy to raise it on alphalend-sdk-js if you agree.

1 — PR body: corrected

Rewritten to state the Pyth Core → Lazer switch and the getConstants() value changes explicitly. Verified the prod constant myself: prodConstants.js has ALPHAFI_LATEST_ORACLE_PACKAGE_ID: 0x869b2d44… and LAZER_PROXY_URL: https://api.alphalend.xyz (staging points at api-staging.alphalend.xyz).

4 and 5 — acknowledged, not changed here

4 (proxy in the build path): agreed, and worth flagging that the blast radius is a bit wider than 13s — fetchLazerUpdateBytes is 3 attempts × 4s timeout plus 300 * 2**attempt backoff + jitter between them, so a wedged proxy stalls a build for ~14–15s before throwing. Not something this PR can fix; needs api.alphalend.xyz/lazer/update on the same monitoring/alerting as the other prod endpoints. Can you confirm that's in place?

5 (@naviprotocol/lending duplication): confirmed, npm ls shows root 2.0.7 plus a nested 2.0.3 under @alphafi/alphalend-sdk. Both pins are exact, so this can't be deduped from here — either root drops to 2.0.3 (a behavioural change I'd rather not smuggle into a dependency bump) or the upstream pin gets relaxed to ^2.0.3. Leaving as-is and tracking separately unless you'd rather align now.

Build, lint, type-check and prettier all clean. No tests in the repo to run.

@11felix
11felix requested review from OctoSauce and Zorag44 and removed request for jangid August 12, 2026 09:23
@PratikArora7
PratikArora7 requested a review from jangid August 12, 2026 09:36
@11felix
11felix removed the request for review from jangid August 12, 2026 09:44
@Zorag44

Zorag44 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Re-reviewed at head 8415956 — CI green (lint/build/test). Verifying the three items held open above against the current diff rather than the reply.

Item 2 (N oracle updates per PTB) — fixed, verified.

  • slushSingleAssetLooping.ts:483-499 — the pre-pass mirrors the loop's branch conditions exactly (ALPHA → {ALPHA, stSUI, SUI}; other reward → {x, SUI}; USDSUI → {asset, USDC, SUI}), so the union it passes is identical to the union of the old per-reward calls. No coin lost, none added. 3 sites → 1.
  • slushLooping.ts:317-327 — same, and the new rewardsToSwap.length === 0 early return is safe: nothing follows the loop in collectAndSwapRewards, so it's equivalent to the old all-continue path.

On the updateBytes correction — agreed, and one piece the quoted 4.1.0 body left out: updatePricesLazer opens with if (coinTypes.length === 0) return; (v4.1.0 src/core/client.ts:135). That doesn't change the conclusion — threading bytes still wouldn't dedupe the verify+ingest pair — but it does close the one thing the hoist could have regressed: a pool with rewardsToClaim: [] and a non-USDSUI asset now reaches updatePrices(tx, []), and that's a true no-op, not a wasted proxy round-trip.

Item 3 (unawaited) — fixed, verified. All 19 updatePrices sites under src/ are awaited at this head.

Item 1 (PR body) — corrected, and the constant checks out where it matters. ALPHAFI_ORACLE in src/utils/constants.ts:104 is 0xce4ca140…, byte-identical to ALPHAFI_ORACLE_OBJECT_ID in alphalend-sdk v4.1.0 prodConstants.ts. The package id moved; the shared oracle object did not — so ingest_lazer_update and collect_reward_and_swap_bluefin_v2 operate on the same object. That was the real risk in the package-id change.

On the 2-updates-per-PTB you left open — agreed, and it's safe, not merely cheap. alphafi_oracle::ingest_lazer_update skips a non-advancing feed rather than aborting (oracle.move:1300: if (price_info.last_updated > feed_ts_sec) { emit LazerPriceSkippedEvent{reason: 11}; continue }). So the second ingest in a deposit/withdraw PTB is a no-op write plus gas and event noise — never a revert. Deferring it is the right call, and +1 on raising the WeakMap<Transaction, …> dedup upstream in alphalend-sdk-js; that's where it belongs.

Correction to the earlier list: looping.ts:726 and :943 were listed as multi-per-PTB sites — they aren't. 726 is in deposit (708), 943 is in withdraw (922), and looping.ts's own collectAndSwapRewards (597) never calls updatePrices, so the Alphalend branch there is already 1 per PTB. singleAssetLooping.ts (330 + 487/512) does carry 2, as you said.

Item 5 (@naviprotocol/lending duplication) — agreed it doesn't belong in a dependency bump; track separately. One thing worth recording from the lockfile: @mysten/sui resolves to a single 2.22.1 at the root with no nested copy under @alphafi/alphalend-sdk, so there's no second Transaction class on the boundary where we hand tx to the SDK. The three dropped aggregator SDKs are gone from the lock and unreferenced in src/.

Approving. Item 4 is the one thing still open, and it's an ops question rather than a code one: api.alphalend.xyz/lazer/update is now in the transaction-build path for every strategy deposit/withdraw/compound, with ~14–15s of retries before it throws. Checking separately that it's on the same monitoring and alerting as the other production endpoints — not a merge blocker.

@Zorag44 Zorag44 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

operate.md: clean — CI green at 8415956; items 1–3 verified fixed against the current diff (oracle object id unchanged, coin-type union preserved, all updatePrices awaited). Residual 2-updates-per-PTB on deposit/withdraw is gas and event noise only — ingest_lazer_update skips a non-advancing feed rather than aborting. Approvals: 1/2 — needs one more before merge.

@11felix
11felix merged commit 503f4cc into main Aug 12, 2026
3 checks passed
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.

4 participants