feat(hypercore)!: address HyperCore by delivery venue, drop balance (RHI-5510) - #731
feat(hypercore)!: address HyperCore by delivery venue, drop balance (RHI-5510)#731kopy-kat wants to merge 3 commits into
balance (RHI-5510)#731Conversation
… (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
| // field-by-field rebuilds could each silently drop (RHI-5510). | ||
| const hyperCoreSpot: NonEvmChain = { | ||
| name: 'HyperCore Spot', | ||
| caip2: 'hypercore:spot', |
There was a problem hiding this comment.
blocker — hypercore: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.
| @@ -0,0 +1,7 @@ | |||
| --- | |||
| '@rhinestone/sdk': minor | |||
There was a problem hiding this comment.
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
|
On the second blocker ( You're right on the mechanics:
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 One correction to the diagnosis: mapping the venues back to numeric 1337 was never what the change did — they map to 1337001/1337002, and Your review also did not name a second instance of the same bug, and there was one: |
| @@ -0,0 +1,7 @@ | |||
| --- | |||
| '@rhinestone/sdk': minor | |||
There was a problem hiding this comment.
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
Part of RHI-5510.
Important
Blocked on the matching
@rhinestone/shared-configsrelease (rhinestonewtf/yeet#246, approved, waiting on CI). This branch cannot typecheck in CI untilhyperCoreSpot/hyperCorePerpexist upstream. Verified locally against a build of that branch: tsc clean across src / tests / type-tests, biome clean.HyperCore is addressed by delivery venue
hyperCoreMainnet→hyperCoreSpotandhyperCorePerp.tokenRequests[].balanceis 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:Why the field had to go, not just get forwarded
balancewas optional, and optional was the defect. This SDK dropped it twice while rebuilding the intent — once inadaptTransaction, then again inbuildIntentRequest— 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
targetChaincannot 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:
...(cond ? { balance } : {})) are exempt from excess-property checking, sotsccould not catch the drop.Removed as a consequence, not deprecated
The
HyperCoreBalancetype and its root export, the field on all four token-request arms — including thebalance?: neveron the non-EVM ones, which existed only to state that Solana and Tron have no venue — and the forwarding block inadaptTransaction.HyperCoreCaip2ChainIdis 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.mdis explicit — "adding, renaming, or removing exports is a breaking change" — and this removeshyperCoreMainnetandHyperCoreBalancefromsrc/index.ts. The changeset carries migration steps: the before/after diff, which venue replaces the retired descriptor, and theshared-configsfloor.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.0explicitly 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 crossesFeeCategory.sponsored(added in 2.1), which is unrelated to HyperCore and needs its own small fix inamounts.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, butchains/caip2.tskeeps its own wire table — bundled deliberately rather than read from shared-configs — and the new ids were never added to it. SoformatCaip2(1337001)emittedeip155:1337001andparseCaip2('hypercore:spot')threw:hyperCoreSpotcould not address the destination it names, which is the exact failure this change exists to remove. Neithertscnor the 550 tests caught it, because the table is data and nothing drove the new ids through it.chains/catalog.tshad a second instance the review did not name:getChainReferencedecided EVM-ness withchainId === 1337, so both venues came backkind: '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 namedisHyperCoreChainId— 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 1337001receivedeip155:1337001).The type test in
test/types/public-boundary.tsasserts both venues satisfy the publicTransactiontype. 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