Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/sdk-useful-usdh-flows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@usdh-kit/sdk': minor
---

Add useful USDH flows: reverse USDH to USDC swaps on HyperCore and bridgeFromCore for linked USDC/USDH spot assets via Hyperliquid sendAsset.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ Pre-release. Public API is unstable until `1.0.0`.

What works today:

- `getQuote()` and `swap()` for `USDC → USDH` end to end (signing + msgpack + IOC limit submission)
- `getQuote()` and `swap()` for `USDC → USDH` and `USDH → USDC` end to end
- `bridgeToCore()` for moving USDC from HyperEVM to HyperCore, with credit polling
- `bridgeFromCore()` for moving linked USDC/USDH spot assets from HyperCore to HyperEVM
- `getHypercoreBalance()` for spendable HyperCore balances (`total - hold`)
- `getRoute()` / `preflightSwap()` to choose direct HyperCore swap vs HyperEVM bridge
- `bridgeAndSwap()` for the common route → bridge → swap retail flow
Expand All @@ -43,7 +44,6 @@ What works today:
Deferred to follow-up PRs:

- USDT pricing and swap (USDT/USDC/USDH double-hop)
- Reverse direction (USDH → USDC) and `bridgeFromCore`
- Multi-chain source via LiFi/Squid (Ethereum, Arbitrum, Base)

## Install
Expand Down Expand Up @@ -97,6 +97,10 @@ const result = await kit.swap({ from: 'USDC', amount })
console.log(`got ${result.received} USDH for ${result.spent} USDC`)
console.log(`realised slippage: ${result.slippageBps}bps`)

// reverse direction on HyperCore
const reverse = await kit.swap({ from: 'USDH', to: 'USDC', amount: 11_000_000n })
console.log(`got ${reverse.received} USDC`)

