Skip to content

cetus-yield-agent: extract range strategy, harden tx flow, migrate to SDK v2#33

Open
RandyPen wants to merge 5 commits into
holonym-foundation:mainfrom
RandyPen:main
Open

cetus-yield-agent: extract range strategy, harden tx flow, migrate to SDK v2#33
RandyPen wants to merge 5 commits into
holonym-foundation:mainfrom
RandyPen:main

Conversation

@RandyPen

@RandyPen RandyPen commented Jun 9, 2026

Copy link
Copy Markdown

Four focused commits that improve the cetus-yield-agent template along two
axes: (1) safer rebalance flow on the live agent, and (2) bring the codebase
forward to the latest @mysten/sui and Cetus CLMM SDKs so it reads cleanly
as a reference for Sui developers.

Summary

  • Range decisions live in one place. A new strategy.ts module exposes a
    pure decideRange({pool, tickHistory, balances, trigger, config}) that
    returns {tickLower, tickUpper, sizingFraction, reason, signals}. Both the
    initial open and the rebalance reopen consume the same decision, so future
    range signals (fee tier, depth, yield-scan APY delta, directional bias)
    plug into one well-typed function instead of being threaded through the IO
    loop. The function is side-effect-free and unit-testable.

  • Transaction finality is verified. A new waitForFinality(digest, label)
    wraps the gRPC client's waitForTransaction and checks
    effects.status.success before the flow proceeds. The old code waited
    setTimeout(5000) after the remove tx and barreled into the reopen step —
    if the remove silently reverted (or just hadn't finalized yet), the agent
    would build the reopen against pre-remove balances. On any non-success
    status the rebalance now aborts and logs the failure for triage.

  • Crash recovery refuses to compound a partial rebalance. A rebalance
    intent file (remove_pending → open_pending → cleared) is written before
    each on-chain step. On startup, reconcileIntent() checks the intent
    against live position state. The dangerous case — a crash between remove
    and open — used to look like "no positions, time to do a fresh open" and
    would deploy (full post-remove balance × open fraction) instead of the
    planned sizing. The reconciler now refuses to auto-recover from
    open_pending with no live position, exits non-zero, and surfaces a
    Matrix alert so an operator inspects the intent file by hand.

  • DRY_RUN simulates via the gRPC client. Setting DRY_RUN=true routes
    every submission through chain.simulateTransaction({ transaction, include: { effects, balanceChanges, events } }) instead of waap-cli send-tx. The
    helper logs effects, gas, and balance changes. A sentinel digest threads
    through waitForFinality so the whole flow runs end-to-end in simulation,
    which lets an operator validate a planned action against current chain
    state before flipping DRY_RUN=false for live submission.

  • SDK migration: latest @mysten/sui + Cetus CLMM v2 SDK. Drops the
    legacy @cetusprotocol/cetus-sui-clmm-sdk@5.4 (which kept the agent
    pinned to @mysten/sui v1 because it imported the removed SuiClient
    symbol). Now uses:

    • @mysten/sui ^2.17.0
    • @cetusprotocol/sui-clmm-sdk ^1.4.5
    • @cetusprotocol/common-sdk ^1.3.7

    All chain access — reads, finality waits, and simulation — flows through
    a single cetus.FullClient (a SuiGrpcClient & ExtendedSuiClient), so
    the agent is gRPC end-to-end. Position discovery uses
    sdk.Position.getPositionList(owner, [POOL_ID]) with a server-side pool
    filter, replacing the getOwnedObjects scan + fields.bits decoding for
    i32 tick indices. The remove call now passes the pool's actual
    rewarder_coin_types (was hardcoded to [], which left active emissions
    uncollected). The open path uses createAddLiquidityFixTokenPayload
    is_open and pos_id are documented public params in v2, replacing the
    as unknown as ... typecast escape hatch the old code needed.

    USDC detection (is it coin_type_a or coin_type_b on this pool?) is
    now dynamic via pool.coinTypeA === USDC_TYPE, so the agent works on
    any USDC pair, not just SUI/USDC.

Why this matters for a reference template

This activity is what new Sui builders fork to get a working CLMM yield
agent on day one. Before this PR the template:

  • Mixed in undocumented SDK fields (is_open, pos_id) via a typecast.
  • Did not wait for tx finality, opening a small window where a partial
    rebalance could deploy multiples of the planned capital.
  • Used @mysten/sui v1 because the bundled Cetus SDK couldn't be bumped.

After this PR it shows the canonical v2-SDK build-and-submit pattern, the
canonical gRPC simulateTransaction flow, and the kind of crash-recovery
guard a 24/7 onchain agent actually needs.

Files

  • agents/cetus-yield-agent/templates/standalone/agent.ts.tpl — main rewrite
  • agents/cetus-yield-agent/templates/standalone/strategy.ts — new pure module
  • agents/cetus-yield-agent/templates/standalone/package.json.tpl — dep bumps
  • agents/cetus-yield-agent/templates/standalone/dot-env.exampleDRY_RUN, INTENT_FILE, gRPC URL doc
  • agents/cetus-yield-agent/activity.json — manifest exposes the new env vars to the catalog

Test plan

  • tsc --noEmit on the rendered template against @mysten/sui@^2.17.0 +
    @cetusprotocol/sui-clmm-sdk@^1.4.5 (verified locally — clean).
  • Scaffold a fresh project via npx @human.tech/create-agent-wallet --activity cetus-yield-agent --runtime standalone against this branch
    and confirm npm install resolves.
  • AGENT_MODE=monitor cycle on testnet — verify the strategy preview
    shows up in position_status events and volatility samples populate.
  • AGENT_MODE=active DRY_RUN=true on mainnet against the SUI/USDC pool
    — confirm dry_run_simulated events log effects + gas without any
    waap-cli send-tx invocation.
  • AGENT_MODE=active DRY_RUN=false on a low-value mainnet pool — observe
    one full rebalance cycle: intent_written
    remove_liquidity_completeintent_written(open_pending)
    rebalance_complete.
  • Simulated crash test: kill the process between remove finality and
    open submission (e.g. SIGKILL after the remove_liquidity_complete
    event), then restart and confirm intent_reconcile_critical fires and
    the agent exits non-zero instead of auto-opening.

RandyPen and others added 5 commits June 9, 2026 14:13
…ion, BigInt price math

Harden the standalone template's swap path and document the 2PC signing
boundary so the project is more usable as a third-party reference.

- README: add "Anatomy of send-tx" section explaining the agent / waap-cli /
  WaaP-server / fullnode data flow, why the subprocess boundary keeps key
  material out of the agent address space, and a pseudocode equivalent for
  developers integrating their own 2PC backend.
- agent.ts.tpl: replace the string-sniffing guessDecimals with a Map-backed
  cache populated via sui.getCoinMetadata. Decimals are preloaded in main()
  for TARGET and QUOTE; getPoolState additionally warms whichever coins the
  pool itself uses.
- agent.ts.tpl: after waap-cli send-tx, call sui.waitForTransaction with
  showEffects + showBalanceChanges. On failure, throw so the tick handler
  records rebalance_failed instead of pretending the swap succeeded. On
  success, extract the agent's positive balance change for the output coin
  and log both the raw and human-decimals amount received.
- agent.ts.tpl: rewrite calculatePrice so the (sqrtPriceX64)^2 / 2^128
  reduction stays in BigInt until a scaled Number conversion at the end,
  avoiding precision loss for pools where the intermediate exceeds 2^53.
  Replace the brittle .includes() coin-type comparison with a normalized
  full-string equality check (addresses padded to 0x + 64 hex chars).

Signed-off-by: Randy Pen <pzm16@tsinghua.org.cn>
Pulls the volatility-adaptive half-width calculation and the open/reopen
sizing rule into a pure, side-effect-free strategy module exposing a
single decideRange() entry point. Both the initial open and the rebalance
reopen now consume one RangeDecision (tickLower, tickUpper, sizingFraction,
reason, signals) instead of duplicating the tick math and bookkeeping.

This is the place to plug in new range signals (fee tier, depth,
yield-scan APY delta, directional bias) without touching the IO loop, and
makes the math unit-testable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Randy Pen <pzm16@tsinghua.org.cn>
…e on startup

Two related safety changes for the active-mode rebalance flow:

1. waitForFinality(): after every signAndSendTx, block on
   sui.waitForTransaction and verify effects.status === 'success'. The
   previous flow replaced this with a blind setTimeout(5000) after the
   remove tx, which let the agent proceed to the reopen step even when the
   remove had silently reverted (or had not finalized at all, leaving the
   reopen to build against pre-remove balances). On any non-success status
   the rebalance now aborts and logs the failure for triage.

2. Rebalance intent file: written before each on-chain step
   (remove_pending → open_pending → cleared on success) and reconciled at
   startup. The dangerous case is a crash after remove but before open
   succeeds — without intent tracking the next cycle sees "no positions"
   and triggers the initial-open path, depositing
   (full post-remove balance × open fraction) instead of the planned
   sizing. The reconciler refuses to auto-recover from open_pending and
   exits non-zero with a Matrix alert so an operator inspects the file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Randy Pen <pzm16@tsinghua.org.cn>
When DRY_RUN=true, signAndSendTx routes through the Sui gRPC client's
dryRunTransaction endpoint instead of submitting via waap-cli. Effects,
gas, and balance changes are logged so an operator can validate a planned
remove or open against current chain state before flipping back to live
submission. The submission returns a DRY_RUN sentinel that
waitForFinality recognizes, so the rest of the flow proceeds end-to-end
in simulation.

Method-name note: this calls core.dryRunTransaction (v1.x SDK name). The
v2 SDK renamed it to core.simulateTransaction — same wire-level gRPC
service. The agent stays on @mysten/sui ^1.14.x because
@cetusprotocol/cetus-sui-clmm-sdk@^5.4.0 still imports the legacy
SuiClient export that v2 dropped. Rename once Cetus ships v2-compatible.

Also documents DRY_RUN, INTENT_FILE, SUI_GRPC_URL, and MIN_SAMPLES_FOR_ADAPTIVE
in dot-env.example and the activity manifest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Randy Pen <pzm16@tsinghua.org.cn>
…/sui-clmm-sdk v1.4

Drops the legacy @cetusprotocol/cetus-sui-clmm-sdk@5.4 (which still pinned
the agent to @mysten/sui v1 because it imported the removed SuiClient
symbol) and switches to the v2-native @cetusprotocol/sui-clmm-sdk@^1.4.5
plus @cetusprotocol/common-sdk@^1.3.7.

Concrete changes:

- One client: sdk.FullClient is a SuiGrpcClient & ExtendedSuiClient, so
  getBalance, getPositionList, waitForTransaction, and simulateTransaction
  all flow through a single gRPC connection. No more parallel JSON-RPC
  SuiClient + separate gRPC client for dry-run.

- Position discovery via sdk.Position.getPositionList(owner, [POOL_ID]) —
  server-side pool filter replaces the StructType-filtered getOwnedObjects
  scan + manual fields.bits decoding for tick indices.

- removeLiquidityPayload now passes the pool's actual rewarder_coin_types
  so rewards are collected on remove (was hardcoded to [], which left
  rewards on the table for pools with active emissions).

- createAddLiquidityFixTokenPayload replaces the createAddLiquidityPayload
  + `as unknown as ...` typecast escape hatch. is_open and pos_id are
  documented public params in v2, no longer undeclared runtime hacks.
  Detects whether USDC is coin_a or coin_b on the pool instead of
  assuming, so the agent works on any USDC pair, not just SUI/USDC.

- Dry-run uses chain.simulateTransaction({ transaction, checksEnabled,
  include: { effects, balanceChanges, events } }) — the proper v2 gRPC
  method, accepting the Transaction object directly instead of base64
  bytes. waitForTransaction migrated to the same v2 gRPC shape.

- SUI_RPC and SUI_GRPC_URL env vars collapse to one configurable gRPC
  endpoint; both names accepted for back-compat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Randy Pen <pzm16@tsinghua.org.cn>
@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

@RandyPen is attempting to deploy a commit to the Holonym Team on Vercel.

A member of the Team first needs to authorize it.

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.

1 participant