Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/dev-552-batch-perps-leverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@polymarket/bindings": patch
"@polymarket/client": patch
---

Add ordered batch Perps leverage updates with per-instrument rejection results.
45 changes: 45 additions & 0 deletions packages/bindings/src/perps/orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,51 @@ export type PerpsUpdateLeverageResult = z.infer<
typeof PerpsUpdateLeverageResultSchema
>;

/**
* A rejected item in a batch Perps leverage update.
*
* @experimental This API may change in a breaking way in any release, including patch releases.
*/
export const PerpsUpdateLeverageRejectionSchema = z
.object({
status: z.literal('err'),
instrument_id: PerpsInstrumentIdSchema,
error: z.string().optional(),
})
.transform((result) => ({
status: result.status,
instrumentId: result.instrument_id,
error: perpsAckError(result.error),
}));

/**
* A rejected item in a batch Perps leverage update.
*
* @experimental This API may change in a breaking way in any release, including patch releases.
*/
export type PerpsUpdateLeverageRejection = z.infer<
typeof PerpsUpdateLeverageRejectionSchema
>;

/**
* The result for one item in a batch Perps leverage update.
*
* @experimental This API may change in a breaking way in any release, including patch releases.
*/
export const PerpsUpdateLeverageBatchResultSchema = z.union([
PerpsUpdateLeverageResultSchema,
PerpsUpdateLeverageRejectionSchema,
]);

/**
* The result for one item in a batch Perps leverage update.
*
* @experimental This API may change in a breaking way in any release, including patch releases.
*/
export type PerpsUpdateLeverageBatchResult = z.infer<
typeof PerpsUpdateLeverageBatchResultSchema
>;

