From bff356b6fd18b34dbae58428766fa924ba3f35b0 Mon Sep 17 00:00:00 2001 From: yaugourt Date: Fri, 15 May 2026 16:49:47 -0400 Subject: [PATCH] feat(sdk): make usdc the canonical quote asset USDH is being sunset on Hyperliquid in favour of USDC. Switch the kit to treat USDC as the default quote while keeping the USDH path working as a legacy opt-in. - listPairs/getPair/getMids default to USDC-quoted spot pairs; pass { quote: 'USDH' } for the legacy USDH list - order layer (placeOrder/cancelOrder/getOpenOrders/getOrderStatus) resolves any spot pair, no longer gated to USDH-bearing pairs - listUsdhSpotPairs/findUsdhSpotPair, swap() and the bridge flows are untouched legacy USDH surfaces - UsdhPair.usdhRole is now optional (absent for non-USDH pairs); demo gallery handles the optional role All 298 sdk tests and 54 widget tests pass. --- .changeset/sdk-usdc-canonical.md | 7 ++ .../registry/previews/market-board.tsx | 6 +- apps/demo/src/lib/gallery-data.ts | 5 +- packages/sdk/src/discovery.ts | 102 +++++++++++---- packages/sdk/src/kit.ts | 29 +++-- packages/sdk/src/orders.ts | 119 ++++++++++-------- packages/sdk/test/discovery.test.ts | 46 ++++++- packages/sdk/test/orders.test.ts | 98 ++++++++++++--- 8 files changed, 304 insertions(+), 108 deletions(-) create mode 100644 .changeset/sdk-usdc-canonical.md 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 15c7d55..ad742e8 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(),