From 5e43d7b77b2f81cc6a07642269f61087823f10a1 Mon Sep 17 00:00:00 2001 From: do0x0ob Date: Thu, 5 Mar 2026 19:30:17 +0800 Subject: [PATCH 1/5] fix: getUsdbSupply use getCoinInfo to avoid InvalidPublicFunctionReturnType - Replace simulateTransaction + borrow_cap_mut with stateService.getCoinInfo - PTB cannot pass &mut references between commands; SDK v2 validates and rejects - Add getUsdbSupply e2e test (expect supply > 0) Made-with: Cursor --- src/client.ts | 25 +++++++------------------ test/e2e/client.test.ts | 10 ++++++++++ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/client.ts b/src/client.ts index 60b1660..5c22ce7 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 = (await this.suiClient.core.mvr.resolveType({ type: this.getUsdbCoinType() })).type; + const { response } = await this.suiClient.stateService.getCoinInfo({ coinType }); + const supply = response.treasury?.totalSupply; + return supply != null ? BigInt(supply) : 0n; } /** diff --git a/test/e2e/client.test.ts b/test/e2e/client.test.ts index 2e7efc7..3f364cd 100644 --- a/test/e2e/client.test.ts +++ b/test/e2e/client.test.ts @@ -49,6 +49,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 () => { From 50b6fa995e012a2d8e467494e7af85bb184d1acb Mon Sep 17 00:00:00 2001 From: do0x0ob Date: Thu, 5 Mar 2026 19:57:18 +0800 Subject: [PATCH 2/5] test: narrow Oracle e2e to SUI-only to avoid getAllOraclePrices flakiness getAllOraclePrices with all coin types fails on main's static config (derivative flow / Hermes batch). Single-coin getOraclePrices(SUI) is stable and sufficient to validate the oracle flow. Also add SUI_GRPC_URL env support for custom RPC. Made-with: Cursor --- test/e2e/client.test.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/e2e/client.test.ts b/test/e2e/client.test.ts index 3f364cd..155a809 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'; @@ -19,17 +21,15 @@ const susdbLpType = '0x38f61c75fa8407140294c84167dd57684580b55c3066883b48dedc344 describe('Interacting with Bucket Client on mainnet', () => { describe('Oracle', () => { it( - 'getAllOraclePrices returns positive numbers for all oracle coin types', + 'getOraclePrices returns positive numbers for SUI (validates oracle flow)', async () => { - const prices = await bucketClient.getAllOraclePrices(); - const coinTypes = bucketClient.getAllOracleCoinTypes(); + // Test SUI only: getAllOraclePrices (all coin types) flakes on main's static config; + // single-coin getOraclePrices is stable and sufficient to validate the oracle flow. + const prices = await bucketClient.getOraclePrices({ coinTypes: [SUI_TYPE_ARG] }); expect(Object.keys(prices).length).toBeGreaterThan(0); - for (const coinType of coinTypes) { - const price = prices[coinType]; - expect(price).toBeDefined(); - expect(typeof price).toBe('number'); - expect(price).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, ); From 8692b063bcf092568a4d6239ceec5ba090a88b5f Mon Sep 17 00:00:00 2001 From: do0x0ob Date: Thu, 5 Mar 2026 19:59:11 +0800 Subject: [PATCH 3/5] fix: use strict equality in getUsdbSupply (eqeqeq lint) Made-with: Cursor --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 5c22ce7..a54c1bf 100644 --- a/src/client.ts +++ b/src/client.ts @@ -172,7 +172,7 @@ export class BucketClient { const coinType = (await this.suiClient.core.mvr.resolveType({ type: this.getUsdbCoinType() })).type; const { response } = await this.suiClient.stateService.getCoinInfo({ coinType }); const supply = response.treasury?.totalSupply; - return supply != null ? BigInt(supply) : 0n; + return supply !== undefined && supply !== null ? BigInt(supply) : 0n; } /** From da5e2a449054a8ee81d55836c6ac2370feba4634 Mon Sep 17 00:00:00 2001 From: do0x0ob Date: Mon, 9 Mar 2026 16:32:45 +0800 Subject: [PATCH 4/5] fix: use getUsdbCoinType() directly for getUsdbSupply (remove resolveType, use original only) Made-with: Cursor --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index a54c1bf..9ef91cd 100644 --- a/src/client.ts +++ b/src/client.ts @@ -169,7 +169,7 @@ export class BucketClient { * &mut which cannot be passed between commands; SDK v2 validates this and rejects). */ async getUsdbSupply(): Promise { - const coinType = (await this.suiClient.core.mvr.resolveType({ type: this.getUsdbCoinType() })).type; + 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; From 382a46e93f038040f5f01aa02999420420eacebd Mon Sep 17 00:00:00 2001 From: do0x0ob Date: Mon, 9 Mar 2026 17:19:27 +0800 Subject: [PATCH 5/5] fix: use upgraded scoin_rule package ID for oracle price aggregation - Update scoin_rule::feed target to 0xb7c07926... (from query-outputs PRICE_OBJS) - Align static config with merge branch test-results/query-outputs.json - Restore strict getAllOraclePrices test assertions Made-with: Cursor --- src/client.ts | 30 ++++++++++++++++-------------- src/consts/config.ts | 2 +- test/e2e/client.test.ts | 36 ++++++++++++++++++++++++++++++++++-- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/client.ts b/src/client.ts index 9ef91cd..c10e2b3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -657,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(), @@ -676,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 }); @@ -685,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) }; }, {}), @@ -956,7 +958,7 @@ export class BucketClient { switch (derivativeInfo.derivativeKind) { case 'sCoin': tx.moveCall({ - target: '0x2252a1cc5a16a0c9f4530005908cced53783d96b4932ca3fa15b1fe9d5935972::scoin_rule::feed', + target: '0xb7c0792630fe4b028437a5554e5c0bef16edaf793210ef32a88fcea443e4d76b::scoin_rule::feed', typeArguments: [coinType, derivativeInfo.underlyingCoinType], arguments: [ collector, 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 155a809..c7ed22d 100644 --- a/test/e2e/client.test.ts +++ b/test/e2e/client.test.ts @@ -23,8 +23,6 @@ describe('Interacting with Bucket Client on mainnet', () => { it( 'getOraclePrices returns positive numbers for SUI (validates oracle flow)', async () => { - // Test SUI only: getAllOraclePrices (all coin types) flakes on main's static config; - // single-coin getOraclePrices is stable and sufficient to validate the oracle flow. const prices = await bucketClient.getOraclePrices({ coinTypes: [SUI_TYPE_ARG] }); expect(Object.keys(prices).length).toBeGreaterThan(0); expect(prices[SUI_TYPE_ARG]).toBeDefined(); @@ -33,6 +31,22 @@ describe('Interacting with Bucket Client on mainnet', () => { }, MAINNET_TIMEOUT_MS, ); + + it( + 'getAllOraclePrices returns positive numbers for all oracle coin types', + async () => { + const prices = await bucketClient.getAllOraclePrices(); + const coinTypes = bucketClient.getAllOracleCoinTypes(); + expect(coinTypes.length).toBeGreaterThan(0); + expect(Object.keys(prices).length).toBe(coinTypes.length); + for (const coinType of coinTypes) { + expect(prices[coinType]).toBeDefined(); + expect(typeof prices[coinType]).toBe('number'); + expect(prices[coinType]).toBeGreaterThan(0); + } + }, + MAINNET_TIMEOUT_MS, + ); }); describe('Config & metadata', () => { @@ -86,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 () => {