function perpsAckError(error: string | undefined): string {
return error ?? 'Perps command was rejected.';
}
Expand Down
4 changes: 4 additions & 0 deletions packages/client/src/actions/perps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export type {
PerpsSessionLifecycleError,
PerpsSessionTradingError,
PerpsTpSlTrigger,
PerpsUpdateLeverageBatchResult,
PerpsUpdateLeverageRejection,
PerpsUpdateLeverageResult,
PlacePerpsOrderRequest,
PlacePerpsOrderResult,
Expand All @@ -125,11 +127,13 @@ export type {
PlacePerpsPositionTpSlResult,
PostPerpsOrdersRequest,
UpdatePerpsLeverageRequest,
UpdatePerpsLeveragesRequest,
UpdatePerpsMarginRequest,
} from '../websockets/perps/session';
export {
ArmPerpsAutoCancelError,
UpdatePerpsLeverageError,
UpdatePerpsLeveragesError,
UpdatePerpsMarginError,
} from '../websockets/perps/session';

Expand Down
4 changes: 4 additions & 0 deletions packages/client/src/decorators/perps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export type {
PerpsSessionLifecycleError,
PerpsSessionTradingError,
PerpsTpSlTrigger,
PerpsUpdateLeverageBatchResult,
PerpsUpdateLeverageRejection,
PerpsUpdateLeverageResult,
PlacePerpsOrderRequest,
PlacePerpsOrderResult,
Expand All @@ -97,6 +99,7 @@ export type {
ResumePerpsSessionRequest,
RevokePerpsCredentialsRequest,
UpdatePerpsLeverageRequest,
UpdatePerpsLeveragesRequest,
UpdatePerpsMarginRequest,
WithdrawFromPerpsRequest,
} from '../actions';
Expand All @@ -114,6 +117,7 @@ export {
OpenPerpsSessionError,
RevokePerpsCredentialsError,
UpdatePerpsLeverageError,
UpdatePerpsLeveragesError,
UpdatePerpsMarginError,
WithdrawFromPerpsError,
} from '../actions';
Expand Down
151 changes: 151 additions & 0 deletions packages/client/src/websockets/perps/actions/trading.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { OrderSide } from '@polymarket/bindings';
import { PerpsTimeInForce } from '@polymarket/bindings/perps';
import { describe, expect, it } from 'vitest';
import { UserInputError } from '../../../errors';
import { createPerpsOpTypedDataPayload } from '../signing';
import {
type PerpsCommandExecutor,
postPerpsOrders,
toPerpsCommandBodyOp,
updatePerpsLeverages,
updatePerpsMargin,
} from './trading';

const CREATE_ORDER_DATA_HASH =
'0x817207b7b8b31044a8f27e43c16e24d9fd5e11d3f106feb962f104f3ef28d52a';
const UPDATE_MARGIN_DATA_HASH =
'0xf61d7d83b4367ce136bf66cfee6a5d41303a8b2d8724f5458f9812c46f5e55b3';
const UPDATE_LEVERAGES_DATA_HASH =
'0xdcdd40c029abb105bb4911882321a203c294e9c4da73a32c5957afdbfb34cfdc';

describe('Perps trading actions', () => {
describe('createPerpsOpTypedDataPayload', () => {
Expand Down Expand Up @@ -137,5 +141,152 @@ describe('Perps trading actions', () => {
}),
).resolves.toBeUndefined();
});

it('signs ordered leverage batches with backend-compatible bytes', async () => {
const client: PerpsCommandExecutor = {
async executeCommand(request, responseSchema) {
const payload = createPerpsOpTypedDataPayload({
chainId: 31_337,
op: request.op,
salt: 1,
timestamp: 1_739_491_200_000,
});

expect(payload.message).toMatchObject({
data: UPDATE_LEVERAGES_DATA_HASH,
salt: 1n,
ts: 1_739_491_200_000n,
});
expect(request.op).toEqual([
'updateLeverages',
[
[1, 5, false],
[2, 10, true],
],
]);
expect(toPerpsCommandBodyOp(request.op)).toEqual({
args: [
{ cross: false, iid: 1, lev: 5 },
{ cross: true, iid: 2, lev: 10 },
],
type: 'updateLeverages',
});

return responseSchema.parse([
{
status: 'ok',
instrument_id: 1,
leverage: 5,
cross: false,
},
{
status: 'err',
instrument_id: 2,
error: 'internal_error',
},
]);
},
};

await expect(
updatePerpsLeverages(client, {
updates: [
{ crossMargin: false, instrumentId: 1, leverage: 5 },
{ crossMargin: true, instrumentId: 2, leverage: 10 },
],
}),
).resolves.toEqual([
{
status: 'ok',
instrumentId: 1,
leverage: 5,
crossMargin: false,
},
{ status: 'err', instrumentId: 2, error: 'internal_error' },
]);
});
});

describe('updatePerpsLeverages', () => {
it.each([1, 100])('accepts the %i-item boundary', async (size) => {
let executions = 0;
const client: PerpsCommandExecutor = {
async executeCommand(request, responseSchema) {
executions++;
expect(request.op[1]).toHaveLength(size);
return responseSchema.parse(
Array.from({ length: size }, (_, instrumentId) => ({
status: 'ok',
instrument_id: instrumentId,
leverage: 1,
cross: true,
})),
);
},
};

await expect(
updatePerpsLeverages(client, {
updates: Array.from({ length: size }, (_, instrumentId) => ({
crossMargin: true,
instrumentId,
leverage: 1,
})),
}),
).resolves.toHaveLength(size);
expect(executions).toBe(1);
});

it.each([
['an empty batch', []],
[
'more than 100 updates',
Array.from({ length: 101 }, (_, instrumentId) => ({
crossMargin: true,
instrumentId,
leverage: 1,
})),
],
[
'duplicate instrument ids',
[
{ crossMargin: true, instrumentId: 1, leverage: 1 },
{ crossMargin: false, instrumentId: 1, leverage: 2 },
],
],
[
'an instrument id above u32',
[
{
crossMargin: true,
instrumentId: 4_294_967_296,
leverage: 1,
},
],
],
[
'leverage above u32',
[
{
crossMargin: true,
instrumentId: 1,
leverage: 4_294_967_296,
},
],
],
])('rejects %s before executing the command', async (_, updates) => {
let executions = 0;
const client: PerpsCommandExecutor = {
async executeCommand() {
executions++;
throw new Error('The executor should not be called.');
},
};

await expect(
updatePerpsLeverages(client, { updates }),
).rejects.toBeInstanceOf(UserInputError);
expect(executions).toBe(0);
});
});
});
Loading