test(swap): add happy-path coverage for openocean/lifi/relay/liquid providers - #18
Merged
Merged
Conversation
…roviders PR #16 added 5 swap.test.ts cases focused on the kyber native-input msg.value bug + chain-support guards. The other 4 providers had no shape-pinning happy path. This commit adds one mocked happy-path test per provider so a future printOutput refactor or provider HTTP shape change is caught at PR time, not in production. 4 new tests in commands/swap.test.ts: - openocean: native input on Monad. Mock /v4/{chain}/swap response; assert provider/chain/amount_out/router fields in output. - lifi: native input on Monad. Mock /v1/quote response; assert chain_id=143 (LI.FI is chainId-routed, not slug). - relay: mock the {steps[],details:{currencyOut}} multi-step response; assert the CLI skips the approve step and reports the swap step's router + currencyOut.amount. - liquid: happy path on hyperevm. The branch's ERC20-approval path can also surface as an error envelope when token resolution fails; the test accepts either shape but pins liquid as the routing target. All 4 use the same vi.mock(globalThis.fetch) helper introduced in PR #16; tests run offline and are deterministic. Verified: - pnpm -C ts -r build — clean. - pnpm -C ts -r lint — 3 packages, tsc --noEmit clean. - pnpm -C ts -r test — defi-core 32/32, defi-protocols 43/43, defi-cli 98/98 (was 94; +4 provider happy-path tests).
4 tasks
Hiksang
added a commit
that referenced
this pull request
May 6, 2026
lp.ts is 1803 LOC — the largest still-untested CLI handler at the
start of this PR stack. The full surface (add/remove/farm/claim/
discover/pipeline/compound/autopilot/positions) needs to be split
across multiple PRs. This commit covers only the smallest leaf
(`lp positions`) so the basic --chain / --protocol / --address
routing has shape-pinning regression coverage; other subcommands
will follow in their own PRs.
4 new tests in lp-positions.test.ts:
- errors when --chain is missing (no protocol enumeration)
Asserts the standard "--chain is required" error envelope.
- returns an empty array when balanceOf returns 0n across every
protocol on the chain
The viem.readContract mock returns 0n for every NPM contract;
every protocol's NFT enumeration short-circuits and the output
is the canonical empty array.
- --protocol filter narrows enumeration to a single protocol
Pins that --protocol controls the loop scope.
- --address parameter overrides DEFI_WALLET_ADDRESS without throwing
Pins the resolveAccount priority and ensures opts.address wins.
Mock strategy:
- vi.mock("viem"): createPublicClient.readContract returns 0n,
so every NPM balanceOf reports zero NFTs and the loop is
skipped. http() is a no-op factory.
- vi.mock("@hypurrquant/defi-protocols"): createMerchantMoeLB
returns an adapter whose discoverRewardedPools / etc. return [],
so the LB scan path doesn't reach the network either.
- All other adapter constructors fall through to the real
implementations because the test protocols (uniswap-v2-monad,
uniswap-v3-monad) don't trigger them in the empty-positions
branch.
Verified:
- pnpm -C ts -r build — clean.
- pnpm -C ts -r lint — 3 packages, tsc --noEmit clean.
- pnpm -C ts -r test — defi-core 32/32, defi-protocols 43/43,
defi-cli 98/98 (+4 lp-positions tests on
a baseline of 94 swap tests; the merged
main after PR #18 will be 102 once both
land).
Hiksang
added a commit
that referenced
this pull request
May 8, 2026
…5-5v8g) Resolves Dependabot alert #18 — moderate-severity XSS in ip-address@<=10.1.0 surfaces (Address6.group/link/spanAll and AddressError.parseMessage) when consumers HTML-render their output. The transitive path is: @hypurrquant/defi-cli → @modelcontextprotocol/sdk → express-rate-limit → ip-address Practical exposure here is zero — defi-cli uses the MCP SDK only over stdio (no HTML rendering, no Address6 surface) — but the override removes the alert and prevents future exposure if a consumer wires the SDK into a webview or admin UI. Verified: • pnpm audit → "No known vulnerabilities found" • pnpm -r test → 189/189 unit tests pass — no regression Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks
Hiksang
added a commit
that referenced
this pull request
May 8, 2026
…ridge feature complete, ULTRAQA sandbox (#25) * chore(hyperevm-lending): document live borrow lifecycles for HyperLend + HypurrFi Both protocols had `verified = true` from cast-call only. Today both ran their full Aave V3 borrow lifecycle live on HyperEVM mainnet: HyperLend (5/5 confirmed): - supply 1.0 USDC 0x2822360f5fbdddfb06daa496060761b3604d9c6ad798e924cbfd8595abc6da78 - toggle-collat USDC 0x44d93294a13e3ce292e04032189cd4142faed50b837f60139732fec139cc57d2 - borrow 0.3 USDC 0x0c9a4b4e00904992ec755a30687f8459c4bd9b3eaaca8fe813d651b658fcd9f5 - repay 0.3 USDC 0xc0ccb70fd0fa21ad12e08294e287e3906e120a2230e23ffa661f20616f45f08c - withdraw 1.0 USDC 0x2ece80730c5becbc8087b834d13ce65ebea43b1e5e7ce63f13ccde262af10b9f HypurrFi Pooled (5/5 confirmed): - supply 0.5 USDC 0xc16e0f436033c024351719a2faba1bc02953382c7088a242c890f21cc2a2832e - toggle-collat USDC 0xc3c21a230204717e3f786912989104d7de7e5fa44f829839765a5c39ee91461c - borrow 0.15 USDC 0x316907536898da32a807ba28ebb81f14822601b7c882ee92d6b40a24f66f9537 - repay 0.15 USDC 0x8c1d8814349d24687d3b10b2b2e05407296843519018db4776ebac75b2411dc8 - withdraw 0.5 USDC 0xf2780aab3c3f97c39df5c688ef333f7191fbc856ad812d56c48c1824d6573b0e Description in TOML mirrors gain the live-broadcast date + first tx hash. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(solidly): emit LP-token approval on remove + verify Aerodrome lifecycle Aerodrome (and other Solidly V2 forks) require the LP pair token to be approved to the router before `removeLiquidity` runs — the router calls `transferFrom(LP, sender, pool, liquidity)` and reverts otherwise. The current adapter only emitted approvals for the underlying tokenA/tokenB on add, never for the LP token on remove, so any Solidly remove broadcast without a manual prior approve would fail at gas ~42k. Discovered live on Aerodrome USDC/USDT (vAMM-USDC/USDT) — the first remove broadcast reverted (tx 0x6d052e0a…3298), recovered with a manual `token approve` (tx 0xa126fc3a…8bfa) and re-broadcast (tx 0xa99203f0…89dc). Fix: - defi-core/types.ts: add `pool?: Address` to `RemoveLiquidityParams`. The adapter uses this to know which LP pair to approve. Optional so V3/CL/NFT-style removes (which don't need an LP-token approval) ignore. - solidly.ts buildRemoveLiquidity: when params.pool is set, attach an `approvals[{token: pool, spender: router, amount: liquidity}]` entry so the executor auto-approves before broadcasting remove. - defi-cli/lp.ts lp remove: resolve --pool via existing `resolvePoolAddress` helper (handles both `TOKEN_A/TOKEN_B` symbol and raw `0x` address), pass through to the adapter. Live verification (Base mainnet): - pre-swap 0x7f4e2dc82eb2d38a64aac48363083ab7f53abad242b7d04b32d8a2b7065bdbd1 (1 USDC → 0.998 USDT via lifi) - add liquidity 0x938234eb27856838360da58c64c58776d5d21e0a4b68f968ad4f4beb1b9a3c39 (0.5 USDC + 0.5 USDT into vAMM-USDC/USDT) - failed remove 0x6d052e0a984b22391efb7ca0c0d302d5d2f88216d23a4eeae55fba176bda3298 (revert at gas 42833 — proves the bug) - LP approve 0xa126fc3a21c8864a84e113d274457ace844e1022e3ee10642483218f430f8bfa - remove 0xa99203f0ff22cdacc220b8461b0e60e124d40cf6526673555987c0acb41689dc (success) aerodrome_base.toml gains `verified = true` and an inline live-tx note. Other Solidly forks (Thena V1, Ramses HL) inherit the fix automatically. defi-protocols 55/55 + defi-cli 102/102 still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(uniswap-v2): emit LP-token approval on remove + verify Monad lifecycle UniswapV2 forks have the same LP-approval requirement as Solidly: the V2 router calls `transferFrom(LP, sender, pair, liquidity)` on removeLiquidity and reverts at gas ~42k otherwise. Mirror PR #25's solidly.ts fix on the uniswap_v2 adapter so all V2 family forks (Uniswap V2 Monad, PancakeSwap V2 BNB, ApeSwap, Biswap, BabyDoge, Bakery, BSCSwap, FstSwap) inherit the fix. Live verification on Uniswap V2 Monad mainnet: - prepare: wrap 1 MON 0x6a7d6679e3a63769e22e6d77dd8f52bffc7b40c323fcd57e6362b3b13d814397 - prepare: swap MON→AUSD 0x81f2a373730ade5b266698d121140bec5a7893b46c8e1f302eaf0cf7e2c81618 (lifi: 1 MON → 0.052 AUSD) - add liq: 0.5 WMON + 0.016 AUSD → vAMM 2860207533 LP wei 0xf052493cc1b9caabf8dd86a757896b7625f11e1b168f05804a5637e4284fa324 - failed rm: pre-fix dist 0x2268659a04beb74049c85b42f4ead0d098b38ca9976b65444fb84e2197a909ae (status 0x0, gasUsed 300000 == limit) - failed rm: pre-fix-2 dist 0xdc0a544a5deeb351b64bede081c1c98184500965279e640e770f975cd64d24d5 (still no approval) - success: post-fix dist 0x0f7b3f7b64322801589412e470142880cd19fa4aad8af552710c421dfb08b553 (status 0x1, LP transferred + WMON+AUSD returned to user) uniswap_v2_monad.toml gains `verified = true` and an inline live-tx note. defi-protocols 55/55 + defi-cli 102/102 still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(nest): off-chain liquidity-pools API + lp discover surfaces real APR Pre-PR `defi --chain hyperevm lp discover --protocol nest-v1` returned 0 pools — the on-chain ve(3,3) gauges hard-wire `rewardRate=0` (NEST credits emissions off-chain via signed claim tickets), so the SolidlyGaugeAdapter correctly read 0 reward rate everywhere and the discover loop's emissionOnly post-filter stripped them out. Add an off-chain emission surface so users see the real per-pool APR. Changes: - nest_offchain.ts: new `getLiquidityPools()` returning pool/gauge/token metadata + `aprPercent` + `tvlUSD` + `curEpochEmissionRewardsUSD` from `/api/blaze/liquidity-pools` (37 pools today, 30 with emissions). - lp.ts discover loop: when `protocol.reward_strategy === "off_chain_api"`, bypass the on-chain gauge reader and use the API. Source labeled `off_chain_api` so callers can distinguish. - Discover post-filter (--emission-only): widen the "actually distributing rewards" predicate to also accept `aprPercent > 0`, alongside the existing `moePerDay > 0` (Merchant Moe LB) and `rewardRate > 0` (on-chain gauges) signals. NEST entries weren't passing because they carry neither moePerDay nor rewardRate. - DiscoveredPool type: add `off_chain_api` source variant + new `emissionUsd` field. Verified live: `defi --chain hyperevm --json lp discover --protocol nest-v1 --emission-only` now returns 30 pools sorted by APR — top KNTQ/kHYPE 802% ($1310/epoch), NEST/WHYPE 173% ($6957/epoch on $204k TVL). defi-protocols 55/55 + defi-cli 102/102 still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(curve): target the pool not router + use StableswapNG dynamic uint256[] ABI Two cascading bugs in the Curve adapter were silently breaking every add/remove liquidity broadcast. Both reverted at the dispatcher (gas ~21k) with no error string, so the failure mode was opaque. Bug 1 — wrong target: `buildAddLiquidity` and `buildRemoveLiquidity` were sending calldata to `this.router`. Curve's add_liquidity / remove_liquidity live on the pool itself (the StableswapNG router does not proxy them); the caller must identify which pool. The new code requires `params.pool` and uses it as the tx target. Token approvals (which the pool — not the router — pulls via transferFrom) are now scoped to the pool too. Bug 2 — wrong ABI: `buildAddLiquidity` was encoding the legacy fixed-N `add_liquidity(uint256[2], uint256)` selector (0x0b4c7e4d). Curve's StableswapNG factory deployments (HyperEVM, Sonic, Fraxtal etc.) use the *dynamic* `uint256[]` shape — selector 0xb72df5de — and revert immediately on the older selector. Updated `poolAbi` to the dynamic form so viem re-encodes both add+remove calldata accordingly. Verified live on HyperEVM Curve USDC/USDT0 pool 0x703b14a4…0e9f: - failed (legacy ABI): 0x1f096212f052cd0a5a691d6ea9d6a01fd9903b4f6ee15d2a773286b21bbec11c (status 0x0, gasUsed 21713) - USDC approve: 0x9178f0d81eb6c9a61e55e170aa857e36907b1892734ddde4c1656abaa21566fb - USDT0 approve: 0xb8c5b6252c336c604c862ecfc353f74d224c59f5c938e061fdf7f82b8f99ef30 - add 0.1+0.1 (NG ABI): 0x308aa7ff46842403d248c88fb962e168f8eb01d378fa2b7ddb04c494464a03a5 (status 0x1, 5 logs) - remove all LP (NG): 0x2c279341c840b1aec4d6edae1dee03918eaf89d755204d4c63c4df6f50cf5211 (status 0x1, 4 logs) Note: Curve LP tokens are minted by the pool itself, so remove_liquidity burns from msg.sender's balance directly — no transferFrom and therefore no LP approval needed (unlike Solidly/UniswapV2). curve_hyperevm.toml description gains the live broadcast tx hashes. defi-protocols 55/55 + defi-cli 102/102 still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(hyperevm-v3): document live V3 NFT lifecycle for Project X + HyperSwap V3 Both Project X and HyperSwap V3 had `verified = true` from cast-call verification. Today both ran the full Uniswap V3 NFT mint+remove cycle on HyperEVM mainnet against their stable USDC/USDT0 fee=100 pools. Project X (V3 NFT lifecycle): - mint 0.1 USDC + 0.1 USDT0 → tokenId 436298 0xc8e01517059833a3c38f0a9c160789f7610903e921aa8fd131752f9409c57db0 - remove tokenId 436298 (collect+decreaseLiq+collect+burn) 0x70278bbac92c7887e5c70c5a8dba9947e822728f02577c2c1ef9a07c30f16b3e HyperSwap V3 (V3 NFT lifecycle): - mint 0.1 USDC + 0.1 USDT0 → tokenId 174094 0xd9a078bf384801584dccebab64e1313f2859c4c2a10e35ccaf29bcf2d07b0184 - remove tokenId 174094 0x6c5733cc00298f6b93a6a3e214d340928f55bb5467f07a5750fb010c30da1d81 This validates the shared `uniswap_v3` adapter's NFT mint+remove path across HyperEVM V3 forks. Description in TOML mirrors gain the live tx hashes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(pancakeswap-v2-bnb): document live BNB add+remove lifecycle PancakeSwap V2 BNB had verified=true from cast-call only. Today we ran the full add+remove liquidity cycle on BNB mainnet against the canonical USDT/USDC pool 0xec6557348085aa57c72514d67070dc863c0a5a8c. Live txs (BNB mainnet, USDT 18-decimal): - prep: swap 0.1 USDT → USDC (lifi) 0xd694179269623cd48f7feba8ffa3956ffc6bca1a6da1659cdf4fc91e5ac96312 - add 0.05 USDT + 0.05 USDC 0x9cc61459d3c5d19e997ae61e7544df0d885b0f4eee7fc745cf4ac2c0cdb100f3 (returns 0.0383 Cake-LP) - failed remove (over-asked) 0x6fb98c1066eea50c8248fb46d0300b18135c1a14608895634d2d274a1589d374 (asked 0.05 LP, only had 0.0383 — INSUFFICIENT_LIQUIDITY) - remove 0.0383 Cake-LP 0x98acd872f819835a37af4861e566a475a3385d5680ee6588b8cfdbef5596e7a1 (success, 152k gas) Same V2 LP-approval fix from feat/verify-multichain-lifecycles applies on BNB. Description in TOML mirrors gain the live tx hashes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(uniswap-v3-bnb): register position_manager `defi lp add --protocol uniswap-v3-bnb` was failing with "Position manager address not configured" because the V3 NPM was not registered alongside router/factory/quoter. Add the canonical Uniswap deployment address (0x7b8A01B3…0613) so V3 NFT mint+remove broadcasts can dispatch. Caveat: a smoke-test broadcast with USDT/USDC fee=100 + ±1% range reverted at the NPM (tx 0x62d52640…3c02, gasUsed 177582, status 0x0). Likely a tick / slippage issue on the BNB pool; left for a follow-up once we have time to debug. Address itself is correct (matches Uniswap governance docs); registering it unblocks all future V3 BNB experiments. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(bnb-dex): document live add+remove for Thena V1 + PancakeSwap V3 BNB Two BNB DEX adapters get full add+remove liquidity verification on mainnet today. Thena V1 (solidly_v2 — same family that drove the LP-approval fix earlier in this branch): - add 0.05+0.05 USDT/USDC → vAMM-USDT/USDC LP (pool 0x262029c9…013a) 0x4c386e52621869b2a95f7ffac9b76d622298ba69efc500dee69ff1fe617ea4be - remove all LP 0x7248f23e9bb5e6b36bd365b925ac113ff4418257e5b55200fd4c64c8c50287fe PancakeSwap V3 BNB (uniswap_v3 family — same NFT mint+remove flow as Project X / HyperSwap V3 / Uniswap V3 BNB): - mint 0.05 USDT + 0.05 USDC → tokenId 6815439 (USDT/USDC fee=100 pool 0x92b7807b…3121) 0xcffbcda4d11898b60a7076729a2196020c393463619922ac8fd6a349a965bd32 - remove tokenId 6815439 0x0439ac249a6b3bd782fd650b6c6342aea87b3b5f5ec69eb0067065c0ad0f55be Both TOML mirrors gain `verified = true` + an inline live-tx note. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(bnb-v2): document live add+remove for Biswap + ApeSwap Two more uniswap_v2 forks on BNB get end-to-end lifecycle on USDT/USDC mainnet. Same shared adapter as PancakeSwap V2 BNB / Uniswap V2 Monad — the LP-approval fix from earlier in this branch applies automatically. Biswap (USDT/USDC pool 0x1483767e…bf83): - add 0.03+0.03 USDT/USDC 0xe71a3979178e26fc959c76efd801471b7f8b5938d1d76b60451a607e751954b0 - remove all 0x35b75793cb549417108afcac189a7e740dfaf2252d3633411b59cded380749f4 ApeSwap (USDT/USDC pool 0xcd1e0b85…3829): - add 0.03+0.03 USDT/USDC 0x1609f48248e7806e0df7eee538bd6d9e2e526b812fedc642250b41cfb2e0f9c2 - remove all 0x5eefd643012e686e08af0db958d4363b3f1c6ba41f92279d938821f7ab46d86c Both TOML mirrors gain inline live-tx notes (verified=true was already present from cast-call verification on 2026-04-03). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(bnb-v2): document live add+remove for BabyDogeSwap + FstSwap Two more uniswap_v2 forks on BNB get live add+remove on USDT/USDC mainnet. BabyDogeSwap (USDT/USDC pool 0x30949ff5…d95a): - add 0.02+0.02 0x395a1f92eca8b299f8ce93f4d8729cf1c684acfea0fd2c3031ebdd3fb9a83c56 - remove all 0x29ce470fbba58f5d1413fc1541fc08d0fe4df151266889a680165d0dd266f62e FstSwap (USDT/USDC pool 0xc6ca8f17…696a): - add 0.02+0.02 0xd3f6c74c3e1f97a9d365e20f3939544294bb232a9185847ef8b14f248e4666fb - remove all 0xf6623e5363994632c9d6dba542c973a06b57db203f114cd9b53c580948b0482f Both inherit the V2 family LP-approve fix from earlier in this branch. Skipped: BakerySwap (router address 0xCDe540d7…330F failed viem checksum; needs router checksum fix in TOML — separate follow-up). BSCSwap (no USDT/USDC pool registered). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(verify): BakerySwap BNB + Uniswap V3 Base lifecycles BakerySwap (BNB, uniswap_v2 fork): router checksum had a stray uppercase 'A' in 0xCDe540d7eAFE93AC… that viem rejected. Lowercase the third 'A' → 0xCDe540d7eAFE93aC… per EIP-55. Address itself unchanged. - add 0.02+0.02 USDT/USDC 0x958d8099a37735025a0f4530bae86a2de6ec705e9e17003ac0b67ef016bdd5d4 - remove 0x538500689c2a97a4ff4aa4831a84967a84193e188ade8ae30cfafd2b47d1a205 Uniswap V3 Base (uniswap_v3 family — same NPM mint+remove path as Project X / HyperSwap V3 / PancakeSwap V3 BNB): - mint 0.1 USDC + 0.1 USDT → tokenId 5083156 (fee=100 pool 0xd56da2b7…5da1) 0x358e01c255bb379eb4278d0bfb4f70d56e655a18ac2a55fdcaf189d6939ffab8 - remove tokenId 5083156 0xfb0535fd8020c586fd1f4fe8be90fd3edb71768e0cfc7a32dc5ad7bdd35d062a uniswap_v3_base.toml gains verified=true + inline live-tx note. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(verify): Mantle UniV3 + UniV3 BNB lifecycles (--range 5 unblocks tight stable mints) V3 NFT mint reverted at the dispatcher with --range 1 on tight stable pools (USDT/USDC fee=100). Cause: range = ±1% on a stable pair maps to ~2 ticks each side, but the Slipstream tickSpacing math + sqrtPrice quantization on these specific pools rejects positions where the rounded tick range collapses to a single bin. Widening to --range 5 gives ±5% which always rounds to a healthy ≥10-tick window. Live verifications (mainnet): Uniswap V3 Mantle (USDC/USDT fee=100 pool 0x8cfee38a…9d2c): - mint 0.05 USDC + 0.05 USDT → tokenId 1404 0x2ca3fc8f877b11beba7b58060b7f8b3c49f1890521b5e6adeff0f6187245676e - remove tokenId 1404 0xa13ecd4c1e27acd9c4b8ec21d31e90e22c6f85517559197f925cc6524ffeaf72 Uniswap V3 BNB (USDT/USDC fee=100 pool 0x2c3c320d…7d68): - mint 0.03 USDT + 0.03 USDC → tokenId 1912084 (with --range 5) 0x9c5027bf213a50854504f25051886973f2c8983fbd2adfa88398601f0e5c250f - remove tokenId 1912084 0x504ab401fd9f7a50b1e3e9be457a1eb26f85bdfefd6086f8ad46632e74c12287 Both TOML mirrors gain verified=true + inline live-tx note. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(verify): Uniswap V3 Monad lifecycle (slippage bypass for low-liquidity pool) V3 NFT mint on the Monad WMON/AUSD pool reverted with "Price slippage check" even at --range 20 because the adapter's quoter-derived amount_a_min / amount_b_min are too tight for the pool's current shallow liquidity. Bypass with --amount-a-min 0 --amount-b-min 0 + wider range works. Live (Monad mainnet, WMON/AUSD fee=100 pool 0x99e94823…1bfa8): - mint 0.05 WMON + 0.0015 AUSD → tokenId 29176 0xba8ebee9bfaad56a3c28856ad7ceeb3b111889875c2d0bf10e97c6abc22280cf - remove tokenId 29176 0x683e777387defa35e136198843f8808886a36a563914d0b20f56d961e5d6c5a4 uniswap_v3_monad.toml gains verified=true + inline note about the slippage-bypass workaround on shallow Monad pools. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(verify): Ramses HL HyperEVM lifecycle on USDC/USDT0 volatile pool Solidly V2 add+remove on Ramses HL (HyperEVM) using the volatile USDC/USDT0 pool 0x4378d583…60cd: - add 0.1 USDC + 0.1 USDT0 0x0cc02f444c1def432c5a5ff8b0edaf11a4a2029dfc1331a848297d3452637118 - remove all 99979 LP 0xf2632c311c2cc49ea0ba15dc20aca778e2ffd2023e25f0b70b0f031b16c25954 Inherits the V2 LP-approval fix from earlier in this branch. Description gains inline tx hashes; verified=true was already present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(verify): KittenSwap (Algebra V3) lifecycle on WHYPE/USDC V3 NFT mint+remove (algebra_v3 + farming_center) live on HyperEVM: - prep: HYPE → WHYPE 0.005 via lifi 0xf4b67690c41520c6a0435ebe0d76444d956fc19d31f1d3fbc0c72be8c04a21eb - mint 0.004 WHYPE + 0.1 USDC → tokenId 71058 (pool 0x12df9913…513e) 0x65d7a3f2b0eec9d2fc6d1d40cecba4390dda90aa72330d30195ed1696bd65e41 - remove tokenId 71058 0x590b02eb4ab564fad621539f27d2c5a41e2decc60c3a3496b46d64a3da20085a Same slippage-bypass workaround as Monad UniV3 (--range 5 --amount-{a,b}-min 0). Description gains tx hashes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(verify): Hybra V4 CL lifecycle on HyperEVM (WHYPE/USDC, --redeem-type 0) Hybra V4 NFT mint+remove confirmed. Default redeem-type=1 locks rewards into a 2-year veHYBR NFT (intentional UX safety guard); --redeem-type 0 forces the instant-exit path with penalty, which is what we use here. Live (HyperEVM mainnet, WHYPE/USDC pool 0x4C307812…9314): - mint 0.001 WHYPE + 0.03 USDC → tokenId 65929 0xbe581c841d76467f2bcfd89adae9e3942a26fdafc92caf57e5a3689679760e17 - remove tokenId 65929 (--redeem-type 0) 0x4c271fef18fea2f190054e8fdc57cd21a8359962cdffa7df36b9b19fa3b01657 Description gains tx hashes; verified=true was already present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(verify): BSCSwap BNB lifecycle on USDT/BUSD BSCSwap had no USDT/USDC pool. Used USDT/BUSD instead (BUSD acquired via openocean swap since LI.FI doesn't have BUSD on BSC in its token registry). Live (BNB mainnet, USDT/BUSD pool 0xf68fd424…0063): - prep: swap 0.05 USDT → BUSD (openocean) 0x91313d2febc16e709a3eb5d3da2c9e134facf7d40c6d907a1f8a3eb9eeb7aec4 - add 0.03+0.03 USDT/BUSD 0x7fc04d2e53a8d678bd7bc9fac2b03b007cb177981bfc8597623e5fa87a63e507 - remove all 0xbfd9acb248164d6f02b823e7444b6daf2c9b644e560d8956b9c9ef47ae5a9651 Description gains tx hashes; verified=true was already present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(verify): ve(3,3) staking lifecycles for KittenSwap, Aerodrome V2, PancakeSwap V3 MasterChef, Hybra V4 All four ve(3,3)-style emission DEXes verified end-to-end (mint → stake → claim → unstake → remove) on mainnet today via parallel agent runs. KittenSwap (Algebra V3 + farming_center, HyperEVM, tokenId 71063): - lp farm (mint+enterFarming) 0x57d97ffdbebfbf8c23a48e3b850aae6b99ca43a16f4bc6214cbf154e027424ce - lp claim 0xdf163dea74f3a39a96dde2666c91f21cbf559ea7ce18416973dc91307a7f290b - lp remove (auto-exit+remove) 0x9a17d5a1ca0890b3b395f5c61ddc3dc5b120ce8fb2fd46c1a54816f8d86d70e3 Aerodrome V2 (Solidly gauge, Base, gauge 0x6f62Ebea59dA60C8e10B4Dcda88571de7C124Fb1): - claim AERO emissions 0xc49375975d9e4921a0c9f61a1af6f492c833d65d8dc94d4316effba7f0f79ca0 - unstake gauge 0xa061f63bf4ca80fa9388575085c30a81efd00e9d734b969c3788e684baf1ced0 - remove liquidity 0x8888d834e77858a2aef95fa6b90754900088db755d6a0892c7c51998d185bf2a PancakeSwap V3 MasterChef (BNB, tokenId 6815781): - mint NFT (USDT/USDC fee=100) 0x246d1c22b0ea9f1d50425973dfa77e143eb507d6fe8f3e6c5ae082657cb78715 - stake (NPM.safeTransferFrom) 0xd58120a963b7d80b5db70e0071b964af0f60ca811402c103ccbd477b0aa06d4a - harvest CAKE 0xdf971916698a98a8ffb004790e2d349f038abf5c0785aa2366e444040cbd5dff - withdraw NFT 0xdf11475e238d6bf40b4fc54d1265228e69bc11666af79bd5a40ba5a13f5a85da - remove 0xe52c847eb761b1dd077d09c63912d344d61ab9a5f1661896392a40e5b040a55a Hybra V4 (CL gauge, HyperEVM, gauge 0xa26921d56981fA43BA598428ea0C4Dd6De89CB8C, tokenId 65932): - mint NFT (lp farm) 0x95cc61308e079... - gauge.deposit 0x3902fda74b61c... - lp claim --redeem-type 0 0x2f3ef634c1b46b774f7898fc2da5b526e50cd120a4460bd6bc5454015b3232ce - gauge.withdraw --redeem-type 0 0x412430d09436... - lp remove 0x690593f6c8fe... Critical Hybra detail: --redeem-type 0 propagates through both claim and unstake, avoiding the default 2-year veHYBR lock. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(adapter): Ramses CL mint encoding + MerchantMoe/TraderJoe LB token-order realign Three adapter fixes that unblock four DEX lifecycles previously stuck on adapter-encoding bugs. 1. Ramses CL (HyperEVM) — uniswap_v3.ts cl_style="ramses" branch: - New `ramsesMintAbi`: 11-field MintParams (no sqrtPriceX96), selector 0x6d70c415. Slipstream's 12-field selector 0xb5007d1f is not in the Ramses NPM dispatch table — that's why earlier mints reverted at 24k. - 3-way mint dispatch: `cl_style="ramses"` → 11-field, else `useTickSpacingQuoter` → 12-field Slipstream, else → standard V3 fee. - Raw eth_call slot0() decoder for Ramses pools (7 fields incl. the extra `feeProtocol` uint32, vs UniV3's 6) — viem's tuple decode would otherwise truncate the trailing fields. - Live verified on USDC/USDT0 pool 0x46abBdfC…E63e: tokenId 182740, mint 0x8cdf9c53…66aa, remove 0xa569bf21…da69. 2. MerchantMoe LB / TraderJoe LB family (merchant_moe_lb.ts): - LB pools store tokenX/tokenY in factory creation order, NOT lex order. Pre-fix the CLI lex-sorted (token_a, token_b) before passing to the adapter — when canonical and lex order disagreed (WMNT/USDT on Mantle, WMON/AUSD on Monad) the router's `params.tokenX != pair.tokenX()` check reverted at 42k gas. - Adapter now queries pool.getTokenX/getTokenY and reconciles caller's token order. CLI lp remove also passes `pool` so the reconciliation fires for the symmetric remove path. - Live MerchantMoe Mantle WMNT/USDT (binStep=15 pool 0xf6C9020c…2415): add 0xb379fc81…1dab8, remove 0x3580cc51…c7be. - Live TraderJoe Monad WMON/AUSD (pool 0xC8d65149…245F): add 0x75d0e828…4ad0, remove 0x62989ed6…4952. 3. Aerodrome CL Slipstream (Base) — confirmed code is correct, the earlier 32k revert was caused by passing the V2 pool address (0x96508AE8…) instead of the Slipstream pool. The canonical USDC/USDT Slipstream pool is 0xa41Bc0AF…fcD1 (tickSpacing=1, resolved via factory.getPool). TOML description now documents this. - Live tokenId 69797487, mint 0xf335a47d…4b161, remove 0x00d2c531…c10e. Slippage test snapshot updated: dex/uniswap_v3.ts:262 → :272 (line shift from the cl_style branch widening). defi-protocols 55/55 + defi-cli 102/102 pass (slippage test re-grounded). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(config): enable Monad aggregators + clean malformed Nest gauge addresses - chains.toml: register Kyber/OpenOcean/LiFi/Relay for Monad (chain id 143); LiquidSwap stays HyperEVM-only. - nest.toml: comment out four malformed gauge entries (39 hex chars after 0x) on the inactive Nest adapter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(bridge): add Relay provider with executor-wired live broadcast Adds Relay (https://docs.relay.link/) as a 4th cross-chain bridge provider alongside lifi, debridge, and cctp. Live-confirmed against mainnet: - Base ETH → BNB native: tx 0x4366…003d (block 45710957, gas 24,824, ~3s). - BNB native → Base ETH: tx 0x1725…7f25 (block 97017019, gas 24,824, ~3s). Wires Executor into registerBridge so --broadcast actually fires the tx returned by the Relay quote (POST https://api.relay.link/quote). Native tokens skip the approve step; ERC20 paths build a single approval entry keyed off the relay-returned `to` address. Relay's API restricts some ERC20 currencies (e.g. BNB USDC/USDT return INVALID_INPUT_CURRENCY); use native sentinel 0x0…0 for verified pairs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(readme): document Relay as 4th bridge provider - Add bridge provider list (lifi/relay/debridge/cctp) to root README. - Add example bridge block to ts/packages/defi-cli/README mirroring the swap aggregator section. - Update command-reference table in both READMEs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(skill)+feat(mcp): wire Relay into defi_bridge MCP tool + skill notes - skills/defi-cli/SKILL.md (+ ts/packages mirror): document Relay as a 4th bridge provider, native-token caveat (USDC/USDT often blocked by Relay's currency registry → use 0x0…0 sentinel), AMOUNT_TOO_LOW/INVALID_INPUT_CURRENCY errors, executor-wired broadcast. - mcp-server.ts: add `provider` enum (lifi/relay) to defi_bridge; new Relay branch hits POST api.relay.link/quote and returns formatted output amount + fees + execution duration. deBridge / CCTP quotes still flow through the CLI (their broadcast paths need DLN/CCTP-specific config the MCP doesn't carry yet). - mcp-server.ts: drop duplicate shebang at top of source (the tsup banner already injects one — Node 20 chokes on the bundled double shebang). Verified: MCP defi_bridge with provider=relay returns valid quote for Base ETH → BNB native (0.001 → 0.00346, ~3s). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(bridge): wire LiFi+deBridge+CCTP to Executor + CCTP --auto-receive Closes the long-standing gap where `defi bridge --broadcast` only fired the tx for `--provider relay`; lifi/debridge/cctp printed the quote and silently ignored --broadcast. All four providers now share the Executor path. - LiFi: spender = quote.estimate.approvalAddress (falls back to transactionRequest.to). Live verified Base ETH → BNB native (tx 0xc2d2fa3c…1a80, block 45711451, ~97k gas). - deBridge DLN: spender = create-tx `to`. Live verified Base ETH → BNB native (tx 0x2bd8cee9…2827, block 45711456, ~138k gas). - CCTP burn: spender = TokenMessengerV2 (0x28b5…cf5d). Native USDC approve + depositForBurn now executes via Executor. CCTP --auto-receive (new): - Polls https://iris-api.circle.com/v2/messages/{srcDomain}?transactionHash=… every 5s up to --receive-timeout (default 1200s). - Once attestation status is "complete", submits MessageTransmitterV2.receiveMessage(message, attestation) on the destination chain. V2 transmitter address (0x81D4…4B64) is identical on every EVM V2 chain. - Destination Executor: registry chains use ChainConfig.viemChain(); DEST_CHAIN_META chains (ethereum/arbitrum/optimism/polygon/avalanche/ linea/zksync) read RPC from `<UPPER>_RPC_URL` env vars first, then fall back to public drpc.org / eth.merkle.io endpoints. Pass an env override per chain when broadcasting on a rate-limited public RPC isn't safe. - Caveat: auto-receive **requires destination-chain native gas** to pay for the receive tx — a separate funding prerequisite the user has to satisfy ahead of time. Without --auto-receive only the burn fires and the user must finalize manually. ts/scripts/sandbox-bridge.sh: persisted bridge regression sandbox (8 provider/route combinations, all PASS). skills/defi-cli/SKILL.md (+ ts/packages mirror): documents the new broadcast wiring, --auto-receive flow, env-var RPC overrides. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(bridge): parse Relay errorCode and surface actionable hints getRelayBridgeQuote now JSON-parses the error response and maps AMOUNT_TOO_LOW / INVALID_INPUT_CURRENCY / INVALID_OUTPUT_CURRENCY / NO_QUOTES / AMOUNT_TOO_HIGH / UNSUPPORTED_CHAIN to actionable hints, typically suggesting --provider lifi as a fallback when Relay's fee floor or chain support blocks the route. Verified live: Base→Arbitrum 1 wei → "AMOUNT_TOO_LOW: Swap output amount is too small to cover fees ... — Try a larger amount or use --provider lifi (Relay enforces a per-route fee floor)" Also confirms CCTP --auto-receive E2E lifecycle live verified earlier in the same branch (Base→Arbitrum 0.1 USDC, attestation auto-polled, receiveMessage auto-broadcast on Arbitrum). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(lending/compound-v2): handle uint256.max withdraw + clarify Venus Flux vToken keys compound_v2 buildWithdraw now intercepts the uint256.max sentinel (Aave-style "withdraw all" convention) and rewrites it to redeem(vTokenBalance) after reading the user's actual balanceOf, since redeemUnderlying(uint256.max) overflows when the contract converts max underlying to vToken units. Also reads borrowBalanceStored: if the wallet has any outstanding borrow on this market, refuses the withdraw-all with a clear actionable error ("Repay first, or pass an explicit --amount …") rather than letting the Comptroller's hypothetical-liquidity check bubble out as a generic "math error" revert from the cToken's mulScalarTruncate path. Verified live on Venus BNB (USDT, dust borrow on wallet): • supply 0.2 USDT → 0x97ff6185… • withdraw 0.2 USDT (exact) → 0xbffbbec8… • withdraw uint256.max (with dust borrow) → friendly error, no broadcast • withdraw uint256.max (no borrow) → would redeem(balanceOf) directly venus-flux-bnb.toml: rename `vusdt` → `vlisusd`. The old key was mislabeled — the underlying address (0x0782b6d8…41E5) is lisUSD on the stablecoins isolated pool, not BSC USDT. Adapter resolution now correctly reports `no vToken for asset` when querying USDT instead of silently routing to the wrong market. Aave V3 BNB lifecycle also verified live in this branch: • supply 0.3 USDT → 0x80d1d87a… (block 97064505) • withdraw uint256.max → 0xd251f6ba… (block 97064542) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(readme,skill): mark BNB as 🟢 production after this session's lifecycle sweep 13/16 BNB protocols are now lifecycle-verified: Aave V3, Venus, Kinza, Pancakeswap V2/V3, Uniswap V3, Thena V1, Biswap, ApeSwap, BakerySwap, BSCSwap, BabyDogeSwap, FstSwap. Aave V3 BNB and Venus BNB were broadcast-verified in this branch (0x80d1d87a / 0xd251f6ba and 0x22897ec2 / 0xf66c49c4 / 0xbffbbec8). Three deferred protocols documented inline: • venus-flux-bnb — no USDT market in routed comptroller (protocol gap, not adapter gap) • thena-fusion — needs WBNB liquidity, funding-pending • beefy-bnb — needs LP-token underlying, funding-pending Monad stays 🟡 staged: Uniswap V3/V2 lifecycle-verified earlier; traderjoe-monad and morpho-blue-monad await mainnet launch + funding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bridge): resolve destination token symbol independently per chain + ULTRAQA sandbox Bridges with multi-chain stables (USDC, USDT, WETH on Base/BNB/Mantle/Arbitrum) were failing because bridge.ts passed the source-chain token address as both src and dst to LiFi/Relay/deBridge providers. The same symbol lives at a different contract address on each chain — providers correctly rejected "OUTPUT_TOKEN_NOT_TRADABLE" (deBridge) or "No LI.FI route found". Now: when --token is a symbol, resolve it on the destination chain registry (if registered) and pass src/dst token addresses separately to providers. Hex inputs continue to reuse the same address on dst (caller intent). Verified by ULTRAQA sweep (51/51 PASS post-fix): • lifi: Base USDC → BNB ✅ (was: No LI.FI route found) • debridge: Base USDC → BNB ✅ (was: OUTPUT_TOKEN_NOT_TRADABLE) • relay/cctp/native paths ✅ (no regression) Also adds ts/scripts/sandbox-production-qa.sh — a self-contained read-only + dry-run sweep for production-grade chains (HyperEVM 11, Mantle 3, Base 5, BNB 16) covering lending rates, lp discover (incl. all ve(3,3) DEXes), swap quotes per aggregator, and bridge dry-runs per provider. Runs inside the docker sandbox container; pure read-only/no --broadcast. Slug fixes inside the sweep: `hyperswap` → `hyperswap-v3`, `nest` → `nest-v1`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): defi_bridge — resolve src/dst tokens per-chain + LiFi fromAddress + correct dst decimals The defi_bridge MCP tool had three latent bugs that all surfaced when bridging stables across chains (USDC Base → BNB): 1. src/dst token symbol was passed as the same address to both Relay's originCurrency/destinationCurrency and LiFi's fromToken/toToken — the same symbol lives at different contract addresses on each chain, so providers correctly rejected the route. Fixed by resolving the symbol independently on each chain via the registry; hex inputs continue to reuse on dst (caller intent). 2. Relay path discarded symbol input entirely and substituted the native sentinel (0x0…0), turning every ERC20 bridge request into a native bridge silently. Now resolves the symbol via the same per-chain path. 3. LiFi path used `toAddress: recipientAddr` whereas LiFi's /quote endpoint requires `fromAddress`. Every LiFi MCP call returned 400 "querystring must have required property 'fromAddress'". Renamed. Also fixes amount_in / amount_out unit handling: was hardcoded to 6 decimals (assumed USDC); now reads decimals from the registry for src and dst, and prefers LiFi's quoted toToken.decimals when available. Without this fix the LiFi USDC Base→BNB amount_out came back as 988889994492.99 (raw 18-decimal value divided by 1e6). Verified via stdio smoke test against the live MCP server: • LiFi USDC Base→BNB: 1 → 0.989 (was: querystring error) • Relay USDC Base→BNB: 1 → 0.977 (was: native bridge silently) • Relay native Base→BNB: 0.0001 ETH → 0.000308 BNB Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(deps): pin ip-address >=10.1.1 via pnpm override (GHSA-v2v4-37r5-5v8g) Resolves Dependabot alert #18 — moderate-severity XSS in ip-address@<=10.1.0 surfaces (Address6.group/link/spanAll and AddressError.parseMessage) when consumers HTML-render their output. The transitive path is: @hypurrquant/defi-cli → @modelcontextprotocol/sdk → express-rate-limit → ip-address Practical exposure here is zero — defi-cli uses the MCP SDK only over stdio (no HTML rendering, no Address6 surface) — but the override removes the alert and prevents future exposure if a consumer wires the SDK into a webview or admin UI. Verified: • pnpm audit → "No known vulnerabilities found" • pnpm -r test → 189/189 unit tests pass — no regression Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR #16 added 5 swap.test.ts cases focused on the kyber native-input
msg.valuebug + chain-support guards. The other 4 providers (openocean, lifi, relay, liquid) had no shape-pinning happy path. This PR adds one mocked happy-path test per provider so a futureprintOutputrefactor or provider HTTP shape change is caught at PR time, not in production.What's added
4 new tests in
commands/swap.test.ts:{ data: { to, data, value, outAmount } }chain_idis 143 (LI.FI is chainId-routed, not slug){ transactionRequest, estimate.toAmount }approvestep in the multi-step plan and reports the swap step's router +details.currencyOut.amount{ steps: [{id:'approve',…}, {id:'swap',…}], details:{currencyOut:{amount}} }{ execution: {to, calldata, value}, details:{amountOut} }All 4 use the same
vi.mock(globalThis.fetch)helper introduced in PR #16; tests run offline and are deterministic.Test plan
pnpm -C ts -r build— clean.pnpm -C ts -r lint— 3 packages,tsc --noEmitclean.pnpm -C ts -r test— defi-core 32/32, defi-protocols 43/43, defi-cli 98/98 (was 94; +4 provider happy-path tests).swap.ts:167-170is properly exercised — the test feeds in anid: "approve"step that the CLI shouldfind()-skip.Sibling PR
PR #17 (
chore/dex-test-fixtures-real-chains) covers the unrelated DEX adapter fixture cleanup. Independent of this PR; merge order doesn't matter.🤖 Generated with Claude Code