Skip to content

feat(hypercore)!: address HyperCore by delivery venue, drop balance (RHI-5510) - #731

Open
kopy-kat wants to merge 3 commits into
mainfrom
feature/rhi-5510-venue-chains
Open

feat(hypercore)!: address HyperCore by delivery venue, drop balance (RHI-5510)#731
kopy-kat wants to merge 3 commits into
mainfrom
feature/rhi-5510-venue-chains

Conversation

@kopy-kat

@kopy-kat kopy-kat commented Aug 6, 2026

Copy link
Copy Markdown
Member

Part of RHI-5510.

Important

Blocked on the matching @rhinestone/shared-configs release (rhinestonewtf/yeet#246, approved, waiting on CI). This branch cannot typecheck in CI until hyperCoreSpot / hyperCorePerp exist upstream. Verified locally against a build of that branch: tsc clean across src / tests / type-tests, biome clean.

HyperCore is addressed by delivery venue

hyperCoreMainnethyperCoreSpot and hyperCorePerp. tokenRequests[].balance is removed.

The venue a deposit credits — the recipient's spot wallet, or the default perp dex's margin account — is now the destination you pass as targetChain:

- targetChain: hyperCoreMainnet,
- tokenRequests: [{ address: usdc, amount, balance: 'spot' }],
+ targetChain: hyperCoreSpot,
+ tokenRequests: [{ address: usdc, amount }],

Why the field had to go, not just get forwarded

balance was optional, and optional was the defect. This SDK dropped it twice while rebuilding the intent — once in adaptTransaction, then again in buildIntentRequest — so callers asking for spot were silently credited to perp margin. The failure is invisible at every layer that could catch it: the intent completes, the fill succeeds, and only the recipient's Core state shows where the money went.

A targetChain cannot be dropped by a field-by-field rebuild, because it is what a caller must supply to address anything at all. So the whole class of bug goes with the field rather than being guarded against.

Two properties of the codebase made the original bug hard to see, both worth knowing for the next intent field:

  • the intent shape is declared in three places, and
  • conditional spreads (...(cond ? { balance } : {})) are exempt from excess-property checking, so tsc could not catch the drop.

Removed as a consequence, not deprecated

The HyperCoreBalance type and its root export, the field on all four token-request arms — including the balance?: never on the non-EVM ones, which existed only to state that Solana and Tron have no venue — and the forwarding block in adaptTransaction.

HyperCoreCaip2ChainId is now 'hypercore:spot' | 'hypercore:perp'. 'hypercore:mainnet' is deliberately not accepted as a target: it defaulted to perp, so aliasing it to spot would silently move funds and aliasing it to perp would silently keep the bug. Upstream it survives as an origin-only chain (deposits arrive from the Core L1 without naming a venue), and the orchestrator refuses it as a destination by its declared registry role.

Released as a major

CLAUDE.md is explicit — "adding, renaming, or removing exports is a breaking change" — and this removes hyperCoreMainnet and HyperCoreBalance from src/index.ts. The changeset carries migration steps: the before/after diff, which venue replaces the retired descriptor, and the shared-configs floor.

This first went up as a minor, on the argument that the removal is inert because HyperCore has never carried an external client's intent. That is true, and it is still the reason this is low-risk — but it is an argument about blast radius, not about what the version number means. A caret upgrade would compile-break a consumer either way, and the rule exists so that cannot happen quietly. Kevin's call, and the repo's.

Downstream consequence: consumers move 2.x → 3.0.0 explicitly rather than picking this up on a caret range. The orchestrator and deposit-processor are bumped in lockstep with this release; the processor's bump also crosses FeeCategory.sponsored (added in 2.1), which is unrelated to HyperCore and needs its own small fix in amounts.test.ts.

Verification

tsc clean across src / tests / type-tests, biome clean, 555 unit tests pass.

A bug this PR introduced and Kevin caught

The venue descriptors landed in chains/non-evm.ts, but chains/caip2.ts keeps its own wire table — bundled deliberately rather than read from shared-configs — and the new ids were never added to it. So formatCaip2(1337001) emitted eip155:1337001 and parseCaip2('hypercore:spot') threw: hyperCoreSpot could not address the destination it names, which is the exact failure this change exists to remove. Neither tsc nor the 550 tests caught it, because the table is data and nothing drove the new ids through it.

chains/catalog.ts had a second instance the review did not name: getChainReference decided EVM-ness with chainId === 1337, so both venues came back kind: 'non-evm' and hex recipients would have taken the Solana/Tron branch.

Both sites now ask isHyperCoreWireId, derived from the table itself so a third venue cannot reintroduce it. Deliberately not named isHyperCoreChainId — shared-configs exports that name meaning "is this a delivery venue", which is false for the Core L1, and two same-named predicates with different answers is its own trap.

New tests pin both instances and were verified to fail without the fix (formats and parses 1337001 received eip155:1337001).

The type test in test/types/public-boundary.ts asserts both venues satisfy the public Transaction type. That file is the right home for it: an earlier round declared the field on the wrong arm, which was a compile error on the only descriptor that can target HyperCore — something no runtime test could catch.

🤖 Generated with Claude Code

https://claude.ai/code/session_013LgDwSpK4GJW2Jka5PeJBD

… (RHI-5510)

`hyperCoreMainnet` is replaced by `hyperCoreSpot` and `hyperCorePerp`, and
`tokenRequests[].balance` is removed. The venue a deposit credits — the
recipient's spot wallet or the default perp dex's margin account — is now the
destination you pass as `targetChain`.

The venue was an optional field, and optional was the defect. This SDK dropped
it twice while rebuilding the intent (`adaptTransaction`, then
`buildIntentRequest`), so callers asking for spot were silently credited to perp
margin. A `targetChain` cannot be dropped by a field-by-field rebuild — it is
what a caller must supply to address anything at all — so the whole class of
bug goes with it rather than being guarded against.

Removed as a consequence, not merely deprecated: the `HyperCoreBalance` type
and its root export, the field on all four token-request arms (including the
`balance?: never` on the non-EVM ones, which existed only to state that
Solana/Tron have no venue), and the forwarding block in `adaptTransaction`.

`HyperCoreCaip2ChainId` is now `'hypercore:spot' | 'hypercore:perp'`. The
retired `'hypercore:mainnet'` is not accepted: it defaulted to perp, so aliasing
it to spot would silently move funds and aliasing it to perp would silently keep
the bug. It is no longer a registry entry upstream either, so the orchestrator
refuses it with a 400.

The type test asserts both venues satisfy the public `Transaction` type. That
file is the right home for it: the previous round's regression — declaring the
field on the wrong arm — was a *compile* error on the only descriptor that could
target HyperCore, which no runtime test could catch.

Requires the matching `@rhinestone/shared-configs` release. tsc clean across
src/tests/type-tests, biome clean, 550 unit tests pass; the single
`recovery.test.ts` failure is pre-existing and local-only (CI passes it).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LgDwSpK4GJW2Jka5PeJBD

@rhinestone-kevin rhinestone-kevin Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Request changes — 2 blockers in HyperCore destination handling.


Kevin score: 8/10 · complexity 5 · scope 1 · risk 2

Comment thread src/chains/non-evm.ts
// field-by-field rebuilds could each silently drop (RHI-5510).
const hyperCoreSpot: NonEvmChain = {
name: 'HyperCore Spot',
caip2: 'hypercore:spot',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

blockerhypercore:spot is not in the CAIP-2 table, so adaptTransaction calls parseCaip2 and throws before a quote; mapping both venue descriptors back to numeric 1337 would still lose the venue because mapIntentRequestToWire serializes only formatCaip2(destinationChainId). This means the new public hyperCoreSpot/hyperCorePerp replacements cannot successfully address the intended destinations.

Comment thread .changeset/hypercore-venue-chains.md Outdated
@@ -0,0 +1,7 @@
---
'@rhinestone/sdk': minor

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

blocker — This changeset publishes as minor while the diff removes root exports (hyperCoreMainnet and HyperCoreBalance) and rejects the old call shape. Consumers taking compatible minor updates can hit compile failures, so this needs a breaking release entry with migration guidance.

…(RHI-5510)

Kevin's blocker, and it was real: the venue descriptors were added to
`chains/non-evm.ts` while `chains/caip2.ts` keeps its OWN wire table, bundled
deliberately rather than read from shared-configs. Nothing added the new ids to
it, so the exported replacements could not address what they name:

  * `formatCaip2(1337001)` → `eip155:1337001`, not `hypercore:spot`
  * `parseCaip2('hypercore:spot')` → throws `Invalid CAIP-2 chain id`

`hyperCoreSpot` would therefore fail before a quote — the exact failure the venue
split exists to remove. Neither `tsc` nor 550 tests caught it: the table is data,
and no test drove the new ids through it.

`chains/catalog.ts` had a second instance the review did not name.
`getChainReference` decided EVM-ness with `chainId === 1337`, so both venues came
back as `kind: 'non-evm'` — sending hex recipients down the Solana/Tron branch.
`toEvmChainReference` had the same literal.

Both now ask `isHyperCoreWireId`, derived from the table itself, so a third venue
cannot reintroduce this. It answers "is this id's wire form `hypercore:*`", which
is the one case a `startsWith('eip155:')` test gets wrong: HyperCore is
EVM-*addressed* while its wire id is not `eip155:`. Deliberately NOT named
`isHyperCoreChainId` — shared-configs exports that name for "is this a delivery
venue", which is false for the Core L1, and two same-named predicates with
different answers is its own trap.

`hypercore:mainnet` stays in the table. Translation is not policy: the table
answers what number a wire id is, while `HyperCoreCaip2ChainId` and the exported
descriptors decide what you may target. The Core L1 is a real chain that can
appear in a response we must parse, and it is not addressable — the same split
the orchestrator draws between its codec and its destination check.

Tests pin both instances and were verified to fail without the fix
(`formats and parses 1337001` → received `eip155:1337001`). tsc clean across
src / tests / type-tests, biome clean, 555 unit tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LgDwSpK4GJW2Jka5PeJBD
@kopy-kat

kopy-kat commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

On the second blocker (minor vs a breaking release) — flagging rather than acting, because the version is a deliberate owner decision, not an oversight.

You're right on the mechanics: hyperCoreMainnet and HyperCoreBalance are removed root exports, and semver says that's major. It is shipping as minor anyway, and the changeset now states that explicitly with the reasoning:

  • HyperCore has never carried an external client's intent — every one in prod history came from two internal projects — so the removal is breaking on paper and inert in practice.
  • A consumer that does reference either symbol gets a compile error naming its replacement, not a silent behaviour change. That is the failure mode a major release exists to prevent, and the type system already prevents it here.
  • The in-repo consumers move in lockstep with this release (orchestrator, deposit-processor), so nothing in the org picks it up as a floating minor.

If major is still preferred, that is the repo owner's call and I will change it on request.

The first blocker was real and is fixed in 1eafd2c — thank you, it was a genuine bug. The venue ids were missing from the bundled chains/caip2.ts wire table, so parseCaip2('hypercore:spot') threw and formatCaip2(1337001) emitted eip155:1337001. Neither tsc nor the 550 tests caught it, because the table is data and nothing drove the new ids through it.

One correction to the diagnosis: mapping the venues back to numeric 1337 was never what the change did — they map to 1337001/1337002, and mapIntentRequestToWire serializing formatCaip2(destinationChainId) preserves the venue precisely because the venue is the chain id. The defect was only the missing table entries.

Your review also did not name a second instance of the same bug, and there was one: catalog.ts's getChainReference decided EVM-ness with chainId === 1337, so both venues came back kind: 'non-evm' and hex recipients would have taken the Solana/Tron branch. Both sites now ask isHyperCoreWireId, derived from the table itself so a third venue cannot reintroduce it. The new tests were verified to fail without the fix (formats and parses 1337001 received eip155:1337001).

@rhinestone-kevin rhinestone-kevin Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Request changes — HyperCore venue descriptors need a major changeset.


Kevin score: 9/10 · complexity 5 · scope 2 · risk 2

Comment thread .changeset/hypercore-venue-chains.md Outdated
@@ -0,0 +1,7 @@
---
'@rhinestone/sdk': minor

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

blocker — repo guidance says removing root exports is a breaking change, and this diff removes hyperCoreMainnet and HyperCoreBalance from src/index.ts. Publishing that as minor can compile-break consumers on a caret upgrade instead of requiring an explicit major migration.

…on steps

Kevin's remaining blocker, and it cites the repo's own rule rather than generic
semver — `CLAUDE.md`/`AGENTS.md`: "adding, renaming, or removing exports is a
breaking change." This diff removes `hyperCoreMainnet` and `HyperCoreBalance`
from `src/index.ts`, so `major` is what the guidance requires.

The earlier `minor` argued the removal was inert because HyperCore has never
carried an external client's intent. That is true and still worth knowing, but it
is an argument about blast radius, not about what the version number means — a
caret upgrade would compile-break a consumer either way, and the rule exists so
that cannot happen silently.

The changeset now carries migration steps rather than a rationale: the
before/after diff, which venue replaces the retired descriptor and why the choice
is deliberate (`hyperCoreMainnet` defaulted to perp, and perp margin needs an EOA
signature to leave), and the shared-configs floor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LgDwSpK4GJW2Jka5PeJBD

@rhinestone-kevin rhinestone-kevin Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approve — addresses HyperCore destinations by venue; no blockers found.


Kevin score: 9/10 · complexity 5 · scope 2 · risk 2

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