// or let the SDK route, bridge if needed, then swap
const routed = await kit.bridgeAndSwap({
from: 'USDC',
Expand All @@ -107,7 +111,7 @@ console.log(`route: ${routed.route.sourceChain}`)
console.log(`order: ${routed.swap.orderId}`)
```

`swap()` submits an IOC limit order priced `slippageBps` above the mid; max slippage is enforced pre-fill by Hyperliquid's matcher. The returned `result.slippageBps` is the realised slippage versus mid.
`swap()` submits an IOC limit order priced from the current mid: `USDC -> USDH` buys up to `mid + slippageBps`, while `USDH -> USDC` sells down to `mid - slippageBps`. The returned `result.slippageBps` is the realised slippage versus mid.

## Widget quickstart

Expand Down Expand Up @@ -143,7 +147,9 @@ A few real flows the SDK is shaped for today. Runnable examples are still on the
## Features (V1)

- `USDC → USDH` quote and swap via the canonical HL spot pair
- `USDH → USDC` reverse swap on HyperCore
- HyperEVM → HyperCore bridge with credit polling (`bridgeToCore`)
- HyperCore → HyperEVM bridge-out for linked USDC/USDH spot assets (`bridgeFromCore`)
- HyperCore balance, route/preflight helpers plus `bridgeAndSwap()` orchestration
- Experimental read-only outcome market metadata, books, and mids
- USDH-only spot order helpers for placing, cancelling, and reading USDH-pair orders
Expand Down
7 changes: 4 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@

TypeScript SDK and React widget for USDH on Hyperliquid.

`usdh-kit` helps apps convert USDC into USDH without reimplementing Hyperliquid spot routing, EIP-712 order signing, HyperEVM bridge transactions, or bridge-credit polling.
`usdh-kit` helps apps work with USDH without reimplementing Hyperliquid spot discovery, EIP-712 order signing, HyperEVM bridge transactions, or bridge-credit polling.

## Packages

| Package | Purpose |
|---|---|
| `@usdh-kit/sdk` | Quote, route, bridge, and swap `USDC -> USDH`. |
| `@usdh-kit/sdk` | Quote, route, bridge, and swap USDH-focused flows. |
| `@usdh-kit/widget` | Embeddable React swap widget built on the SDK. |

## What works today

* Quote and swap `USDC -> USDH` on the canonical Hyperliquid spot pair.
* Quote and swap `USDC -> USDH` and `USDH -> USDC` on the canonical Hyperliquid spot pair.
* Route from existing HyperCore USDC when available.
* Bridge USDC from HyperEVM to HyperCore, wait for credit, then swap.
* Bridge linked USDC/USDH spot assets from HyperCore back to HyperEVM.
* Use approved Hyperliquid agent wallets so browser apps do not ask Rabby or other injected wallets to sign L1 order payloads directly.
* Display HyperEVM and HyperCore balances for USDC and USDH in the widget.

Expand Down
58 changes: 42 additions & 16 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ packages/sdk/src
├── kit.ts entry point: createUsdhKit(config) → UsdhKit
├── pair-resolver.ts caches the USDH/USDC spot pair from /info
├── pricing.ts decimal parsing, mid-price computation
├── bridge.ts HyperEVM HyperCore transfer + credit polling
├── bridge.ts HyperEVM HyperCore transfers + credit polling
├── signing.ts EIP-712 typed-data signing for HL L1 actions
├── msgpack.ts canonical msgpack encoding (action_hash input)
├── abi.ts ERC-20 approve/deposit/transfer encoding for bridge txs
Expand All @@ -27,7 +27,7 @@ packages/sdk/src

## Initial setup

`createUsdhKit({ network, signer, accountAddress?, evmWallet?, slippageBps?, fetch?, timeoutMs?, logger? })` validates the config synchronously and returns an object exposing `swap`, `getQuote`, `getRoute`, `preflightSwap`, `bridgeAndSwap`, and `bridgeToCore`. Two transport clients are created lazily — one for read (`/info`) and one for write (`/exchange`).
`createUsdhKit({ network, signer, accountAddress?, evmWallet?, slippageBps?, fetch?, timeoutMs?, logger? })` validates the config synchronously and returns an object exposing `swap`, `getQuote`, `getRoute`, `preflightSwap`, `bridgeAndSwap`, `bridgeToCore`, and `bridgeFromCore`. Two transport clients are created lazily — one for read (`/info`) and one for write (`/exchange`).

When `signer` is an approved Hyperliquid agent wallet, set `accountAddress` to the user's master wallet. Reads, routing, balances, and bridge ownership use `accountAddress`; L1 order signatures use `signer`.

Expand All @@ -36,9 +36,13 @@ The USDH/USDC pair is resolved on first call (cached for the kit's lifetime) by
## getQuote

```
QuoteInput → resolvePair() → info.l2Book(pair.name) → midPrice18(book)
→ estimatedReceived
→ return Quote
QuoteInput
↓ resolve direction (USDC → USDH buy, or USDH → USDC sell)
↓ resolvePair()
↓ info.l2Book(pair.name)
↓ midPrice18(book)
↓ amount / price or amount * price
↓ return Quote { from, to, estimatedReceived }
```

No signing. No state. Quote is valid for 30 seconds (`validUntil`).
Expand All @@ -47,30 +51,32 @@ No signing. No state. Quote is valid for 30 seconds (`validUntil`).

```
RouteInput
↓ validate source + amount + slippage
↓ validate source + target + amount + slippage
↓ resolvePair()
↓ info.l2Book(pair.name) → Quote
↓ info.spotClearinghouseState(user) → HyperCore source balance
↓ spendable = total - hold (floored at zero)
↓ requiredHypercoreBalance = amount + slippage buffer + HC fee buffer
↓ requiredHypercoreBalance = source amount + optional buy buffer + HC fee buffer
↓ choose sourceChain:
├── HyperCore covers → sourceChain: 'hypercore'
└── otherwise → sourceChain: 'hyperevm'
↓ return SwapRoute { quote, sourceChain, requiresBridge, canSwap, balances }
```

`preflightSwap()` is an alias for `getRoute()` so UI code can use the name that
best matches its intent. These helpers inspect spendable HyperCore balance only; they do not read the user's HyperEVM ERC20 balance. `getHypercoreBalance()` is exposed separately for apps that want to display `total`, `hold`, and `available` without computing a route.
best matches its intent. These helpers inspect spendable HyperCore balance only; they do not read the user's HyperEVM ERC20 balance. `USDH → USDC` is HyperCore-only in v1 because there is no HyperEVM direct swap router in scope. `getHypercoreBalance()` is exposed separately for apps that want to display `total`, `hold`, and `available` without computing a route.

## swap (USDC path)
## swap

```
SwapInput
↓ resolve direction
↓ resolvePair()
↓ info.l2Book(pair.name)
↓ midPrice18(book)
↓ limitPrice18 = mid * (10000 + slippageBps) / 10000
↓ build msgpack action: { type: 'order', orders: [{ a, b: true, p, s, r: false, t: { limit: { tif: 'Ioc' } } }], grouping: 'na' }
↓ buy: limitPrice18 = mid * (10000 + slippageBps) / 10000
↓ sell: limitPrice18 = mid * (10000 - slippageBps) / 10000
↓ build msgpack action: { type: 'order', orders: [{ a, b, p, s, r: false, t: { limit: { tif: 'Ioc' } } }], grouping: 'na' }
↓ signL1Action({ signer, action, nonce, network, expiresAfter })
│ ├── canonical msgpack encode of action
│ ├── keccak256 → action_hash
Expand All @@ -83,7 +89,7 @@ SwapInput
└── error → throw NetworkError(`order error: ${...}`)
```

The IOC limit ensures Hyperliquid's matcher rejects fills at worse than `mid + slippageBps`. The kit's realised slippage (`SwapResult.slippageBps`) is computed from `avgPx` vs `mid`.
For `USDC → USDH`, the order buys USDH (`b: true`) with a max price of `mid + slippageBps`. For `USDH → USDC`, the order sells USDH (`b: false`) with a min price of `mid - slippageBps`. The kit's realised slippage (`SwapResult.slippageBps`) is computed from `avgPx` vs `mid`.

## bridgeToCore

Expand All @@ -102,6 +108,24 @@ No explicit HyperCore-side signing — the credit is automatic once the EVM tx c

For USDC, the wallet can see two HyperEVM transactions: `approve` if allowance is insufficient, then `deposit`. The final `USDC → USDH` trade is a HyperCore order signed separately by the configured `signer` (usually an approved agent in browser apps).

## bridgeFromCore

```
BridgeFromCoreInput
↓ resolve linked token + system address from spotMeta
↓ require signer.address === accountAddress
↓ read spendable HyperCore balance
↓ sign user-signed sendAsset action
├── EIP-712 typed data domain ('HyperliquidSignTransaction', chainId 0x66eee)
└── token = USDC or tokenName:tokenId, destination = system address
↓ exchange.submit({ action, signature, nonce })
↓ return BridgeFromCoreResult { status: 'submitted', systemAddress, recipient, submittedAt }
```

`bridgeFromCore()` supports linked `USDC` and `USDH` spot assets. It does not accept an arbitrary HyperEVM recipient in v1: Hyperliquid credits the EVM-side account associated with the Core action sender, so the safest public API is sender-owned bridge-out only.
The helper resolves after the `sendAsset` action is accepted; it does not poll
for the later HyperEVM credit.

## bridgeAndSwap

```
Expand All @@ -120,8 +144,10 @@ BridgeAndSwapInput

The helper emits optional progress events: `route`, `bridging`, `swapping`,
`done`. It intentionally re-quotes inside `swap()` after a bridge completes so
the order limit is based on fresh book state. `BridgeAndSwapError` is reserved
for lifecycle failures where phase context matters; route blockers still throw
the order limit is based on fresh book state. Reverse `USDH → USDC` routes do
not bridge from HyperEVM first; callers can run `bridgeFromCore()` after the
swap if they need the USDC on HyperEVM. `BridgeAndSwapError` is reserved for
lifecycle failures where phase context matters; route blockers still throw
`MissingEvmWalletError` or `InsufficientBalanceError` directly.

## Errors
Expand All @@ -132,7 +158,7 @@ All SDK errors extend `UsdhKitError`. Subclasses give consumers `instanceof` gra
- `InsufficientBalanceError` — pre-flight balance check failed
- `BridgeTimeoutError` — credit never landed within timeout
- `BridgeAndSwapError` — wraps unexpected `bridgeAndSwap` route, bridge, or swap failures with `phase`, `route`, optional `bridge`, and `cause`; `isBridgeAndSwapError()` narrows both class instances and structural copies
- `InvalidInputError` — amount, decimal string, or other input is malformed
- `InvalidInputError` — amount, decimal string, bridge ownership, or other input is malformed
- `SigningError` — `signer.signTypedData` rejected or returned invalid sig
- `NetworkError` — `/info` or `/exchange` fetch failed, or HL returned a protocol-level error
- `NotImplementedError` — feature deferred (e.g. USDT path)
Expand All @@ -147,7 +173,7 @@ Outcome reads are experimental and read-only. The SDK validates `outcomeMeta`,
derives encoded side coins like `#200`, and reuses the hardened `l2Book()` path
for books. It does not place outcome orders or claim a settlement asset.

`ExchangeClient` is internal — consumers should call `kit.swap()` rather than building actions themselves.
`ExchangeClient` is internal — consumers should call `kit.swap()`, `kit.bridgeFromCore()`, or the order helpers rather than building actions themselves.

## Bridge polling internals

Expand Down
28 changes: 26 additions & 2 deletions docs/bridge-and-swap.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,32 @@ The wallet may show two Rabby prompts:

After the HyperCore credit lands, the approved agent signs the USDH order. The connected wallet should not receive a third popup for the order itself.

## Reverse direction

`USDH -> USDC` is a HyperCore-only spot swap:

```ts
await kit.swap({
from: 'USDH',
to: 'USDC',
amount: 11_000_000n,
})
```

If the user wants USDC back on HyperEVM, call `bridgeFromCore()` after the swap:

```ts
const swap = await kit.swap({ from: 'USDH', to: 'USDC', amount: 11_000_000n })
const bridgeOut = await kit.bridgeFromCore({ asset: 'USDC', amount: swap.received })
console.log(bridgeOut.status, bridgeOut.submittedAt)
```

`bridgeFromCore()` uses Hyperliquid's user-signed `sendAsset` action to the
token system address. The HyperEVM recipient is the sender of that Core action,
so the configured `signer` must be the master account, not an approved agent
for a separate `accountAddress`. The helper resolves when Hyperliquid accepts
the action, not when the later HyperEVM credit is confirmed.

## Progress events

Use `onProgress` for UI state:
Expand Down Expand Up @@ -74,7 +100,5 @@ try {

## Current limitations

* Reverse direction (`USDH -> USDC`) is not part of V1.
* `bridgeFromCore` is deferred.
* USDT routing is deferred.
* Allowance-aware approval skipping is a planned UX optimization.
2 changes: 2 additions & 0 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Hyperliquid-specific terms used across `@usdh-kit/sdk` and `@usdh-kit/widget`.

**Bridge polling** — `bridgeToCore` submits the EVM bridge transaction, then polls `spotClearinghouseState` until the credit lands (default timeout 180s). The kit returns once the credit is confirmed; no extra HyperCore signing needed.

**Bridge out** — `bridgeFromCore` submits a Hyperliquid `sendAsset` action to the linked token system address so a HyperCore spot asset can move back to HyperEVM. The signer must be the same account whose HyperCore balance is being spent.

## Trading

**Spot pair** — Hyperliquid spot market. Identified by an alias like `@230` (USDH/USDC) and a numeric `assetIndex`. The pair has a base token (USDH), a quote token (USDC), and decimal conventions for both.
Expand Down
11 changes: 11 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ const kit = createUsdhKit({
})
```

### `InvalidInputError: bridgeFromCore requires signer.address to match accountAddress`

`bridgeFromCore()` signs a user-owned HyperCore `sendAsset` action. Approved
agent wallets are useful for spot orders, but they cannot bridge funds out for a
master account. Create a separate kit with the master wallet as `signer` before
calling `bridgeFromCore()`.

`bridgeFromCore()` returns `status: 'submitted'` after Hyperliquid accepts the
action. The HyperEVM-side credit is asynchronous, so confirm the EVM balance
before starting a dependent HyperEVM transaction.

Browser apps that use an approved agent should also pass `accountAddress` so bridge ownership and balance reads stay tied to the master wallet:

```ts
Expand Down
Loading
Loading