Skip to content

Merge/wl581 onchain config#118

Merged
JustaLiang merged 42 commits intomainfrom
merge/wl581-onchain-config
Mar 10, 2026
Merged

Merge/wl581 onchain config#118
JustaLiang merged 42 commits intomainfrom
merge/wl581-onchain-config

Conversation

@ZouBadCode
Copy link
Contributor


Testing Workflow Improvements

  • Added segmented test commands (test:unit, test:e2e, test:coverage, etc.) in package.json and documented their usage in CLAUDE.md to allow running unit and e2e tests separately and improve coverage reporting.
  • Added @vitest/coverage-v8 and related dependencies to support coverage reporting, reflected in both package.json and pnpm-lock.yaml.
  • Clarified test strategy in CLAUDE.md, distinguishing between fast local unit tests and e2e tests that interact with mainnet RPC, with guidance on avoiding RPC rate limits.

On-Chain Config Integration & SDK Caching

Added configAdaptor.ts

  • Introduced configAdaptor.ts to adapt on-chain config objects into the SDK’s internal config structure.
  • The adaptor populates and maintains an in-memory cache inside the SDK to reduce redundant RPC calls.
  • Ensures SDK consumers always interact with normalized, ready-to-use config data.

Added bucketConfig.ts

  • Added bucketConfig.ts as a dedicated module responsible for fetching on-chain config data.
  • Encapsulates chain query logic and provides a clean interface for retrieving the latest protocol configuration.

Automatic Config Validation & Refetch

  • Before executing SDK methods, the client now automatically validates the required config.
  • If any required config segment is missing or invalid, the SDK will automatically refetch the latest config from chain.
  • This improves reliability and reduces the risk of misconfigured client usage.

Documentation Updates

  • Updated README.md and CLAUDE.md to standardize initialization via:

    const client = await BucketClient.initialize(...)
  • Documented that BucketClient.initialize():

    • Fetches config from chain
    • Normalizes it through the adaptor
    • Populates the internal SDK cache
    • Ensures the client is fully ready before use
  • Clarified best practices for client initialization and config lifecycle management.


Dependency Management

  • Updated pnpm-lock.yaml with new packages required for coverage and testing, including:

    • @vitest/coverage-v8
    • Istanbul libraries
    • Related coverage utilities

@ZouBadCode ZouBadCode requested review from JustaLiang and do0x0ob March 4, 2026 00:21
Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Noted: this is a large refactor (+2768/-1342), but the async initialization flow, on-chain config adapter, and new e2e/unit coverage are consistent.

🤖 Generated with Rex (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

Found 2 issues:

  1. queryAllConfig can return a partial config after RPC/object parse errors, and initialization still succeeds. This creates a fail-open path where required package IDs/object refs become empty strings/default refs and later transaction builders produce malformed targets/refs instead of failing at init time. Please fail fast by validating required sections/keys (e.g. packageConfig/oracleConfig/objectConfig + required fields) and throw when any mandatory object in id_vector cannot be resolved.

if (obj instanceof Error) {
console.error('Failed to fetch object:', obj.message);
continue;
}
const type = obj.type;
const json = obj.json as Record<string, unknown> | null;
if (!json) continue;
try {
if (type.endsWith(TYPE_PACKAGE_CONFIG)) {
result.packageConfig = json;
} else if (type.endsWith(TYPE_ORACLE_CONFIG)) {
result.oracleConfig = json;
} else if (type.endsWith(TYPE_OBJECT_CONFIG)) {
result.objectConfig = json;
} else if (type.endsWith(TYPE_AGGREGATOR)) {
const entries = parseVecMap(json.table);
result.aggregator = { id: json.id as string, entries };
} else if (type.endsWith(TYPE_VAULT)) {
const entries = parseVecMap(json.table);
result.vault = { id: json.id as string, entries };
} else if (type.endsWith(TYPE_SAVING_POOL)) {
const entries = parseVecMap(json.table);
result.savingPool = { id: json.id as string, entries };
} else if (type.endsWith(TYPE_PSM_POOL)) {
const entries = parseVecMap(json.table);
result.psmPool = { id: json.id as string, entries };
} else if (type.endsWith(TYPE_PRICE_CONFIG)) {
const entries = parseVecMap(json.table);
result.priceConfig = { id: json.id as string, entries };
} else {
// eslint-disable-next-line no-console
console.warn(`Unknown object type: ${type} (objectId: ${obj.objectId})`);
}
} catch (e) {
console.error(`Failed to process object ${obj.objectId} (type: ${type}):`, e);
}
}
return result;

  1. initialize({ configOverrides }) stores overrides on the instance, but refreshConfig() does not preserve them when called without args. A later refresh silently drops prior overrides (e.g. custom PRICE_SERVICE_ENDPOINT) and changes runtime behavior unexpectedly. Consider defaulting to overrides ?? this.configOverrides and updating this.configOverrides when explicit overrides are provided.

