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(),