From 8824d028314f5287fa61177860ff2cb8e2a09227 Mon Sep 17 00:00:00 2001 From: felix11 Date: Mon, 20 Jul 2026 20:09:45 +0400 Subject: [PATCH 1/4] feat: migrate Navi integration to @naviprotocol/lending v2 with gRPC client for rewards --- package-lock.json | 32 ++++++++++++++++++++++++++++ package.json | 5 +++-- src/models/blockchain.ts | 16 +++++++++++++- src/naviprotocol-lending.d.ts | 10 +++++++++ src/strategies/lending.ts | 39 ++++++++++++----------------------- src/strategies/looping.ts | 39 ++++++++++++----------------------- 6 files changed, 86 insertions(+), 55 deletions(-) create mode 100644 src/naviprotocol-lending.d.ts diff --git a/package-lock.json b/package-lock.json index 4544b3e..dd96e00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@alphafi/stsui-sdk": "^2.0.2", "@cetusprotocol/aggregator-sdk": "^1.5.5", "@cetusprotocol/common-sdk": "^1.3.7", + "@naviprotocol/lending": "^2.0.3", "@pythnetwork/pyth-sui-js": "^2.2.0", "bn.js": "^5.2.2", "decimal.js": "^10.6.0", @@ -2101,6 +2102,31 @@ "@scure/base": "^2.0.0" } }, + "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", @@ -6074,6 +6100,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 56f61ba..7d7c88b 100644 --- a/package.json +++ b/package.json @@ -45,12 +45,14 @@ "@alphafi/stsui-sdk": "^2.0.2", "@cetusprotocol/aggregator-sdk": "^1.5.5", "@cetusprotocol/common-sdk": "^1.3.7", + "@naviprotocol/lending": "^2.0.3", "@pythnetwork/pyth-sui-js": "^2.2.0", "bn.js": "^5.2.2", "decimal.js": "^10.6.0", "dotenv": "^17.2.3" }, "devDependencies": { + "@mysten/sui": "^2.17.0", "@types/bn.js": "^5.1.6", "@types/jest": "^29.5.14", "@types/node": "^22.13.10", @@ -64,8 +66,7 @@ "tsx": "^4.19.2", "typedoc": "^0.28.0", "typescript": "^5.8.2", - "typescript-eslint": "^8.14.0", - "@mysten/sui": "^2.17.0" + "typescript-eslint": "^8.14.0" }, "peerDependencies": { "@mysten/sui": "^2.17.0" diff --git a/src/models/blockchain.ts b/src/models/blockchain.ts index 8db12d5..f559dd9 100644 --- a/src/models/blockchain.ts +++ b/src/models/blockchain.ts @@ -4,11 +4,14 @@ * Reads and transaction simulation go through GraphQL (`gqlClient`), using * `gqlClient.core.simulateTransaction` for simulation (server-side build + gas * resolution). A JSON-RPC client (`pythSuiClient`) is retained only for the - * Pyth `SuiPythClient` integration, which still requires it. + * Pyth `SuiPythClient` integration, which still requires it. A gRPC (v2 Core) + * client (`suiGrpcClient`) is used where a Core-capable client is required + * (e.g. the Navi reward `devInspect`). */ import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'; import { SuiGraphQLClient } from '@mysten/sui/graphql'; +import { SuiGrpcClient } from '@mysten/sui/grpc'; import { graphql } from '@mysten/sui/graphql/schema'; import { Transaction, TransactionObjectArgument } from '@mysten/sui/transactions'; import { Network } from '@alphafi/alphalend-sdk'; @@ -17,6 +20,7 @@ export type BlockchainOptions = { network: Network; pythSuiClient?: SuiJsonRpcClient; graphqlUrl?: string; + grpcUrl?: string; }; export class Blockchain { @@ -25,6 +29,8 @@ export class Blockchain { gqlClient: SuiGraphQLClient; /** Retained only for the Pyth `SuiPythClient` integration (needs JSON-RPC). */ pythSuiClient: SuiJsonRpcClient; + /** gRPC (v2 Core) client for reads needing a Core-capable client (e.g. Navi reward `devInspect`). */ + suiGrpcClient: SuiGrpcClient; constructor(options: BlockchainOptions) { this.network = options.network; @@ -46,6 +52,14 @@ export class Blockchain { url: this.graphqlUrl, network: options.network === 'testnet' ? 'testnet' : 'mainnet', }); + this.suiGrpcClient = new SuiGrpcClient({ + network: options.network === 'testnet' ? 'testnet' : 'mainnet', + baseUrl: + options.grpcUrl ?? + (options.network === 'testnet' + ? 'https://fullnode.testnet.sui.io' + : 'https://fullnode.mainnet.sui.io'), + }); } /** diff --git a/src/naviprotocol-lending.d.ts b/src/naviprotocol-lending.d.ts new file mode 100644 index 0000000..f61cbab --- /dev/null +++ b/src/naviprotocol-lending.d.ts @@ -0,0 +1,10 @@ +// 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 one symbol we use so it type-resolves. Remove if NAVI fixes their packaging. +declare module '@naviprotocol/lending' { + export function getUserAvailableLendingRewards( + address: string, + options?: { client: unknown } & Record, + ): Promise>; +} diff --git a/src/strategies/lending.ts b/src/strategies/lending.ts index b77c215..dfe955e 100644 --- a/src/strategies/lending.ts +++ b/src/strategies/lending.ts @@ -27,6 +27,7 @@ import { WORMHOLE_STATE_ID, } from '../utils/constants.js'; import { SuiPriceServiceConnection, SuiPythClient } from '@pythnetwork/pyth-sui-js'; +import { getUserAvailableLendingRewards } from '@naviprotocol/lending'; /** * Lending Strategy for single-asset pools with lending protocol integration @@ -272,34 +273,20 @@ export class LendingStrategy extends BaseStrategy< private async getAvailableRewards(address: string): Promise> { try { - // Call the integration API - const apiUrl = this.context.apiBaseUrl; - const response = await fetch( - `${apiUrl}/navi-params/rewards?address=${encodeURIComponent(address)}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error( - errorData.error || - errorData.message || - `Failed to fetch rewards: ${response.status} ${response.statusText}`, - ); + const rewards = await getUserAvailableLendingRewards(address, { + client: this.context.blockchain.suiGrpcClient, + }); + // Group by asset coin type (same shape the integration API returned). + const rewardsByAsset: Record = {}; + for (const reward of rewards) { + const assetKey = reward.assetCoinType; + if (!assetKey) continue; + if (!rewardsByAsset[assetKey]) rewardsByAsset[assetKey] = []; + rewardsByAsset[assetKey].push(reward); } - - const data = await response.json(); - - // The API returns { address, rewards, timestamp } - // We just need the rewards object - return data.rewards || {}; + return rewardsByAsset; } catch (error: any) { - console.error('Error fetching Navi rewards from API:', error); + console.error('Error fetching Navi rewards:', error); throw new Error(`Failed to fetch Navi rewards: ${error.message}`); } } diff --git a/src/strategies/looping.ts b/src/strategies/looping.ts index 6d09354..1aa11e9 100644 --- a/src/strategies/looping.ts +++ b/src/strategies/looping.ts @@ -23,6 +23,7 @@ import { } from '../utils/constants.js'; import { stSuiExchangeRate, getConf as getStSuiConf } from '@alphafi/stsui-sdk'; import { SuiPriceServiceConnection, SuiPythClient } from '@pythnetwork/pyth-sui-js'; +import { getUserAvailableLendingRewards } from '@naviprotocol/lending'; /** * Looping Strategy for leveraged positions with automated compounding @@ -289,34 +290,20 @@ export class LoopingStrategy extends BaseStrategy< private async getAvailableRewards(address: string): Promise> { try { - // Call the integration API - const apiUrl = this.context.apiBaseUrl; - const response = await fetch( - `${apiUrl}/navi-params/rewards?address=${encodeURIComponent(address)}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error( - errorData.error || - errorData.message || - `Failed to fetch rewards: ${response.status} ${response.statusText}`, - ); + const rewards = await getUserAvailableLendingRewards(address, { + client: this.context.blockchain.suiGrpcClient, + }); + // Group by asset coin type (same shape the integration API returned). + const rewardsByAsset: Record = {}; + for (const reward of rewards) { + const assetKey = reward.assetCoinType; + if (!assetKey) continue; + if (!rewardsByAsset[assetKey]) rewardsByAsset[assetKey] = []; + rewardsByAsset[assetKey].push(reward); } - - const data = await response.json(); - - // The API returns { address, rewards, timestamp } - // We just need the rewards object - return data.rewards || {}; + return rewardsByAsset; } catch (error: any) { - console.error('Error fetching Navi rewards from API:', error); + console.error('Error fetching Navi rewards:', error); throw new Error(`Failed to fetch Navi rewards: ${error.message}`); } } From 7bb84a4b48f0ac768653c1260dd0a361d1fdfe6a Mon Sep 17 00:00:00 2001 From: felix11 Date: Wed, 22 Jul 2026 17:22:30 +0400 Subject: [PATCH 2/4] fix: normalize Navi v2 reward lookup keys so SUI rewards aren't dropped --- src/strategies/lending.ts | 5 +++-- src/strategies/looping.ts | 23 +++++++++-------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/strategies/lending.ts b/src/strategies/lending.ts index dfe955e..0b6f37c 100644 --- a/src/strategies/lending.ts +++ b/src/strategies/lending.ts @@ -14,6 +14,7 @@ import { PoolBalance, PoolData, SingleTvl } from '../models/types.js'; import { StrategyContext } from '../models/strategyContext.js'; import { DepositOptions, WithdrawOptions } from '../core/types.js'; import { Transaction, TransactionResult } from '@mysten/sui/transactions'; +import { normalizeStructTag } from '@mysten/sui/utils'; import { BUCKET_CONFIG, CLOCK_PACKAGE_ID, @@ -301,8 +302,8 @@ export class LendingStrategy extends BaseStrategy< await this.context.getCoinsBySymbols(['NAVX', 'SUI', 'vSUI', 'DEEP', 'USDC', 'wUSDC']); if (claimableRewards) { - for (const reward of claimableRewards[this.poolLabel.asset.type] - ? claimableRewards[this.poolLabel.asset.type] + for (const reward of claimableRewards[normalizeStructTag(this.poolLabel.asset.type)] + ? claimableRewards[normalizeStructTag(this.poolLabel.asset.type)] : []) { if (this.poolLabel.asset.name === 'wBTC') { if (reward.rewardCoinType === navxCoin.coinType) { diff --git a/src/strategies/looping.ts b/src/strategies/looping.ts index d778905..2a25fde 100644 --- a/src/strategies/looping.ts +++ b/src/strategies/looping.ts @@ -8,6 +8,7 @@ import { PoolBalance, PoolData, SingleTvl } from '../models/types.js'; import { StrategyContext } from '../models/strategyContext.js'; import { DepositOptions, WithdrawOptions } from '../core/types.js'; import { Transaction, TransactionResult } from '@mysten/sui/transactions'; +import { normalizeStructTag } from '@mysten/sui/utils'; import { ALPHALEND_LENDING_PROTOCOL_ID, CLOCK_PACKAGE_ID, @@ -545,14 +546,12 @@ export class LoopingStrategy extends BaseStrategy< await this.collectAndSwapRewardsTxb( tx, rewardCoinSet, - claimableRewards[this.poolLabel.supplyAsset.type], + claimableRewards[normalizeStructTag(this.poolLabel.supplyAsset.type)], ); await this.collectAndSwapRewardsTxb( tx, rewardCoinSet, - claimableRewards[ - '0000000000000000000000000000000000000000000000000000000000000002::sui::SUI' - ], + claimableRewards[normalizeStructTag('0x2::sui::SUI')], ); } else if (this.poolLabel.supplyAsset.name === 'HASUI') { const claimableRewards = await this.getAvailableRewards( @@ -561,28 +560,24 @@ export class LoopingStrategy extends BaseStrategy< await this.collectAndSwapRewardsTxb( tx, rewardCoinSet, - claimableRewards[this.poolLabel.supplyAsset.type], + claimableRewards[normalizeStructTag(this.poolLabel.supplyAsset.type)], ); await this.collectAndSwapRewardsTxb( tx, rewardCoinSet, - claimableRewards[ - '0000000000000000000000000000000000000000000000000000000000000002::sui::SUI' - ], + claimableRewards[normalizeStructTag('0x2::sui::SUI')], ); } else if (this.poolLabel.supplyAsset.name === 'stSUI') { const claimableRewards = await this.getAvailableRewards(''); await this.collectAndSwapRewardsTxb( tx, rewardCoinSet, - claimableRewards[this.poolLabel.supplyAsset.type], + claimableRewards[normalizeStructTag(this.poolLabel.supplyAsset.type)], ); await this.collectAndSwapRewardsTxb( tx, rewardCoinSet, - claimableRewards[ - '0000000000000000000000000000000000000000000000000000000000000002::sui::SUI' - ], + claimableRewards[normalizeStructTag('0x2::sui::SUI')], ); } else if (this.poolLabel.supplyAsset.name === 'USDC') { const claimableRewards = await this.getAvailableRewards( @@ -591,12 +586,12 @@ export class LoopingStrategy extends BaseStrategy< await this.collectAndSwapRewardsTxb( tx, rewardCoinSet, - claimableRewards[this.poolLabel.supplyAsset.type], + claimableRewards[normalizeStructTag(this.poolLabel.supplyAsset.type)], ); await this.collectAndSwapRewardsTxb( tx, rewardCoinSet, - claimableRewards[this.poolLabel.borrowAsset.type], + claimableRewards[normalizeStructTag(this.poolLabel.borrowAsset.type)], ); } } From e57926d42ffc084d52e4c3aa8a9e19f5a60b7518 Mon Sep 17 00:00:00 2001 From: felix11 Date: Wed, 22 Jul 2026 23:32:29 +0400 Subject: [PATCH 3/4] 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 25c7329..a17e02f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@alphafi/stsui-sdk": "^2.0.2", "@cetusprotocol/aggregator-sdk": "^1.5.5", "@cetusprotocol/common-sdk": "^1.3.7", - "@naviprotocol/lending": "^2.0.3", + "@naviprotocol/lending": "2.0.3", "@pythnetwork/pyth-sui-js": "^2.2.0", "bn.js": "^5.2.2", "decimal.js": "^10.6.0", diff --git a/package.json b/package.json index 166b6f9..834f17b 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@alphafi/stsui-sdk": "^2.0.2", "@cetusprotocol/aggregator-sdk": "^1.5.5", "@cetusprotocol/common-sdk": "^1.3.7", - "@naviprotocol/lending": "^2.0.3", + "@naviprotocol/lending": "2.0.3", "@pythnetwork/pyth-sui-js": "^2.2.0", "bn.js": "^5.2.2", "decimal.js": "^10.6.0", From 5a816b4795a1fbc874d49d6ebfce5560a8f8abef Mon Sep 17 00:00:00 2001 From: felix11 Date: Fri, 24 Jul 2026 01:58:15 +0400 Subject: [PATCH 4/4] fix: normalize Navi reward group keys and fix snake_case dedup field --- src/strategies/lending.ts | 6 ++++-- src/strategies/looping.ts | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/strategies/lending.ts b/src/strategies/lending.ts index 0b6f37c..f5ba32c 100644 --- a/src/strategies/lending.ts +++ b/src/strategies/lending.ts @@ -280,8 +280,10 @@ export class LendingStrategy extends BaseStrategy< // Group by asset coin type (same shape the integration API returned). const rewardsByAsset: Record = {}; for (const reward of rewards) { - const assetKey = reward.assetCoinType; - if (!assetKey) continue; + if (!reward.assetCoinType) continue; + // Normalize the group key so it matches the normalizeStructTag(...) lookups below; + // otherwise a NAVI format change silently drops rewards. + const assetKey = normalizeStructTag(reward.assetCoinType); if (!rewardsByAsset[assetKey]) rewardsByAsset[assetKey] = []; rewardsByAsset[assetKey].push(reward); } diff --git a/src/strategies/looping.ts b/src/strategies/looping.ts index 2a25fde..88bad0e 100644 --- a/src/strategies/looping.ts +++ b/src/strategies/looping.ts @@ -297,8 +297,10 @@ export class LoopingStrategy extends BaseStrategy< // Group by asset coin type (same shape the integration API returned). const rewardsByAsset: Record = {}; for (const reward of rewards) { - const assetKey = reward.assetCoinType; - if (!assetKey) continue; + if (!reward.assetCoinType) continue; + // Normalize the group key so it matches the normalizeStructTag(...) lookups below; + // otherwise a NAVI format change silently drops rewards. + const assetKey = normalizeStructTag(reward.assetCoinType); if (!rewardsByAsset[assetKey]) rewardsByAsset[assetKey] = []; rewardsByAsset[assetKey].push(reward); } @@ -487,7 +489,7 @@ export class LoopingStrategy extends BaseStrategy< }); } } - rewardCoinSet.add(reward.reward_coin_type); + rewardCoinSet.add(reward.rewardCoinType); } } }