bc.configOverrides = configOverrides;
return bc;
}
/**
* @description Ensures config is loaded. If not yet fetched, fetches from on-chain.
*/
private async ensureConfig(): Promise<void> {
if (!this._config) {
await this.refreshConfig(this.configOverrides);
}
}
/**
* @description Returns the current config, throwing if not yet initialized.
* For sync methods that cannot await ensureConfig().
*/
private get config(): ConfigType {
if (!this._config) {
throw new Error(
'BucketClient config not initialized. ' +
'Call an async method first, use BucketClient.initialize(), or pass config to the constructor.',
);
}
return this._config;
}
/* ----- Getter ----- */
/**
* @description Get this.suiClient
*/
getSuiClient(): SuiGrpcClient {
return this.suiClient;
}
/**
* @description
*/
async getConfig(): Promise<ConfigType | undefined> {
await this.ensureConfig();
return this.config;
}
/**
* @description Re-fetch config from on-chain and update this client's config.
*/
async refreshConfig(overrides?: Partial<ConfigType>): Promise<void> {
const onchainConfig = await queryAllConfig(this.suiClient, this.network);
this._config = convertOnchainConfig(onchainConfig, overrides);
}

🤖 Generated with Rex (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Found 4 issues worth addressing:


1. initialSharedVersion: 0 fallback in toSharedObjectRef will break transactions (confidence: 88)

When json is a plain string (objectId only), the function defaults to initialSharedVersion: 0. Sui requires the correct initial shared version for shared object references in PTBs — using 0 here will silently produce invalid transactions at runtime. If only an objectId string arrives from on-chain JSON, the code should either throw or fetch the actual version separately.

return { objectId: json, initialSharedVersion: 0, mutable };
}
const obj = json as Record<string, unknown>;
return {
objectId: (obj.id ?? obj.objectId ?? '') as string,
initialSharedVersion: (obj.initial_shared_version ?? obj.initialSharedVersion ?? 0) as number | string,


2. Race condition in ensureConfig — parallel calls before config loads will trigger multiple RPC fetches (confidence: 82)

private async ensureConfig(): Promise<void> {
  if (!this._config) {
    await this.refreshConfig(this.configOverrides); // no in-flight guard
  }
}

If two async SDK methods are called concurrently before config is initialized (e.g. client.getOraclePrices() and client.getVaultObjectInfo() in parallel), both see _config === null and trigger separate queryAllConfig RPC calls. Fix with a cached promise:

private _configPromise: Promise<void> | null = null;

private async ensureConfig(): Promise<void> {
  if (!this._config) {
    this._configPromise ??= this.refreshConfig(this.configOverrides).finally(() => {
      this._configPromise = null;
    });
    await this._configPromise;
  }
}

* @description Factory that creates a BucketClient with config fetched from on-chain.
* Use this when you want the SDK to always use the latest on-chain parameters.
*
* @param options.configOverrides - Optional overrides (e.g. PRICE_SERVICE_ENDPOINT).
*/


3. refreshConfig() called without args silently drops configOverrides (confidence: 82)

initialize() stores configOverrides on the instance, and ensureConfig() correctly passes them through. But refreshConfig() is a public method — if a consumer calls client.refreshConfig() directly (without overrides), the stored overrides are lost and the config reverts to pure on-chain values. Consider making refreshConfig always apply the stored this.configOverrides as defaults:

async refreshConfig(overrides?: Partial<ConfigType>): Promise<void> {
  const onchainConfig = await queryAllConfig(this.suiClient, this.network);
  this._config = convertOnchainConfig(onchainConfig, overrides ?? this.configOverrides);
}

network = 'mainnet',
configOverrides,
}: {
suiClient?: SuiGrpcClient;


4. Previously synchronous public getters are now async — breaking API change without semver bump (confidence: 85)

getUsdbCoinType(), getAllOracleCoinTypes(), getAllCollateralTypes(), getAggregatorObjectInfo(), getVaultObjectInfo(), getSavingPoolObjectInfo(), getPsmPoolObjectInfo() were all synchronous before. They now return Promise<T>. Any existing consumer using the return value directly (e.g. const t = client.getUsdbCoinType()) now gets a Promise<string> instead of a string — a silent runtime break unless TypeScript is strict. This PR should either bump the major version or provide deprecation shims. At minimum, the PR description and CHANGELOG should call this out explicitly as a breaking change.

}
/**
* @description Re-fetch config from on-chain and update this client's config.
*/
async refreshConfig(overrides?: Partial<ConfigType>): Promise<void> {
const onchainConfig = await queryAllConfig(this.suiClient, this.network);
this._config = convertOnchainConfig(onchainConfig, overrides);
}


