diff --git a/src/client.ts b/src/client.ts index 7d56491..c10e2b3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -164,26 +164,15 @@ export class BucketClient { /* ----- Queries ----- */ /** - * @description + * @description Get total USDB supply via RPC. + * Uses stateService.getCoinInfo (avoids PTB reference limitation: borrow_cap_mut returns + * &mut which cannot be passed between commands; SDK v2 validates this and rejects). */ async getUsdbSupply(): Promise { - const tx = new Transaction(); - - const treasury = tx.moveCall({ - target: `${this.config.USDB_PACKAGE_ID}::usdb::borrow_cap_mut`, - arguments: [this.treasury(tx)], - }); - tx.moveCall({ - target: `0x2::coin::total_supply`, - typeArguments: [this.getUsdbCoinType()], - arguments: [treasury], - }); - tx.setSender(DUMMY_ADDRESS); - const res = await this.suiClient.simulateTransaction({ transaction: tx, include: { commandResults: true } }); - if (res.$kind === 'FailedTransaction' || !res.commandResults?.[1]?.returnValues) { - return 0n; - } - return BigInt(bcs.u64().parse(res.commandResults![1].returnValues[0].bcs)); + const coinType = this.getUsdbCoinType(); + const { response } = await this.suiClient.stateService.getCoinInfo({ coinType }); + const supply = response.treasury?.totalSupply; + return supply !== undefined && supply !== null ? BigInt(supply) : 0n; } /** @@ -668,16 +657,11 @@ export class BucketClient { return; } poolInfo.reward.rewardTypes.forEach((rewardType) => { - const rewarder = tx.moveCall({ - target: `${this.config.SAVING_INCENTIVE_PACKAGE_ID}::saving_incentive::get_rewarder`, - typeArguments: [lpType, rewardType], - arguments: [tx.sharedObjectRef(poolInfo.reward!.rewardManager)], - }); tx.moveCall({ - target: `${this.config.SAVING_INCENTIVE_PACKAGE_ID}::saving_incentive::realtime_reward_amount`, + target: `${this.config.SAVING_INCENTIVE_PACKAGE_ID}::saving_incentive::get_realtime_reward_amount`, typeArguments: [lpType, rewardType], arguments: [ - rewarder, + tx.sharedObjectRef(poolInfo.reward!.rewardManager), tx.sharedObjectRef(poolInfo.pool), tx.pure.address(accountId ?? address), tx.object.clock(), @@ -687,8 +671,12 @@ export class BucketClient { }); tx.setSender(DUMMY_ADDRESS); const res = await this.suiClient.simulateTransaction({ transaction: tx, include: { commandResults: true } }); - if (res.$kind === 'FailedTransaction' || !res.commandResults) { - return {}; + if (res.$kind === 'FailedTransaction') { + const err = (res as { FailedTransaction?: { status?: { error?: unknown } } }).FailedTransaction?.status?.error; + throw Object.assign(new Error('Failed to fetch account saving pool rewards'), { cause: err ?? res }); + } + if (!res.commandResults) { + throw new Error('Failed to fetch account saving pool rewards'); } return lpTypes.reduce((result, lpType) => { const poolInfo = this.getSavingPoolObjectInfo({ lpType }); @@ -696,15 +684,18 @@ export class BucketClient { if (!poolInfo.reward?.rewardTypes?.length) { return result; } - const responses = res.commandResults!.splice(0, 2 * poolInfo.reward.rewardTypes.length); + const responses = res.commandResults!.splice(0, poolInfo.reward.rewardTypes.length); return { ...result, [lpType]: poolInfo.reward.rewardTypes.reduce((result, rewardType, index) => { - if (!responses[index]?.returnValues) { - return result; + const amountRes = responses[index]?.returnValues; + if (!amountRes) { + throw new Error( + `Failed to fetch account saving pool rewards: missing result for ${lpType} reward ${rewardType}`, + ); } - const realtimeReward = bcs.u64().parse(responses[index + 1].returnValues[0].bcs); + const realtimeReward = bcs.u64().parse(amountRes[0].bcs); return { ...result, [rewardType]: BigInt(realtimeReward) }; }, {}), diff --git a/src/consts/config.ts b/src/consts/config.ts index f4426cd..e6a74c1 100644 --- a/src/consts/config.ts +++ b/src/consts/config.ts @@ -30,7 +30,7 @@ export const CONFIG: Record = { PSM_PACKAGE_ID: '0x8a46afe6ab0b972e2ee3923f5669104ae9465c12b84cd5e78ea1ff6404e4bfaf', FLASH_PACKAGE_ID: '0xca00bbb7ecfe11dbb527efab6145b358f2a95aa11a8aae9b9f475176430cf42a', SAVING_PACKAGE_ID: '0x5b60c3cdbb9ee31b856f7f8a6d4d6c8b91e3c0036a138fb9927529b05fd00373', - SAVING_INCENTIVE_PACKAGE_ID: '0x39692320d6fc01c27315a7972ed4717e4fd32eed43531ad6f55fd7f24b74e207', + SAVING_INCENTIVE_PACKAGE_ID: '0xd419027a4b92b1916c62c08da27520576bcf34d0a8c40884491dca2f5d992719', BORROW_INCENTIVE_PACKAGE_ID: '0xef9719a3392d3742ae09c42ebf01030d88b1a75651430932be1eb017b79fb372', BLACKLIST_PACKAGE_ID: '0x6d7b711a5e614f99f87fdbfbf915fea0fa96af769130ac226c3cbcf07efb90f3', diff --git a/test/e2e/client.test.ts b/test/e2e/client.test.ts index 2e7efc7..c7ed22d 100644 --- a/test/e2e/client.test.ts +++ b/test/e2e/client.test.ts @@ -10,7 +10,9 @@ import { coinWithBalance, destroyZeroCoin, getZeroCoin } from '../../src/utils/t const MAINNET_TIMEOUT_MS = 20_000; const network = 'mainnet'; const testAccount = '0x7a718956581fbe4a568d135fef5161024e74af87a073a1489e57ebef53744652'; -const suiClient = new SuiGrpcClient({ network, baseUrl: 'https://fullnode.mainnet.sui.io:443' }); +// Use SUI_GRPC_URL for custom RPC (avoids public mainnet rate limits) +const rpcUrl = process.env.SUI_GRPC_URL ?? 'https://fullnode.mainnet.sui.io:443'; +const suiClient = new SuiGrpcClient({ network, baseUrl: rpcUrl }); const bucketClient = new BucketClient({ suiClient, network }); const usdbCoinType = bucketClient.getUsdbCoinType(); const usdcCoinType = '0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC'; @@ -18,17 +20,29 @@ const susdbLpType = '0x38f61c75fa8407140294c84167dd57684580b55c3066883b48dedc344 describe('Interacting with Bucket Client on mainnet', () => { describe('Oracle', () => { + it( + 'getOraclePrices returns positive numbers for SUI (validates oracle flow)', + async () => { + const prices = await bucketClient.getOraclePrices({ coinTypes: [SUI_TYPE_ARG] }); + expect(Object.keys(prices).length).toBeGreaterThan(0); + expect(prices[SUI_TYPE_ARG]).toBeDefined(); + expect(typeof prices[SUI_TYPE_ARG]).toBe('number'); + expect(prices[SUI_TYPE_ARG]).toBeGreaterThan(0); + }, + MAINNET_TIMEOUT_MS, + ); + it( 'getAllOraclePrices returns positive numbers for all oracle coin types', async () => { const prices = await bucketClient.getAllOraclePrices(); const coinTypes = bucketClient.getAllOracleCoinTypes(); - expect(Object.keys(prices).length).toBeGreaterThan(0); + expect(coinTypes.length).toBeGreaterThan(0); + expect(Object.keys(prices).length).toBe(coinTypes.length); for (const coinType of coinTypes) { - const price = prices[coinType]; - expect(price).toBeDefined(); - expect(typeof price).toBe('number'); - expect(price).toBeGreaterThan(0); + expect(prices[coinType]).toBeDefined(); + expect(typeof prices[coinType]).toBe('number'); + expect(prices[coinType]).toBeGreaterThan(0); } }, MAINNET_TIMEOUT_MS, @@ -49,6 +63,16 @@ describe('Interacting with Bucket Client on mainnet', () => { MAINNET_TIMEOUT_MS, ); + it( + 'getUsdbSupply returns positive bigint (mainnet has circulating USDB)', + async () => { + const supply = await bucketClient.getUsdbSupply(); + expect(typeof supply).toBe('bigint'); + expect(supply).toBeGreaterThan(0n); + }, + MAINNET_TIMEOUT_MS, + ); + it( 'getFlashMintInfo returns feeRate and partner config', async () => { @@ -76,6 +100,24 @@ describe('Interacting with Bucket Client on mainnet', () => { }); describe('Rewards', () => { + it( + 'getAccountSavingPoolRewards returns record for SUSDB (validates SAVING_INCENTIVE_PACKAGE_ID)', + async () => { + const rewards = await bucketClient.getAccountSavingPoolRewards({ + address: testAccount, + lpTypes: [susdbLpType], + }); + expect(typeof rewards).toBe('object'); + expect(rewards).toHaveProperty(susdbLpType); + const susdbRewards = rewards[susdbLpType]!; + for (const [, amount] of Object.entries(susdbRewards)) { + expect(typeof amount).toBe('bigint'); + expect(amount).toBeGreaterThanOrEqual(0n); + } + }, + MAINNET_TIMEOUT_MS, + ); + it( 'buildClaimBorrowRewardsTransaction dry run succeeds and has reward balance changes', async () => {