SPB Git

Repository Public

record.mjs 7.9 KB 102 lines javascript

1// Records real, READ-ONLY answers of the dYdX v4 indexer and the GMX v2 public APIs as test fixtures.2// Run: node packages/derivatives/test/fixtures/record.mjs [dydx|gmx]3// Only GET requests, one public GraphQL query and websocket subscriptions; nothing is signed or sent.4// Each HTTP fixture is { recordedAt, url, status, text } where `text` is the exact body received (kept as text so that JSON5// numbers such as GMX candle prices are never re-serialised through a JS number). Large lists are trimmed with `trimmed`.6import { mkdirSync, writeFileSync } from 'node:fs';78const here = new URL('.', import.meta.url);9const sleep = (ms) => new Promise((r) => setTimeout(r, ms));10async function rec(venue, name, url, o = {}) {11  const r = await fetch(url, o.init);12  let text = await r.text();13  let trimmed;14  if (o.trim && r.ok) { const t = o.trim(JSON.parse(text)); text = JSON.stringify(t.body); trimmed = t.note; }15  mkdirSync(new URL(`./${venue}/`, here), { recursive: true });16  writeFileSync(new URL(`./${venue}/${name}.json`, here), JSON.stringify({ recordedAt: new Date().toISOString(), url, status: r.status, ...(trimmed ? { trimmed } : {}), text }, null, 1) + '\n');17  console.log(venue, name, r.status, text.length);18  await sleep(250);19  return text;20}21async function recWs(venue, name, url, subs, ms, keep) {22  const out = [];23  await new Promise((resolve) => {24    const ws = new WebSocket(url);25    ws.onopen = () => { for (const s of subs) ws.send(JSON.stringify(s)); };26    ws.onmessage = (m) => { if (out.length < keep) out.push(JSON.parse(m.data)); };27    ws.onerror = () => resolve();28    setTimeout(() => { ws.close(); resolve(); }, ms);29  });30  writeFileSync(new URL(`./${venue}/${name}.json`, here), JSON.stringify({ recordedAt: new Date().toISOString(), url, sent: subs, messages: out }) + '\n');31  console.log(venue, name, out.length, 'messages');32}3334async function dydx() {35  const M = 'https://indexer.dydx.trade/v4', T = 'https://indexer.v4testnet.dydx.exchange/v4';36  // public market data: mainnet37  await rec('dydx', 'perpetualMarkets', `${M}/perpetualMarkets`);38  await rec('dydx', 'orderbook-BTC-USD', `${M}/orderbooks/perpetualMarket/BTC-USD`);39  await rec('dydx', 'orderbook-unknown', `${M}/orderbooks/perpetualMarket/NOPE-USD`);40  await rec('dydx', 'trades-BTC-USD', `${M}/trades/perpetualMarket/BTC-USD?limit=50`);41  await rec('dydx', 'candles-BTC-USD-1HOUR', `${M}/candles/perpetualMarkets/BTC-USD?resolution=1HOUR&limit=24`);42  await rec('dydx', 'historicalFunding-BTC-USD', `${M}/historicalFunding/BTC-USD?limit=48`);43  await rec('dydx', 'height', `${M}/height`);44  // account reads on mainnet are refused from the recording location (HTTP 403 GEOBLOCKED): recorded as such, with an address45  // that cannot exist (all-zero payload) so that no third party's account is involved46  await rec('dydx', 'mainnet-account-geoblocked', `${M}/addresses/dydx1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqd4mhgw/subaccountNumber/0`);47  // account shapes: public testnet accounts (found in recent testnet blocks; testnet funds have no value)48  const A = 'dydx1zzz66hkkjy6apat9w2enw37484v3xhqnlducf7', B = 'dydx19ykejm094kzj054wt3lks3fk9kuwfu9h0z8hcs';49  await rec('dydx', 'testnet-perpetualMarkets', `${T}/perpetualMarkets`, { trim: (b) => ({ note: 'markets: only those referenced by the account fixtures', body: { markets: Object.fromEntries(Object.entries(b.markets).filter(([k]) => ['BTC-USD', 'ETH-USD', 'LINK-USD', 'TRX-USD', 'TAO-USD'].includes(k))) } }) });50  await rec('dydx', 'testnet-subaccount', `${T}/addresses/${A}/subaccountNumber/0`);51  await rec('dydx', 'testnet-address', `${T}/addresses/${A}`);52  await rec('dydx', 'testnet-subaccount-unknown', `${T}/addresses/dydx1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqd4mhgw/subaccountNumber/0`);53  await rec('dydx', 'testnet-positions', `${T}/perpetualPositions?address=${A}&subaccountNumber=0&status=OPEN`);54  await rec('dydx', 'testnet-orders-history', `${T}/orders?address=${A}&subaccountNumber=0&limit=20&returnLatestOrders=true`);55  // resting orders of testnet market makers churn every few seconds: take the first account that has some right now56  for (let i = 0; i < 12; i++) {57    const a = [B, 'dydx12laptmp99yttf5k7s0uzqer6cj44u3ynj60sx3', 'dydx1a5c5yk88zcax7s3dzt72lrrwex6efnfmweycvs', 'dydx1wcxcrcmapf7n0jq5082q5m9rk2654y2kc8dfqg'][i % 4];58    if ((await rec('dydx', 'testnet-orders-open', `${T}/orders?address=${a}&subaccountNumber=0&status=OPEN,UNTRIGGERED&limit=10`)).length > 2) break;59  }60  await rec('dydx', 'testnet-fills', `${T}/fills?address=${A}&subaccountNumber=0&limit=20`);61  await rec('dydx', 'testnet-fundingPayments', `${T}/fundingPayments?address=${A}&subaccountNumber=0&limit=20`);62  // websocket: mainnet public channels, testnet subaccount channel63  await recWs('dydx', 'ws-mainnet-public', 'wss://indexer.dydx.trade/v4/ws', [64    { type: 'subscribe', channel: 'v4_orderbook', id: 'BTC-USD' }, { type: 'subscribe', channel: 'v4_trades', id: 'BTC-USD' },65    { type: 'subscribe', channel: 'v4_markets' }, { type: 'subscribe', channel: 'v4_orderbook', id: 'NOPE-USD' }, { type: 'ping' },66  ], 20000, 160);67  await recWs('dydx', 'ws-testnet-subaccount', 'wss://indexer.v4testnet.dydx.exchange/v4/ws', [{ type: 'subscribe', channel: 'v4_subaccounts', id: `${B}/0` }], 12000, 12);68}6970async function gmx() {71  const V1 = 'https://arbitrum.gmxapi.io/v1', OR = 'https://arbitrum-api.gmxinfra.io';72  const KEEP = /^(BTC|ETH|SOL|LINK|LTC|DOGE)\/USD |^SWAP-ONLY \[USDC-USDT\]/;73  const bySym = (key) => (b) => ({ note: `only markets whose ${key} matches ${KEEP}`, body: b.filter((m) => KEEP.test(m[key])) });74  const markets = JSON.parse(await rec('gmx', 'v1-markets', `${V1}/markets`));75  await rec('gmx', 'v1-markets-tickers', `${V1}/markets/tickers`, { trim: bySym('symbol') });76  await rec('gmx', 'v1-markets-config', `${V1}/markets/config`, { trim: bySym('name') });77  await rec('gmx', 'v1-tokens-info', `${V1}/tokens/info`);78  await rec('gmx', 'v1-pairs', `${V1}/pairs`);79  await rec('gmx', 'v1-ohlcv-BTC-1h', `${V1}/prices/ohlcv?symbol=BTC&timeframe=1h&limit=24`);80  await rec('gmx', 'v1-ohlcv-unknown', `${V1}/prices/ohlcv?symbol=NOPE&timeframe=1h&limit=2`);81  const btc = markets.find((m) => m.symbol === 'BTC/USD [BTC-USDC]').marketTokenAddress;82  await rec('gmx', 'v1-rates-BTC-7d', `${V1}/rates?period=7d&address=${btc}`);83  await rec('gmx', 'oracle-prices-tickers', `${OR}/prices/tickers`);84  await rec('gmx', 'oracle-tokens', `${OR}/tokens`);85  await rec('gmx', 'oracle-markets-info', `${OR}/markets/info`, { trim: (b) => ({ note: `only markets whose name matches ${KEEP}`, body: { markets: b.markets.filter((m) => KEEP.test(m.name)) } }) });86  await rec('gmx', 'oracle-candles-BTC-1h', `${OR}/prices/candles?tokenSymbol=BTC&period=1h&limit=24`);87  // public trade feed: Subsquid GraphQL (documented at https://docs.gmx.io/docs/api/graphql)88  const q = `{ tradeActions(limit: 30, orderBy: timestamp_DESC, where: { eventName_eq: "OrderExecuted", marketAddress_eq: "${btc}", orderType_in: [2,3,4,5,6,7,8] }) { id eventName orderType orderKey marketAddress isLong sizeDeltaUsd sizeDeltaInTokens executionPrice timestamp transactionHash } }`;89  await rec('gmx', 'graphql-tradeActions-BTC', 'https://gmx.squids.live/gmx-synthetics-arbitrum:prod/api/graphql', { init: { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ query: q }) } });90  // account reads: a public Arbitrum address seen placing orders in the public trade feed on the recording day91  const acct = process.env.GMX_FIXTURE_ACCOUNT ?? '0x46a3BA57e146A4906bc68759Fb7ad1E74d3B66f6';92  await rec('gmx', 'v1-positions', `${V1}/positions?address=${acct}`);93  await rec('gmx', 'v1-orders', `${V1}/orders?address=${acct}`);94  await rec('gmx', 'v1-trades', `${V1}/trades?address=${acct}&limit=60`);95  await rec('gmx', 'v1-positions-empty', `${V1}/positions?address=0x1111111111111111111111111111111111111111`);96  await rec('gmx', 'v1-positions-bad-address', `${V1}/positions?address=0x123`);97}9899const which = process.argv[2];100if (!which || which === 'dydx') await dydx();101if (!which || which === 'gmx') await gmx();102
Files main
Menu

Theme