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
9 changes: 4 additions & 5 deletions package-lock.json

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

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +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",
"@naviprotocol/lending": "2.0.7",
"bn.js": "^5.2.2",
"decimal.js": "^10.6.0",
"dotenv": "^17.2.3"
Expand Down
19 changes: 12 additions & 7 deletions scripts/testRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import {
StrategyContext,
} from '../src/index.js';
import { AlphaVaultPoolLabel } from '../src/strategies/alphaVault.js';
import { dryRunTransactionBlock, executeTransactionBlock, getExecStuff } from './utils.js';
import {
dryRunTransactionBlock,
executeTransactionBlock,
getExecStuff,
getSuiClient,
} from './utils.js';
import fs from 'fs';

// async function test() {
Expand Down Expand Up @@ -241,15 +246,15 @@ async function withdraw() {
network: 'mainnet',
});
const tx = await sdk.withdraw({
poolId: '0xccda433a3324dc743478c7f79cce584628f6303501281167a3f4b386187c8c63',
withdrawMax: false,
poolId: '0x17743a10e89b108fd7c048e7737ce09082e3ef91f416ee93c2566c5dd3f438db',
withdrawMax: true,
amount: '5_000_00',
address,
// coinType: '0xd1b72982e40348d069bb1ff701e634c117bb5f741f44dff91e472d3b01461e55::stsui::STSUI',
});
tx.setGasBudget(2e8);
// dryRunTransactionBlock(tx);
executeTransactionBlock(tx);
dryRunTransactionBlock(tx);
// executeTransactionBlock(tx);
}
async function claimSlushWithdraw() {
const { address, keypair, suiClient } = getExecStuff();
Expand Down Expand Up @@ -361,9 +366,9 @@ async function updatePool() {
dryRunTransactionBlock(tx, address);
// executeTransactionBlock(tx);
}
updatePool();
// updatePool();
// claimAirdrop();
// withdraw();
withdraw();
// poolsData();
// portfolioData();
// claimSlushWithdraw();
Expand Down
72 changes: 40 additions & 32 deletions scripts/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { fromBase64 } from '@mysten/bcs';
import { SuiJsonRpcClient as SuiClient } from '@mysten/sui/jsonRpc';
import { SuiGraphQLClient } from '@mysten/sui/graphql';

import dotenv from 'dotenv';
import { Transaction } from '@mysten/sui/transactions';

dotenv.config();

/**
* @deprecated Public fullnodes no longer serve JSON-RPC, so this client fails on any call.
* Use {@link getGqlClient} instead; it backs the simulate/execute helpers below.
*/
export function getSuiClient(network: string) {
const mainnetUrl = 'https://fullnode.mainnet.sui.io/';
const testnetUrl = 'https://fullnode.testnet.sui.io/';
Expand All @@ -24,6 +29,16 @@ export function getSuiClient(network: string) {
});
}

/** GraphQL client — builds, simulates and executes server-side. */
export function getGqlClient(network: string = process.env.NETWORK ?? 'mainnet') {
return new SuiGraphQLClient({
url:
network === 'testnet'
? 'https://graphql.testnet.sui.io/graphql'
: 'https://graphql.mainnet.sui.io/graphql',
});
}

