Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions apps/e2e/src/nuanze-client/candlesQueries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
NUANZE_CANDLE_INTERVALS,
NuanzeCandleInterval,
NuanzeServerFailureError,
} from '@nadohq/nuanze-client';
import assert from 'node:assert/strict';
import { before, describe, test } from 'node:test';
import {
assertBigNumberFinite,
assertBoolean,
assertEnumMember,
assertNonEmptyString,
} from '../utils/assertions';
import { debugPrint } from '../utils/debugPrint';
import { delay } from '../utils/delay';
import { createTestContext } from '../utils/runWithContext';
import { TEST_DELAYS, TEST_TIMEOUTS } from '../utils/testConstants';
import { RunContext } from '../utils/types';

/** UTC ISO 8601 with a required `Z`, as the Nuanze contract specifies. */
const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;

void describe(
'[nuanze-client]: candles',
{ timeout: TEST_TIMEOUTS.DEFAULT },
() => {
let tc: RunContext;
let ticker: string;

before(async () => {
await delay(TEST_DELAYS.LONG);
tc = createTestContext();
const [first] = (await tc.nuanze.getMarkets({ venue: 'perp' })).markets;
assert.ok(first, 'expected at least one perp market');
ticker = first.ticker;
});

void test('returns candles oldest to newest', async () => {
const response = await tc.nuanze.getMarketCandles({
ticker,
venue: 'perp',
interval: '1h',
limit: 24,
});
debugPrint('Market candles', response);

assert.match(
response.asOf,
ISO_UTC,
'asOf should be a UTC ISO timestamp',
);
assert.equal(response.ticker, ticker);
assertEnumMember(response.interval, NUANZE_CANDLE_INTERVALS, 'interval');
assert.ok(response.candles.length > 0, 'candles should not be empty');
assert.ok(response.candles.length <= 24, 'candles should respect limit');

const openTimes = response.candles.map((candle) => candle.openTime);
assert.deepEqual(
openTimes,
[...openTimes].sort(),
'candles should be ordered oldest-to-newest',
);

for (const [i, candle] of response.candles.entries()) {
const label = `candles[${i}]`;
assert.match(candle.openTime, ISO_UTC, `${label}.openTime`);
assertBigNumberFinite(candle.open, `${label}.open`);
assertBigNumberFinite(candle.high, `${label}.high`);
assertBigNumberFinite(candle.low, `${label}.low`);
assertBigNumberFinite(candle.close, `${label}.close`);
assertBigNumberFinite(candle.volume, `${label}.volume`);
assertBoolean(candle.complete, `${label}.complete`);
}

const last = response.candles[response.candles.length - 1];
if (last && !last.complete) {
assert.equal(last.complete, false, 'current bucket is incomplete');
}
});

void test('rejects an unknown interval with BAD_REQUEST', async () => {
try {
await tc.nuanze.getMarketCandles({
ticker,
interval: '1m' as NuanzeCandleInterval,
});
assert.fail('expected BAD_REQUEST for an unknown interval');
} catch (error) {
assert.ok(
error instanceof NuanzeServerFailureError,
'should throw NuanzeServerFailureError',
);
assert.equal(error.errorCode, 'BAD_REQUEST');
assert.equal(error.httpStatus, 400);
assertNonEmptyString(error.requestId, 'error.requestId');
}
});
},
);
122 changes: 122 additions & 0 deletions apps/e2e/src/nuanze-client/flowsQueries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
NUANZE_FLOW_EVENT_TYPES,
NUANZE_FLOW_TIMEFRAMES,
NuanzeCollateralFlow,
NuanzeFlowEventTypeFilter,
NuanzeServerFailureError,
} from '@nadohq/nuanze-client';
import assert from 'node:assert/strict';
import { before, describe, test } from 'node:test';
import {
assertArrayElements,
assertBigNumberFinite,
assertBoolean,
assertEnumMember,
assertNonEmptyString,
assertNonNegativeInteger,
assertNumber,
} from '../utils/assertions';
import { debugPrint } from '../utils/debugPrint';
import { delay } from '../utils/delay';
import { createTestContext } from '../utils/runWithContext';
import { TEST_DELAYS, TEST_TIMEOUTS } from '../utils/testConstants';
import { RunContext } from '../utils/types';

/** UTC ISO 8601 with a required `Z`, as the Nuanze contract specifies. */
const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;

