Skip to content

Commit eeda13c

Browse files
committed
feat(core): openapi schema for hosted-mode enrichment fields (v2.49.1)
Bring core/src/server/openapi.yaml and the auto-generated docs/api-reference/openapi.json in line with v2.49.0. SDK models (python + typescript) were updated in the v2.49.0 release; the schemas were stale and downstream codegen consumers missed the new hosted-mode fields. core/src/types.ts: - Order/UserTrade/Position: + nullable txHash, chain, blockNumber - Position: flip outcomeLabel/entryPrice/currentPrice/unrealizedPnL to optional+nullable; + new currentValue field - Balance: + nullable venue - BuiltOrder: + nullable expiry core/scripts/generate-openapi.js: - AST extractor emits nullable: true for T | null unions (previously dropped because TS parses `null` inside a union as a LiteralType wrapping NullKeyword, not bare NullKeyword) - STATIC_SCHEMAS.ErrorDetail expanded from {message} to full envelope with code (24-value enum), retryable, exchange, detail - STATIC_SCHEMAS.ExchangeOptions added (pmxtApiKey, walletAddress, signer, privateKey, baseUrl, apiKey, autoStartServer) core/src/exchanges/mock/index.ts: - Null-safety fallback (existing.entryPrice ?? price) required by TS strict-null-checks now that Position.entryPrice is nullable core/src/server/openapi.yaml + docs/api-reference/openapi.json regenerated via node core/scripts/generate-openapi.js. Both files are produced from the same generator pass so they stay in sync. Tests: npx tsc --noEmit clean; npm test 9/9 pass on openapi/schema/ types subset.
1 parent 9c6c219 commit eeda13c

5 files changed

Lines changed: 910 additions & 1526 deletions

File tree

core/scripts/generate-openapi.js

