Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@omni-bridge/solana",
"@omni-bridge/btc",
"@omni-bridge/starknet",
"@omni-bridge/hypercore",
"@omni-bridge/sdk"
]
],
Expand Down
7 changes: 7 additions & 0 deletions .changeset/hypercore-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@omni-bridge/hypercore": minor
"@omni-bridge/core": minor
"@omni-bridge/evm": minor
---

support transfers from HyperEVM and HyperCore
15 changes: 14 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"reference/evm",
"reference/near",
"reference/solana",
"reference/btc"
"reference/btc",
"reference/hypercore"
]
}
]
Expand Down
288 changes: 288 additions & 0 deletions docs/reference/hypercore.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
---
title: "@omni-bridge/hypercore"
description: HyperCore (Hyperliquid L1) action builder for outbound transfers from spot balances
---

## Import

```typescript
import {
createHyperCoreBuilder,
postExchangeAction,
splitSignature,
buildSendToEvmWithDataTypedData,
parseSpotId,
resolveSpotToken,
resolveSpotTokenCached,
fetchSpotMeta,
encodeTransferAction,
encodeInitTransferAction,
formatAmount,
ACTION_TRANSFER,
ACTION_INIT_TRANSFER,
HYPERCORE_API_URL,
HYPEREVM_CHAIN_ID,
HYPERLIQUID_CHAIN,
DEFAULT_SIGNATURE_CHAIN_ID,
DEFAULT_GAS_LIMIT_TRANSFER,
DEFAULT_GAS_LIMIT_INIT_TRANSFER,
SEND_TO_EVM_WITH_DATA_TYPE_NAME,
} from "@omni-bridge/hypercore"
```

This package builds the EIP-712 `sendToEvmWithData` user-signed action that lets a HyperCore (Hyperliquid L1) spot holder bridge out — either to a HyperEVM address (pool release) or to any other chain (routed via `OmniBridge.initTransfer`). For bridging *into* HyperCore from another chain, use the inbound helper `buildHyperliquidTransferParams` in `@omni-bridge/core`.

---

## createHyperCoreBuilder

Factory function that returns a `HyperCoreBuilder` scoped to a network.

### Signature

```typescript
function createHyperCoreBuilder(config: HyperCoreBuilderConfig): HyperCoreBuilder
```

### Parameters

<ResponseField name="config" type="HyperCoreBuilderConfig" required>
Configuration object.
<Expandable title="HyperCoreBuilderConfig">
<ResponseField name="network" type="'mainnet' | 'testnet'" required>
Selects the Hyperliquid REST endpoint, `hyperliquidChain` action field, and HyperEVM `destinationChainId`.
</ResponseField>

<ResponseField name="apiUrl" type="string">
Override the Hyperliquid REST base URL. Defaults to `HYPERCORE_API_URL[network]`.
</ResponseField>

<ResponseField name="signatureChainId" type="string">
Hex chain id embedded in the EIP-712 domain. Defaults to `"0x66eee"` (Arb-Sepolia, matching the Hyperliquid Python SDK convention). Only its uniqueness across chains matters for replay protection — not tied to Arbitrum execution.
</ResponseField>

<ResponseField name="fetch" type="typeof fetch">
Custom `fetch` implementation (for tests, proxies, or polyfills). Defaults to the global `fetch`.
</ResponseField>
</Expandable>
</ResponseField>

### Returns

<ResponseField name="HyperCoreBuilder" type="HyperCoreBuilder">
A HyperCore action builder instance.
<Expandable title="HyperCoreBuilder">
<ResponseField name="network" type="'mainnet' | 'testnet'">
The configured network (readonly).
</ResponseField>

<ResponseField name="apiUrl" type="string">
The configured Hyperliquid REST base URL (readonly). Useful when passing `apiUrl` to `postExchangeAction`.
</ResponseField>

