Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions .changeset/dev-466-perps-server-time.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@polymarket/bindings': minor
'@polymarket/client': minor
---

Exposes the public Perps server clock.
15 changes: 15 additions & 0 deletions packages/bindings/src/perps/market.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import {
GetServerTimeResponseSchema,
PerpsFeeScheduleEntrySchema,
PerpsFundingIntervalSchema,
PerpsInstrumentSchema,
Expand All @@ -9,6 +10,20 @@ import {

const txHash = `0x${'1'.repeat(64)}`;

describe('GetServerTimeResponseSchema', () => {
it('parses an epoch-millisecond server time', () => {
expect(
GetServerTimeResponseSchema.parse({ time: 1_766_000_000_000 }),
).toEqual({ time: 1_766_000_000_000 });
});

it('rejects a non-integer server time', () => {
expect(() =>
GetServerTimeResponseSchema.parse({ time: 1_766_000_000_000.5 }),
).toThrow();
});
});

describe('PerpsFundingIntervalSchema', () => {
it('accepts positive whole-hour funding intervals', () => {
expect(PerpsFundingIntervalSchema.parse('1h')).toBe('1h');
Expand Down
7 changes: 7 additions & 0 deletions packages/bindings/src/perps/market.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,13 @@ export type PerpsFeesInfo = z.infer<typeof PerpsFeesInfoSchema>;
*/
export const FetchPerpsFeesResponseSchema = PerpsFeesInfoSchema;

/**
* @experimental This API may change in a breaking way in any release, including patch releases.
*/
export const GetServerTimeResponseSchema = z.object({
time: EpochMillisecondsSchema,
});

/**
* @experimental This API may change in a breaking way in any release, including patch releases.
*/
Expand Down
20 changes: 18 additions & 2 deletions packages/client/src/actions/perps.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EpochMilliseconds } from '@polymarket/bindings';
import { PerpsInstrumentCategory } from '@polymarket/bindings/perps';
import { describe, expectTypeOf, it } from 'vitest';
import type {
Expand Down Expand Up @@ -28,14 +29,19 @@ import type {
PlacePerpsOrderWithTpSlRequest,
PlacePerpsPositionTpSlRequest,
PostPerpsOrdersRequest,
PublicPerpsActions,
RevokePerpsCredentialsRequest,
FetchPerpsInstrumentsRequest as RootFetchPerpsInstrumentsRequest,
UpdatePerpsLeverageRequest,
UpdatePerpsMarginRequest,
WithdrawFromPerpsRequest,
} from '../index';
import { FetchPerpsTickerError, UpdatePerpsMarginError } from '../index';
import type { FetchPerpsInstrumentsRequest } from './perps';
import {
FetchPerpsTickerError,
GetServerTimeError,
UpdatePerpsMarginError,
} from '../index';
import type { FetchPerpsInstrumentsRequest, getServerTime } from './perps';

describe('FetchPerpsInstrumentsRequest', () => {
it('allows current instrument filters', () => {
Expand All @@ -57,6 +63,15 @@ describe('FetchPerpsInstrumentsRequest', () => {
});

describe('public Perps exports', () => {
it('exposes branded server time returns', () => {
expectTypeOf<ReturnType<typeof getServerTime>>().toEqualTypeOf<
Promise<EpochMilliseconds>
>();
expectTypeOf<
ReturnType<PublicPerpsActions['getServerTime']>
>().toEqualTypeOf<Promise<EpochMilliseconds>>();
});

it('exports Perps request types from the root entry point', () => {
expectTypeOf<RootFetchPerpsInstrumentsRequest>().toEqualTypeOf<FetchPerpsInstrumentsRequest>();
expectTypeOf<FetchPerpsTickerRequest>().toEqualTypeOf<{
Expand Down Expand Up @@ -105,6 +120,7 @@ describe('public Perps exports', () => {

expectTypeOf<RootPerpsSessionErrors>().toEqualTypeOf<RootPerpsSessionErrors>();
void FetchPerpsTickerError;
void GetServerTimeError;
void UpdatePerpsMarginError;
});
});
25 changes: 25 additions & 0 deletions packages/client/src/actions/perps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
import type { BaseClient } from '../clients';
import { ServiceClient } from '../ServiceClient';
import {
getServerTime,
listPerpsCandles,
listPerpsFundingHistory,
listPerpsTrades,
Expand All @@ -27,6 +28,30 @@ describe('Perps actions', () => {
server.close();
});

it('gets the server time from the public info endpoint', async () => {
Comment thread
kartojal marked this conversation as resolved.
Outdated
const time = 1_766_000_000_000;
server.use(
http.get(`${root}/v1/info/time`, ({ request }) => {
expect(request.url).toBe(`${root}/v1/info/time`);
return HttpResponse.json({ time });
}),
);

await expect(getServerTime(createClient())).resolves.toBe(time);
});

it('rejects malformed server time responses', async () => {
server.use(
http.get(`${root}/v1/info/time`, () =>
HttpResponse.json({ time: 'not-a-timestamp' }),
),
);

await expect(getServerTime(createClient())).rejects.toMatchObject({
name: 'UnexpectedResponseError',
});
});

it('continues candle pages from the next interval boundary', async () => {
const requests: URLSearchParams[] = [];
server.use(
Expand Down
44 changes: 44 additions & 0 deletions packages/client/src/actions/perps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
type EpochMilliseconds,
type PaginationCursor,
PaginationCursorSchema,
toPaginationCursor,
Expand All @@ -12,6 +13,7 @@ import {
FetchPerpsStatisticsResponseSchema,
FetchPerpsTickersResponseSchema,
FetchPerpsTradesResponseSchema,
GetServerTimeResponseSchema,
type PerpsBook,
PerpsBookSchema,
type PerpsCandle,
Expand Down Expand Up @@ -793,6 +795,48 @@ export async function fetchPerpsFees(
return response.feeSchedule;
}

/**
* @experimental This API may change in a breaking way in any release, including patch releases.
*/
export type GetServerTimeError =
| RateLimitError
| RequestRejectedError
| TransportError
| UnexpectedResponseError;
/**
* @experimental This API may change in a breaking way in any release, including patch releases.
*/
export const GetServerTimeError = makeErrorGuard(
RateLimitError,
RequestRejectedError,
TransportError,
UnexpectedResponseError,
);

/**
* Gets the current Perps server time as a Unix timestamp in milliseconds.
*
* @remarks
* This is a low-level function. Most SDK consumers should prefer the client instance API.
* This read does not change how the SDK timestamps or signs Perps requests.
*
* @throws {@link GetServerTimeError}
* Thrown on failure.
*
* @experimental This API may change in a breaking way in any release, including patch releases.
*/
export async function getServerTime(
Comment thread
kartojal marked this conversation as resolved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What developer workflow is this standalone read intended to solve? DEV-466 starts by asking where server time is needed and whether the SDK should abstract clock synchronization, but this PR leaves signed requests on Date.now() and gives callers no way to feed the returned value into signing. If server time is needed for correctness, I would expect the SDK to own the offset and synchronization rather than require callers to fetch it. If there is an independent use case for exposing the raw read, can we document it? As a smaller naming point, getServerTime is generic on the flat client and does not follow the existing fetchPerps* convention.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trial period: automated Brunson reviews on this repo are being evaluated. Suggestions are a work in progress and not necessarily accurate or vetted by the team yet — please verify before acting on them.

Checked both claims against head 1a2fe01 — they hold, and the second one has a concrete victim in-tree.

  • No seam into signing. Every signed Perps op takes ts from the local clock with no override: createPerpsCredentials (packages/client/src/actions/perps.ts:1422-1423), revokePerpsCredentials (:1265), withdrawFromPerps (:1369), and PerpsSession.#createSignedCommand (packages/client/src/websockets/perps/session.ts:983-984). PerpsOpSignatureRequest is module-private, so there is no public path for the returned value to reach a signature.
  • Skew already bites in one place. packages/client/src/actions/perps.ts:1496 compares the server-issued proxyKey.expiresAt against local Date.now(), and :1422 derives the requested expiry from Date.now() + expiresIn. That is a real cross-clock comparison today, and getServerTime() as shipped does not fix it — nothing applies the offset. If the answer to DEV-466 is "the SDK owns synchronization", that call site is the one to fix first.
  • Naming. getServerTime is the only get*-prefixed method across every decorator in packages/client/src/decorators/; everything else is fetch*/list*, and every other public Perps read carries the domain word (fetchPerpsBook, fetchPerpsFees, …). AGENTS.md defines only list* and fetch*. fetchPerpsServerTime conforms on both axes.

My read: the raw endpoint read is a reasonable primitive, but as merged it is a probe nobody in the SDK consumes, and the TSDoc's "does not change how the SDK timestamps or signs" is documenting the gap rather than closing it. Either land the offset alongside it or state the standalone use case in the TSDoc.

Slack discussion

client: BaseClient,
): Promise<EpochMilliseconds> {
const response = await unwrap(
client.perps
.get('/v1/info/time')
.andThen(validateWith(GetServerTimeResponseSchema)),
);

return response.time;
}

const PerpsCandlesCursorStateSchema = z.object({
kind: z.literal('perpsCandles'),
instrumentId: PerpsInstrumentIdSchema,
Expand Down
22 changes: 22 additions & 0 deletions packages/client/src/decorators/perps.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EpochMilliseconds } from '@polymarket/bindings';
import type {
PerpsBook,
PerpsCandle,
Expand All @@ -20,6 +21,7 @@ import {
fetchPerpsInstruments,
fetchPerpsTicker,
fetchPerpsTickers,
getServerTime,
type ListPerpsCandlesRequest,
type ListPerpsFundingHistoryRequest,
type ListPerpsTradesRequest,
Expand Down Expand Up @@ -108,6 +110,7 @@ export {
FetchPerpsInstrumentsError,
FetchPerpsTickerError,
FetchPerpsTickersError,
GetServerTimeError,
ListPerpsCandlesError,
ListPerpsFundingHistoryError,
ListPerpsTradesError,
Expand Down Expand Up @@ -258,6 +261,24 @@ export type PublicPerpsActions = {
* @experimental This API may change in a breaking way in any release, including patch releases.
*/
fetchPerpsFees(): Promise<PerpsFeeScheduleEntry[]>;

/**
* Gets the current Perps server time as a Unix timestamp in milliseconds.
*
* @remarks
* This read does not change how the SDK timestamps or signs Perps requests.
Comment thread
kartojal marked this conversation as resolved.
*
* @example
* ```ts
* const serverTime = await client.getServerTime();
* ```
*
* @throws {@link GetServerTimeError}
* Thrown on failure.
*
* @experimental This API may change in a breaking way in any release, including patch releases.
*/
getServerTime(): Promise<EpochMilliseconds>;
};

/**
Expand Down Expand Up @@ -361,6 +382,7 @@ export function perpsActions(
fetchPerpsInstruments: (request) => fetchPerpsInstruments(client, request),
fetchPerpsTicker: (request) => fetchPerpsTicker(client, request),
fetchPerpsTickers: (request) => fetchPerpsTickers(client, request),
getServerTime: () => getServerTime(client),
listPerpsCandles: (request) => listPerpsCandles(client, request),
listPerpsFundingHistory: (request) =>
listPerpsFundingHistory(client, request),
Expand Down
16 changes: 16 additions & 0 deletions packages/client/tests/integration/perps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {

const DEFAULT_PERPS_CREDENTIAL_EXPIRES_IN = 7 * 24 * 60 * 60 * 1000;
const MAX_PERPS_PRICE_SIGNIFICANT_FIGURES = 5;
const MAX_SERVER_CLOCK_SKEW_MS = 60_000;

const [instrument] = await publicClient
.fetchPerpsInstruments()
Expand All @@ -31,6 +32,21 @@ const [ticker] = await publicClient
.then(expectNonEmptyArray);

describe('Perps integration', () => {
it('fetches the Perps server time in epoch milliseconds', async ({
publicClient,
}) => {
const startedAt = Date.now();
const serverTime = await publicClient.getServerTime();
const completedAt = Date.now();

expect(serverTime).toBeGreaterThanOrEqual(
startedAt - MAX_SERVER_CLOCK_SKEW_MS,
);
expect(serverTime).toBeLessThanOrEqual(
completedAt + MAX_SERVER_CLOCK_SKEW_MS,
);
});

it.runIf(runMeteredTests)(
'deposits and withdraws the same Perps amount',
async ({ secureClientWithDepositWallet }) => {
Expand Down
Loading