From c00f004367336e41a19a594a2f5df8db4d5cdec8 Mon Sep 17 00:00:00 2001 From: felix11 Date: Mon, 20 Jul 2026 20:09:08 +0400 Subject: [PATCH 1/2] feat: use @naviprotocol/lending v2 for flash loans, drop vendored copy --- README.md | 9 +- package-lock.json | 32 +++ package.json | 1 + src/core/flashRepay.ts | 2 +- src/naviprotocol-lending.d.ts | 41 ++++ src/vendor/naviFlashloan.ts | 424 ---------------------------------- 6 files changed, 82 insertions(+), 427 deletions(-) create mode 100644 src/naviprotocol-lending.d.ts delete mode 100644 src/vendor/naviFlashloan.ts diff --git a/README.md b/README.md index 6919d62..cf9d97e 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,13 @@ npm install @alphafi/alphalend-sdk an ESM context or via a bundler. - **No `postinstall` / patches.** Earlier work-in-progress used a `patch-package` hook to fix `@naviprotocol/lending` under v2; that would have broken consumer - installs. The flash-loan helpers are now vendored (`src/vendor/naviFlashloan.ts`), - so installing this package runs no lifecycle scripts and pulls no Navi dependency. + installs. The flash-loan helpers now come from `@naviprotocol/lending@^2` + (a runtime dependency), which is `@mysten/sui` v2-native and import-safe — the + previous vendored copy has been removed. Installing this package still runs no + lifecycle scripts. (NAVI v2's split `.d.ts` don't resolve under + `moduleResolution: NodeNext`, so a small local declaration shim, + `src/naviprotocol-lending.d.ts`, types the flash-loan helpers; the runtime + bundle itself is unaffected.) ## Getting Started diff --git a/package-lock.json b/package-lock.json index 27497cb..3a90c35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@7kprotocol/sdk-ts": "^3.4.1", "@cetusprotocol/aggregator-sdk": "^1.5.5", + "@naviprotocol/lending": "^2.0.3", "@pythnetwork/pyth-sui-js": "^2.2.0", "@types/bn.js": "^5.2.0", "decimal.js": "^10.5.0", @@ -1682,6 +1683,31 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@naviprotocol/lending": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@naviprotocol/lending/-/lending-2.0.3.tgz", + "integrity": "sha512-pQuYaJDST7npkfl5YV5/QD4xERzo889OZmqRxPXeB+J7uHj2VbhWzSkd64FDbgL1L/RItxci0WCS3bP2QYjQAQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "9.1.2", + "lodash.camelcase": "4.3.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@mysten/sui": ">=2.0.0" + } + }, + "node_modules/@naviprotocol/lending/node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/@noble/curves": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", @@ -7215,6 +7241,12 @@ "license": "MIT", "optional": true }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", diff --git a/package.json b/package.json index 5c3dac7..6269f6f 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "dependencies": { "@7kprotocol/sdk-ts": "^3.4.1", "@cetusprotocol/aggregator-sdk": "^1.5.5", + "@naviprotocol/lending": "^2.0.3", "@pythnetwork/pyth-sui-js": "^2.2.0", "@types/bn.js": "^5.2.0", "decimal.js": "^10.5.0", diff --git a/src/core/flashRepay.ts b/src/core/flashRepay.ts index 7dc5de8..1e4f06d 100644 --- a/src/core/flashRepay.ts +++ b/src/core/flashRepay.ts @@ -6,7 +6,7 @@ import { flashloanPTB, repayFlashLoanPTB, getAllFlashLoanAssets, -} from "../vendor/naviFlashloan.js"; +} from "@naviprotocol/lending"; import { Decimal } from "decimal.js"; import { FlashRepayParams } from "./types.js"; import type { AlphalendClient } from "./client.js"; diff --git a/src/naviprotocol-lending.d.ts b/src/naviprotocol-lending.d.ts new file mode 100644 index 0000000..edb6588 --- /dev/null +++ b/src/naviprotocol-lending.d.ts @@ -0,0 +1,41 @@ +// NAVI v2 (@naviprotocol/lending) ships ESM `.d.ts` files with extensionless +// relative re-exports, which don't resolve under `moduleResolution: NodeNext` +// (the runtime bundle is a single self-contained file and works fine). Declare +// the flash-loan helpers we use so they type-resolve. Remove if NAVI fixes +// their packaging. +declare module "@naviprotocol/lending" { + type NaviTx = import("@mysten/sui/transactions").Transaction; + type NaviTxResult = import("@mysten/sui/transactions").TransactionResult; + + export function getAllFlashLoanAssets(options?: { + env?: "prod" | "dev"; + cacheTime?: number; + disableCache?: boolean; + market?: string; + }): Promise< + Array<{ + max: string; + min: string; + assetId: number; + poolId: string; + supplierFee: number; + flashloanFee: number; + coinType: string; + }> + >; + + export function flashloanPTB( + tx: NaviTx, + identifier: string, + amount: number | NaviTxResult, + options?: { env?: "prod" | "dev"; market?: string }, + ): Promise; + + export function repayFlashLoanPTB( + tx: NaviTx, + identifier: string, + receipt: NaviTxResult | string, + coinObject: NaviTxResult | string, + options?: { env?: "prod" | "dev"; market?: string }, + ): Promise; +} diff --git a/src/vendor/naviFlashloan.ts b/src/vendor/naviFlashloan.ts deleted file mode 100644 index c189677..0000000 --- a/src/vendor/naviFlashloan.ts +++ /dev/null @@ -1,424 +0,0 @@ -/** - * Vendored Navi flash-loan helpers. - * - * WHY THIS EXISTS - * --------------- - * `@naviprotocol/lending@1.4.6` (the latest release as of this writing) is not - * compatible with `@mysten/sui` v2. Two problems: - * 1. Its dist imports `SuiClient` / `getFullnodeUrl` from `@mysten/sui/client`, - * which v2 removed (they moved to `@mysten/sui/jsonRpc` as - * `SuiJsonRpcClient` / `getJsonRpcFullnodeUrl`). - * 2. It eagerly runs `new SuiClient({ url: getFullnodeUrl("mainnet") })` at - * module top-level, so merely importing it throws under v2. - * Because `@alphafi/alphalend-sdk`'s entry point re-exports the flash-repay path, - * importing the SDK *at all* would crash for any consumer on `@mysten/sui` v2. - * - * We previously worked around this with a `patch-package` patch, but that only - * applies in this repo's own dev/CI install — it is not shipped to npm consumers - * (patches aren't published, and patch-package can't reliably patch a hoisted - * transitive dep). So instead we vendor the three flash-loan helpers we use, - * faithfully ported from Navi's public source: - * https://github.com/naviprotocol/naviprotocol-monorepo/tree/main/packages/lending/src - * (flashloan.ts, config.ts, pool.ts, utils.ts, market.ts — vendored from v1.4.6). - * - * This module imports ONLY v2-native `@mysten/sui` symbols and does no eager - * client construction, so it is safe for external consumers with no patch. - * - * SCOPE: only the flash-loan path is vendored. `getPools` is intentionally - * simplified to return the raw API payload — the flash-loan helpers only read - * `suiCoinType` / `contract.pool` / `id` / `isDeprecated`, so Navi's BigNumber - * display-field decoration and e-mode handling are omitted on purpose. - * - * If/when Navi ships a `@mysten/sui` v2-clean release, delete this file and go - * back to importing from `@naviprotocol/lending`. - */ - -import type { Transaction } from "@mysten/sui/transactions"; -import { normalizeStructTag } from "@mysten/sui/utils"; - -// Telemetry value Navi's API expects in its `sdk=` query param. Kept at the -// version we vendored these helpers from so the API behaves identically. -const NAVI_SDK_VERSION = "1.4.6"; - -/** Configuration is cached for 5 minutes (matches Navi's DEFAULT_CACHE_TIME). */ -export const DEFAULT_CACHE_TIME = 1000 * 60 * 5; - -const DEFAULT_MARKET_IDENTITY = "main"; - -const MARKETS = { - main: { id: 0, key: "main", name: "Main Market" }, - ember: { id: 1, key: "ember", name: "Ember Market" }, - rwa: { id: 2, key: "rwa", name: "Matrixdock Market" }, - "sui-eco": { id: 3, key: "sui-eco", name: "Sui Eco Market" }, -} as const; - -type MarketConfig = { id: number; key: string; name: string }; -type MarketIdentity = number | string | MarketConfig; - -function getMarketConfig(marketIdentity: MarketIdentity): MarketConfig { - const configs = Object.values(MARKETS); - const config = configs.find((marketConfig) => { - if (typeof marketIdentity === "number") { - return marketConfig.id === marketIdentity; - } - if (typeof marketIdentity === "string") { - return marketConfig.key === marketIdentity; - } - return marketConfig.id === marketIdentity.id; - }); - if (!config) { - throw new Error(`Market not found`); - } - return config; -} - -// Navi's public read API requires no auth/headers. Navi additionally sends a -// `User-Agent` telemetry string in Node; we omit it (it's a forbidden header in -// browsers and not required by the API). -const requestHeaders: HeadersInit = {}; - -export interface EnvOption { - env: "prod" | "dev"; -} -export interface CacheOption { - cacheTime: number; - disableCache: boolean; -} -export interface MarketOption { - market: string; -} - -/** Flash loan asset configuration returned by Navi's flashloan API. */ -export interface FlashloanAsset { - max: string; - min: string; - assetId: number; - poolId: string; - supplierFee: number; - flashloanFee: number; - coinType: string; -} - -/** Subset of Navi's pool shape that the flash-loan path reads. */ -interface Pool { - suiCoinType: string; - id: number; - isDeprecated: boolean; - contract: { pool: string }; -} - -/** Subset of Navi's lending config that the flash-loan path reads. */ -interface LendingConfig { - package: string; - storage: string; - flashloanConfig: string; - version: number; -} - -/** A coin type, a pool id, or an already-resolved pool object. */ -type AssetIdentifier = string | number | Pool; - -// A transaction object argument (Result/NestedResult/Input/GasCoin). Navi types -// this loosely; the only thing that matters is "object => pass through, else -// wrap via the formatter". -type TxValue = unknown; - -// --------------------------------------------------------------------------- -// caching helpers (ported from Navi utils.ts) -// --------------------------------------------------------------------------- - -function argsKey(args: unknown[]): string { - const serialized: unknown[] = []; - args.forEach((option, index) => { - const isLast = index === args.length - 1; - if (typeof option === "object" && option !== null && isLast) { - const rest = { ...(option as Record) }; - delete rest.client; - delete rest.disableCache; - delete rest.cacheTime; - serialized.push(rest); - } else { - serialized.push(option); - } - }); - return JSON.stringify(serialized); -} - -/** Dedupe concurrent identical calls — returns the in-flight promise. */ -function withSingleton Promise>( - fn: T, -): T { - const promiseMap: Record | null> = {}; - return ((...args: never[]) => { - const key = argsKey(args); - const existing = promiseMap[key]; - if (existing) { - return existing; - } - const created = fn(...args).finally(() => { - delete promiseMap[key]; - }); - promiseMap[key] = created; - return created; - }) as T; -} - -/** Time-based memoization keyed on args, honoring `cacheTime`/`disableCache`. */ -function withCache Promise>(fn: T): T { - const cache: Record = {}; - return ((...args: never[]) => { - const options = args[args.length - 1] as Partial | undefined; - const key = argsKey(args); - const cacheData = cache[key]; - if (!options?.disableCache && typeof cacheData?.data !== "undefined") { - if ( - typeof options?.cacheTime === "undefined" || - options.cacheTime > Date.now() - cacheData.cacheAt - ) { - return Promise.resolve(cacheData.data); - } - } - return fn(...args).then((result) => { - cache[key] = { data: result, cacheAt: Date.now() }; - return result; - }); - }) as T; -} - -function normalizeCoinType(coinType: string): string { - return normalizeStructTag(coinType); -} - -function parsePoolUID( - uid: string, -): { marketKey: string; poolId: number } | null { - const [marketKey, poolId] = uid.split("-"); - if (!marketKey || !poolId) { - return null; - } - return { marketKey, poolId: parseInt(poolId) }; -} - -/** `object => pass through, else wrap with the given tx formatter`. */ -function parseTxValue(value: TxValue, format: (v: never) => unknown): unknown { - if (typeof value === "object") { - return value; - } - return format(value as never); -} - -// --------------------------------------------------------------------------- -// API readers (ported from config.ts / pool.ts / flashloan.ts) -// --------------------------------------------------------------------------- - -const getConfig = withCache( - withSingleton( - async ( - options?: Partial, - ): Promise => { - const market = getMarketConfig( - options?.market || DEFAULT_MARKET_IDENTITY, - ); - const url = `https://open-api.naviprotocol.io/api/navi/config?env=${ - options?.env || "prod" - }&sdk=${NAVI_SDK_VERSION}&market=${market.key}`; - const res = await fetch(url, { headers: requestHeaders }).then((r) => - r.json(), - ); - return res.data; - }, - ), -); - -const getPools = withCache( - withSingleton( - async ( - options?: Partial, - ): Promise => { - const markets = (options?.markets || [MARKETS.main.key]).map((identity) => - getMarketConfig(identity), - ); - const url = `https://open-api.naviprotocol.io/api/navi/pools?env=${ - options?.env || "prod" - }&sdk=${NAVI_SDK_VERSION}&market=${markets - .map((market) => market.key) - .join(",")}`; - const res: { data: Pool[] } = await fetch(url, { - headers: requestHeaders, - }).then((r) => r.json()); - // NOTE: Navi decorates each pool with BigNumber display fields here; the - // flash-loan path doesn't read them, so we return the raw payload. - return res.data; - }, - ), -); - -async function getPool( - identifier: AssetIdentifier, - options?: Partial, -): Promise { - let market = options?.market; - if (typeof identifier === "string") { - const parsedUID = parsePoolUID(identifier); - if (parsedUID) { - market = parsedUID.marketKey; - identifier = parsedUID.poolId; - } - } - const pools = await getPools({ - ...options, - markets: [market || DEFAULT_MARKET_IDENTITY], - cacheTime: DEFAULT_CACHE_TIME, - }); - - if (typeof identifier === "object") { - return identifier; - } - - const pool = pools.find((p) => { - if (typeof identifier === "string") { - return normalizeCoinType(p.suiCoinType) === normalizeCoinType(identifier); - } - if (typeof identifier === "number") { - return p.id === identifier; - } - return false; - }); - - if (!pool) { - throw new Error(`Pool not found`); - } - if (pool.isDeprecated) { - console.log( - `The lending pool for coinType ${pool.suiCoinType} is going to be deprecated.`, - ); - } - return pool; -} - -// --------------------------------------------------------------------------- -// public flash-loan helpers (ported from flashloan.ts) -// --------------------------------------------------------------------------- - -/** Get all available flash loan assets from the Navi API (cached). */ -export const getAllFlashLoanAssets = withCache( - withSingleton( - async ( - options?: Partial, - ): Promise => { - const url = `https://open-api.naviprotocol.io/api/navi/flashloan?env=${ - options?.env || "prod" - }&sdk=${NAVI_SDK_VERSION}&market=${ - options?.market || DEFAULT_MARKET_IDENTITY - }`; - const res = await fetch(url, { headers: requestHeaders }).then((r) => - r.json(), - ); - return Object.keys(res.data).map((coinType) => ({ - ...res.data[coinType], - coinType, - })); - }, - ), -); - -/** - * Add a Navi flash-loan borrow to the PTB. - * @returns `[balance, receipt]` — receipt is needed for {@link repayFlashLoanPTB}. - * @throws if the pool does not support flash loans. - */ -export async function flashloanPTB( - tx: Transaction, - identifier: AssetIdentifier, - amount: number | TxValue, - options?: Partial, -) { - const config = await getConfig({ - ...options, - cacheTime: DEFAULT_CACHE_TIME, - }); - const pool = await getPool(identifier, options); - - const flashLoanAssets = await getAllFlashLoanAssets({ - ...options, - cacheTime: DEFAULT_CACHE_TIME, - }); - - const isSupport = flashLoanAssets.some( - (asset) => - normalizeCoinType(asset.coinType) === normalizeCoinType(pool.suiCoinType), - ); - if (!isSupport) { - throw new Error("Pool does not support flashloan"); - } - - if (config.version === 1) { - const [balance, receipt] = tx.moveCall({ - target: `${config.package}::lending::flash_loan_with_ctx`, - arguments: [ - tx.object(config.flashloanConfig), - tx.object(pool.contract.pool), - parseTxValue(amount, tx.pure.u64) as never, - ], - typeArguments: [pool.suiCoinType], - }); - return [balance, receipt]; - } else { - const [balance, receipt] = tx.moveCall({ - target: `${config.package}::lending::flash_loan_with_ctx_v2`, - arguments: [ - tx.object(config.flashloanConfig), - tx.object(pool.contract.pool), - parseTxValue(amount, tx.pure.u64) as never, - tx.object("0x05"), - ], - typeArguments: [pool.suiCoinType], - }); - return [balance, receipt]; - } -} - -/** - * Add a Navi flash-loan repayment to the PTB. - * @returns `[balance]` left over after repayment. - * @throws if the pool does not support flash loans. - */ -export async function repayFlashLoanPTB( - tx: Transaction, - identifier: AssetIdentifier, - receipt: TxValue | string, - coinObject: TxValue, - options?: Partial, -) { - const config = await getConfig({ - ...options, - cacheTime: DEFAULT_CACHE_TIME, - }); - const pool = await getPool(identifier, options); - - const flashLoanAssets = await getAllFlashLoanAssets({ - ...options, - cacheTime: DEFAULT_CACHE_TIME, - }); - - const isSupport = flashLoanAssets.some( - (asset) => - normalizeCoinType(asset.coinType) === normalizeCoinType(pool.suiCoinType), - ); - if (!isSupport) { - throw new Error("Pool does not support flashloan"); - } - - const [balance] = tx.moveCall({ - target: `${config.package}::lending::flash_repay_with_ctx`, - arguments: [ - tx.object("0x06"), - tx.object(config.storage), - tx.object(pool.contract.pool), - parseTxValue(receipt, tx.object) as never, - parseTxValue(coinObject, tx.object) as never, - ], - typeArguments: [pool.suiCoinType], - }); - return [balance]; -} From 63c24ea3e45dbffbd24b09c05b46673daea87c19 Mon Sep 17 00:00:00 2001 From: felix11 Date: Wed, 22 Jul 2026 17:31:13 +0400 Subject: [PATCH 2/2] chore: pin @naviprotocol/lending to 2.0.3 to make version bumps deliberate --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a90c35..4ebe89a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@7kprotocol/sdk-ts": "^3.4.1", "@cetusprotocol/aggregator-sdk": "^1.5.5", - "@naviprotocol/lending": "^2.0.3", + "@naviprotocol/lending": "2.0.3", "@pythnetwork/pyth-sui-js": "^2.2.0", "@types/bn.js": "^5.2.0", "decimal.js": "^10.5.0", diff --git a/package.json b/package.json index 6269f6f..02da981 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "dependencies": { "@7kprotocol/sdk-ts": "^3.4.1", "@cetusprotocol/aggregator-sdk": "^1.5.5", - "@naviprotocol/lending": "^2.0.3", + "@naviprotocol/lending": "2.0.3", "@pythnetwork/pyth-sui-js": "^2.2.0", "@types/bn.js": "^5.2.0", "decimal.js": "^10.5.0",