<ResponseField name="buildTransfer" type="(params: HyperCoreTransferParams) => Promise<HyperCoreUnsignedAction>">
Builds the unsigned `sendToEvmWithData` action and EIP-712 envelope. See below.
</ResponseField>
</Expandable>
</ResponseField>

---

## buildTransfer

Builds the unsigned `sendToEvmWithData` action. Picks `ACTION_TRANSFER` (`0x00`, pool release to a HyperEVM address) when `recipient` is `hlevm:0x...`, otherwise `ACTION_INIT_TRANSFER` (`0x01`, calls `OmniBridge.initTransfer`). Resolves the `HlBridgeToken` contract and on-EVM decimals via `/info spotMeta` unless those are supplied explicitly.

### Signature

```typescript
buildTransfer(params: HyperCoreTransferParams): Promise<HyperCoreUnsignedAction>
```

### Parameters

<ResponseField name="params" type="HyperCoreTransferParams" required>
<Expandable title="HyperCoreTransferParams">
<ResponseField name="spotId" type="string" required>
Hyperliquid spot identifier in the canonical `"NAME:0x<32hex>"` form (e.g. `"USDC:0x6d1e7cde53ba9467b783cb7c530ce054"`). Names alone are **not** accepted: Hyperliquid permits multiple tokens to share a `name`, so the 32-byte `tokenId` is the only unambiguous handle.
</ResponseField>

<ResponseField name="amount" type="bigint" required>
Amount in **bridge ERC-20 wei** (= `weiDecimals + evm_extra_wei_decimals` from `/info spotMeta`). Must be `> 0n`.
</ResponseField>

<ResponseField name="recipient" type="OmniAddress" required>
Destination address. `hlevm:0x...` → on-HyperEVM pool release; any other chain (`near:`, `eth:`, `sol:`, …) → routed via `OmniBridge.initTransfer`.
</ResponseField>

<ResponseField name="fee" type="bigint">
Bridge fee in bridge ERC-20 wei. Only used when `recipient` is **not** HyperEVM. Must satisfy `0n <= fee < amount`. Defaults to `0n`.
</ResponseField>

<ResponseField name="message" type="string">
Optional message forwarded with the `OmniBridge.initTransfer` event. Ignored for HyperEVM recipients. Defaults to `""`.
</ResponseField>

<ResponseField name="gasLimit" type="number">
HyperEVM-side gas limit for the resulting system tx. Defaults to `DEFAULT_GAS_LIMIT_TRANSFER` (300k) for pool release, `DEFAULT_GAS_LIMIT_INIT_TRANSFER` (800k) for routed transfers.
</ResponseField>

<ResponseField name="hlBridgeToken" type="Address">
Pre-resolved HlBridgeToken contract. When supplied **together with `decimals`**, skips the `/info spotMeta` lookup. Useful for offline/deterministic builds.
</ResponseField>

<ResponseField name="decimals" type="number">
Pre-resolved HlBridgeToken ERC-20 `.decimals()` (= `weiDecimals + evm_extra_wei_decimals`). Required together with `hlBridgeToken`.
</ResponseField>
</Expandable>
</ResponseField>

### Returns

<ResponseField name="HyperCoreUnsignedAction" type="HyperCoreUnsignedAction">
<Expandable title="HyperCoreUnsignedAction">
<ResponseField name="action" type="SendToEvmWithDataAction">
Ready-to-post action JSON (without signature). POST this in the `/exchange` envelope along with the signature and nonce.
</ResponseField>

<ResponseField name="typedData" type="HyperCoreTypedData">
EIP-712 envelope: `{ domain, types, primaryType, message, digest }`. Sign `digest` directly with any 65-byte ECDSA wallet, or pass `domain`/`types`/`message` to `walletClient.signTypedData(...)` for a structured wallet prompt.
</ResponseField>

<ResponseField name="hlBridgeToken" type="Address">
Resolved HlBridgeToken contract on HyperEVM (also present in `action.destinationRecipient`, lowercased).
</ResponseField>
</Expandable>
</ResponseField>

### Example

