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: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +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",
"@pythnetwork/pyth-sui-js": "^2.2.0",
"bn.js": "^5.2.2",
"decimal.js": "^10.6.0",
Expand Down
16 changes: 15 additions & 1 deletion src/models/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,6 +20,7 @@ export type BlockchainOptions = {
network: Network;
pythSuiClient?: SuiJsonRpcClient;
graphqlUrl?: string;
grpcUrl?: string;
};

export class Blockchain {
Expand All @@ -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;
Expand All @@ -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'),
});
}

/**
Expand Down
10 changes: 10 additions & 0 deletions src/naviprotocol-lending.d.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>,
): Promise<Array<{ assetCoinType: string; [key: string]: unknown }>>;
}
46 changes: 18 additions & 28 deletions src/strategies/lending.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -27,6 +28,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
Expand Down Expand Up @@ -272,34 +274,22 @@ export class LendingStrategy extends BaseStrategy<

private async getAvailableRewards(address: string): Promise<Record<string, any[]>> {
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<string, any[]> = {};
for (const reward of rewards) {
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);
}

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}`);
}
}
Expand All @@ -314,8 +304,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) {
Expand Down
66 changes: 25 additions & 41 deletions src/strategies/looping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,6 +24,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
Expand Down Expand Up @@ -289,34 +291,22 @@ export class LoopingStrategy extends BaseStrategy<

private async getAvailableRewards(address: string): Promise<Record<string, any[]>> {
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<string, any[]> = {};
for (const reward of rewards) {
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);
}

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}`);
}
}
Expand Down Expand Up @@ -499,7 +489,7 @@ export class LoopingStrategy extends BaseStrategy<
});
}
}
rewardCoinSet.add(reward.reward_coin_type);
rewardCoinSet.add(reward.rewardCoinType);
}
}
}
Expand Down Expand Up @@ -558,14 +548,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(
Expand All @@ -574,28 +562,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(
Expand All @@ -604,12 +588,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)],
);
}
}
Expand Down
Loading