export function getExecStuff() {
if (!process.env.PK_B64) {
throw new Error('env var PK_B64 not configured');
Expand All @@ -48,45 +63,38 @@ export function getExecStuff() {
}

export async function dryRunTransactionBlock(txb: Transaction, add?: string) {
const { suiClient, address } = getExecStuff();
const { address } = getExecStuff();
txb.setSender(add ?? address);

add ? txb.setSender(add) : txb.setSender(address);
// txb.setGasBudget(1e9);
try {
const serializedTxb = await txb.build({ client: suiClient });
suiClient
.dryRunTransactionBlock({
transactionBlock: serializedTxb,
})
.then((res) => {
// console.log(JSON.stringify(res, null, 2));
console.log(res.effects.status, res.balanceChanges, res.events.length);
})
.catch((error) => {
console.error(error);
});
const gql = getGqlClient();
// Server-side build + gas resolution, so no separate txb.build() is needed.
const res = await gql.core.simulateTransaction({
transaction: txb,
include: { effects: true, balanceChanges: true, events: true },
});
const txData = res.$kind === 'Transaction' ? res.Transaction : res.FailedTransaction;
console.log(txData?.effects?.status, txData?.balanceChanges, txData?.events?.length ?? 0);
} catch (e) {
console.log(e);
}
}

export async function executeTransactionBlock(txb: Transaction) {
const { keypair, suiClient } = getExecStuff();
const { keypair, address } = getExecStuff();
txb.setSenderIfNotSet(address);

await suiClient
.signAndExecuteTransaction({
signer: keypair,
transaction: txb,
requestType: 'WaitForLocalExecution',
options: {
showEffects: true,
showBalanceChanges: true,
showObjectChanges: true,
},
})
.then((res) => {
console.log(JSON.stringify(res, null, 2));
})
.catch((error) => {
console.error(error);
try {
const gql = getGqlClient();
const bytes = await txb.build({ client: gql });
const { signature } = await keypair.signTransaction(bytes);
const res = await gql.core.executeTransaction({
transaction: bytes,
signatures: [signature],
include: { effects: true },
});
console.log(JSON.stringify(res, null, 2));
} catch (error) {
console.error(error);
}
}
10 changes: 5 additions & 5 deletions src/models/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
*
* 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. A gRPC (v2 Core)
* client (`suiGrpcClient`) is used where a Core-capable client is required
* (e.g. the Navi reward `devInspect`).
* resolution). A JSON-RPC client (`pythSuiClient`) is retained for the admin
* helpers that still need JSON-RPC object reads. 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';
Expand All @@ -27,7 +27,7 @@ export class Blockchain {
network: Network;
graphqlUrl: string;
gqlClient: SuiGraphQLClient;
/** Retained only for the Pyth `SuiPythClient` integration (needs JSON-RPC). */
/** Retained for admin helpers that still need JSON-RPC object reads. */
pythSuiClient: SuiJsonRpcClient;
/** gRPC (v2 Core) client for reads needing a Core-capable client (e.g. Navi reward `devInspect`). */
suiGrpcClient: SuiGrpcClient;
Expand Down
10 changes: 0 additions & 10 deletions src/naviprotocol-lending.d.ts

This file was deleted.

89 changes: 52 additions & 37 deletions src/strategies/lending.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ import {
GLOBAL_CONFIGS,
NAVI_CONFIG,
POOLS,
PYTH_STATE_ID,
SUI_SYSTEM_STATE,
VERSIONS,
WORMHOLE_STATE_ID,
} from '../utils/constants.js';
import { SuiPriceServiceConnection, SuiPythClient } from '@pythnetwork/pyth-sui-js';
import { getUserAvailableLendingRewards } from '@naviprotocol/lending';
import {
getConfig,
getPriceFeeds,
getUserAvailableLendingRewards,
updateOraclePricesPTB,
updatePythPriceFeeds,
} from '@naviprotocol/lending';

/**
* Lending Strategy for single-asset pools with lending protocol integration
Expand Down Expand Up @@ -295,11 +298,17 @@ export class LendingStrategy extends BaseStrategy<
}

private async collectAndClaimRewards(tx: Transaction) {
const claimableRewards = await this.getAvailableRewards(
const naviAccount =
NAVI_CONFIG.ACCOUNT_ADDRESSES[
this.poolLabel.asset.name as keyof typeof NAVI_CONFIG.ACCOUNT_ADDRESSES
],
);
];
if (!naviAccount) {
// Otherwise this reaches NAVI's SDK as tx.pure.address(undefined) and fails as a BCS error.
throw new Error(
`NAVI_CONFIG.ACCOUNT_ADDRESSES has no entry for asset '${this.poolLabel.asset.name}'`,
);
}
const claimableRewards = await this.getAvailableRewards(naviAccount);
const [navxCoin, suiCoin, vsuiCoin, deepCoin, usdcCoin, wusdcCoin] =
await this.context.getCoinsBySymbols(['NAVX', 'SUI', 'vSUI', 'DEEP', 'USDC', 'wUSDC']);

Expand Down Expand Up @@ -793,7 +802,6 @@ export class LendingStrategy extends BaseStrategy<
} else if (this.poolLabel.asset.name === 'wBTC') {
await this.updateSingleTokenPrice(
tx,
NAVI_CONFIG.PRICE_FEED[this.poolLabel.asset.name].pythPriceInfo,
NAVI_CONFIG.PRICE_FEED[this.poolLabel.asset.name].feedId,
);

Expand Down Expand Up @@ -828,8 +836,6 @@ export class LendingStrategy extends BaseStrategy<
} else {
await this.updateSingleTokenPrice(
tx,
NAVI_CONFIG.PRICE_FEED[this.poolLabel.asset.name as keyof typeof NAVI_CONFIG.PRICE_FEED]
.pythPriceInfo,
NAVI_CONFIG.PRICE_FEED[this.poolLabel.asset.name as keyof typeof NAVI_CONFIG.PRICE_FEED]
.feedId,
);
Expand Down Expand Up @@ -864,33 +870,43 @@ export class LendingStrategy extends BaseStrategy<
}
}

private async updateSingleTokenPrice(tx: Transaction, pythPriceInfo: string, feedId: string) {
const pythClient = new SuiPythClient(
this.context.blockchain.pythSuiClient,
PYTH_STATE_ID,
WORMHOLE_STATE_ID,
);
const pythConnection = new SuiPriceServiceConnection('https://hermes.pyth.network');

const priceFeedUpdateData = await pythConnection.getPriceFeedsUpdateData([pythPriceInfo]);
const priceInfoObjectIds = await pythClient.updatePriceFeeds(tx, priceFeedUpdateData, [
pythPriceInfo,
]);

/**
* Overrides this tx's NAVI `lending_core` linkage by calling the latest package directly.
*
* Our pool packages reach `lending_core` through linkage tables frozen at publish time. Sui
* resolves one version per original package per tx, so this direct call pulls those transitive
* calls forward. The package comes from NAVI's config — the same value their own SDK helpers
* use — so it tracks their upgrades and can't drift or collide. `version_verification` asserts
* `storage.version == constants::version()`, so a wrong version fails loudly.
*/
private async overrideNaviLendingCoreLinkage(tx: Transaction) {
const { package: lendingCore } = await getConfig();
tx.moveCall({
target: `${NAVI_CONFIG.ORACLE_PRO_PACKAGE_ID}::oracle_pro::update_single_price_v2`,
arguments: [
tx.object(CLOCK_PACKAGE_ID),
tx.object(NAVI_CONFIG.ORACLE_CONFIG),
tx.object(NAVI_CONFIG.PRICE_ORACLE_ID),
tx.object(NAVI_CONFIG.SUPRA_ORACLE_HOLDER),
tx.object(priceInfoObjectIds[0]),
tx.object(NAVI_CONFIG.NAVI_AGGREGATOR),
tx.pure.address(feedId),
],
target: `${lendingCore}::storage::version_verification`,
arguments: [tx.object(NAVI_CONFIG.NAVI_STORAGE_ID)],
});
}

private async updateSingleTokenPrice(tx: Transaction, feedId: string) {
const feed = (await getPriceFeeds()).find((f) => f.feedId === feedId);
if (!feed) {
throw new Error(`NAVI oracle config has no price feed for feedId ${feedId}`);
}
// NAVI treats both as optional; a supra/switchboard-only feed would otherwise surface as
// an opaque Hermes error or `tx.object('')` inside updateOraclePricesPTB.
if (!feed.pythPriceFeedId || !feed.pythPriceInfoObject) {
throw new Error(`NAVI price feed ${feedId} has no Pyth feed configured`);
}
await this.overrideNaviLendingCoreLinkage(tx);
// Pass our client so the SuiPythClient object reads use the configured endpoint rather
// than NAVI's module-level default (the public mainnet fullnode).
const naviOptions = { client: this.context.blockchain.suiGrpcClient };
// Post unconditionally: updateOraclePricesPTB's own flag gates on a stale check that
// misparses the on-chain price struct, so it never posts.
await updatePythPriceFeeds(tx, [feed.pythPriceFeedId], naviOptions);
await updateOraclePricesPTB(tx, [feed], naviOptions);
}

async withdraw(tx: Transaction, options: WithdrawOptions) {
if (this.receiptObjects.length === 0) {
throw new Error('No receipt found');
Expand Down Expand Up @@ -977,15 +993,16 @@ export class LendingStrategy extends BaseStrategy<
);
this.context.blockchain.sendCoinToAddressBalance(
tx,
ALPHA_COIN_TYPE,
// ALPHA_COIN_TYPE is stored bare to match on-chain `type_name::get` VecMap keys;
// a type argument needs the 0x prefix or the tx fails to parse.
`0x${ALPHA_COIN_TYPE}`,
options.address,
alphaCoin,
);
}
} else if (this.poolLabel.asset.name === 'wBTC') {
await this.updateSingleTokenPrice(
tx,
NAVI_CONFIG.PRICE_FEED[this.poolLabel.asset.name].pythPriceInfo,
NAVI_CONFIG.PRICE_FEED[this.poolLabel.asset.name].feedId,
);

Expand Down Expand Up @@ -1022,8 +1039,6 @@ export class LendingStrategy extends BaseStrategy<
} else {
await this.updateSingleTokenPrice(
tx,
NAVI_CONFIG.PRICE_FEED[this.poolLabel.asset.name as keyof typeof NAVI_CONFIG.PRICE_FEED]
.pythPriceInfo,
NAVI_CONFIG.PRICE_FEED[this.poolLabel.asset.name as keyof typeof NAVI_CONFIG.PRICE_FEED]
.feedId,
);
Expand Down
Loading
Loading