Lines changed: 157 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,14 +1047,37 @@ function typeNodeToSchema(node, sourceFile) {
10471047

10481048
case ts.SyntaxKind.UnionType: {
10491049
const members = node.types;
1050+
// Track whether the union includes `null` (an explicit `null` member
1051+
// means the field is nullable on the wire even if also `?:` optional).
1052+
// We strip `null` / `undefined` before classifying the union shape,
1053+
// then re-apply `nullable: true` to the resulting schema so the JSON
1054+
// shape stays a single type rather than a `oneOf [T, null]`.
1055+
// TS parses `null` inside a union as `LiteralType` wrapping
1056+
// `NullKeyword` (not bare `NullKeyword`), so check both forms.
1057+
const isNullMember = (t) =>
1058+
t.kind === ts.SyntaxKind.NullKeyword ||
1059+
(t.kind === ts.SyntaxKind.LiteralType &&
1060+
t.literal &&
1061+
t.literal.kind === ts.SyntaxKind.NullKeyword);
1062+
const isUndefinedMember = (t) =>
1063+
t.kind === ts.SyntaxKind.UndefinedKeyword;
1064+
const includesNull = members.some(isNullMember);
10501065
const nonNull = members.filter(
1051-
t =>
1052-
t.kind !== ts.SyntaxKind.NullKeyword &&
1053-
t.kind !== ts.SyntaxKind.UndefinedKeyword
1066+
t => !isNullMember(t) && !isUndefinedMember(t)
10541067
);
10551068

10561069
if (nonNull.length === 0) return null;
10571070

1071+
const withNullable = (schema) => {
1072+
if (!includesNull || !schema) return schema;
1073+
// OpenAPI 3.0: nullable is a sibling keyword on the schema itself.
1074+
// For $ref values we wrap in allOf so nullable doesn't get dropped.
1075+
if (schema.$ref) {
1076+
return { allOf: [schema], nullable: true };
1077+
}
1078+
return { ...schema, nullable: true };
1079+
};
1080+
10581081
// All string literals → enum
10591082
if (
10601083
nonNull.every(
@@ -1063,17 +1086,22 @@ function typeNodeToSchema(node, sourceFile) {
10631086
t.literal.kind === ts.SyntaxKind.StringLiteral
10641087
)
10651088
) {
1066-
return { type: 'string', enum: nonNull.map(t => t.literal.text) };
1089+
return withNullable({
1090+
type: 'string',
1091+
enum: nonNull.map(t => t.literal.text),
1092+
});
10671093
}
10681094

1069-
if (nonNull.length === 1) return typeNodeToSchema(nonNull[0], sourceFile);
1095+
if (nonNull.length === 1) {
1096+
return withNullable(typeNodeToSchema(nonNull[0], sourceFile));
1097+
}
10701098

10711099
const schemas = nonNull
10721100
.map(t => typeNodeToSchema(t, sourceFile))
10731101
.filter(s => s !== null);
10741102
if (schemas.length === 0) return null;
1075-
if (schemas.length === 1) return schemas[0];
1076-
return { oneOf: schemas };
1103+
if (schemas.length === 1) return withNullable(schemas[0]);
1104+
return withNullable({ oneOf: schemas });
10771105
}
10781106

10791107
case ts.SyntaxKind.LiteralType: {
@@ -1664,8 +1692,129 @@ const STATIC_SCHEMAS = {
16641692
},
16651693
ErrorDetail: {
16661694
type: 'object',
1695+
description:
1696+
'Structured error envelope returned inside `BaseResponse.error` and `ErrorResponse.error`. ' +
1697+
'Hosted-mode endpoints populate `code`, `retryable`, and optionally `exchange` / `detail`; ' +
1698+
'legacy local-mode endpoints may still return only `message`.',
1699+
properties: {
1700+
message: {
1701+
type: 'string',
1702+
description: 'Human-readable error message.',
1703+
},
1704+
code: {
1705+
type: 'string',
1706+
description:
1707+
'Stable machine-readable error code. Hosted-mode errors use the `HostedTradingError` family ' +
1708+
'(e.g. `INSUFFICIENT_ESCROW_BALANCE`, `BUILT_ORDER_EXPIRED`); pre-hosted local errors use the ' +
1709+
'legacy family (e.g. `BAD_REQUEST`, `NOT_FOUND`).',
1710+
enum: [
1711+
// Hosted-mode error codes (v2.49.0+)
1712+
'HOSTED_TRADING_ERROR',
1713+
'INSUFFICIENT_ESCROW_BALANCE',
1714+
'ORDER_SIZE_TOO_SMALL',
1715+
'INVALID_API_KEY',
1716+
'OUTCOME_NOT_FOUND',
1717+
'CATALOG_UNAVAILABLE',
1718+
'BUILT_ORDER_EXPIRED',
1719+
'INVALID_SIGNATURE',
1720+
'NO_LIQUIDITY',
1721+
'MISSING_WALLET_ADDRESS',
1722+
// Pre-hosted (legacy) error codes
1723+
'BAD_REQUEST',
1724+
'AUTHENTICATION_ERROR',
1725+
'PERMISSION_DENIED',
1726+
'NOT_FOUND',
1727+
'ORDER_NOT_FOUND',
1728+
'MARKET_NOT_FOUND',
1729+
'EVENT_NOT_FOUND',
1730+
'RATE_LIMIT_EXCEEDED',
1731+
'INVALID_ORDER',
1732+
'INSUFFICIENT_FUNDS',
1733+
'VALIDATION_ERROR',
1734+
'NETWORK_ERROR',
1735+
'EXCHANGE_NOT_AVAILABLE',
1736+
'NOT_SUPPORTED',
1737+
],
1738+
},
1739+
retryable: {
1740+
type: 'boolean',
1741+
description:
1742+
'Hint for clients: when `true`, the same request may succeed on retry (e.g. transient network ' +
1743+
'or rate-limit conditions); when `false`, the caller should not retry without modifying the ' +
1744+
'request.',
1745+
},
1746+
exchange: {
1747+
type: 'string',
1748+
nullable: true,
1749+
description:
1750+
"Venue the error originated from, when known (e.g. 'polymarket', 'kalshi').",
1751+
},
1752+
detail: {
1753+
type: 'object',
1754+
additionalProperties: {},
1755+
nullable: true,
1756+
description:
1757+
'Free-form hosted-mode detail blob. Shape depends on `code` — e.g. for ' +
1758+
'`INSUFFICIENT_ESCROW_BALANCE` it may include `{ requested, available }`; for ' +
1759+
'`ORDER_SIZE_TOO_SMALL` it may include `{ min }`; for `BUILT_ORDER_EXPIRED` it may include ' +
1760+
'`{ expiry }`.',
1761+
},
1762+
},
1763+
},
1764+
ExchangeOptions: {
1765+
type: 'object',
1766+
description:
1767+
'Constructor-level options for venue clients (Polymarket, Kalshi, Opinion, etc.).\n' +
1768+
'Hosted mode is the default when pmxtApiKey is set; otherwise the SDK runs against\n' +
1769+
'a local sidecar with venue credentials.',
16671770
properties: {
1668-
message: { type: 'string' },
1771+
pmxtApiKey: {
1772+
type: 'string',
1773+
description:
1774+
'PMXT customer API key. When set, the SDK routes to api.pmxt.dev (catalog) and ' +
1775+
'trade.pmxt.dev (trading). Get one at pmxt.dev/dashboard.',
1776+
},
1777+
walletAddress: {
1778+
type: 'string',
1779+
nullable: true,
1780+
description:
1781+
'EVM wallet address for hosted reads/writes. Required for endpoints that operate on a wallet ' +
1782+
'(balances, positions, trades, open orders).',
1783+
},
1784+
signer: {
1785+
type: 'object',
1786+
nullable: true,
1787+
description:
1788+
'Optional pre-built signer for hosted writes. If absent and privateKey is set, the SDK ' +
1789+
'auto-wraps privateKey into a signer.',
1790+
},
1791+
privateKey: {
1792+
type: 'string',
1793+
nullable: true,
1794+
description:
1795+
'Private key. In hosted mode, used to derive an EIP-712 signer for writes (wraps into ' +
1796+
'EthAccountSigner/EthersSigner). In self-hosted mode, used as the venue credential directly.',
1797+
},
1798+
baseUrl: {
1799+
type: 'string',
1800+
nullable: true,
1801+
description:
1802+
'Explicit base URL override. When unset, the SDK uses api.pmxt.dev when pmxtApiKey is set, ' +
1803+
'or the local sidecar otherwise.',
1804+
},
1805+
apiKey: {
1806+
type: 'string',
1807+
nullable: true,
1808+
description:
1809+
'Venue-side API key (e.g. Polymarket CLOB key). Only relevant for self-hosted mode.',
1810+
},
1811+
autoStartServer: {
1812+
type: 'boolean',
1813+
nullable: true,
1814+
description:
1815+
'Auto-start the local sidecar when running self-hosted. Defaults to true when no pmxtApiKey ' +
1816+
'is set, false when hosted.',
1817+
},
16691818
},
16701819
},
16711820
BaseRequest: {

core/src/exchanges/mock/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,11 @@ export class MockExchange extends PredictionMarketExchange {
463463
const outcome = market?.outcomes.find(o => o.outcomeId === params.outcomeId);
464464

465465
if (existing) {
466-
const epx = existing.entryPrice * existing.size;
466+
// entryPrice became nullable in v2.49 for hosted-mode positions,
467+
// but the mock exchange always populates it on first fill — fall
468+
// back to the new fill price if a prior fill somehow left it unset.
469+
const prevEntry = existing.entryPrice ?? price;
470+
const epx = prevEntry * existing.size;
467471
const npx = price * sizeDelta;
468472
const newEntry = (epx + npx) / newSize;
469473
const ep = round(newEntry, 4);

0 commit comments

Comments
 (0)