void describe(
'[nuanze-client]: flows',
{ timeout: TEST_TIMEOUTS.DEFAULT },
() => {
let tc: RunContext;

before(async () => {
await delay(TEST_DELAYS.LONG);
tc = createTestContext();
});

void test('lists collateral events newest first', async () => {
const response = await tc.nuanze.getCollateralFlows({
timeframe: '24h',
limit: 20,
});
debugPrint('Collateral flows', response);

assert.match(
response.asOf,
ISO_UTC,
'asOf should be a UTC ISO timestamp',
);
assertArrayElements(response.events, assertCollateralFlowShape, 'events');

for (let i = 1; i < response.events.length; i++) {
const prev = response.events[i - 1];
const cur = response.events[i];
assert.ok(
prev.timestamp > cur.timestamp ||
(prev.timestamp === cur.timestamp && prev.id > cur.id),
'events should be ordered by timestamp then id descending',
);
}
});

void test('returns flow aggregates', async () => {
const response = await tc.nuanze.getCollateralFlowSummary({
timeframe: '24h',
});
debugPrint('Collateral flow summary', response);

assertEnumMember(response.timeframe, NUANZE_FLOW_TIMEFRAMES, 'timeframe');
assertBigNumberFinite(response.deposited, 'deposited');
assertBigNumberFinite(response.withdrawn, 'withdrawn');
assertBigNumberFinite(response.net, 'net');
assertBigNumberFinite(response.gross, 'gross');
assertNonNegativeInteger(response.depositCount, 'depositCount');
assertNonNegativeInteger(response.withdrawalCount, 'withdrawalCount');
});

void test('rejects an unknown event type with BAD_REQUEST', async () => {
try {
await tc.nuanze.getCollateralFlows({
eventType: 'transfer' as NuanzeFlowEventTypeFilter,
});
assert.fail('expected BAD_REQUEST for an unknown event type');
} catch (error) {
assert.ok(
error instanceof NuanzeServerFailureError,
'should throw NuanzeServerFailureError',
);
assert.equal(error.errorCode, 'BAD_REQUEST');
assert.equal(error.httpStatus, 400);
assertNonEmptyString(error.requestId, 'error.requestId');
}
});
},
);

function assertCollateralFlowShape(
event: NuanzeCollateralFlow,
label: string,
): void {
assertNumber(event.id, `${label}.id`);
assertEnumMember(
event.eventType,
NUANZE_FLOW_EVENT_TYPES,
`${label}.eventType`,
);
assertNumber(event.productId, `${label}.productId`);
assertNonEmptyString(event.symbol, `${label}.symbol`);
assertBigNumberFinite(event.assetAmount, `${label}.assetAmount`);
assertNonEmptyString(event.owner, `${label}.owner`);
assert.match(event.owner, /^0x[0-9a-f]{40}$/, `${label}.owner`);
assertNonEmptyString(event.subaccountName, `${label}.subaccountName`);
assert.match(event.timestamp, ISO_UTC, `${label}.timestamp`);
assertBoolean(event.tags.whale, `${label}.tags.whale`);
assertBoolean(event.tags.smartMoney, `${label}.tags.smartMoney`);
assertBoolean(event.tags.freshWallet, `${label}.tags.freshWallet`);

if (event.usdValue !== null) {
assertBigNumberFinite(event.usdValue, `${label}.usdValue`);
}
}
80 changes: 80 additions & 0 deletions apps/e2e/src/nuanze-client/fundingQueries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
NUANZE_MARKET_VENUES,
NuanzeFundingRate,
NuanzeServerFailureError,
} from '@nadohq/nuanze-client';
import assert from 'node:assert/strict';
import { before, describe, test } from 'node:test';
import {
assertArrayElements,
assertBigNumberFinite,
assertEnumMember,
assertNonEmptyString,
assertNumber,
} from '../utils/assertions';
import { debugPrint } from '../utils/debugPrint';
import { delay } from '../utils/delay';
import { createTestContext } from '../utils/runWithContext';
import { TEST_DELAYS, TEST_TIMEOUTS } from '../utils/testConstants';
import { RunContext } from '../utils/types';

/** UTC ISO 8601 with a required `Z`, as the Nuanze contract specifies. */
const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;

void describe(
'[nuanze-client]: funding',
{ timeout: TEST_TIMEOUTS.DEFAULT },
() => {
let tc: RunContext;

before(async () => {
await delay(TEST_DELAYS.LONG);
tc = createTestContext();
});

void test('lists latest perpetual funding rates', async () => {
const response = await tc.nuanze.getFundingRates();
debugPrint('Funding rates', response);

assert.match(
response.asOf,
ISO_UTC,
'asOf should be a UTC ISO timestamp',
);
assert.ok(response.rates.length > 0, 'rates should not be empty');
assertArrayElements(response.rates, assertFundingRateShape, 'rates');

for (const rate of response.rates) {
assert.equal(rate.venue, 'perp', `${rate.ticker} should be a perp`);
assert.equal(rate.productId % 2, 0, `${rate.ticker} productId`);
}
});

void test('rejects a spot venue with BAD_REQUEST', async () => {
try {
await tc.nuanze.getFundingRates({
venue: 'spot' as 'perp',
});
assert.fail('expected BAD_REQUEST for a spot venue');
} catch (error) {
assert.ok(
error instanceof NuanzeServerFailureError,
'should throw NuanzeServerFailureError',
);
assert.equal(error.errorCode, 'BAD_REQUEST');
assert.equal(error.httpStatus, 400);
assertNonEmptyString(error.requestId, 'error.requestId');
}
});
},
);

function assertFundingRateShape(rate: NuanzeFundingRate, label: string): void {
assertNumber(rate.productId, `${label}.productId`);
assertNonEmptyString(rate.symbol, `${label}.symbol`);
assertNonEmptyString(rate.ticker, `${label}.ticker`);
assertEnumMember(rate.venue, NUANZE_MARKET_VENUES, `${label}.venue`);
assertBigNumberFinite(rate.rate, `${label}.rate`);
assertBigNumberFinite(rate.annualizedRate, `${label}.annualizedRate`);
assert.match(rate.observedAt, ISO_UTC, `${label}.observedAt`);
}
Loading
Loading