🤖 Generated with Jarvis (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review (on-chain config request path)

這版修正方向很對(fail-fast on fetch/parse、ensureConfig race、refresh override 保留、shared version enrichment 都補上了 👍)。

我再看 queryAllConfig 的請求邏輯後,還有一個高風險邊界:

  1. 目前仍未驗證「必要 config section 是否齊全」

queryAllConfig 結尾直接 return result,但沒有檢查 packageConfig / oracleConfig / objectConfig 是否都存在。若 RPC 回傳不完整(例如 id_vector 少項、未知 type 被 warn 後略過、或服務端只回部分 objects),流程仍會成功返回,後續 convertOnchainConfig 會用空字串/預設值填充,變成延遲失敗。

建議在返回前加 required-section validation(至少三個核心 section + 可選地驗證關鍵欄位非空),缺任一個就 throw,讓初始化在入口就 fail-fast。

參考位置:

try {
if (type.endsWith(TYPE_PACKAGE_CONFIG)) {
result.packageConfig = json;
} else if (type.endsWith(TYPE_ORACLE_CONFIG)) {
result.oracleConfig = json;
} else if (type.endsWith(TYPE_OBJECT_CONFIG)) {
result.objectConfig = json;
} else if (type.endsWith(TYPE_AGGREGATOR)) {
const entries = parseVecMap(json.table);
result.aggregator = { id: json.id as string, entries };
} else if (type.endsWith(TYPE_VAULT)) {
const entries = parseVecMap(json.table);
result.vault = { id: json.id as string, entries };
} else if (type.endsWith(TYPE_SAVING_POOL)) {
const entries = parseVecMap(json.table);
result.savingPool = { id: json.id as string, entries };
} else if (type.endsWith(TYPE_PSM_POOL)) {
const entries = parseVecMap(json.table);
result.psmPool = { id: json.id as string, entries };
} else if (type.endsWith(TYPE_PRICE_CONFIG)) {
const entries = parseVecMap(json.table);
result.priceConfig = { id: json.id as string, entries };
} else {
// eslint-disable-next-line no-console
console.warn(`Unknown object type: ${type} (objectId: ${obj.objectId})`);
}
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
throw new Error(`Failed to process config sub-object ${obj.objectId} (type: ${type}): ${msg}`);
}
}
return result;

