From a7efcb47f19285f5561646edf5f13deb68ec4664 Mon Sep 17 00:00:00 2001 From: felix11 Date: Mon, 3 Aug 2026 20:57:59 +0400 Subject: [PATCH 1/3] fix(coinHelpers): cap merges at 180 objects and count every coin type --- src/core/coinHelpers.ts | 321 ++++++++++++++++++++++++---------------- 1 file changed, 193 insertions(+), 128 deletions(-) diff --git a/src/core/coinHelpers.ts b/src/core/coinHelpers.ts index e61da30..3d89eda 100644 --- a/src/core/coinHelpers.ts +++ b/src/core/coinHelpers.ts @@ -11,25 +11,42 @@ import { TransactionObjectArgument, } from "@mysten/sui/transactions"; import { graphql } from "@mysten/sui/graphql/schema"; +import { SuiGrpcClient } from "@mysten/sui/grpc"; +import type { SuiClientTypes } from "@mysten/sui/client"; import { normalizeStructTag, SUI_TYPE_ARG } from "@mysten/sui/utils"; import { Network } from "../constants/index.js"; import { Blockchain } from "../models/blockchain.js"; +/** Default gRPC endpoints, overridable per call via `grpcUrl`. */ +const GRPC_URL: Record = { + mainnet: "https://fullnode.mainnet.sui.io:443", + testnet: "https://fullnode.testnet.sui.io:443", + devnet: "https://fullnode.devnet.sui.io:443", +}; + export type MergeCoinsOutput = "address-balance" | "coin-object"; export interface CoinTypeCount { coinType: string; + /** Capped at {@link MAX_COINS_PER_TX}; see `hasMoreCoinObjects`. */ coinObjectCount: number; + /** True when the address holds more objects of this type than were counted. */ + hasMoreCoinObjects: boolean; } /** - * Max coin objects consolidated per transaction. Keeps a single `mergeCoins` - * command under the 512-arguments-per-command protocol limit and `send_funds` - * command counts under the 1024-commands limit. Callers with more coin - * objects re-run the merge until one remains. + * Max coin objects consolidated per transaction, and the cap on every per-type + * coin-object read; callers re-run until one remains. Bound by transaction + * size, not the 512-argument command limit: each coin is an owned-object input + * costing ~79 serialized bytes, against a 16384 limit on the gasless + * transaction. 180 measures ~14.3KB, leaving headroom for the gas coin and + * address-balance withdrawal the SUI paths add on top. */ -const MAX_COINS_PER_TX = 500; +const MAX_COINS_PER_TX = 180; + +/** Kept low: the public fullnode rate-limits bursts. See {@link withRetry}. */ +const COUNT_CONCURRENCY = 5; /** * Min SUI address balance (in MIST) for paying gas from the address balance @@ -40,70 +57,67 @@ const MIN_ADDRESS_BALANCE_FOR_GAS = 100_000_000n; interface CoinObjectRef { objectId: string; - version: number; + version: string; digest: string; coinType: string; balance?: bigint; } /** - * List every coin type held as coin objects by `address`, with the number of - * `Coin` objects per type. Coins held purely in the address balance - * (accumulator, zero coin objects) do not appear. + * Number of `Coin` objects per coin type held by `address`. Coins held + * purely in the address balance (accumulator) do not appear. + * + * Each type is counted separately, capped at {@link MAX_COINS_PER_TX}. Listing + * the address's objects and grouping by type instead cannot work: objects are + * not listed grouped by type, so an address holding 100K objects of one coin + * fills the whole listing and every other type is missed. */ export async function getCoinObjectCounts( address: string, network: Network, + grpcUrl?: string, ): Promise { - const blockchain = new Blockchain(network); - const coins = await getCoinObjects(blockchain, address); + const client = grpcClient(network, grpcUrl); + const coinTypes = await listCoinTypes(client, address); - const counts = new Map(); - for (const coin of coins) { - counts.set(coin.coinType, (counts.get(coin.coinType) ?? 0) + 1); - } + const counts = await mapWithConcurrency( + coinTypes, + COUNT_CONCURRENCY, + async (coinType): Promise => ({ + coinType, + ...(await countCoinObjectsOfType(client, address, coinType)), + }), + ); - return [...counts.entries()] - .map(([coinType, coinObjectCount]) => ({ coinType, coinObjectCount })) + return counts + .filter((c) => c.coinObjectCount > 0) .sort((a, b) => b.coinObjectCount - a.coinObjectCount); } /** - * Build a transaction that consolidates all `Coin` objects owned by - * `address` into a single coin object or into the address balance - * (accumulator). - * - * With `coin-object` output any existing address balance is also withdrawn - * and merged in, so the full balance ends up in one coin object. With - * `address-balance` output the coin objects are sent to the address balance - * via `0x2::coin::send_funds`. + * Build a transaction consolidating `address`'s `Coin` objects into a + * single coin object or into the address balance (accumulator). With + * `coin-object` output any existing address balance is withdrawn and merged in + * too. Handles at most {@link MAX_COINS_PER_TX} objects — re-run until one + * remains. * - * The sender (and gas payer) is `address` itself. Gas payment must be set - * explicitly for SUI because the automatic build-time gas resolution can only - * pick SUI coins that are not inputs of the transaction, and here every SUI - * coin is one. The largest SUI coin is reserved as the gas coin (it alone - * must cover the gas budget): with `coin-object` output everything merges - * into it, and with `address-balance` output it stays as a coin object. - * Exception: with `address-balance` output, when the pre-existing SUI address - * balance already exceeds {@link MIN_ADDRESS_BALANCE_FOR_GAS} no gas coin is - * reserved — gas resolves against the address balance and every coin is sent, - * leaving none behind. (Funds deposited by this same transaction cannot pay - * its gas, so a first run that leaves the gas coin can be re-run once the - * address balance is funded.) + * Gas payment is set explicitly for SUI: build-time gas resolution can only + * pick SUI coins that are not transaction inputs, and here every SUI coin is + * one. The largest SUI coin is reserved as gas (it alone must cover the + * budget), except with `address-balance` output when the address balance + * already exceeds {@link MIN_ADDRESS_BALANCE_FOR_GAS} — then gas resolves + * against it and every coin object is sent, leaving none behind. * - * Consolidates at most {@link MAX_COINS_PER_TX} coin objects per transaction - * — re-run until one coin object remains. - * - * The address balance is read at build time and withdrawn as an exact amount - * (the protocol does not support entire-balance withdrawals yet), so - * concurrent accumulator activity between build and execution fails the - * transaction with "Invalid withdraw reservation" — rebuild and retry. + * The address balance is withdrawn as an exact amount read at build time (the + * protocol has no entire-balance withdrawal), so concurrent accumulator + * activity fails the transaction with "Invalid withdraw reservation". */ export async function buildMergeCoinsTransaction( coinType: string, output: MergeCoinsOutput, address: string, network: Network, + grpcUrl?: string, ): Promise { const blockchain = new Blockchain(network); const normalizedCoinType = normalizeStructTag(coinType); @@ -120,13 +134,10 @@ export async function buildMergeCoinsTransaction( output === "address-balance" && addressBalance >= MIN_ADDRESS_BALANCE_FOR_GAS; - // Balances are only needed to pick a gas coin that can cover the budget - const withBalance = isSui && !useAddressBalanceGas; - const coins = await getCoinObjects( - blockchain, + const coins = await getCoinObjectsOfType( + grpcClient(network, grpcUrl), address, normalizedCoinType, - withBalance, ); const tx = new Transaction(); @@ -219,9 +230,9 @@ export async function buildMergeCoinsTransaction( } /** - * `send_funds` each coin into `address`'s address balance, sharing a single - * recipient input across all calls (each `tx.pure` call would otherwise add - * a duplicate input). + * Merge `coins` into the first, then deposit it with one `send_funds` call. + * Two commands regardless of coin count — per-coin `send_funds` calls exceed + * the transaction size limit well before {@link MAX_COINS_PER_TX} coins. */ function sendCoinsToAddressBalance( tx: Transaction, @@ -229,14 +240,19 @@ function sendCoinsToAddressBalance( address: string, coins: CoinObjectRef[], ) { - const recipient = tx.pure.address(address); - for (const coin of coins) { - tx.moveCall({ - target: "0x2::coin::send_funds", - typeArguments: [coinType], - arguments: [tx.object(coin.objectId), recipient], - }); + const [target, ...rest] = coins; + const targetArg = tx.object(target.objectId); + if (rest.length > 0) { + tx.mergeCoins( + targetArg, + rest.map((c) => tx.object(c.objectId)), + ); } + tx.moveCall({ + target: "0x2::coin::send_funds", + typeArguments: [coinType], + arguments: [targetArg, tx.pure.address(address)], + }); } /** @@ -282,84 +298,133 @@ async function getAddressBalance( return BigInt(response.data?.address?.balance?.addressBalance ?? 0); } +/** gRPC-web over fetch; works in browsers and Node. */ +function grpcClient(network: Network, grpcUrl?: string): SuiGrpcClient { + return new SuiGrpcClient({ + network, + baseUrl: grpcUrl ?? GRPC_URL[network], + }); +} + +/** Retry a rate-limited fullnode call; counting every coin type is a burst. */ +async function withRetry(fn: () => Promise): Promise { + const delaysMs = [500, 1500, 4000]; + for (let attempt = 0; ; attempt += 1) { + try { + return await fn(); + } catch (error) { + const code = (error as { code?: string }).code; + const retryable = code === "RESOURCE_EXHAUSTED" || code === "UNAVAILABLE"; + if (!retryable || attempt >= delaysMs.length) throw error; + await new Promise((resolve) => setTimeout(resolve, delaysMs[attempt])); + } + } +} + /** - * Paginated fetch of the `Coin` objects owned by `owner` (all coin - * objects when `coinType` is omitted), with the object refs needed for gas - * payment. Coin balances are only fetched when `withBalance` is set (used to - * pick a SUI gas coin). + * Page through one coin type, up to {@link MAX_COINS_PER_TX} objects. Returns + * whether more objects of the type exist beyond those visited. */ -async function getCoinObjects( - blockchain: Blockchain, +async function eachCoinPage( + client: SuiGrpcClient, owner: string, - coinType?: string, - withBalance = false, -): Promise { - const query = graphql(` - query getCoinObjectsOfType( - $owner: SuiAddress! - $type: String! - $cursor: String - $withBalance: Boolean = false - ) { - address(address: $owner) { - objects(filter: { type: $type }, after: $cursor) { - pageInfo { - hasNextPage - endCursor - } - nodes { - address - version - digest - contents { - type { - repr - } - json @include(if: $withBalance) - } - } - } - } - } - `); + coinType: string, + onPage: (coins: SuiClientTypes.Coin[]) => void, +): Promise { + let visited = 0; + let cursor: string | undefined; + for (;;) { + const page = await withRetry(() => + client.core.listCoins({ + owner, + coinType, + cursor, + limit: MAX_COINS_PER_TX - visited, + }), + ); + onPage(page.objects); + visited += page.objects.length; + if (visited >= MAX_COINS_PER_TX) return page.hasNextPage; + if (!page.hasNextPage || !page.cursor) return false; + cursor = page.cursor; + } +} +/** + * The `Coin` objects owned by `owner` — refs for gas payment, and + * balances for picking a SUI gas coin. Capped at {@link MAX_COINS_PER_TX}, + * exactly what the next merge consolidates, so 100K-object addresses still + * load in bounded time. + */ +async function getCoinObjectsOfType( + client: SuiGrpcClient, + owner: string, + coinType: string, +): Promise { const out: CoinObjectRef[] = []; - let cursor: string | null = null; - let hasMore = true; - while (hasMore) { - const variables: { - owner: string; - type: string; - cursor: string | null; - withBalance: boolean; - } = { - owner, - type: coinType ? `0x2::coin::Coin<${coinType}>` : "0x2::coin::Coin", - cursor, - withBalance, - }; - const response = await blockchain.gqlClient.query({ query, variables }); - const conn = response.data?.address?.objects; - for (const node of conn?.nodes ?? []) { - const repr = node?.contents?.type?.repr; - if (!node?.address || node.version == null || !node.digest || !repr) { - continue; - } - const json = node.contents?.json as { balance?: string } | undefined; + await eachCoinPage(client, owner, coinType, (coins) => { + for (const coin of coins) { out.push({ - objectId: node.address, - version: node.version, - digest: node.digest, - // repr is `0x…2::coin::Coin`; extract the inner type T - coinType: repr.slice(repr.indexOf("<") + 1, -1), - ...(withBalance ? { balance: BigInt(json?.balance ?? 0) } : {}), + objectId: coin.objectId, + version: coin.version, + digest: coin.digest, + coinType, + balance: BigInt(coin.balance), }); } - if (conn?.pageInfo?.hasNextPage && conn.pageInfo.endCursor) { - cursor = conn.pageInfo.endCursor; - } else { - hasMore = false; + }); + return out; +} + +/** Number of `Coin` objects owned by `owner`, capped as above. */ +async function countCoinObjectsOfType( + client: SuiGrpcClient, + owner: string, + coinType: string, +): Promise<{ coinObjectCount: number; hasMoreCoinObjects: boolean }> { + let coinObjectCount = 0; + const hasMoreCoinObjects = await eachCoinPage( + client, + owner, + coinType, + (coins) => { + coinObjectCount += coins.length; + }, + ); + return { coinObjectCount, hasMoreCoinObjects }; +} + +/** + * Coin types `owner` holds as coin objects, from the node's coin index. Types + * held purely in the address balance have nothing to merge and are skipped. + */ +async function listCoinTypes( + client: SuiGrpcClient, + owner: string, +): Promise { + const coinTypes: string[] = []; + let cursor: string | null = null; + do { + const page = await withRetry(() => + client.core.listBalances({ owner, cursor }), + ); + for (const balance of page.balances) { + if (BigInt(balance.coinBalance) > 0n) coinTypes.push(balance.coinType); } + cursor = page.hasNextPage ? page.cursor : null; + } while (cursor); + return coinTypes; +} + +/** Run `fn` over `items`, at most `limit` at a time, preserving order. */ +async function mapWithConcurrency( + items: T[], + limit: number, + fn: (item: T) => Promise, +): Promise { + const out: R[] = []; + for (let i = 0; i < items.length; i += limit) { + out.push(...(await Promise.all(items.slice(i, i + limit).map(fn)))); } return out; } From 7a65a48f608bfa8f15b45975911842669cc1eff0 Mon Sep 17 00:00:00 2001 From: felix11 Date: Tue, 4 Aug 2026 00:35:20 +0400 Subject: [PATCH 2/3] refactor(scripts): migrate off deprecated JSON-RPC to gRPC and drop the Pyth SDK --- package-lock.json | 10 +- package.json | 1 - scripts/pythTest.ts | 52 ---------- scripts/testRun.ts | 177 ++++++++++++++++------------------- scripts/updatePriceScript.ts | 41 ++++---- scripts/utils.ts | 87 +++++++++-------- src/core/client.ts | 41 +------- src/utils/oracle.ts | 50 ---------- 8 files changed, 160 insertions(+), 299 deletions(-) delete mode 100644 scripts/pythTest.ts diff --git a/package-lock.json b/package-lock.json index 401e045..47db30b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@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", "dotenv": "^17.2.2", @@ -1801,6 +1800,7 @@ "resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sui-js/-/pyth-sui-js-2.4.0.tgz", "integrity": "sha512-FvHlvU/fGcfAoHazU3UxZQBUMy90qER8SbcXcr0u2g+kwlq7qn4doadESZ7g1sgFfFwZgInLjRFIu7ZT4413zA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@mysten/sui": "^1.3.0", "@pythnetwork/hermes-client": "2.1.0", @@ -1815,6 +1815,7 @@ "resolved": "https://registry.npmjs.org/@pythnetwork/hermes-client/-/hermes-client-2.1.0.tgz", "integrity": "sha512-XOtP5dvHfKNl+uvFzXCMI9OL7VdJ8eXsv5ahy6ZB3ArZ+UMM4U4OrPYQPwLvJwlpkBXpEqsJKlMawcoyCB+yMA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@zodios/core": "^10.9.6", "eventsource": "^3.0.5", @@ -3082,7 +3083,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/baseline-browser-mapping": { "version": "2.10.33", @@ -3238,6 +3240,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -5101,7 +5104,8 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/ignore": { "version": "5.3.2", diff --git a/package.json b/package.json index 9cfd1a2..ebe87b9 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "@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", "dotenv": "^17.2.2", diff --git a/scripts/pythTest.ts b/scripts/pythTest.ts deleted file mode 100644 index 79ce108..0000000 --- a/scripts/pythTest.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - SuiPriceServiceConnection, - SuiPythClient, -} from "@pythnetwork/pyth-sui-js"; -import { getConstants } from "../src/constants"; -import { getExecStuff } from "./testRun"; -import { Transaction } from "@mysten/sui/transactions"; - -async function run() { - const { suiClient, keypair } = getExecStuff(); - const constants = getConstants("mainnet"); - const pythClient = new SuiPythClient( - suiClient, - constants.PYTH_STATE_ID, - constants.WORMHOLE_STATE_ID, - ); - const pythConnection = new SuiPriceServiceConnection( - "https://hermes.pyth.network", - ); - - const tx = new Transaction(); - const priceIDs = [ - "0a03c915d98ab4084795d283e20f08d7130acd826bca180754b120bfc202f2fb", - ]; - const priceFeedUpdateData = - await pythConnection.getPriceFeedsUpdateData(priceIDs); - - await pythClient.updatePriceFeeds(tx, priceFeedUpdateData, priceIDs); - - await suiClient - .signAndExecuteTransaction({ - signer: keypair, - transaction: tx, - requestType: "WaitForLocalExecution", - options: { - showEffects: true, - showBalanceChanges: true, - showObjectChanges: true, - }, - }) - .then((res) => { - console.log(JSON.stringify(res, null, 2)); - }) - .catch((error) => { - console.error(error); - }); -} - -run().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/scripts/testRun.ts b/scripts/testRun.ts index f606dbd..418eb8d 100644 --- a/scripts/testRun.ts +++ b/scripts/testRun.ts @@ -5,29 +5,25 @@ import { getConstants } from "../src/constants/index.js"; import { AlphalendClient } from "../src/core/client.js"; import * as dotenv from "dotenv"; import { setPrices } from "../src/utils/helper.js"; -import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc"; -import { - SuiPriceServiceConnection, - SuiPythClient, -} from "@pythnetwork/pyth-sui-js"; +import { SuiGrpcClient } from "@mysten/sui/grpc"; dotenv.config(); export function getSuiClient(network?: string) { - const mainnetUrl = "https://fullnode.mainnet.sui.io/"; - const testnetUrl = "https://fullnode.testnet.sui.io/"; - const devnetUrl = "https://fullnode.devnet.sui.io/"; + const mainnetUrl = "https://fullnode.mainnet.sui.io:443"; + const testnetUrl = "https://fullnode.testnet.sui.io:443"; + const devnetUrl = "https://fullnode.devnet.sui.io:443"; - let rpcUrl = devnetUrl; + let grpcUrl = devnetUrl; if (network === "mainnet") { - rpcUrl = mainnetUrl; + grpcUrl = mainnetUrl; } else if (network === "testnet") { - rpcUrl = testnetUrl; + grpcUrl = testnetUrl; } - return new SuiJsonRpcClient({ - url: rpcUrl, + return new SuiGrpcClient({ network: network ?? "mainnet", + baseUrl: process.env.SUI_GRPC_URL ?? grpcUrl, }); } @@ -59,20 +55,17 @@ export async function dryRunTransactionBlock( ) { const { suiClient } = getExecStuff(); txb.setSender(address); - txb.setGasBudget(1e9); + txb.setGasBudget(1e8); try { - const serializedTxb = await txb.build({ client: suiClient }); - await suiClient - .dryRunTransactionBlock({ - transactionBlock: serializedTxb, - }) - .then((res) => { - console.log(JSON.stringify(res, null, 2)); - // console.log(res.effects.status, res.balanceChanges); - }) - .catch((error) => { - console.error(error); - }); + const res = await suiClient.simulateTransaction({ + transaction: txb, + include: { + effects: true, + balanceChanges: true, + }, + }); + console.log(JSON.stringify(res, null, 2)); + // console.log(res.effects.status, res.balanceChanges); } catch (e) { console.log(e); } @@ -146,53 +139,50 @@ async function zapInSupply() { // zapInSupply(); async function borrow() { - const alc = new AlphalendClient("testnet"); + const alc = new AlphalendClient("mainnet"); const address = - "0x8948f801fa2325eedb4b0ad4eb0a55bfb318acc531f3a2f0cddd8daa9b4a8c94"; + "0xe136f0b6faf27ee707725f38f2aeefc51c6c31cc508222bee5cbc4f5fcf222c3"; const tx: Transaction | undefined = await alc.borrow({ address: address, positionCapId: - "0x04aef463126fea9cc518a37abc8ae8367f68c8eceeef31790b2da6be852d9d4b", + "0xf9ca35f404dd3c1ea10c381dd3e1fe8a0c4586adf5e186f4eb52307462a5af7d", coinType: - "0x3a8117ec753fb3c404b3a3762ba02803408b9eccb7e31afb8bbb62596d778e9a::testcoin2::TESTCOIN2", - marketId: "2", - amount: 100000000000n, - priceUpdateCoinTypes: [], + "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC", + marketId: "6", + amount: 100_000n, + priceUpdateCoinTypes: [ + "0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::USDSUI", + "0xd0e89b2af5e4910726fbcd8b8dd37bb79b29e5f83f7491bca830e94f7f226d29::eth::ETH", + "0x66629328922d609cf15af779719e248ae0e63fe0b9d9739623f763b33a9c97da::esui::ESUI", + ], }); if (tx) { - dryRunTransactionBlock(tx, address); + // dryRunTransactionBlock(tx, address); + await executeTransactionBlock(tx); } } +// borrow(); -export async function executeTransactionBlock() { +export async function executeTransactionBlock(tx: Transaction) { const { keypair, suiClient } = getExecStuff(); - const tx = new Transaction(); - const constants = getConstants("testnet"); - // removeAlternate(tx); - // await removeCoinFromOracle( - // tx, - // constants.ALPHAFI_ORACLE_ADMIN_CAP_ID, - // "0x3a8117ec753fb3c404b3a3762ba02803408b9eccb7e31afb8bbb62596d778e9a::testcoin2::TESTCOIN2", - // "testnet", - // ); - // await setPrice(tx, "0x2::sui::SUI", 10, 10, 1); - await suiClient - .signAndExecuteTransaction({ + const constants = getConstants("mainnet"); + tx.setGasBudget(1e8); + try { + const res = await suiClient.signAndExecuteTransaction({ signer: keypair, transaction: tx, - requestType: "WaitForLocalExecution", - options: { - showEffects: true, - showBalanceChanges: true, - showObjectChanges: true, + include: { + effects: true, + balanceChanges: true, + objectTypes: true, }, - }) - .then((res) => { - console.log(JSON.stringify(res, null, 2)); - }) - .catch((error) => { - console.error(error); }); + // Replaces the JSON-RPC "WaitForLocalExecution" request type. + await suiClient.waitForTransaction({ result: res }); + console.log(JSON.stringify(res, null, 2)); + } catch (error) { + console.error(error); + } } // executeTransactionBlock(); @@ -211,7 +201,7 @@ async function getUserPortfolio() { process.exit(1); } const result = await client.getUserPortfolioWithCachedMarkets( - "0xe66862b7f2656b6b2c0bb580aa4aff561782e7e218bf143433e60efd4bfe179e", + "0x8e3ab1581df48a7bdb72fa8d2138877c432420c503a4a9f03b762387f9dcd600", markets, ); console.log(result); @@ -225,30 +215,51 @@ async function getUserPortfolio() { async function withdraw() { const alc = new AlphalendClient("mainnet"); const address = - "0x8c5c76fa46a645ce5f636342ad6b0514a55f8c1518671920cd92d284695aff78"; + "0xe136f0b6faf27ee707725f38f2aeefc51c6c31cc508222bee5cbc4f5fcf222c3"; const tx: Transaction | undefined = await alc.withdraw({ address: address, positionCapId: - "0x8de2193d00fe660a90d823125fbd300774dbba553d9eb353e7451c419fe55a8d", + "0xf9ca35f404dd3c1ea10c381dd3e1fe8a0c4586adf5e186f4eb52307462a5af7d", coinType: - "0x66629328922d609cf15af779719e248ae0e63fe0b9d9739623f763b33a9c97da::esui::ESUI", - marketId: "21", - amount: 10_000_000_000n, + "0x356a26eb9e012a68958082340d4c4116e7f55615cf27affcff209cf0ae544f59::wal::WAL", + marketId: "7", + amount: 1n, priceUpdateCoinTypes: [ "0x66629328922d609cf15af779719e248ae0e63fe0b9d9739623f763b33a9c97da::esui::ESUI", - "0xd1b72982e40348d069bb1ff701e634c117bb5f741f44dff91e472d3b01461e55::stsui::STSUI", - "0x7262fb2f7a3a14c888c438a3cd9b912469a58cf60f367352c46584262e8299aa::ika::IKA", - "0x2::sui::SUI", + "0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::USDSUI", + "0xd0e89b2af5e4910726fbcd8b8dd37bb79b29e5f83f7491bca830e94f7f226d29::eth::ETH", + "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC", + "0x356a26eb9e012a68958082340d4c4116e7f55615cf27affcff209cf0ae544f59::wal::WAL", ], }); if (tx) { - dryRunTransactionBlock(tx, address); + // dryRunTransactionBlock(tx, address); + executeTransactionBlock(tx); } } withdraw(); +async function supply() { + const alc = new AlphalendClient("mainnet"); + const address = + "0xe136f0b6faf27ee707725f38f2aeefc51c6c31cc508222bee5cbc4f5fcf222c3"; + const tx: Transaction | undefined = await alc.supply({ + address: address, + positionCapId: + "0xf9ca35f404dd3c1ea10c381dd3e1fe8a0c4586adf5e186f4eb52307462a5af7d", + coinType: + "0x66629328922d609cf15af779719e248ae0e63fe0b9d9739623f763b33a9c97da::esui::ESUI", + marketId: "21", + amount: 100_000_000n, + }); + if (tx) { + executeTransactionBlock(tx); + } +} +// supply(); + async function run() { - const { suiClient, keypair, address } = getExecStuff(); + const { keypair, address } = getExecStuff(); // 🔥 TEST FLASH REPAY // Choose which test to run: // Basic flash repay test (with default 1% slippage) @@ -260,35 +271,8 @@ async function run() { // Other tests (commented out) // const { suiClient, keypair, address } = getExecStuff(); // const tx = new Transaction(); - const constants = getConstants("mainnet"); - const pythClient = new SuiPythClient( - suiClient, - constants.PYTH_STATE_ID, - constants.WORMHOLE_STATE_ID, - ); - const pythConnection = new SuiPriceServiceConnection( - "https://hermes.pyth.network", - ); // const positionCapId = // "0xf9ca35f404dd3c1ea10c381dd3e1fe8a0c4586adf5e186f4eb52307462a5af7d"; - // await getPriceInfoObjectIdsWithUpdate( - // tx, - // [pythPriceFeedIdMap[coinType]], - // pythClient, - // pythConnection, - // ); - // console.log(pythPriceFeedIdMap[coinType]); - const priceInfoObjectIds = await pythClient.getPriceFeedObjectId( - "93da3352f9f1d105fdfe4971cfa80e9dd777bfc5d0f683ebb6e1294b92137bb7", - ); - // const priceFeedUpdateData = await pythConnection.getPriceFeedsUpdateData([ - // "14890ba9c221092cba3d6ce86846d61f8606cefaf3dfc20bf3e2ab99de2644c0", - // ]); - // const priceInfoObjectIds = await pythClient.createPriceFeed( - // tx, - // priceFeedUpdateData, - // ); - console.log(priceInfoObjectIds); // const tx = await updatePricesCaller(); // const tx = await alc.supply({ // marketId: "1", @@ -339,5 +323,4 @@ async function run() { // }); // } } - // run(); diff --git a/scripts/updatePriceScript.ts b/scripts/updatePriceScript.ts index 9a5c807..7cf30fe 100644 --- a/scripts/updatePriceScript.ts +++ b/scripts/updatePriceScript.ts @@ -2,7 +2,6 @@ import cron from "node-cron"; import { AlphalendClient } from "../src/core/client"; import { Transaction } from "@mysten/sui/transactions"; import { getExecStuff } from "./testRun"; -import { SuiTransactionBlockResponse } from "@mysten/sui/jsonRpc"; cron.schedule("* * * * *", async () => { const { suiClient, keypair } = getExecStuff(); @@ -10,27 +9,33 @@ cron.schedule("* * * * *", async () => { const tx = new Transaction(); await alphalendClient.updatePrices(tx, [ "0x2::sui::SUI", - "0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP", - "0x356a26eb9e012a68958082340d4c4116e7f55615cf27affcff209cf0ae544f59::wal::WAL", - "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC", + "0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::USDSUI", + "0x876a4b7bce8aeaef60464c11f4026903e9afacab79b9b142686158aa86560b50::xbtc::XBTC", + // "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC", + "0x66629328922d609cf15af779719e248ae0e63fe0b9d9739623f763b33a9c97da::esui::ESUI", ]); - await suiClient - .signAndExecuteTransaction({ + try { + const res = await suiClient.signAndExecuteTransaction({ signer: keypair, transaction: tx, - requestType: "WaitForLocalExecution", - options: { - showEffects: true, - showBalanceChanges: true, - showObjectChanges: true, + include: { + effects: true, + balanceChanges: true, + objectTypes: true, }, - }) - .then((res: SuiTransactionBlockResponse) => { - console.log("Transaction executed successfully"); - console.log(res.digest); - }) - .catch((error) => { - console.error(error); }); + if (res.$kind === "FailedTransaction") { + console.error( + `Transaction ${res.FailedTransaction.digest} failed:`, + res.FailedTransaction.status.error?.message, + ); + return; + } + await suiClient.waitForTransaction({ result: res }); + console.log("Transaction executed successfully"); + console.log(res.Transaction.digest); + } catch (error) { + console.error(error); + } }); diff --git a/scripts/utils.ts b/scripts/utils.ts index 33af63e..d3b9405 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -1,12 +1,22 @@ import { fromBase64 } from "@mysten/bcs"; import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"; -import { SuiJsonRpcClient, getJsonRpcFullnodeUrl } from "@mysten/sui/jsonRpc"; +import { SuiGrpcClient } from "@mysten/sui/grpc"; import * as dotenv from "dotenv"; import { Transaction } from "@mysten/sui/transactions"; dotenv.config(); +type SuiNetwork = "mainnet" | "testnet" | "devnet" | "localnet"; + +/** Default gRPC endpoints, overridable via the SUI_GRPC_URL env var. */ +const GRPC_URL: Record = { + mainnet: "https://fullnode.mainnet.sui.io:443", + testnet: "https://fullnode.testnet.sui.io:443", + devnet: "https://fullnode.devnet.sui.io:443", + localnet: "http://127.0.0.1:9000", +}; + export function getExecStuff() { if (!process.env.PK_B64) { throw new Error("env var PK_B64 not configured"); @@ -22,10 +32,10 @@ export function getExecStuff() { throw new Error("env var NETWORK not configured"); } - const suiClient = new SuiJsonRpcClient({ - url: getJsonRpcFullnodeUrl( - process.env.NETWORK as "mainnet" | "testnet" | "devnet" | "localnet", - ), + const network = process.env.NETWORK as SuiNetwork; + const suiClient = new SuiGrpcClient({ + network, + baseUrl: process.env.SUI_GRPC_URL ?? GRPC_URL[network], }); return { address, keypair, suiClient }; @@ -34,41 +44,36 @@ export function getExecStuff() { export async function executeTransactionBlock(txb: Transaction) { const { keypair, suiClient } = getExecStuff(); - await suiClient - .signAndExecuteTransaction({ + try { + const res = await suiClient.signAndExecuteTransaction({ signer: keypair, transaction: txb, - requestType: "WaitForLocalExecution", - options: { - showEffects: true, - showBalanceChanges: true, - showObjectChanges: true, + include: { + effects: true, + balanceChanges: true, + objectTypes: true, }, - }) - .then((res) => { - console.log(JSON.stringify(res, null, 2)); - }) - .catch((error) => { - console.error(error); }); + // Replaces the JSON-RPC "WaitForLocalExecution" request type. + await suiClient.waitForTransaction({ result: res }); + console.log(JSON.stringify(res, null, 2)); + } catch (error) { + console.error(error); + } } export async function dryRunTransactionBlock(txb: Transaction) { const { suiClient, address } = getExecStuff(); txb.setSender(address); 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); - }) - .catch((error) => { - console.error(error); - }); + const res = await suiClient.simulateTransaction({ + transaction: txb, + include: { + effects: true, + balanceChanges: true, + }, + }); + console.log(JSON.stringify(res, null, 2)); } catch (e) { console.log(e); } @@ -93,17 +98,17 @@ export async function simulateTransactionBlock( // .catch((error) => { // console.error(error); // }); - await suiClient - .devInspectTransactionBlock({ - transactionBlock: txb, - sender: address, - }) - .then((res) => { - console.log(JSON.stringify(res, null, 2)); - }) - .catch((error) => { - console.error(error); - }); + // checksEnabled: false mirrors devInspectTransactionBlock, allowing + // non-entry/non-public functions to be inspected. + const res = await suiClient.simulateTransaction({ + transaction: txb, + checksEnabled: false, + include: { + effects: true, + commandResults: true, + }, + }); + console.log(JSON.stringify(res, null, 2)); } catch (e) { console.log(e); } diff --git a/src/core/client.ts b/src/core/client.ts index bb20cbb..cd6fc53 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -1,8 +1,3 @@ -import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc"; -import { - SuiPriceServiceConnection, - SuiPythClient, -} from "@pythnetwork/pyth-sui-js"; import { getAlphafiConstants, getConstants, @@ -14,9 +9,7 @@ import { TransactionResult, TransactionArgument, } from "@mysten/sui/transactions"; -import { - appendOracleToLendingBridge, -} from "../utils/oracle.js"; +import { appendOracleToLendingBridge } from "../utils/oracle.js"; import { appendLazerUpdate, fetchLazerUpdateBytes } from "../utils/lazer.js"; import { SupplyParams, @@ -69,7 +62,7 @@ const MIN_SUI_STAKE_AMOUNT = 3n; // 3 mists * * The main entry point for interacting with the AlphaLend protocol: * - Provides methods for all protocol actions (supply, borrow, withdraw, repay, claimRewards, liquidate) - * - Handles connection to the Sui blockchain and Pyth oracle + * - Handles connection to the Sui blockchain and the price oracle * - Manages transaction building for protocol interactions * - Exposes query methods for protocol state, markets, and user positions * - Initializes and coordinates price feed updates @@ -77,8 +70,6 @@ const MIN_SUI_STAKE_AMOUNT = 3n; // 3 mists */ export class AlphalendClient { - pythClient: SuiPythClient; - pythConnection: SuiPriceServiceConnection; network: Network; constants: Constants; lendingProtocol: LendingProtocol; @@ -101,10 +92,8 @@ export class AlphalendClient { /** * Creates a new AlphaLend client instance. * - * The SDK connects to Sui via GraphQL. The only remaining JSON-RPC usage - * is an internal, minimal `SuiClient` passed to `@pythnetwork/pyth-sui-js`'s - * `SuiPythClient` constructor (that SDK has not yet migrated to GraphQL). - * It is never exposed on the public surface. + * The SDK connects to Sui via GraphQL and gRPC only; it makes no JSON-RPC + * calls. * * @param network One of the supported Sui networks. * @param graphqlUrl Optional GraphQL endpoint override. If omitted, a default @@ -119,28 +108,6 @@ export class AlphalendClient { this.network = network; this.constants = getConstants(network); - // Minimal internal SuiClient ONLY for SuiPythClient (upstream dep still - // uses JSON-RPC). All other reads go through Blockchain (GraphQL). - const pythFullnodeUrl = - network === "mainnet" - ? "https://alphalen-suimain-ef6f.mainnet.sui.rpcpool.com/" - : network === "testnet" - ? "https://fullnode.testnet.sui.io/" - : "https://fullnode.devnet.sui.io/"; - const pythSuiClient = new SuiJsonRpcClient({ - url: pythFullnodeUrl, - network, - }); - this.pythClient = new SuiPythClient( - pythSuiClient, - this.constants.PYTH_STATE_ID, - this.constants.WORMHOLE_STATE_ID, - ); - this.pythConnection = new SuiPriceServiceConnection( - network === "mainnet" - ? "https://hermes.pyth.network" - : "https://hermes-beta.pyth.network", - ); this.lendingProtocol = new LendingProtocol(network, graphqlUrl); this.blockchain = new Blockchain(network, graphqlUrl); this.sevenKGateway = new SevenKGateway(); diff --git a/src/utils/oracle.ts b/src/utils/oracle.ts index 699cbb5..a4e3f84 100644 --- a/src/utils/oracle.ts +++ b/src/utils/oracle.ts @@ -7,10 +7,6 @@ * - Handles the connection between external price feeds and the lending protocol */ import { Inputs, Transaction } from "@mysten/sui/transactions"; -import { - SuiPriceServiceConnection, - SuiPythClient, -} from "@pythnetwork/pyth-sui-js"; import { Constants } from "../constants/types.js"; /** @@ -23,52 +19,6 @@ export interface UpdatePriceTransactionArgs { coinType: string; } -/** - * Fetches price feed data from Pyth and adds update instructions to the transaction - * - * @param tx - The transaction to add price updates to - * @param priceIDs - Array of Pyth price feed IDs - * @param pythClient - SuiPythClient instance - * @param pythConnection - SuiPriceServiceConnection instance - * @returns Promise resolving to an array of price info object IDs - */ -export async function getPriceInfoObjectIdsWithUpdate( - tx: Transaction, - priceIDs: string[], - pythClient: SuiPythClient, - pythConnection: SuiPriceServiceConnection, -): Promise { - const priceFeedUpdateData = - await pythConnection.getPriceFeedsUpdateData(priceIDs); - - const priceInfoObjectIds = await pythClient.updatePriceFeeds( - tx, - priceFeedUpdateData, - priceIDs, - ); - - return priceInfoObjectIds; -} - -/** - * Retrieves price info object IDs from Pyth without updating them - * - * @param priceIDs - Array of Pyth price feed IDs - * @param pythClient - SuiPythClient instance - * @returns Promise resolving to an array of price info object IDs or undefined - */ -export async function getPriceInfoObjectIdsWithoutUpdate( - priceIDs: string[], - pythClient: SuiPythClient, -): Promise<(string | undefined)[]> { - const priceInfoObjectIds = await Promise.all( - priceIDs.map((priceId) => { - return pythClient.getPriceFeedObjectId(priceId); - }), - ); - return priceInfoObjectIds; -} - export function appendOracleToLendingBridge( tx: Transaction, coinType: string, From 33cd06c1206215fc50c9ef0f84eece0fbcfc246a Mon Sep 17 00:00:00 2001 From: felix11 Date: Tue, 4 Aug 2026 00:51:15 +0400 Subject: [PATCH 3/3] feat(coinHelpers): export MAX_COINS_PER_TX for consumers --- src/core/coinHelpers.ts | 2 +- src/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/coinHelpers.ts b/src/core/coinHelpers.ts index 3d89eda..7c7dd21 100644 --- a/src/core/coinHelpers.ts +++ b/src/core/coinHelpers.ts @@ -43,7 +43,7 @@ export interface CoinTypeCount { * transaction. 180 measures ~14.3KB, leaving headroom for the gas coin and * address-balance withdrawal the SUI paths add on top. */ -const MAX_COINS_PER_TX = 180; +export const MAX_COINS_PER_TX = 180; /** Kept low: the public fullnode rate-limits bursts. See {@link withRetry}. */ const COUNT_CONCURRENCY = 5; diff --git a/src/index.ts b/src/index.ts index 910b76f..4bb4d4e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,5 +22,6 @@ export { export { getCoinObjectCounts, buildMergeCoinsTransaction, + MAX_COINS_PER_TX, } from "./core/coinHelpers.js"; export type { MergeCoinsOutput, CoinTypeCount } from "./core/coinHelpers.js";