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
7 changes: 7 additions & 0 deletions .changeset/sdk-usdc-canonical.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@usdh-kit/sdk': minor
---

Make USDC the canonical default quote for discovery and relax order pair resolution to any spot pair.

`listPairs()`, `getPair()`, and `getMids()` now default to USDC-quoted pairs. Pass `{ quote: 'USDH' }` to retain the legacy USDH-quoted behaviour. `placeOrder`, `cancelOrder`, `getOpenOrders`, and `getOrderStatus` now accept any spot pair from `spotMeta`, not only USDH-bearing ones. `getOpenOrders()` with no pair filter returns all open orders instead of filtering to USDH pairs. All USDH pair paths and the swap/bridge layer are unchanged.
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,11 @@ function PairSelect({
<span className="min-w-0">
<span className="block truncate font-medium">{pair.label}</span>
<span className="mt-1 block text-xs text-neutral-500">
{pair.role === 'base' ? 'USDH base' : 'USDH quote'}
{pair.role === undefined
? 'USDC quote'
: pair.role === 'base'
? 'USDH base'
: 'USDH quote'}
</span>
</span>
<span className="self-center text-right text-sm tabular-nums">{pair.mid}</span>
Expand Down
5 changes: 3 additions & 2 deletions apps/demo/src/lib/gallery-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export interface GalleryPair {
name: string
label: string
index: number
role: 'base' | 'quote'
/** USDH's role in the pair. Absent for USDC-quoted pairs with no USDH leg. */
role?: 'base' | 'quote'
mid: string
}

Expand Down Expand Up @@ -242,7 +243,7 @@ function toGalleryPair(pair: UsdhPair, mids: Record<string, string>): GalleryPai
name: pair.name,
label: `${pair.base}/${pair.quote}`,
index: pair.index,
role: pair.usdhRole,
...(pair.usdhRole !== undefined && { role: pair.usdhRole }),
mid: mids[pair.name] ?? mids[`@${pair.index}`] ?? '-',
}
}
Expand Down
102 changes: 80 additions & 22 deletions packages/sdk/src/discovery.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { NetworkError } from './errors.js'
import type { InfoClient, NSigFigs } from './transport/info.js'
import type { L2Book, SpotMeta, SpotToken } from './transport/types.js'
import type { L2Book, SpotMeta, SpotPair, SpotToken } from './transport/types.js'

const USDH_TOKEN_NAME = 'USDH'
const USDC_TOKEN_NAME = 'USDC'

export interface UsdhPair {
kind: 'spot'
Expand All @@ -12,16 +13,23 @@ export interface UsdhPair {
base: string
/** Quote token name. */
quote: string
/** Whether USDH is the base or quote of the pair. */
usdhRole: 'base' | 'quote'
/**
* Whether USDH is the base or quote of the pair. `undefined` for pairs that
* do not involve USDH (e.g. HYPE/USDC).
*/
usdhRole?: 'base' | 'quote'
/** Index into `spotMeta.universe`. */
index: number
/** Token indices [base, quote]. */
tokens: [number, number]
}

export interface ListUsdhPairsOpts {
quote?: 'USDH'
/**
* Filter by quote token. Defaults to `'USDC'` (USDC-quoted pairs).
* Pass `'USDH'` to opt into the legacy USDH-quoted pair list.
*/
quote?: 'USDH' | 'USDC'
kind?: 'spot'
}

Expand All @@ -32,7 +40,11 @@ export interface GetUsdhPairInput {
}

export interface GetMidsOpts {
quote?: 'USDH'
/**
* Filter mid prices by quote token. Defaults to `'USDC'` (USDC-quoted pairs
* only). Pass `'USDH'` to get USDH-quoted pair mids instead.
*/
quote?: 'USDH' | 'USDC'
}

/**
Expand Down Expand Up @@ -66,6 +78,48 @@ export function listUsdhSpotPairs(meta: SpotMeta): UsdhPair[] {
return out
}

/**
* Build a `UsdhPair` record for every spot pair in `spotMeta`, regardless of
* whether USDH is involved. `usdhRole` is set only when USDH appears as base
* or quote; it is `undefined` for pairs such as HYPE/USDC.
*/
function listAllSpotPairs(meta: SpotMeta): UsdhPair[] {
const tokens = tokenIndexMap(meta.tokens)
const usdhIndex = meta.tokens.find((t) => t.name === USDH_TOKEN_NAME)?.index
const out: UsdhPair[] = []
for (const pair of meta.universe) {
const [baseIdx, quoteIdx] = pair.tokens
const baseToken = tokens.get(baseIdx)
const quoteToken = tokens.get(quoteIdx)
if (baseToken === undefined || quoteToken === undefined) continue
out.push(spotPairToUsdhPair(pair, baseToken, quoteToken, usdhIndex))
}
return out
}

function spotPairToUsdhPair(
pair: SpotPair,
baseToken: SpotToken,
quoteToken: SpotToken,
usdhIndex: number | undefined,
): UsdhPair {
const base: UsdhPair = {
kind: 'spot',
name: pair.name,
base: baseToken.name,
quote: quoteToken.name,
index: pair.index,
tokens: pair.tokens,
}
if (usdhIndex !== undefined && pair.tokens[0] === usdhIndex) {
return { ...base, usdhRole: 'base' }
}
if (usdhIndex !== undefined && pair.tokens[1] === usdhIndex) {
return { ...base, usdhRole: 'quote' }
}
return base
}

/**
* Find a single USDH-bearing spot pair by base/quote token names. Orientation
* is strict: `{ base: 'HYPE', quote: 'USDH' }` will not match `USDH/HYPE`.
Expand All @@ -84,41 +138,44 @@ export function findUsdhSpotPair(meta: SpotMeta, input: GetUsdhPairInput): UsdhP
}

/**
* Cache-aware listing of USDH spot pairs. Caches `spotMeta` once and indexes
* pairs by name so repeated `getPair` lookups do not refetch.
* Cache-aware listing of spot pairs. Defaults to USDC-quoted pairs; pass
* `{ quote: 'USDH' }` to opt into the legacy USDH-quoted pair list.
* Caches `spotMeta` once and indexes all pairs by name so repeated `getPair`
* lookups do not refetch.
*/
export function createDiscovery(info: InfoClient): {
listPairs(opts?: ListUsdhPairsOpts): Promise<UsdhPair[]>
getPair(input: GetUsdhPairInput): Promise<UsdhPair>
getBook(pair: string, opts?: { nSigFigs?: NSigFigs }): Promise<L2Book>
getMids(opts?: GetMidsOpts): Promise<Record<string, string>>
} {
let pairsCache: Promise<UsdhPair[]> | null = null
let allPairsCache: Promise<UsdhPair[]> | null = null
/** Index by `@<index>` name for O(1) lookups. */
let byName: Map<string, UsdhPair> | null = null

async function loadPairs(): Promise<UsdhPair[]> {
if (pairsCache === null) {
pairsCache = info.spotMeta().then((meta) => {
const pairs = listUsdhSpotPairs(meta)
async function loadAllPairs(): Promise<UsdhPair[]> {
if (allPairsCache === null) {
allPairsCache = info.spotMeta().then((meta) => {
const pairs = listAllSpotPairs(meta)
byName = new Map(pairs.map((p) => [p.name, p]))
return pairs
})
}
return pairsCache
return allPairsCache
}

return {
async listPairs(opts) {
assertSpotKind(opts?.kind)
const pairs = await loadPairs()
if (opts?.quote === USDH_TOKEN_NAME) {
return pairs.filter((p) => p.quote === USDH_TOKEN_NAME)
}
return pairs
const pairs = await loadAllPairs()
// Default to USDC-quoted pairs; pass `quote: 'USDH'` for the legacy path.
const quoteFilter = opts?.quote ?? USDC_TOKEN_NAME
return pairs.filter((p) => p.quote === quoteFilter)
},
async getPair(input) {
assertSpotKind(input.kind)
const pairs = await loadPairs()
const pairs = await loadAllPairs()
// Fast path: try the canonical `@<index>` name form if stored by name.
const cached = byName?.get(`${input.base}/${input.quote}`)
if (cached !== undefined) return cached
const match = pairs.find((p) => p.base === input.base && p.quote === input.quote)
Expand All @@ -132,11 +189,12 @@ export function createDiscovery(info: InfoClient): {
},
async getMids(opts) {
const all = await info.allMids()
if (opts?.quote !== USDH_TOKEN_NAME) return all
const pairs = await loadPairs()
// Default to USDC-quoted pairs; pass `quote: 'USDH'` for the legacy path.
const quoteFilter = opts?.quote ?? USDC_TOKEN_NAME
const pairs = await loadAllPairs()
const out: Record<string, string> = {}
for (const p of pairs) {
if (p.quote !== USDH_TOKEN_NAME) continue
if (p.quote !== quoteFilter) continue
const mid = all[p.name] ?? all[`@${p.index}`]
if (mid !== undefined) out[p.name] = mid
}
Expand Down
29 changes: 20 additions & 9 deletions packages/sdk/src/kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,23 @@ export interface UsdhKit {
* account because the HyperEVM recipient is the sender of the Core action.
*/
bridgeFromCore(input: BridgeFromCoreInput): Promise<BridgeFromCoreResult>
/** List USDH-bearing spot pairs from `spotMeta`. Cached after the first call. */
/**
* List spot pairs from `spotMeta`. Defaults to USDC-quoted pairs. Pass
* `{ quote: 'USDH' }` to get the legacy USDH-quoted pair list instead.
* Cached after the first call.
*/
listPairs(opts?: ListUsdhPairsOpts): Promise<UsdhPair[]>
/** Find one USDH-bearing spot pair by base/quote token names. */
/**
* Find one spot pair by base/quote token names. Works for any quote token
* (USDC, USDH, or other); not restricted to USDH-bearing pairs.
*/
getPair(input: GetUsdhPairInput): Promise<UsdhPair>
/** Fetch the L2 book for a live pair name, usually `@<spotIndex>`. */
getBook(pair: string, opts?: { nSigFigs?: NSigFigs }): Promise<L2Book>
/** Fetch mid prices, optionally filtered to USDH-quote pairs. */
/**
* Fetch mid prices filtered by quote token. Defaults to USDC-quoted pairs.
* Pass `{ quote: 'USDH' }` to get mids for USDH-quoted pairs instead.
*/
getMids(opts?: GetMidsOpts): Promise<Record<string, string>>
/** List experimental read-only outcome markets from Hyperliquid outcome metadata. */
listOutcomeMarkets(): Promise<UsdhOutcomeMarket[]>
Expand All @@ -131,19 +141,20 @@ export interface UsdhKit {
getOutcomeBook(input: GetOutcomeBookInput): Promise<L2Book>
/** Fetch mid prices keyed by encoded outcome side coin, e.g. `#200`. */
getOutcomeMids(): Promise<Record<string, string>>
/** Place a USDH-pair spot order. Accepts `listPairs()` names or token aliases like `USDH/USDC`. */
/** Place a spot order on any pair. Accepts `listPairs()` names or token-pair aliases like `HYPE/USDC` or `USDH/USDC`. */
placeOrder(input: PlaceOrderInput): Promise<PlaceOrderResult>
/** Cancel a resting USDH-pair order by oid. */
/** Cancel a resting spot order by oid. */
cancelOrder(input: CancelOrderInput): Promise<CancelOrderResult>
/** List the user's USDH-pair resting open orders, optionally filtered to one pair. */
/** List the user's resting open orders. Without a pair filter, returns all open orders. */
getOpenOrders(input?: GetOpenOrdersInput): Promise<OpenOrder[]>
/** Fetch a single USDH-pair order's status by pair and oid. */
/** Fetch a single spot order's status by pair and oid. */
getOrderStatus(input: GetOrderStatusInput): Promise<OrderStatusResponse>
}

/**
* Create a kit bound to a network and signer. Validates input synchronously
* and lazily resolves the USDH/USDC pair on first call.
* Create a kit bound to a network and signer. Validates input synchronously.
* Discovery defaults to USDC-quoted pairs; pass `{ quote: 'USDH' }` to the
* discovery methods to access the legacy USDH-quoted pair list.
*/
export function createUsdhKit(config: KitConfig): UsdhKit {
validateConfig(config)
Expand Down
Loading
Loading