From bc33baf6aa73e63527dadb231c9a2bc8211b951f Mon Sep 17 00:00:00 2001 From: Preyam Rao Date: Wed, 22 Jul 2026 00:01:02 +0530 Subject: [PATCH 1/4] feat(oracle): default mainnet price updates to Lazer --- __tests__/lazer.test.ts | 54 ++++++++++++++++------------------- src/constants/devConstants.ts | 2 +- src/core/client.ts | 19 ++---------- src/core/types.ts | 1 - 4 files changed, 27 insertions(+), 49 deletions(-) diff --git a/__tests__/lazer.test.ts b/__tests__/lazer.test.ts index c9f87b8..08f8c71 100644 --- a/__tests__/lazer.test.ts +++ b/__tests__/lazer.test.ts @@ -34,9 +34,8 @@ const errStatus = (status = 503) => ({ }); function buildLazerClient(proxyUrl: string) { - const client = new AlphalendClient("testnet", undefined, { + const client = new AlphalendClient("mainnet", undefined, { coinMetadataMap: new Map(), - useLazer: true, }); client.constants.LAZER_PROXY_URL = proxyUrl; jest @@ -187,11 +186,9 @@ describe("appendOracleToLendingBridge", () => { }); }); -describe("price-refresh routing (useLazer)", () => { - it("updatePrices routes to the Lazer path when useLazer is set", async () => { - const client = new AlphalendClient("mainnet", undefined, { - useLazer: true, - }); +describe("price-refresh routing", () => { + it("updatePrices routes to the Lazer path", async () => { + const client = new AlphalendClient("mainnet"); const spy = jest .spyOn(client, "updatePricesLazer") .mockImplementation(async () => {}); @@ -200,10 +197,8 @@ describe("price-refresh routing (useLazer)", () => { expect(spy).toHaveBeenCalledWith(tx, ["0x2::sui::SUI"]); }); - it("updateAllPrices routes to the Lazer path when useLazer is set", async () => { - const client = new AlphalendClient("mainnet", undefined, { - useLazer: true, - }); + it("updateAllPrices routes to the Lazer path", async () => { + const client = new AlphalendClient("mainnet"); const spy = jest .spyOn(client, "updatePricesLazer") .mockImplementation(async () => {}); @@ -211,6 +206,22 @@ describe("price-refresh routing (useLazer)", () => { await client.updateAllPrices(tx, ["0x2::sui::SUI"]); expect(spy).toHaveBeenCalledWith(tx, ["0x2::sui::SUI"]); }); + + it("keeps the Pyth path on testnet while its Lazer verifier is v1", async () => { + const client = new AlphalendClient("testnet", undefined, { + coinMetadataMap: new Map(), + }); + const lazerSpy = jest + .spyOn(client, "updatePricesLazer") + .mockImplementation(async () => {}); + jest + .spyOn(client.blockchain, "getInitialSharedVersion") + .mockResolvedValue("123"); + + await client.updatePrices(new Transaction(), []); + + expect(lazerSpy).not.toHaveBeenCalled(); + }); }); describe("Lazer price refresh", () => { @@ -259,9 +270,7 @@ describe("Lazer price refresh", () => { } return okBody("0x010203"); }); - const client = new AlphalendClient("testnet", undefined, { - useLazer: true, - }); + const client = new AlphalendClient("mainnet"); client.constants.LAZER_PROXY_URL = "https://api.example"; jest .spyOn(client.blockchain, "getInitialSharedVersion") @@ -307,25 +316,10 @@ describe("Pyth path intact (updatePriceTransaction)", () => { }); }); -describe("default price source (no premature cutover)", () => { - it("a fresh client defaults to Pyth (useLazer=false) on every network", () => { - for (const network of ["mainnet", "testnet", "devnet"] as Network[]) { - expect(new AlphalendClient(network).useLazer).toBe(false); - } - }); - - it("lets apps opt into Lazer through client options", () => { - expect( - new AlphalendClient("mainnet", undefined, { useLazer: true }).useLazer, - ).toBe(true); - }); -}); - describe("Lazer fail-closed at the client boundary", () => { it("propagates a proxy failure and never falls back to the Pyth path", async () => { - const client = new AlphalendClient("testnet", undefined, { + const client = new AlphalendClient("mainnet", undefined, { coinMetadataMap: new Map(), - useLazer: true, }); installFetch().mockRejectedValue(new Error("ECONNREFUSED")); diff --git a/src/constants/devConstants.ts b/src/constants/devConstants.ts index e933ee9..cb95882 100644 --- a/src/constants/devConstants.ts +++ b/src/constants/devConstants.ts @@ -72,7 +72,7 @@ export const devConstants: Constants = { // Pyth Lazer (Pyth Pro) Constants. // NOTE: this testnet package is still v1 and lacks parse_and_verify_le_ecdsa_update_v2, so the - // Lazer verify call won't resolve on testnet — keep useLazer off here until Pyth ships a v2 testnet package. + // SDK keeps Pyth price updates on testnet until Pyth ships a v2 testnet package. LAZER_PACKAGE_ID: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21", LAZER_STATE_ID: diff --git a/src/core/client.ts b/src/core/client.ts index a339bab..691d301 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -87,7 +87,6 @@ export class AlphalendClient { blockchain: Blockchain; sevenKGateway: SevenKGateway; cetusSwap: CetusSwap; - useLazer: boolean; // Dynamic coin metadata properties private coinMetadataMap: Map = new Map(); @@ -148,7 +147,6 @@ export class AlphalendClient { this.blockchain = new Blockchain(network, graphqlUrl); this.sevenKGateway = new SevenKGateway(); this.cetusSwap = new CetusSwap("mainnet"); - this.useLazer = options?.useLazer ?? false; // If a coin metadata map is provided, use it and mark as initialized if (options?.coinMetadataMap) { @@ -190,21 +188,8 @@ export class AlphalendClient { } } - /** - * Updates price information for assets from Pyth oracle - * - * This method: - * 1. Gathers price feed IDs for the specified coins - * 2. Fetches the latest price data from Pyth oracle - * 3. Adds price update instructions to the transaction - * 4. Updates the protocol with new price information - * - * @param tx - Transaction object to add price update calls to - * @param coinTypes - Array of fully qualified coin types (e.g., "0x2::sui::SUI") - * @returns Transaction object with price update calls - */ async updatePrices(tx: Transaction, coinTypes: string[]) { - if (this.useLazer) { + if (this.network === "mainnet") { await this.updatePricesLazer(tx, coinTypes); return; } @@ -258,7 +243,7 @@ export class AlphalendClient { } async updateAllPrices(tx: Transaction, coinTypes: string[]) { - if (this.useLazer) { + if (this.network === "mainnet") { await this.updatePricesLazer(tx, coinTypes); return; } diff --git a/src/core/types.ts b/src/core/types.ts index 91b2c73..f9e9b6e 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -20,7 +20,6 @@ import { Decimal } from "decimal.js"; */ export interface AlphalendClientOptions { coinMetadataMap?: Map; - useLazer?: boolean; } /** From 59e0714903935f819549f5de9769c558eca108b2 Mon Sep 17 00:00:00 2001 From: Preyam Rao Date: Wed, 22 Jul 2026 00:06:18 +0530 Subject: [PATCH 2/4] refactor(oracle): remove Pyth routing fallback --- __tests__/lazer.test.ts | 22 +------- src/constants/devConstants.ts | 2 - src/core/client.ts | 101 +--------------------------------- 3 files changed, 5 insertions(+), 120 deletions(-) diff --git a/__tests__/lazer.test.ts b/__tests__/lazer.test.ts index 08f8c71..4abf245 100644 --- a/__tests__/lazer.test.ts +++ b/__tests__/lazer.test.ts @@ -34,7 +34,7 @@ const errStatus = (status = 503) => ({ }); function buildLazerClient(proxyUrl: string) { - const client = new AlphalendClient("mainnet", undefined, { + const client = new AlphalendClient("testnet", undefined, { coinMetadataMap: new Map(), }); client.constants.LAZER_PROXY_URL = proxyUrl; @@ -206,22 +206,6 @@ describe("price-refresh routing", () => { await client.updateAllPrices(tx, ["0x2::sui::SUI"]); expect(spy).toHaveBeenCalledWith(tx, ["0x2::sui::SUI"]); }); - - it("keeps the Pyth path on testnet while its Lazer verifier is v1", async () => { - const client = new AlphalendClient("testnet", undefined, { - coinMetadataMap: new Map(), - }); - const lazerSpy = jest - .spyOn(client, "updatePricesLazer") - .mockImplementation(async () => {}); - jest - .spyOn(client.blockchain, "getInitialSharedVersion") - .mockResolvedValue("123"); - - await client.updatePrices(new Transaction(), []); - - expect(lazerSpy).not.toHaveBeenCalled(); - }); }); describe("Lazer price refresh", () => { @@ -270,7 +254,7 @@ describe("Lazer price refresh", () => { } return okBody("0x010203"); }); - const client = new AlphalendClient("mainnet"); + const client = new AlphalendClient("testnet"); client.constants.LAZER_PROXY_URL = "https://api.example"; jest .spyOn(client.blockchain, "getInitialSharedVersion") @@ -318,7 +302,7 @@ describe("Pyth path intact (updatePriceTransaction)", () => { describe("Lazer fail-closed at the client boundary", () => { it("propagates a proxy failure and never falls back to the Pyth path", async () => { - const client = new AlphalendClient("mainnet", undefined, { + const client = new AlphalendClient("testnet", undefined, { coinMetadataMap: new Map(), }); installFetch().mockRejectedValue(new Error("ECONNREFUSED")); diff --git a/src/constants/devConstants.ts b/src/constants/devConstants.ts index cb95882..fe80c3d 100644 --- a/src/constants/devConstants.ts +++ b/src/constants/devConstants.ts @@ -71,8 +71,6 @@ export const devConstants: Constants = { PYTH_PRICE_PATH: "/api/latest_price_feeds", // Pyth Lazer (Pyth Pro) Constants. - // NOTE: this testnet package is still v1 and lacks parse_and_verify_le_ecdsa_update_v2, so the - // SDK keeps Pyth price updates on testnet until Pyth ships a v2 testnet package. LAZER_PACKAGE_ID: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21", LAZER_STATE_ID: diff --git a/src/core/client.ts b/src/core/client.ts index 691d301..c8b5d54 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -16,8 +16,6 @@ import { } from "@mysten/sui/transactions"; import { appendOracleToLendingBridge, - getPriceInfoObjectIdsWithUpdate, - updatePriceTransaction, } from "../utils/oracle.js"; import { appendLazerUpdate, fetchLazerUpdateBytes } from "../utils/lazer.js"; import { @@ -189,106 +187,11 @@ export class AlphalendClient { } async updatePrices(tx: Transaction, coinTypes: string[]) { - if (this.network === "mainnet") { - await this.updatePricesLazer(tx, coinTypes); - return; - } - // Auto-initialize market data if needed - await this.ensureInitialized(); - - // De-duplicate: a coin appearing twice (e.g. supplied and borrowed) would - // otherwise add redundant Pyth verify + update_price moveCalls, bloating the - // transaction and gas with no benefit. Mirrors updateAllPrices(). - const uniqueCoinTypes = [...new Set(coinTypes)]; - - const updatePriceFeedIds: string[] = []; - for (const coinType of uniqueCoinTypes) { - if (!this.getPythSponsored(coinType)) { - updatePriceFeedIds.push(this.getPythPriceFeedId(coinType)); - } - } - - // The Pyth fetch (Hermes round-trip + price-feed moveCalls) and resolving - // the oracle's initial shared version are independent, so run them - // concurrently. getInitialSharedVersion only reads (it never mutates tx), - // so the Pyth update calls it appends still precede the update_price calls - // built in the loop below — transaction ordering is preserved. - const [, oracleInitialSharedVersion] = await Promise.all([ - updatePriceFeedIds.length > 0 - ? getPriceInfoObjectIdsWithUpdate( - tx, - updatePriceFeedIds, - this.pythClient, - this.pythConnection, - ) - : Promise.resolve(), - this.blockchain.getInitialSharedVersion( - this.constants.ALPHAFI_ORACLE_OBJECT_ID, - ), - ]); - - for (const coinType of uniqueCoinTypes) { - // Use dynamic data from GraphQL API - const priceInfoObjectId = this.getPythPriceInfoObjectId(coinType); - updatePriceTransaction( - tx, - { - priceInfoObject: priceInfoObjectId, - coinType: coinType, - }, - this.constants, - oracleInitialSharedVersion, - ); - } + await this.updatePricesLazer(tx, coinTypes); } async updateAllPrices(tx: Transaction, coinTypes: string[]) { - if (this.network === "mainnet") { - await this.updatePricesLazer(tx, coinTypes); - return; - } - // Auto-initialize market data if needed - await this.ensureInitialized(); - - // De-duplicate so a repeated coin doesn't add redundant update_price - // moveCalls in the loop below (the feed-id set was already de-duplicated). - const uniqueCoinTypes = [...new Set(coinTypes)]; - - // Use dynamic data with fallback to hardcoded - const updatePriceFeedIds: string[] = Array.from( - new Set( - uniqueCoinTypes.map((coinType) => this.getPythPriceFeedId(coinType)), - ), - ); - - // The Pyth fetch and resolving the oracle's initial shared version are - // independent; run them concurrently. getInitialSharedVersion only reads - // (never mutates tx), so the Pyth update calls still precede the - // update_price calls built in the loop below — ordering is preserved. - const [, oracleInitialSharedVersion] = await Promise.all([ - getPriceInfoObjectIdsWithUpdate( - tx, - updatePriceFeedIds, - this.pythClient, - this.pythConnection, - ), - this.blockchain.getInitialSharedVersion( - this.constants.ALPHAFI_ORACLE_OBJECT_ID, - ), - ]); - - for (const coinType of uniqueCoinTypes) { - const priceInfoObjectId = this.getPythPriceInfoObjectId(coinType); - updatePriceTransaction( - tx, - { - priceInfoObject: priceInfoObjectId, - coinType: coinType, - }, - this.constants, - oracleInitialSharedVersion, - ); - } + await this.updatePricesLazer(tx, coinTypes); } /** From cf19cf3a414cd5aa6a2e7b32869062e1dbea878e Mon Sep 17 00:00:00 2001 From: Preyam Rao Date: Wed, 22 Jul 2026 00:15:39 +0530 Subject: [PATCH 3/4] docs(oracle): retain testnet Lazer verifier warning --- src/constants/devConstants.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/constants/devConstants.ts b/src/constants/devConstants.ts index fe80c3d..e933ee9 100644 --- a/src/constants/devConstants.ts +++ b/src/constants/devConstants.ts @@ -71,6 +71,8 @@ export const devConstants: Constants = { PYTH_PRICE_PATH: "/api/latest_price_feeds", // Pyth Lazer (Pyth Pro) Constants. + // NOTE: this testnet package is still v1 and lacks parse_and_verify_le_ecdsa_update_v2, so the + // Lazer verify call won't resolve on testnet — keep useLazer off here until Pyth ships a v2 testnet package. LAZER_PACKAGE_ID: "0xf5bd2141967507050a91b58de3d95e77c432cd90d1799ee46effc27430a68c21", LAZER_STATE_ID: From 13a9eebb43b78d74f791f818ba4d18a7e9152270 Mon Sep 17 00:00:00 2001 From: Preyam Rao Date: Mon, 27 Jul 2026 22:22:14 +0530 Subject: [PATCH 4/4] feat(oracle): accept caller-supplied signed Lazer updates Every method taking priceUpdateCoinTypes accepts lazerUpdateBytes, and updatePrices/updateAllPrices/updatePricesLazer take it as a third arg; when present the SDK makes no request to AlphaLend's proxy, so a CORS-blocked browser or an API outage doesn't prevent transacting. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 35 +++++++++++++++ __tests__/lazer.test.ts | 99 ++++++++++++++++++++++++++++++++++++++--- src/core/client.ts | 66 +++++++++++++++++++++------ src/core/flashRepay.ts | 6 ++- src/core/types.ts | 14 ++++++ 5 files changed, 199 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index cf9d97e..2bb0b27 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,41 @@ tx.setGasBudget(100_000_000); await wallet.signAndExecuteTransaction(tx); ``` +#### Supplying your own signed Lazer update + +By default the SDK fetches a signed Pyth Lazer payload from AlphaLend's proxy. If you have your own, +hand it over directly — every method that takes `priceUpdateCoinTypes` accepts `lazerUpdateBytes`, +and `updatePrices` / `updateAllPrices` / `updatePricesLazer` take it as a third argument: + +```typescript +await client.updatePrices(tx, coinTypes, myBytes); + +const tx = await client.borrow({ + /* ...normal params... */ + priceUpdateCoinTypes: coinTypes, + lazerUpdateBytes: myBytes, +}); +``` + +Pass the raw signed update as bytes — what sits behind the `hex` field of a Lazer response, and what +`pyth_lazer::parse_and_verify_le_ecdsa_update_v2` verifies on-chain. With bytes supplied, building a +transaction makes no request to AlphaLend's proxy, so a browser blocked by CORS or an outage on our +side doesn't stop you from transacting. + +Two things to get right. Lazer is a push stream: you hold one long-lived subscription over a fixed +set of numeric feed ids and a new signed payload arrives every second, each covering that whole set. + +- **Subscribe to at least the coins you transact on.** `oracle::ingest_lazer_update` skips absent + feeds rather than failing, so a coin your subscription omits keeps its previous price and shows up + later as a staleness abort that points nowhere near the real cause. Feed ids are numeric — read + the coin-type mapping on-chain with `oracle::get_lazer_feed_ids_for_coin` against + `ALPHAFI_ORACLE_OBJECT_ID`. +- **Pass the newest payload, not a held one.** Payloads carry a signed timestamp the on-chain + verifier bounds against the clock. + +On testnet and devnet the high-level methods skip Lazer entirely (the canonical testnet Lazer +package predates the v2 verifier), so `lazerUpdateBytes` only takes effect there via `updatePrices`. + ### Supply Collateral ```typescript diff --git a/__tests__/lazer.test.ts b/__tests__/lazer.test.ts index 4abf245..e8e9dde 100644 --- a/__tests__/lazer.test.ts +++ b/__tests__/lazer.test.ts @@ -1,5 +1,7 @@ import { jest } from "@jest/globals"; import { Transaction } from "@mysten/sui/transactions"; +import { fromBase64 } from "@mysten/sui/utils"; +import { bcs } from "@mysten/sui/bcs"; import { appendLazerUpdate, fetchLazerUpdateBytes, @@ -44,6 +46,28 @@ function buildLazerClient(proxyUrl: string) { return client; } +/** + * The signed payload the tx actually carries into parse_and_verify_le_ecdsa_update_v2. + * + * Resolved via the verify call's own argument rather than "the first Pure input" — real call sites + * (flashRepay) append pure inputs before the price update. + */ +function lazerPayloadOf(tx: Transaction): number[] { + const data = tx.getData(); + const verify = data.commands.find( + (command) => + command.MoveCall?.function === "parse_and_verify_le_ecdsa_update_v2", + ); + const arg = verify?.MoveCall?.arguments.at(-1); + if (arg?.$kind !== "Input") { + throw new Error("transaction carries no Lazer verify call"); + } + const input = data.inputs[arg.Input]; + if (input.$kind !== "Pure") + throw new Error("Lazer payload input is not pure"); + return [...bcs.vector(bcs.u8()).parse(fromBase64(input.Pure.bytes))]; +} + function expectCompleteLazerRefresh( tx: Transaction, constants: Constants, @@ -119,9 +143,9 @@ describe("fetchLazerUpdateBytes", () => { ok: true, json: async () => ({}), }); - await expect( - fetchLazerUpdateBytes("https://api.example"), - ).rejects.toThrow(/hex/i); + await expect(fetchLazerUpdateBytes("https://api.example")).rejects.toThrow( + /hex/i, + ); }); it("retries a transient failure, then succeeds", async () => { @@ -138,14 +162,18 @@ describe("fetchLazerUpdateBytes", () => { it("fails closed after exhausting retries (never falls back to Pyth)", async () => { const fetchMock = installFetch(); fetchMock.mockResolvedValue(errStatus(503)); - await expect(fetchLazerUpdateBytes("https://api.example")).rejects.toThrow(); + await expect( + fetchLazerUpdateBytes("https://api.example"), + ).rejects.toThrow(); expect(fetchMock).toHaveBeenCalledTimes(3); }); it("fails closed on a network-level error", async () => { const fetchMock = installFetch(); fetchMock.mockRejectedValue(new Error("ECONNREFUSED")); - await expect(fetchLazerUpdateBytes("https://api.example")).rejects.toThrow(); + await expect( + fetchLazerUpdateBytes("https://api.example"), + ).rejects.toThrow(); expect(fetchMock).toHaveBeenCalledTimes(3); }); }); @@ -194,7 +222,7 @@ describe("price-refresh routing", () => { .mockImplementation(async () => {}); const tx = new Transaction(); await client.updatePrices(tx, ["0x2::sui::SUI"]); - expect(spy).toHaveBeenCalledWith(tx, ["0x2::sui::SUI"]); + expect(spy).toHaveBeenCalledWith(tx, ["0x2::sui::SUI"], undefined); }); it("updateAllPrices routes to the Lazer path", async () => { @@ -204,7 +232,7 @@ describe("price-refresh routing", () => { .mockImplementation(async () => {}); const tx = new Transaction(); await client.updateAllPrices(tx, ["0x2::sui::SUI"]); - expect(spy).toHaveBeenCalledWith(tx, ["0x2::sui::SUI"]); + expect(spy).toHaveBeenCalledWith(tx, ["0x2::sui::SUI"], undefined); }); }); @@ -275,6 +303,63 @@ describe("Lazer price refresh", () => { }); }); +describe("caller-supplied Lazer updates", () => { + it("per-call bytes are used as-is, with no provider and no proxy", async () => { + const fetchMock = installFetch(); + const client = buildLazerClient("https://api.example"); + const tx = new Transaction(); + + await client.updatePrices( + tx, + [client.constants.SUI_COIN_TYPE], + new Uint8Array([3, 1, 4]), + ); + + expect(fetchMock).not.toHaveBeenCalled(); + expect(lazerPayloadOf(tx)).toEqual([3, 1, 4]); + expectCompleteLazerRefresh(tx, client.constants, 1); + }); + + it("per-call bytes take precedence over an installed provider", async () => { + installFetch(); + const provider = jest.fn(() => new Uint8Array([9, 9, 9])); + const client = buildLazerClient("https://api.example", provider); + const tx = new Transaction(); + + await client.updatePrices( + tx, + [client.constants.SUI_COIN_TYPE], + new Uint8Array([1, 2]), + ); + + expect(provider).not.toHaveBeenCalled(); + expect(lazerPayloadOf(tx)).toEqual([1, 2]); + }); + + it("per-call bytes reach a high-level method (withdraw)", async () => { + const fetchMock = installFetch(); + const client = new AlphalendClient("mainnet", undefined, { + coinMetadataMap: new Map(), + }); + jest + .spyOn(client.blockchain, "getInitialSharedVersion") + .mockResolvedValue("123"); + + const tx = await client.withdraw({ + marketId: "1", + amount: 1_000n, + coinType: client.constants.SUI_COIN_TYPE, + positionCapId: `0x${"1".repeat(64)}`, + address: `0x${"2".repeat(64)}`, + priceUpdateCoinTypes: [client.constants.SUI_COIN_TYPE], + lazerUpdateBytes: new Uint8Array([8, 8]), + }); + + expect(fetchMock).not.toHaveBeenCalled(); + expect(lazerPayloadOf(tx)).toEqual([8, 8]); + }); +}); + describe("Pyth path intact (updatePriceTransaction)", () => { it("builds update_price_from_pyth + the lending bridge, with no Lazer calls", () => { const tx = new Transaction(); diff --git a/src/core/client.ts b/src/core/client.ts index 8f49457..bb20cbb 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -164,10 +164,14 @@ export class AlphalendClient { return this.coinMetadataMap; } - async updatePricesLazer(tx: Transaction, coinTypes: string[]): Promise { + async updatePricesLazer( + tx: Transaction, + coinTypes: string[], + updateBytes?: Uint8Array, + ): Promise { if (coinTypes.length === 0) return; const [bytes, oracleInitialSharedVersion] = await Promise.all([ - fetchLazerUpdateBytes(this.constants.LAZER_PROXY_URL), + updateBytes ?? fetchLazerUpdateBytes(this.constants.LAZER_PROXY_URL), this.blockchain.getInitialSharedVersion( this.constants.ALPHAFI_ORACLE_OBJECT_ID, ), @@ -186,12 +190,20 @@ export class AlphalendClient { } } - async updatePrices(tx: Transaction, coinTypes: string[]) { - await this.updatePricesLazer(tx, coinTypes); + async updatePrices( + tx: Transaction, + coinTypes: string[], + updateBytes?: Uint8Array, + ) { + await this.updatePricesLazer(tx, coinTypes, updateBytes); } - async updateAllPrices(tx: Transaction, coinTypes: string[]) { - await this.updatePricesLazer(tx, coinTypes); + async updateAllPrices( + tx: Transaction, + coinTypes: string[], + updateBytes?: Uint8Array, + ) { + await this.updatePricesLazer(tx, coinTypes, updateBytes); } /** @@ -570,7 +582,11 @@ export class AlphalendClient { // First update prices to ensure latest oracle values if (this.network === "mainnet") { - await this.updatePrices(tx, params.priceUpdateCoinTypes); + await this.updatePrices( + tx, + params.priceUpdateCoinTypes, + params.lazerUpdateBytes, + ); } else { await setPrices(tx); } @@ -679,7 +695,11 @@ export class AlphalendClient { // First update prices to ensure latest oracle values if (this.network === "mainnet") { - await this.updatePrices(tx, params.priceUpdateCoinTypes); + await this.updatePrices( + tx, + params.priceUpdateCoinTypes, + params.lazerUpdateBytes, + ); } else { await setPrices(tx); } @@ -833,7 +853,11 @@ export class AlphalendClient { // First update prices to ensure latest oracle values if (this.network === "mainnet") { - await this.updatePrices(tx, params.priceUpdateCoinTypes); + await this.updatePrices( + tx, + params.priceUpdateCoinTypes, + params.lazerUpdateBytes, + ); } else { await setPrices(tx); } @@ -1156,7 +1180,11 @@ export class AlphalendClient { params.priceUpdateCoinTypes && params.priceUpdateCoinTypes.length > 0 ) { - await this.updatePrices(tx, params.priceUpdateCoinTypes); + await this.updatePrices( + tx, + params.priceUpdateCoinTypes, + params.lazerUpdateBytes, + ); } // Prefer the on-chain-derived claimable estimate over params.rewardAmounts: @@ -1414,7 +1442,11 @@ export class AlphalendClient { // reward-collection loop below, preserving transaction ordering. const [, { rewardInput, claimableAmounts }] = await Promise.all([ shouldUpdatePrices - ? this.updatePrices(tx, params.priceUpdateCoinTypes!) + ? this.updatePrices( + tx, + params.priceUpdateCoinTypes!, + params.lazerUpdateBytes, + ) : Promise.resolve(), getClaimRewardInput( this.blockchain, @@ -1569,9 +1601,17 @@ export class AlphalendClient { // First update prices to ensure latest oracle values if (this.network === "mainnet") { if (params.updateAllPrices) { - await this.updateAllPrices(tx, params.priceUpdateCoinTypes); + await this.updateAllPrices( + tx, + params.priceUpdateCoinTypes, + params.lazerUpdateBytes, + ); } else { - await this.updatePrices(tx, params.priceUpdateCoinTypes); + await this.updatePrices( + tx, + params.priceUpdateCoinTypes, + params.lazerUpdateBytes, + ); } } else { await setPrices(tx); diff --git a/src/core/flashRepay.ts b/src/core/flashRepay.ts index 1e4f06d..036b35d 100644 --- a/src/core/flashRepay.ts +++ b/src/core/flashRepay.ts @@ -255,7 +255,11 @@ export async function buildFlashRepayTransaction( // Step 4: Update oracle prices — refresh on-chain oracle prices for all coins in the position so withdraw/collateral math is correct (mainnet only). if (client.network === "mainnet") { - await client.updatePrices(tx, priceUpdateCoinTypes); + await client.updatePrices( + tx, + priceUpdateCoinTypes, + params.lazerUpdateBytes, + ); } // Step 5: Withdraw collateral — remove collateral in withdraw-coin (amount = flash loan value + slippage buffer so enough after swap to repay Navi). Uses promise so the withdrawn coin can be used in the next step. diff --git a/src/core/types.ts b/src/core/types.ts index 46487c0..379cf9a 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -110,6 +110,8 @@ export interface WithdrawParams { /** Coin types of the coins whose price needs to be updated * (Will have to pass all market coin types that user has supplied or borrowed in and current market coin type in which user is withdrawing) */ priceUpdateCoinTypes: string[]; + /** Your own signed Lazer update for this transaction; skips the fetch from AlphaLend's proxy. */ + lazerUpdateBytes?: Uint8Array; } /** @@ -132,6 +134,8 @@ export interface ZapOutWithdrawParams { /** Coin types of the coins whose price needs to be updated * (Will have to pass all market coin types that user has supplied or borrowed in and current market coin type in which user is withdrawing) */ priceUpdateCoinTypes: string[]; + /** Your own signed Lazer update for this transaction; skips the fetch from AlphaLend's proxy. */ + lazerUpdateBytes?: Uint8Array; /** Slippage for the swap (e.g., 0.01 for 1%) */ slippage: number; /** Withdraw coin type (e.g., "0x2::sui::SUI") */ @@ -181,6 +185,8 @@ export interface BorrowParams { /** Coin types of the coins whose price needs to be updated * (Will have to pass all market coin types that user has supplied or borrowed in and current market coin type in which user is borrowing) */ priceUpdateCoinTypes: string[]; + /** Your own signed Lazer update for this transaction; skips the fetch from AlphaLend's proxy. */ + lazerUpdateBytes?: Uint8Array; } /** @@ -255,6 +261,8 @@ export interface ClaimSwapAndSupplyOrRepayOrTransferParams { slippage: number; /** Coin types of user's supplied assets (for price updates) */ priceUpdateCoinTypes: string[]; + /** Your own signed Lazer update for this transaction; skips the fetch from AlphaLend's proxy. */ + lazerUpdateBytes?: Uint8Array; /** Optional map of reward coin types to their amounts in base units; used as fallback when the on-chain claimable estimate is unavailable */ rewardAmounts?: Map; /** If true, claims rewards < $0.01 directly to wallet (only swap checkbox case) */ @@ -286,6 +294,8 @@ export interface ClaimAndSupplyOrRepayParams { supplyMarkets?: Map; /** Coin types for price updates */ priceUpdateCoinTypes: string[]; + /** Your own signed Lazer update for this transaction; skips the fetch from AlphaLend's proxy. */ + lazerUpdateBytes?: Uint8Array; } /** @@ -310,6 +320,8 @@ export interface LiquidateParams { /** Coin types of the coins whose price needs to be updated * (Will have to pass all market coin types that user has supplied or borrowed in and current market coin type in which is being liquidated) */ priceUpdateCoinTypes: string[]; + /** Your own signed Lazer update for this transaction; skips the fetch from AlphaLend's proxy. */ + lazerUpdateBytes?: Uint8Array; /** Whether to update all prices */ updateAllPrices?: boolean; } @@ -458,4 +470,6 @@ export interface FlashRepayParams { address: string; slippage: number; repayAmountBaseUnits?: string; + /** Your own signed Lazer update for this transaction; skips the fetch from AlphaLend's proxy. */ + lazerUpdateBytes?: Uint8Array; }