```typescript
import { createHyperCoreBuilder, postExchangeAction, splitSignature } from "@omni-bridge/hypercore"
import { privateKeyToAccount } from "viem/accounts"

const builder = createHyperCoreBuilder({ network: "mainnet" })

// 1. Build the unsigned action.
const unsigned = await builder.buildTransfer({
spotId: "USDC:0x6d1e7cde53ba9467b783cb7c530ce054",
amount: 1_000_000n, // 1 USDC at on-EVM 6 decimals
recipient: "near:alice.near",
fee: 0n,
message: "",
})

// 2. Sign the precomputed EIP-712 digest with the user's HyperCore wallet.
const account = privateKeyToAccount("0x...")
const signature = await account.sign({ hash: unsigned.typedData.digest })

// 3. POST to Hyperliquid /exchange.
await postExchangeAction({
apiUrl: builder.apiUrl,
action: unsigned.action,
signature: splitSignature(signature),
})
```

---

## postExchangeAction

Thin POST helper that submits the signed action envelope to Hyperliquid's `/exchange`. Throws on non-2xx or `status: "err"` response.

### Signature

```typescript
function postExchangeAction(
options: PostExchangeActionOptions,
): Promise<PostExchangeResult>
```

### Parameters

<ResponseField name="options" type="PostExchangeActionOptions" required>
<Expandable title="PostExchangeActionOptions">
<ResponseField name="apiUrl" type="string" required>
Hyperliquid REST base URL (no `/exchange` suffix).
</ResponseField>

<ResponseField name="action" type="SendToEvmWithDataAction" required>
The action returned by `buildTransfer`.
</ResponseField>

<ResponseField name="signature" type="ActionSignature" required>
`{ r, s, v }` produced from signing the EIP-712 digest. Use `splitSignature(sig)` to split a 65-byte hex signature.
</ResponseField>

<ResponseField name="fetch" type="typeof fetch">
Custom `fetch` implementation. Defaults to the global `fetch`.
</ResponseField>
</Expandable>
</ResponseField>

This call does **not** wait for the downstream HyperEVM `CoreReceived` log. The system transaction lands asynchronously after `/exchange` accepts the action — subscribe via your own RPC tooling if you need landing confirmation.

---

## Spot meta resolvers

`/info spotMeta` is the source of truth for the HlBridgeToken contract and on-EVM decimals of every Hyperliquid spot token. The builder calls it transparently; the helpers below let you query it directly.

<ResponseField name="parseSpotId" type="(spotId: string) => { name: string; tokenId: string }">
Validates and splits a canonical `"NAME:0x<32hex>"` identifier. Throws on malformed input.
</ResponseField>

<ResponseField name="resolveSpotToken" type="(apiUrl: string, spotId: string, options?: SpotMetaFetchOptions) => Promise<SpotTokenInfo>">
Fetch `/info spotMeta` and return `{ spotId, hlBridgeToken, decimals }` for the entry whose `tokenId` matches.
</ResponseField>

<ResponseField name="resolveSpotTokenCached" type="(apiUrl: string, spotId: string, options?: SpotMetaFetchOptions) => Promise<SpotTokenInfo>">
Same as `resolveSpotToken`, but memoizes the `/info` response per `apiUrl` for the life of the process. The builder uses this internally so repeat builds within a session don't re-hit `/info`.
</ResponseField>

<ResponseField name="fetchSpotMeta" type="(apiUrl: string, options?: SpotMetaFetchOptions) => Promise<SpotMetaToken[]>">
Low-level: returns the full token table.
</ResponseField>

---

## Action `data` encoders and EIP-712

<ResponseField name="encodeTransferAction" type="(recipient: Address) => Hex">
`0x00 || abi.encode(address recipient)` — the `data` payload routed to the `ACTION_TRANSFER` path in `HlBridgeToken` (pool release).
</ResponseField>

<ResponseField name="encodeInitTransferAction" type="(fee: bigint, recipient: OmniAddress, message: string) => Hex">
`0x01 || abi.encode(uint128 fee, string recipient, string message)` — the `data` payload that triggers `OmniBridge.initTransfer`.
</ResponseField>