🤖 Generated with Rex (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (Round 2)

四個問題都有對應處理,整體方向正確。剩下一個需要留意:


✅ 已修 — queryAllConfig fail-fast
continue 改成 throw,partial config 問題解決。對應測試也更新了,Good。

✅ 已修 — refreshConfig 保留 overrides
overrides ?? this.configOverrides

✅ 已修 — enrichSharedObjectRefs 補 initialSharedVersion
深克隆 + batch fetch + 正確 apply,邏輯清楚。


⚠️ 仍有問題 — _configInitPromise 失敗後無法重試 (confidence: 82)

private async ensureConfig(): Promise<void> {
  if (this._config) return;
  if (!this._configInitPromise) {
    this._configInitPromise = this.refreshConfig(this.configOverrides);
  }
  await this._configInitPromise; // ← 若 reject,下次仍 await 同一個 rejected promise
}

_configInitPromise 失敗後永遠不會被清除。只要 refreshConfig 因為網路閃斷 throw 一次,this._config 仍是 null、this._configInitPromise 是 rejected promise,後續所有 ensureConfig 呼叫都會吃到那個舊的 rejection,client 永久不可用,無法重試。建議在 finally 清除:

private async ensureConfig(): Promise<void> {
  if (this._config) return;
  if (!this._configInitPromise) {
    this._configInitPromise = this.refreshConfig(this.configOverrides)
      .finally(() => { this._configInitPromise = null; });
  }
  await this._configInitPromise;
}

/**
* @description Ensures config is loaded. If not yet fetched, fetches from on-chain.
* Uses cached promise to avoid race when multiple async methods call concurrently.
*/
private async ensureConfig(): Promise<void> {
if (this._config) return;
if (!this._configInitPromise) {


其他部分 LGTM,測試覆蓋也到位。

🤖 Generated with Jarvis (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review (latest push)

queryAllConfig 這次補的 required-section / required-field validation 看起來 OK,方向正確 👍

目前還有一個 blocker 仍在:

  1. _configInitPromise 在初始化失敗後沒有 reset

ensureConfig() 會快取 this._configInitPromise,但若第一次 refreshConfig() reject(例如暫時性 RPC/network error),這個 rejected promise 會一直留著。之後所有 ensureConfig() 都會直接 await 同一個 rejected promise,client 無法自動重試,等於永久卡死。

建議在設置 promise 時加 finally 清理:

if (!this._configInitPromise) {
  this._configInitPromise = this.refreshConfig(this.configOverrides)
    .finally(() => {
      this._configInitPromise = null;
    });
}
await this._configInitPromise;

參考位置:

private async ensureConfig(): Promise<void> {
if (this._config) return;
if (!this._configInitPromise) {
this._configInitPromise = this.refreshConfig(this.configOverrides);
}
await this._configInitPromise;
}

🤖 Generated with Rex (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (Round 3)

Required-section 驗證加了,邏輯清楚,測試覆蓋也很完整。

但上一輪指出的 _configInitPromise 問題還沒修到


⚠️ _configInitPromise 失敗後未清除,client 無法從網路錯誤中恢復 (confidence: 82)

// src/client.ts — 目前狀態
private async ensureConfig(): Promise<void> {
  if (this._config) return;
  if (!this._configInitPromise) {
    this._configInitPromise = this.refreshConfig(this.configOverrides);
    // ← 沒有 finally 清除
  }
  await this._configInitPromise;
}

refreshConfig 因網路閃斷 reject 一次後,_configInitPromise 永遠保留那個 rejected promise,後續所有 ensureConfig 呼叫都重新 await 同一個已失敗的 promise,client 永久不可用。Fix 一行:

this._configInitPromise = this.refreshConfig(this.configOverrides)
  .finally(() => { this._configInitPromise = null; });

/**
* @description Ensures config is loaded. If not yet fetched, fetches from on-chain.
* Uses cached promise to avoid race when multiple async methods call concurrently.
*/
private async ensureConfig(): Promise<void> {
if (this._config) return;
if (!this._configInitPromise) {


其他部分(required-section 驗證、測試覆蓋)全部 LGTM,修完這個就可以 approve 了。

🤖 Generated with Jarvis (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review

Latest commit resolves the remaining blocker.

  • ensureConfig() now clears _configInitPromise on failure, so transient init errors no longer permanently poison the client.
  • Added retry-focused unit coverage (clears _configInitPromise on failure so subsequent calls can retry).
  • Previously discussed on-chain config request-path fixes (fail-fast sub-object handling + required section validation) are still in place.

LGTM. Approving.

🤖 Generated with Rex (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

所有問題都解決了:

  • queryAllConfig fail-fast(不再回傳 partial config)
  • ✅ required-section 驗證
  • refreshConfig 保留 configOverrides
  • ensureConfig race condition
  • initialSharedVersion enrichment
  • _configInitPromise 失敗後清除,允許重試

LGTM 🚀

🤖 Generated with Jarvis (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review (BucketClient initialization changes)

Found 1 issue:

  1. initialize({ configObjectId }) and refreshConfig() now have inconsistent config source behavior. Initial load can come from a custom entry object, but refreshConfig() always calls queryAllConfig(this.suiClient, this.network) and drops that custom source. After first refresh, the client may silently switch back to default network config, which is risky for test/custom deployments. Please persist configObjectId on the instance and reuse it in refreshConfig().

static async initialize({
suiClient,
network = 'mainnet',
configObjectId,
config: configParam,
configOverrides,
}: {
suiClient?: SuiGrpcClient;
network?: Network;
configObjectId?: string;
config?: ConfigType;
configOverrides?: Partial<ConfigType>;
} = {}): Promise<BucketClient> {
const rpcUrl = NETWORK_RPC_URLS[network] ?? NETWORK_RPC_URLS['mainnet']!;
const client = suiClient ?? new SuiGrpcClient({ network, baseUrl: rpcUrl });
let config: ConfigType;
if (configParam) {
config = await enrichSharedObjectRefs(configParam, client);
} else {
const onchainConfig = await queryAllConfig(client, network, configObjectId);
config = convertOnchainConfig(onchainConfig, configOverrides);
config = await enrichSharedObjectRefs(config, client);
}
const bc = new BucketClient({ suiClient: client, network, config });
bc.configOverrides = configOverrides;
return bc;
}
private get config(): ConfigType {
return this._config;
}
/* ----- Getter ----- */
/**
* @description Get this.suiClient
*/
getSuiClient(): SuiGrpcClient {
return this.suiClient;
}
/**
* @description
*/
getConfig(): ConfigType {
return this.config;
}
/**
* @description Re-fetch config from on-chain and update this client's config.
* When overrides is omitted, preserves configOverrides from initialize().
*/
async refreshConfig(overrides?: Partial<ConfigType>): Promise<void> {
const onchainConfig = await queryAllConfig(this.suiClient, this.network);
let config = convertOnchainConfig(onchainConfig, overrides ?? this.configOverrides);

🤖 Generated with Rex (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (Round 5 — 初始化重構)

這次改法方向完全正確,大幅簡化了複雜度。


✅ 設計改進

  • Constructor 改成 config 必填 → 徹底消除 lazy init 邏輯(ensureConfig_configInitPromise 全刪)
  • Getter 全部回到 sync (getConfig(), getUsdbCoinType() 等) → 解決之前的 breaking API change 問題
  • initialize() 加了 configObjectIdconfig 注入點 → 方便測試,不用打真實 RPC
  • queryAllConfig 多一個 optional configObjectId 參數 → 設計一致

整體 LGTM 🚀


⚠️ 一個小地方值得留意(non-blocking)

initialize()configParam 路徑沒有套用 configOverrides

if (configParam) {
  config = await enrichSharedObjectRefs(configParam, client);
  // ← configOverrides 這裡沒有 apply,但 bc.configOverrides 還是設了
} else {
  config = convertOnchainConfig(onchainConfig, configOverrides); // ← 這裡有套用
  config = await enrichSharedObjectRefs(config, client);
}
const bc = new BucketClient({ suiClient: client, network, config });
bc.configOverrides = configOverrides;

結果:傳 config + configOverridesinitialize() 時,初始 config 不含 overrides,但之後呼叫 refreshConfig() 會套用。行為有點不一致。
這個路徑主要給測試用,影響面小;但如果這是 intentional,建議在 JSDoc 補一句說明,避免誤用。


Non-blocking,可 merge。

🤖 Generated with Jarvis (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configObjectId 正確存在 instance、refreshConfig() 也傳進去了,行為一致。JSDoc 也補清楚了(包含 config + configOverrides 的行為說明)。測試直接驗 queryAllConfig 被呼叫時帶了正確的 configObjectId,覆蓋到位。

LGTM,可以 merge 🚀

🤖 Generated with Jarvis (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review

Latest commit addresses the remaining initialization-path concern.

  • BucketClient now persists configObjectId from initialize().
  • refreshConfig() reuses that same configObjectId, so config source no longer silently switches back to default entry config.
  • Unit coverage was added for this exact behavior (refreshConfig() uses configObjectId from initialize).
  • JSDoc clarifications around config + configOverrides behavior are also helpful.

LGTM. Approving.

🤖 Generated with Rex (OpenClaw)

@do0x0ob
Copy link
Contributor

do0x0ob commented Mar 4, 2026


PR 變更摘要

核心變更

  1. On-chain config

    • 從鏈上取得 config,不再使用靜態 CONFIG[network]
    • 新增 bucketConfig.tsconfigAdapter.ts 負責查詢與轉換
  2. BucketClient 初始化

    • 建構子改為必須傳入 config
    • 新增 BucketClient.initialize() 作為唯一從鏈上取得 config 的入口
    • 支援 configObjectIdconfigconfigOverrides 參數
  3. API 調整

    • getConfig() 改為 sync(config 在建構時已存在)
    • 多個 getter 改為 sync(如 getUsdbCoinTypegetAllCollateralTypes 等)
    • 新增 refreshConfig() 用於重新從鏈上取得 config
    • savingPoolWithdraw 改為 sync,與 main 一致
  4. PR review 修正

    • refreshConfig() 會沿用 initialize()configObjectId
    • 補充 JSDoc 說明 config + configOverrides 的行為

其他

  • 測試:E2E 與 unit 測試配合新 API 調整
  • README:更新 Quick Start 與 API 說明
  • .gitignore:加入 .pnpm-store

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — 移除包裝層重構

✅ LGTM

  • private _config + getter 合併成直接的 private config field,更簡潔
  • 所有 _method() private 包裝全部移除,邏輯直接 inline 到 public method — 沒有漏掉任何調用點
  • 內部互呼(aggregatePricesgetAccountPositions 等)已全部更新為直接呼叫 public 方法,行為一致
  • README flashBurn 參數修正(flashMintReceipt)也是正確的

無新問題引入,可以 merge 🚀

🤖 Generated with Jarvis (OpenClaw)

@do0x0ob do0x0ob force-pushed the merge/wl581-onchain-config branch from 2329df6 to f94f48c Compare March 5, 2026 09:25
Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

Found 1 issue:

  1. Silent failure in enrichSharedObjectRefs — when getObjects returns an Error for any object, the error is silently skipped with continue. The ref keeps initialSharedVersion: 0, which will cause PTB rejection at transaction time with no useful error message pointing back to the version enrichment step.

collect(config.SAVING_POOL_INCENTIVE_GLOBAL_CONFIG_OBJ);
collect(config.FLASH_GLOBAL_CONFIG_OBJ);
collect(config.BLACKLIST_OBJ);
for (const priceInfo of Object.values(config.PRICE_OBJS)) {
switch (priceInfo.variant) {
case 'SCOIN':
collect(priceInfo.scoinRuleConfig);

Consider throwing (or at least logging a warning) when an object lookup fails during enrichment:

if (obj instanceof Error) {
  console.warn(`enrichSharedObjectRefs: failed to fetch object for version enrichment: ${obj.message}`);
  continue;
}

This makes the failure surface immediately instead of silently building a broken config.

🤖 Generated with Jarvis (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

Found 1 issue:

Silent error swallowing in enrichSharedObjectRefs — fetch failures leave refs with initialSharedVersion: 0

if (obj instanceof Error) continue;
const owner = (obj as { owner?: { $kind?: string; Shared?: { initialSharedVersion?: string } } }).owner;
if (owner?.$kind === 'Shared' && owner.Shared?.initialSharedVersion) {
versionMap.set(obj.objectId, owner.Shared.initialSharedVersion);
}
}

When client.getObjects() returns an Error for one of the shared-object refs, the loop silently skips it and the ref stays at initialSharedVersion: 0. Sui validators will reject any PTB that references a shared object with initialSharedVersion: 0, producing a confusing low-level error at tx submission time instead of a clear SDK error at initialization.

Suggest either:

// Option A — throw fast (preferred)
if (obj instanceof Error) {
  throw new Error(`Failed to fetch shared object ${obj.objectId} for version enrichment: ${obj.message}`);
}

// Option B — warn and let callers discover the issue
if (obj instanceof Error) {
  console.warn(`enrichSharedObjectRefs: failed to fetch ${obj.objectId}, initialSharedVersion will be 0: ${obj.message}`);
  continue;
}

Everything else looks solid. The on-chain config migration is clean — queryAllConfig fail-fasts on required sections, the configOverrides merge order is correct, and the BucketClient.initialize() async factory pattern is a good API design. The Option<T>bcs.option() and module 'object' casing fixes in _generated/utils/index.ts are correct bug fixes.

🤖 Generated with Jarvis (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix confirmed ✅

The loop was refactored to for (let i = 0; i < objects.length; i++) so objectIds[i] is accessible when logging. The console.warn now surfaces the objectId and error message, making any future enrichment failures debuggable. Reasonable choice to warn-and-continue rather than throw (lets the SDK still function for other refs).

No further issues found. LGTM.

🤖 Generated with Jarvis (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review (latest commit 24295a9)

Found 1 issue:

Missing case 'Unihouse' in getDerivativePrice switch → runtime throw for Unihouse derivatives

? 'BFBTC'
: derivative_kind === 'TLP'
? 'TLP'
: 'sCoin'; // fallback
const priceConfig = this.getPriceConfigInfo(dk);
switch (derivative_kind) {
case 'sCoin':
case 'Scallop': {
if (!('SCOIN' in priceConfig) || !priceConfig.SCOIN)
throw new Error('Unexpected price config variant for sCoin');
const pc = priceConfig.SCOIN;
tx.moveCall({
target: `${pc.package}::scoin_rule::feed`,
typeArguments: [coinType, underlying_coin_type],
arguments: [
collector,
tx.sharedObjectRef(pc.scoin_rule_config),
underlyingPriceResult,
tx.sharedObjectRef(pc.scallop_version),
tx.sharedObjectRef(pc.scallop_market),

The dk mapping explicitly includes 'Unihouse''gCoin':

derivative_kind === 'GCoin' || derivative_kind === 'gCoin' || derivative_kind === 'Unihouse'
  ? 'gCoin'

But the switch (derivative_kind) below only has case 'gCoin': and case 'GCoin': — no case 'Unihouse':. So any coin with derivative_kind === 'Unihouse' will fall through to default: throw.

Fix:

case 'gCoin':
case 'GCoin':
case 'Unihouse': {   // ← add this
  ...
}

Everything else in this refactor looks correct. The type alignment with generated BCS types (SharedObjectRef[''], initialSharedVersion: string, snake_case field names, discriminated union shape for AggregatorObjectInfo / PriceConfigInfo) is clean and consistent throughout configAdapter.ts, client.ts, and tests.

🤖 Generated with Jarvis (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix confirmed ✅ — case 'Unihouse': added to the same block as gCoin/GCoin, consistent with the dk mapping. No further issues found. LGTM.

🤖 Generated with Jarvis (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

Found 1 issue:

  1. Wrong field name in test — config.PSM_OBJS should be config.PSM_POOL_OBJS (field was renamed in this PR but the test wasn't updated)

await run('getAllOracleCoinTypes', () => Promise.resolve(bucketClient.getAllOracleCoinTypes()));
await run('getAllCollateralTypes', () => Promise.resolve(bucketClient.getAllCollateralTypes()));
await run('getAggregatorObjectInfo', () =>

config.PSM_OBJS doesn't exist on ConfigType — the correct field is PSM_POOL_OBJS. The ?? {} fallback silently hides the typo, so firstPsmCoinType will always fall back to usdcCoinType instead of reading from actual config.


Everything else looks solid. The on-chain config migration is well-structured:

  • Fail-fast validation in queryAllConfig
  • enrichSharedObjectRefs dedup + warn-on-error is reasonable ✅
  • snake_case field rename (rewarder_id, reward_type, etc.) is consistent across client + types ✅
  • get_realtime_reward_amount refactor for saving pool rewards (avoids PTB ref limitation) ✅
  • getUsdbSupply switch from PTB simulation to stateService.getCoinInfo
  • Error handling upgraded from silent-empty to explicit throws ✅

🤖 Generated with Jarvis (OpenClaw)

Copy link
Collaborator

@bucket-bot bucket-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

The fix looks good — PSM_OBJSPSM_POOL_OBJS is corrected, and the new assertions (toBeGreaterThan(0) for PSM/saving/collateral pools) properly fail-fast instead of silently falling back. No further issues found.

🤖 Generated with Jarvis (OpenClaw)

do0x0ob and others added 26 commits March 9, 2026 18:06
…ides, ensureConfig race, initialSharedVersion enrichment

- queryAllConfig: fail-fast on sub-object fetch/parse errors instead of returning partial config
- refreshConfig: preserve configOverrides when called without args
- ensureConfig: use cached promise to prevent concurrent RPC calls
- enrichSharedObjectRefs: fetch initialSharedVersion for refs with objectId-only (fixes PTB tx failure)
- Add unit tests for all four fixes

Made-with: Cursor
…onfig) before queryAllConfig return

- Add required-section and key-field validation to fail-fast on incomplete RPC response
- Throw when any required section is missing or key field is empty
- Add unit tests for validation failure cases

Made-with: Cursor
- bucketConfig: missing oracleConfig/objectConfig, empty pyth_state_id, empty treasury_obj
- configAdapter: empty objectId skip, deduplication, non-Shared owner skip, no-refs case
- client: explicit refreshConfig overrides, ensureConfig when config already loaded

Made-with: Cursor
…edge tests

- client: clear _configInitPromise in ensureConfig catch so subsequent calls can retry
- client: fix mockImplementation type for queryAllConfig
- test: add reverse tests (enrichSharedObjectRefs immutability, refreshConfig({}), no retry on success)
- test: add expect.fail pattern for required section throw case
- prettier

Made-with: Cursor
- Add await to all async method calls (getAllCollateralTypes, build*,
  flashMint/flashBurn, getAggregatorObjectInfo, getVaultObjectInfo, etc.)
- Update version 1.2.8 -> 2.0.1
- Fix Example Project link (client.test.ts -> test/e2e/ directory)
- Constructor config is optional (lazy-load from chain)

Made-with: Cursor
Made-with: Cursor
- Replace private _config + getter with direct config field (match main style)
- Fix flashBurn param: use flashMintReceipt in README Pattern 2
- Update example project path to actual e2e test files
- Bump version in README to 2.0.1

Made-with: Cursor
…config

- Replace _generated_onchain_config with _generated/bucket_onchain_config
- Update BucketOnchainConfig to use raw on-chain JSON shape (Record<string, unknown>)
- bucketConfig now defines local types; generated modules export BCS structs, not TS types
- Add bucket_onchain_config bindings (copied from external source)
- Remove codegen tooling (sui-codegen.config.ts, scripts, @mysten/codegen)
- Add sui-codegen.config.ts to .gitignore, pnpm-lock.yaml to .prettierignore

Made-with: Cursor
…jectRefs

Silent failure left initialSharedVersion at 0 when object fetch failed,
causing PTB to fail at execution with no clear indication of the cause.
Now logs objectId and error message for easier debugging.

Made-with: Cursor
…ated types

- Use SharedObjectRef, RewarderInfo, PsmPoolObjectInfo from generated $inferType
- Switch config parsing to snake_case and enum shapes ({ Pyth: {...} }, { SCOIN: {...} }, etc.)
- Keep manual types for AggregatorObjectInfo, PriceConfigInfo (MoveEnum requires $kind; we parse from JSON)
- Keep manual types for VaultObjectInfo, SavingPoolObjectInfo (optional rewarders/reward when empty)
- Change initialSharedVersion to string type to match BCS schema
- Update client.ts, configAdapter, and all related tests

Made-with: Cursor
On-chain derivative_kind can be 'Unihouse'; dk mapping already maps it to
'gCoin', but the switch lacked the case and threw Unsupported derivative kind.

Made-with: Cursor
…rnType

- Replace simulateTransaction + borrow_cap_mut with stateService.getCoinInfo
- PTB cannot pass &mut references between commands; SDK v2 validates and rejects
- Tighten test assertion: expect supply > 0 (was >= 0, masked failures)

Made-with: Cursor
…ls until realtime_reward_amount_by_manager)

Made-with: Cursor
…s, unit tests

- Add all-query-outputs.test.ts with getAccountBorrowRewards using coinTypesWithRewarders
- Add unit tests for client, bucketConfig, configAdapter, resolvers, transaction
- Extend cdp.test, oracle.test

Note: getAccountSavingPoolRewards / getUserSavings / getAccountSavings tests remain
skipped — not yet fixed (awaiting contract upgrade for PTB reference limitation).

Made-with: Cursor
- Replace get_rewarder + realtime_reward_amount with single get_realtime_reward_amount
  call to avoid PTB reference limitation (references cannot be passed between commands)
- Simplify response parsing: one result per reward type instead of two
- Re-enable savings-rewards e2e tests (getUserSavings, getAccountSavings,
  getAccountSavingPoolRewards) now that contract upgrade is deployed
- Remove pool: 'forks' from vitest config

Made-with: Cursor
- Assert required config (PSM_POOL_OBJS, SAVING_POOL_OBJS, collateral types) at start; fail fast if missing
- Add runOrSkip for APIs that need optional config (e.g. vault with rewarders); record explicit skip reason
- Use config-derived values directly instead of ?? fallbacks (firstPsmCoinType, firstLpType, firstCoinType)
- Fix PSM_OBJS typo → PSM_POOL_OBJS
- Mark skipped APIs with ⊘ in report output

Made-with: Cursor
- cdp: use it.skipIf for buildClosePositionTransaction when test account has no SUI position
- savings-rewards: use it.skipIf for getAccountBorrowRewards when API returns no SUI entry
- vaults: assert flowRates non-empty when SUI vault has rewarders
- rewards: assert reward balance changes when buildClaimBorrowRewards returns coins
- all-query-outputs: add aggregateBasicPrices with multiple Pyth coin types (4)

Made-with: Cursor
…Type); add onchain-config tests + lint fix

Made-with: Cursor
@do0x0ob do0x0ob force-pushed the merge/wl581-onchain-config branch from c37fb58 to fbeb4a0 Compare March 9, 2026 10:25
@JustaLiang JustaLiang merged commit ebd8883 into main Mar 10, 2026
3 of 4 checks passed
@JustaLiang JustaLiang deleted the merge/wl581-onchain-config branch March 10, 2026 10:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants