diff --git a/.changeset/sdk-usdc-canonical.md b/.changeset/sdk-usdc-canonical.md
new file mode 100644
index 0000000..59bb5cb
--- /dev/null
+++ b/.changeset/sdk-usdc-canonical.md
@@ -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.
diff --git a/apps/demo/src/components/registry/previews/market-board.tsx b/apps/demo/src/components/registry/previews/market-board.tsx
index 385dd92..cc17fad 100644
--- a/apps/demo/src/components/registry/previews/market-board.tsx
+++ b/apps/demo/src/components/registry/previews/market-board.tsx
@@ -298,7 +298,11 @@ function PairSelect({
{pair.label}
- {pair.role === 'base' ? 'USDH base' : 'USDH quote'}
+ {pair.role === undefined
+ ? 'USDC quote'
+ : pair.role === 'base'
+ ? 'USDH base'
+ : 'USDH quote'}
{pair.mid}
diff --git a/apps/demo/src/lib/gallery-data.ts b/apps/demo/src/lib/gallery-data.ts
index 066d31d..b7704d6 100644
--- a/apps/demo/src/lib/gallery-data.ts
+++ b/apps/demo/src/lib/gallery-data.ts
@@ -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
}
@@ -242,7 +243,7 @@ function toGalleryPair(pair: UsdhPair, mids: Record): 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}`] ?? '-',
}
}
diff --git a/packages/sdk/src/discovery.ts b/packages/sdk/src/discovery.ts
index a274240..b1cd289 100644
--- a/packages/sdk/src/discovery.ts
+++ b/packages/sdk/src/discovery.ts
@@ -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'
@@ -12,8 +13,11 @@ 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]. */
@@ -21,7 +25,11 @@ export interface UsdhPair {
}
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'
}
@@ -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'
}
/**
@@ -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`.
@@ -84,8 +138,10 @@ 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
@@ -93,32 +149,33 @@ export function createDiscovery(info: InfoClient): {
getBook(pair: string, opts?: { nSigFigs?: NSigFigs }): Promise
getMids(opts?: GetMidsOpts): Promise>
} {
- let pairsCache: Promise | null = null
+ let allPairsCache: Promise | null = null
+ /** Index by `@` name for O(1) lookups. */
let byName: Map | null = null
- async function loadPairs(): Promise {
- if (pairsCache === null) {
- pairsCache = info.spotMeta().then((meta) => {
- const pairs = listUsdhSpotPairs(meta)
+ async function loadAllPairs(): Promise {
+ 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 `@` 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)
@@ -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 = {}
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
}
diff --git a/packages/sdk/src/kit.ts b/packages/sdk/src/kit.ts
index 468e6db..ca6be31 100644
--- a/packages/sdk/src/kit.ts
+++ b/packages/sdk/src/kit.ts
@@ -115,13 +115,23 @@ export interface UsdhKit {
* account because the HyperEVM recipient is the sender of the Core action.
*/
bridgeFromCore(input: BridgeFromCoreInput): Promise
- /** 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
- /** 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
/** Fetch the L2 book for a live pair name, usually `@`. */
getBook(pair: string, opts?: { nSigFigs?: NSigFigs }): Promise
- /** 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>
/** List experimental read-only outcome markets from Hyperliquid outcome metadata. */
listOutcomeMarkets(): Promise
@@ -131,19 +141,20 @@ export interface UsdhKit {
getOutcomeBook(input: GetOutcomeBookInput): Promise
/** Fetch mid prices keyed by encoded outcome side coin, e.g. `#200`. */
getOutcomeMids(): Promise>
- /** 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
- /** Cancel a resting USDH-pair order by oid. */
+ /** Cancel a resting spot order by oid. */
cancelOrder(input: CancelOrderInput): Promise
- /** 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
- /** 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
}
/**
- * 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)
diff --git a/packages/sdk/src/orders.ts b/packages/sdk/src/orders.ts
index 69555fe..77b2c9c 100644
--- a/packages/sdk/src/orders.ts
+++ b/packages/sdk/src/orders.ts
@@ -1,4 +1,4 @@
-import { type UsdhPair, findUsdhSpotPair, listUsdhSpotPairs } from './discovery.js'
+import type { UsdhPair } from './discovery.js'
import { InvalidInputError, NetworkError } from './errors.js'
import { formatDecimal, formatSpotPrice, midPrice18, parseDecimal } from './pricing.js'
import { signL1Action } from './signing.js'
@@ -8,7 +8,13 @@ import {
isOrderResponse,
} from './transport/exchange.js'
import type { InfoClient } from './transport/info.js'
-import type { OpenOrder, OrderStatusResponse, SpotPair, SpotToken } from './transport/types.js'
+import type {
+ OpenOrder,
+ OrderStatusResponse,
+ SpotMeta,
+ SpotPair,
+ SpotToken,
+} from './transport/types.js'
import type { Address } from './types/hex.js'
import type { Network } from './types/network.js'
import type { Signer } from './types/signer.js'
@@ -23,7 +29,7 @@ export type OrderSide = 'buy' | 'sell'
export type Tif = 'Gtc' | 'Ioc' | 'Alo'
export interface PlaceOrderInput {
- /** USDH-bearing spot pair. Accepts `listPairs()` names like `@230` or aliases like `USDH/USDC`. */
+ /** Spot pair to trade. Accepts `listPairs()` names like `@230` or token-pair aliases like `HYPE/USDC` or `USDH/USDC`. */
pair: string
side: OrderSide
/** Size in base-token units, decimal string (e.g. "1.5"). */
@@ -49,7 +55,7 @@ export interface PlaceOrderResult {
}
export interface CancelOrderInput {
- /** USDH-bearing spot pair. Accepts `listPairs()` names like `@230` or aliases like `USDH/USDC`. */
+ /** Spot pair the order belongs to. Accepts `listPairs()` names like `@230` or token-pair aliases like `HYPE/USDC`. */
pair: string
/** Order id to cancel. */
oid: number
@@ -60,12 +66,12 @@ export interface CancelOrderResult {
}
export interface GetOpenOrdersInput {
- /** Optional USDH-bearing spot pair filter. Accepts the same formats as `placeOrder`. */
+ /** Optional spot pair filter. Accepts the same formats as `placeOrder`. */
pair?: string
}
export interface GetOrderStatusInput {
- /** USDH-bearing spot pair the order is expected to belong to. */
+ /** Spot pair the order is expected to belong to. */
pair: string
/** Order id to inspect. */
oid: number
@@ -88,9 +94,9 @@ interface PairContext {
}
/**
- * Build a Track 3 USDH-only order layer on top of the existing transport.
- * Lookups go through `findUsdhSpotPair` so callers cannot place orders on
- * pairs where USDH is neither base nor quote.
+ * Build a general spot order layer on top of the existing transport. Resolves
+ * any spot pair from `spotMeta`, including USDC-quoted pairs. USDH-bearing
+ * pairs remain fully supported as a legacy path.
*/
export function createOrders(deps: OrdersDeps): {
placeOrder(input: PlaceOrderInput): Promise
@@ -216,16 +222,13 @@ export function createOrders(deps: OrdersDeps): {
async getOpenOrders(input) {
const pair = readOptionalPair(input)
- const pairFilter = pair === undefined ? null : await resolveOrderCoinNames(deps.info, pair)
const openOrders = await deps.info.frontendOpenOrders(deps.accountAddress)
- if (pairFilter !== null) {
- return openOrders.filter((order) => pairFilter.has(order.coin))
+ if (pair === undefined) {
+ // No pair filter: return all open orders across any spot pair.
+ return openOrders
}
- if (openOrders.length === 0) {
- return []
- }
- const usdhCoins = await resolveOrderCoinNames(deps.info)
- return openOrders.filter((order) => usdhCoins.has(order.coin))
+ const pairFilter = await resolveOrderCoinNames(deps.info, pair)
+ return openOrders.filter((order) => pairFilter.has(order.coin))
},
async getOrderStatus(input) {
@@ -241,7 +244,7 @@ export function createOrders(deps: OrdersDeps): {
return status
}
if (!orderMatchesPair(status.order.order, ctx.pair)) {
- throw new InvalidInputError(`order ${oid} is not on USDH pair ${ctx.pair.name}`)
+ throw new InvalidInputError(`order ${oid} is not on pair ${ctx.pair.name}`)
}
return status
},
@@ -307,6 +310,10 @@ async function resolvePairContext(info: InfoClient, pairInput: string): Promise<
assertPairInput(pairInput)
const meta = await info.spotMeta()
const tokens = new Map(meta.tokens.map((t) => [t.index, t]))
+ const usdhIndex = meta.tokens.find((t) => t.name === USDH_TOKEN_NAME)?.index
+
+ // Fast path: the input is an `@` or canonical name found directly in
+ // the universe list. Accepts any spot pair, not only USDH-bearing ones.
const universePair = meta.universe.find((p) => p.name === pairInput)
if (universePair !== undefined) {
const baseToken = tokens.get(universePair.tokens[0])
@@ -314,7 +321,7 @@ async function resolvePairContext(info: InfoClient, pairInput: string): Promise<
if (baseToken === undefined || quoteToken === undefined) {
throw new NetworkError(`token metadata missing for pair ${pairInput}`)
}
- const pair = usdhPairFromUniversePair(universePair, baseToken, quoteToken)
+ const pair = anySpotPairToUsdhPair(universePair, baseToken, quoteToken, usdhIndex)
return {
pair,
baseSzDecimals: baseToken.szDecimals,
@@ -322,11 +329,13 @@ async function resolvePairContext(info: InfoClient, pairInput: string): Promise<
}
}
+ // Alias path: the input is `BASE/QUOTE`. Search across all spot pairs so
+ // that USDC-quoted pairs (e.g. HYPE/USDC) resolve in addition to USDH pairs.
const alias = parsePairAlias(pairInput)
if (alias === null) {
throw new InvalidInputError(`pair ${pairInput} not found in spotMeta`)
}
- const pair = findInputUsdhSpotPair(meta, alias)
+ const pair = findAnySpotPairByAlias(meta, alias)
const baseToken = tokens.get(pair.tokens[0])
if (baseToken === undefined) {
throw new NetworkError(`token metadata missing for pair ${pairInput}`)
@@ -338,19 +347,9 @@ async function resolvePairContext(info: InfoClient, pairInput: string): Promise<
}
}
-async function resolveOrderCoinNames(info: InfoClient, pairInput?: string): Promise> {
- if (pairInput !== undefined) {
- const ctx = await resolvePairContext(info, pairInput)
- return orderCoinNames(ctx.pair)
- }
- const meta = await info.spotMeta()
- const names = new Set()
- for (const pair of listUsdhSpotPairs(meta)) {
- for (const name of orderCoinNames(pair)) {
- names.add(name)
- }
- }
- return names
+async function resolveOrderCoinNames(info: InfoClient, pairInput: string): Promise> {
+ const ctx = await resolvePairContext(info, pairInput)
+ return orderCoinNames(ctx.pair)
}
function readOptionalPair(input: GetOpenOrdersInput | undefined): string | undefined {
@@ -387,37 +386,51 @@ function parsePairAlias(pair: string): { base: string; quote: string } | null {
return { base, quote }
}
-function findInputUsdhSpotPair(
- meta: Parameters[0],
- input: Parameters[1],
-): UsdhPair {
- try {
- return findUsdhSpotPair(meta, input)
- } catch (error) {
- if (error instanceof NetworkError && error.message.startsWith('pair ')) {
- throw new InvalidInputError(error.message)
- }
- throw error
- }
-}
-
-function usdhPairFromUniversePair(
+/**
+ * Build a `UsdhPair` from any universe pair and its resolved tokens. Sets
+ * `usdhRole` when one of the tokens is USDH; omits it otherwise (compatible
+ * with `exactOptionalPropertyTypes`).
+ * This is the general (non-USDH-restricted) version used by order resolution.
+ */
+function anySpotPairToUsdhPair(
pair: SpotPair,
baseToken: SpotToken,
quoteToken: SpotToken,
+ usdhIndex: number | undefined,
): UsdhPair {
- if (baseToken.name !== USDH_TOKEN_NAME && quoteToken.name !== USDH_TOKEN_NAME) {
- throw new InvalidInputError(`pair must have ${USDH_TOKEN_NAME} as base or quote`)
- }
- return {
+ const base: UsdhPair = {
kind: 'spot',
name: pair.name,
base: baseToken.name,
quote: quoteToken.name,
- usdhRole: baseToken.name === USDH_TOKEN_NAME ? 'base' : 'quote',
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
+}
+
+/**
+ * Search all spot pairs in `spotMeta` by `BASE/QUOTE` alias. Accepts any
+ * quote token (USDC, USDH, or other). Throws `InvalidInputError` if not found.
+ */
+function findAnySpotPairByAlias(meta: SpotMeta, alias: { base: string; quote: string }): UsdhPair {
+ const tokens = new Map(meta.tokens.map((t) => [t.index, t]))
+ const usdhIndex = meta.tokens.find((t) => t.name === USDH_TOKEN_NAME)?.index
+ for (const universePair of meta.universe) {
+ const baseToken = tokens.get(universePair.tokens[0])
+ const quoteToken = tokens.get(universePair.tokens[1])
+ if (baseToken === undefined || quoteToken === undefined) continue
+ if (baseToken.name === alias.base && quoteToken.name === alias.quote) {
+ return anySpotPairToUsdhPair(universePair, baseToken, quoteToken, usdhIndex)
+ }
+ }
+ throw new InvalidInputError(`pair ${alias.base}/${alias.quote} not found in spotMeta`)
}
function orderCoinNames(pair: UsdhPair): Set {
diff --git a/packages/sdk/test/discovery.test.ts b/packages/sdk/test/discovery.test.ts
index 04f3c37..63b0e09 100644
--- a/packages/sdk/test/discovery.test.ts
+++ b/packages/sdk/test/discovery.test.ts
@@ -162,11 +162,40 @@ describe('createDiscovery', () => {
expect(info.spotMeta).toHaveBeenCalledOnce()
})
- it('filters listPairs to USDH-quote pairs when requested', async () => {
+ it('defaults listPairs to USDC-quoted pairs', async () => {
+ const discovery = createDiscovery(stubInfo())
+ // @230 = USDH/USDC (quote USDC) and HYPE/USDC (quote USDC) are both USDC-quoted.
+ expect((await discovery.listPairs()).map((p) => p.name)).toEqual(['@230', 'HYPE/USDC'])
+ })
+
+ it('returns USDC-quoted pairs when quote is explicitly USDC', async () => {
+ const discovery = createDiscovery(stubInfo())
+ expect((await discovery.listPairs({ quote: 'USDC' })).map((p) => p.name)).toEqual([
+ '@230',
+ 'HYPE/USDC',
+ ])
+ })
+
+ it('filters listPairs to USDH-quote pairs when requested (legacy path)', async () => {
const discovery = createDiscovery(stubInfo())
expect((await discovery.listPairs({ quote: 'USDH' })).map((p) => p.name)).toEqual(['@232'])
})
+ it('resolves getPair for a non-USDH pair (HYPE/USDC)', async () => {
+ const discovery = createDiscovery(stubInfo())
+ const pair = await discovery.getPair({ base: 'HYPE', quote: 'USDC' })
+ expect(pair.name).toBe('HYPE/USDC')
+ expect(pair.base).toBe('HYPE')
+ expect(pair.quote).toBe('USDC')
+ expect(pair.usdhRole).toBeUndefined()
+ })
+
+ it('resolves getPair for USDH-bearing pairs (legacy)', async () => {
+ const discovery = createDiscovery(stubInfo())
+ const pair = await discovery.getPair({ base: 'HYPE', quote: 'USDH' })
+ expect(pair.usdhRole).toBe('quote')
+ })
+
it('rejects unsupported pair kinds', async () => {
const discovery = createDiscovery(stubInfo())
await expect(
@@ -184,13 +213,20 @@ describe('createDiscovery', () => {
expect(l2Book).toHaveBeenCalledWith('USDH/USDC', 5)
})
- it('returns the raw allMids map when no quote filter is set', async () => {
- const allMids = vi.fn(async () => ({ BTC: '60000', '@0': '1.0001' }))
+ it('defaults getMids to USDC-quoted pairs', async () => {
+ const allMids = vi.fn(async () => ({ BTC: '60000', '@230': '1.0001', 'HYPE/USDC': '28.5' }))
+ const discovery = createDiscovery(stubInfo({ allMids }))
+ // @230 (USDH/USDC) and HYPE/USDC are both USDC-quoted in the fixture.
+ expect(await discovery.getMids()).toEqual({ '@230': '1.0001', 'HYPE/USDC': '28.5' })
+ })
+
+ it('filters mids to USDC-quoted pairs when quote is explicitly USDC', async () => {
+ const allMids = vi.fn(async () => ({ 'HYPE/USDC': '28.5', '@232': '42.5' }))
const discovery = createDiscovery(stubInfo({ allMids }))
- expect(await discovery.getMids()).toEqual({ BTC: '60000', '@0': '1.0001' })
+ expect(await discovery.getMids({ quote: 'USDC' })).toEqual({ 'HYPE/USDC': '28.5' })
})
- it('filters mids to USDH-quote pairs and rekeys by pair name', async () => {
+ it('filters mids to USDH-quote pairs and rekeys by pair name (legacy)', async () => {
const allMids = vi.fn(async () => ({
BTC: '60000',
'@230': '1.0001',
diff --git a/packages/sdk/test/orders.test.ts b/packages/sdk/test/orders.test.ts
index d859aa1..75eec6f 100644
--- a/packages/sdk/test/orders.test.ts
+++ b/packages/sdk/test/orders.test.ts
@@ -260,18 +260,43 @@ describe('placeOrder', () => {
expect(px).toBeLessThan(40.4)
})
- it('rejects a pair where USDH is neither base nor quote', async () => {
+ it('places a limit order on a non-USDH spot pair (HYPE/USDC)', async () => {
+ const exchange = exchangeOk({
+ type: 'order',
+ data: { statuses: [{ resting: { oid: 9001 } }] },
+ })
const orders = createOrders({
info: stubInfo(),
- exchange: exchangeOk({ type: 'order', data: { statuses: [] } }),
+ exchange,
signer: stubSigner(),
network: 'mainnet',
accountAddress: ACCOUNT,
nextNonce: nonceFactory(),
})
- await expect(
- orders.placeOrder({ pair: 'HYPE/USDC', side: 'buy', size: '1', price: '40' }),
- ).rejects.toThrow(/USDH as base or quote/)
+
+ const result = await orders.placeOrder({
+ pair: 'HYPE/USDC',
+ side: 'buy',
+ size: '1',
+ price: '40',
+ })
+
+ expect(result).toEqual({ oid: 9001, status: 'resting', filledSize: '', avgPrice: '' })
+ const [submitArgs] = (exchange.submit as ReturnType).mock.calls[0] ?? []
+ expect(submitArgs?.action).toMatchObject({
+ type: 'order',
+ grouping: 'na',
+ orders: [
+ {
+ a: 10_999, // 10000 + index 999
+ b: true,
+ p: '40',
+ s: '1',
+ r: false,
+ t: { limit: { tif: 'Gtc' } },
+ },
+ ],
+ })
})
it('rejects an unknown pair alias', async () => {
@@ -425,18 +450,23 @@ describe('cancelOrder', () => {
})
})
- it('rejects a non-USDH pair', async () => {
+ it('cancels an order on a non-USDH pair (HYPE/USDC)', async () => {
+ const exchange = exchangeOk({ type: 'cancel', data: { statuses: ['success'] } })
const orders = createOrders({
info: stubInfo(),
- exchange: exchangeOk({ type: 'cancel', data: { statuses: ['success'] } }),
+ exchange,
signer: stubSigner(),
network: 'mainnet',
accountAddress: ACCOUNT,
nextNonce: nonceFactory(),
})
- await expect(orders.cancelOrder({ pair: 'HYPE/USDC', oid: 1 })).rejects.toThrow(
- /USDH as base or quote/,
- )
+ const result = await orders.cancelOrder({ pair: 'HYPE/USDC', oid: 5 })
+ expect(result).toEqual({ oid: 5 })
+ const [submitArgs] = (exchange.submit as ReturnType).mock.calls[0] ?? []
+ expect(submitArgs?.action).toEqual({
+ type: 'cancel',
+ cancels: [{ a: 10_999, o: 5 }],
+ })
})
it('throws on a non-success cancel status', async () => {
@@ -472,7 +502,7 @@ describe('cancelOrder', () => {
})
describe('getOpenOrders / getOrderStatus', () => {
- it('filters getOpenOrders to USDH-bearing spot orders', async () => {
+ it('returns all open orders when no pair filter is given', async () => {
const usdhBase = openOrder('@230', 1)
const usdhQuoteAlias = openOrder('HYPE/USDH', 2)
const nonUsdhSpot = openOrder('@999', 3)
@@ -486,7 +516,7 @@ describe('getOpenOrders / getOrderStatus', () => {
nextNonce: nonceFactory(),
})
- await expect(orders.getOpenOrders()).resolves.toEqual([usdhBase, usdhQuoteAlias])
+ await expect(orders.getOpenOrders()).resolves.toEqual([usdhBase, usdhQuoteAlias, nonUsdhSpot])
expect(frontendOpenOrders).toHaveBeenCalledWith(ACCOUNT)
})
@@ -539,7 +569,7 @@ describe('getOpenOrders / getOrderStatus', () => {
)
})
- it('rejects getOrderStatus when the order is not a USDH pair', async () => {
+ it('rejects getOrderStatus when the order belongs to a different pair', async () => {
const orders = createOrders({
info: stubInfo({ orderStatus: vi.fn(async () => orderStatus('@999', 42)) }),
exchange: exchangeOk({}),
@@ -549,9 +579,7 @@ describe('getOpenOrders / getOrderStatus', () => {
nextNonce: nonceFactory(),
})
- await expect(orders.getOrderStatus({ pair: '@230', oid: 42 })).rejects.toThrow(
- /is not on USDH pair/,
- )
+ await expect(orders.getOrderStatus({ pair: '@230', oid: 42 })).rejects.toThrow(/is not on pair/)
})
it('returns unknownOid without pair ownership checks', async () => {
@@ -570,6 +598,44 @@ describe('getOpenOrders / getOrderStatus', () => {
})
})
+ it('filters getOpenOrders to a USDC-quoted pair (HYPE/USDC)', async () => {
+ const usdhOrder = openOrder('@230', 1)
+ const usdcOrder = openOrder('@999', 2)
+ const usdcAlias = openOrder('HYPE/USDC', 3)
+ const frontendOpenOrders = vi.fn(async () => [usdhOrder, usdcOrder, usdcAlias])
+ const orders = createOrders({
+ info: stubInfo({ frontendOpenOrders }),
+ exchange: exchangeOk({}),
+ signer: stubSigner(),
+ network: 'mainnet',
+ accountAddress: ACCOUNT,
+ nextNonce: nonceFactory(),
+ })
+
+ // Both '@999' and 'HYPE/USDC' are coin names for the HYPE/USDC pair.
+ await expect(orders.getOpenOrders({ pair: 'HYPE/USDC' })).resolves.toEqual([
+ usdcOrder,
+ usdcAlias,
+ ])
+ })
+
+ it('fetches getOrderStatus by oid for a non-USDH spot pair', async () => {
+ const orderStatusClient = vi.fn(async () => orderStatus('@999', 77))
+ const orders = createOrders({
+ info: stubInfo({ orderStatus: orderStatusClient }),
+ exchange: exchangeOk({}),
+ signer: stubSigner(),
+ network: 'mainnet',
+ accountAddress: ACCOUNT,
+ nextNonce: nonceFactory(),
+ })
+
+ await expect(orders.getOrderStatus({ pair: 'HYPE/USDC', oid: 77 })).resolves.toEqual(
+ orderStatus('@999', 77),
+ )
+ expect(orderStatusClient).toHaveBeenCalledWith(ACCOUNT, 77)
+ })
+
it('rejects a negative oid in getOrderStatus', async () => {
const orders = createOrders({
info: stubInfo(),