<ResponseField name="buildSendToEvmWithDataTypedData" type="(action: SendToEvmWithDataAction) => HyperCoreTypedData">
Build the EIP-712 envelope (`domain`, `types`, `primaryType`, `message`) plus a precomputed `digest` for an already-assembled action. `buildTransfer` does this for you; reach for it only when you've constructed an action by hand.
</ResponseField>

<ResponseField name="splitSignature" type="(sig: Hex) => ActionSignature">
Splits a 65-byte hex signature (`viem.sign({ hash })` output) into the `{ r, s, v }` envelope expected by Hyperliquid's `/exchange`.
</ResponseField>

---

## Utilities

<ResponseField name="formatAmount" type="(amount: bigint, decimals: number) => string">
Convert a raw bigint to Hyperliquid's compact decimal-string format (no trailing zeros, single leading zero before the decimal point). `decimals` is the HlBridgeToken ERC-20's `.decimals()` (i.e. `weiDecimals + evm_extra_wei_decimals`), **not** `szDecimals`.
</ResponseField>

---

## Constants

| Constant | Type | Value / role |
|---|---|---|
| `ACTION_TRANSFER` | `number` | `0x00` — pool-release action tag. |
| `ACTION_INIT_TRANSFER` | `number` | `0x01` — `OmniBridge.initTransfer` action tag. |
| `HYPERCORE_API_URL` | `Record<Network, string>` | Per-network Hyperliquid REST base URL. |
| `HYPEREVM_CHAIN_ID` | `Record<Network, number>` | `999` (mainnet) / `998` (testnet). Used as `destinationChainId` in the action JSON. |
| `HYPERLIQUID_CHAIN` | `Record<Network, "Mainnet" \| "Testnet">` | Embedded in the action JSON's `hyperliquidChain` field. |
| `DEFAULT_SIGNATURE_CHAIN_ID` | `string` | `"0x66eee"` (Arb-Sepolia). EIP-712 domain `chainId`; only its cross-chain uniqueness matters. |
| `DEFAULT_GAS_LIMIT_TRANSFER` | `number` | `300_000` — pool-release gas limit. |
| `DEFAULT_GAS_LIMIT_INIT_TRANSFER` | `number` | `800_000` — routed-transfer gas limit. |
| `SEND_TO_EVM_WITH_DATA_TYPE_NAME` | `string` | `"HyperliquidTransaction:SendToEvmWithData"` — EIP-712 primary type. |
18 changes: 1 addition & 17 deletions packages/core/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,7 @@ function getContractAddress(addresses: ChainAddresses, chain: ChainKind): string
case ChainKind.Pol:
return addresses.pol.bridge
case ChainKind.HyperEvm:
throw new ValidationError(
"HyperEVM bridge is not yet configured in the SDK",
"UNSUPPORTED_CHAIN",
{ chain: ChainKind[chain] },
)
return addresses.hlevm.bridge
case ChainKind.Abs:
return addresses.abs.bridge
case ChainKind.Strk:
Expand Down Expand Up @@ -212,18 +208,6 @@ class BridgeImpl implements Bridge {
})
}

// HyperEVM is present in the ChainKind enum so borsh discriminants align
// with the on-chain contract, but transfers involving it are not yet
// supported by the SDK until RPC URLs and chain configs are added.
if (sourceChain === ChainKind.HyperEvm || destChain === ChainKind.HyperEvm) {
const offending = sourceChain === ChainKind.HyperEvm ? sourceChain : destChain
throw new ValidationError(
"HyperEVM transfers are not yet supported by the SDK",
"UNSUPPORTED_CHAIN",
{ chain: ChainKind[offending] },
)
}

// Validate EVM addresses have proper checksum
if (isEvmChain(sourceChain)) {
const senderAddr = getAddress(params.sender)
Expand Down
Loading
Loading