pmxtjs 2.48.5 → 2.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/generated/src/models/OrderLevel.d.ts +6 -0
- package/dist/esm/generated/src/models/OrderLevel.js +2 -0
- package/dist/esm/generated/src/models/UnifiedMarket.d.ts +2 -2
- package/dist/esm/generated/src/models/UnifiedMarket.js +2 -4
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/pmxt/client.d.ts +106 -5
- package/dist/esm/pmxt/client.js +400 -6
- package/dist/esm/pmxt/constants.d.ts +11 -0
- package/dist/esm/pmxt/constants.js +13 -0
- package/dist/esm/pmxt/errors.d.ts +3 -0
- package/dist/esm/pmxt/errors.js +9 -0
- package/dist/esm/pmxt/escrow.d.ts +39 -0
- package/dist/esm/pmxt/escrow.js +78 -0
- package/dist/esm/pmxt/feed-client.d.ts +3 -0
- package/dist/esm/pmxt/feed-client.js +11 -2
- package/dist/esm/pmxt/hosted-errors.d.ts +84 -0
- package/dist/esm/pmxt/hosted-errors.js +186 -0
- package/dist/esm/pmxt/hosted-mappers.d.ts +45 -0
- package/dist/esm/pmxt/hosted-mappers.js +291 -0
- package/dist/esm/pmxt/hosted-routing.d.ts +69 -0
- package/dist/esm/pmxt/hosted-routing.js +119 -0
- package/dist/esm/pmxt/hosted-typed-data.d.ts +36 -0
- package/dist/esm/pmxt/hosted-typed-data.js +580 -0
- package/dist/esm/pmxt/models.d.ts +46 -8
- package/dist/esm/pmxt/server-manager.d.ts +4 -0
- package/dist/esm/pmxt/server-manager.js +6 -0
- package/dist/esm/pmxt/signers.d.ts +57 -0
- package/dist/esm/pmxt/signers.js +50 -0
- package/dist/esm/pmxt/ws-client.js +2 -1
- package/dist/generated/src/models/OrderLevel.d.ts +6 -0
- package/dist/generated/src/models/OrderLevel.js +2 -0
- package/dist/generated/src/models/UnifiedMarket.d.ts +2 -2
- package/dist/generated/src/models/UnifiedMarket.js +2 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/pmxt/client.d.ts +106 -5
- package/dist/pmxt/client.js +399 -5
- package/dist/pmxt/constants.d.ts +11 -0
- package/dist/pmxt/constants.js +14 -1
- package/dist/pmxt/errors.d.ts +3 -0
- package/dist/pmxt/errors.js +11 -1
- package/dist/pmxt/escrow.d.ts +39 -0
- package/dist/pmxt/escrow.js +82 -0
- package/dist/pmxt/feed-client.d.ts +3 -0
- package/dist/pmxt/feed-client.js +11 -2
- package/dist/pmxt/hosted-errors.d.ts +84 -0
- package/dist/pmxt/hosted-errors.js +201 -0
- package/dist/pmxt/hosted-mappers.d.ts +45 -0
- package/dist/pmxt/hosted-mappers.js +302 -0
- package/dist/pmxt/hosted-routing.d.ts +69 -0
- package/dist/pmxt/hosted-routing.js +126 -0
- package/dist/pmxt/hosted-typed-data.d.ts +36 -0
- package/dist/pmxt/hosted-typed-data.js +619 -0
- package/dist/pmxt/models.d.ts +46 -8
- package/dist/pmxt/server-manager.d.ts +4 -0
- package/dist/pmxt/server-manager.js +6 -0
- package/dist/pmxt/signers.d.ts +57 -0
- package/dist/pmxt/signers.js +55 -0
- package/dist/pmxt/ws-client.js +2 -1
- package/generated/docs/OrderLevel.md +2 -0
- package/generated/package.json +1 -1
- package/generated/src/models/OrderLevel.ts +8 -0
- package/generated/src/models/UnifiedMarket.ts +4 -5
- package/index.ts +1 -0
- package/package.json +11 -2
- package/pmxt/client.ts +495 -9
- package/pmxt/constants.ts +15 -0
- package/pmxt/errors.ts +11 -0
- package/pmxt/escrow.ts +93 -0
- package/pmxt/feed-client.ts +14 -2
- package/pmxt/hosted-errors.ts +216 -0
- package/pmxt/hosted-mappers.ts +312 -0
- package/pmxt/hosted-routing.ts +165 -0
- package/pmxt/hosted-typed-data.ts +767 -0
- package/pmxt/models.ts +65 -8
- package/pmxt/server-manager.ts +7 -0
- package/pmxt/signers.ts +86 -0
- package/pmxt/ws-client.ts +2 -1
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hosted trading EIP-712 validation guardrails.
|
|
4
|
+
*
|
|
5
|
+
* Three layers (mirrors `sdks/python/pmxt/_hosted_typeddata.py`):
|
|
6
|
+
* 1. Schema validation — per-route shape, domain, types, message keys, deadline
|
|
7
|
+
* 2. Economic match — typed_data economics agree with the user's build request
|
|
8
|
+
* 3. Post-sign — exact 65-byte length, low-s canonical, v ∈ {27, 28}, recovery
|
|
9
|
+
*
|
|
10
|
+
* Layer 3 uses the optional `ethers` peer dependency for typed-data verification.
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.SECP256K1_HALF_N = void 0;
|
|
47
|
+
exports.validateTypedData = validateTypedData;
|
|
48
|
+
exports.validateEconomics = validateEconomics;
|
|
49
|
+
exports.verifySignature = verifySignature;
|
|
50
|
+
const hosted_errors_1 = require("./hosted-errors");
|
|
51
|
+
const hosted_mappers_1 = require("./hosted-mappers");
|
|
52
|
+
// The constants module is updated in a parallel-agent change to add these
|
|
53
|
+
// allowlists. We import them at runtime so we don't hard-fail if the change
|
|
54
|
+
// hasn't landed yet (the imports are typed below).
|
|
55
|
+
const constants = __importStar(require("./constants"));
|
|
56
|
+
const EIP712_DOMAIN_FIELDS = [
|
|
57
|
+
{ name: "name", type: "string" },
|
|
58
|
+
{ name: "version", type: "string" },
|
|
59
|
+
{ name: "chainId", type: "uint256" },
|
|
60
|
+
{ name: "verifyingContract", type: "address" },
|
|
61
|
+
];
|
|
62
|
+
const ORDER_PARAMS_FIELDS = [
|
|
63
|
+
{ name: "user", type: "address" },
|
|
64
|
+
{ name: "tokenId", type: "uint256" },
|
|
65
|
+
{ name: "worstPrice", type: "uint256" },
|
|
66
|
+
{ name: "maxCostUsdc", type: "uint256" },
|
|
67
|
+
{ name: "deadline", type: "uint256" },
|
|
68
|
+
{ name: "nonce", type: "uint256" },
|
|
69
|
+
];
|
|
70
|
+
const SELL_ORDER_PARAMS_FIELDS = [
|
|
71
|
+
{ name: "user", type: "address" },
|
|
72
|
+
{ name: "tokenId", type: "uint256" },
|
|
73
|
+
{ name: "tokenAmount", type: "uint256" },
|
|
74
|
+
{ name: "worstPrice", type: "uint256" },
|
|
75
|
+
{ name: "deadline", type: "uint256" },
|
|
76
|
+
{ name: "nonce", type: "uint256" },
|
|
77
|
+
];
|
|
78
|
+
const CROSS_CHAIN_ORDER_PARAMS_FIELDS = [
|
|
79
|
+
{ name: "user", type: "address" },
|
|
80
|
+
{ name: "tokenId", type: "uint256" },
|
|
81
|
+
{ name: "maxCostUsdc", type: "uint256" },
|
|
82
|
+
{ name: "worstPrice", type: "uint256" },
|
|
83
|
+
{ name: "destEscrow", type: "address" },
|
|
84
|
+
{ name: "oracleKey", type: "address" },
|
|
85
|
+
{ name: "deadline", type: "uint256" },
|
|
86
|
+
{ name: "nonce", type: "uint256" },
|
|
87
|
+
];
|
|
88
|
+
const CROSS_CHAIN_SELL_PAY_PARAMS_FIELDS = [
|
|
89
|
+
{ name: "user", type: "address" },
|
|
90
|
+
{ name: "tokenId", type: "uint256" },
|
|
91
|
+
{ name: "tokenAmount", type: "uint256" },
|
|
92
|
+
{ name: "worstPrice", type: "uint256" },
|
|
93
|
+
{ name: "deadline", type: "uint256" },
|
|
94
|
+
{ name: "nonce", type: "uint256" },
|
|
95
|
+
];
|
|
96
|
+
const CROSS_CHAIN_SELL_PULL_PARAMS_FIELDS = [
|
|
97
|
+
{ name: "user", type: "address" },
|
|
98
|
+
{ name: "tokenId", type: "uint256" },
|
|
99
|
+
{ name: "tokenAmount", type: "uint256" },
|
|
100
|
+
{ name: "deadline", type: "uint256" },
|
|
101
|
+
{ name: "nonce", type: "uint256" },
|
|
102
|
+
];
|
|
103
|
+
const CANCEL_ORDER_FIELDS = [
|
|
104
|
+
{ name: "user", type: "address" },
|
|
105
|
+
{ name: "path", type: "uint8" },
|
|
106
|
+
{ name: "nonce", type: "uint256" },
|
|
107
|
+
{ name: "deadline", type: "uint256" },
|
|
108
|
+
];
|
|
109
|
+
const CANCEL_PULL_FIELDS = [
|
|
110
|
+
{ name: "user", type: "address" },
|
|
111
|
+
{ name: "nonce", type: "uint256" },
|
|
112
|
+
{ name: "deadline", type: "uint256" },
|
|
113
|
+
];
|
|
114
|
+
function messageKeysFromFields(fields) {
|
|
115
|
+
return new Set(fields.map((f) => f.name));
|
|
116
|
+
}
|
|
117
|
+
const PREFUNDED_DOMAIN = {
|
|
118
|
+
name: "PreFundedEscrow",
|
|
119
|
+
version: "1",
|
|
120
|
+
chainId: 137,
|
|
121
|
+
allowlistKey: "prefunded",
|
|
122
|
+
};
|
|
123
|
+
const VENUE_DOMAIN = {
|
|
124
|
+
name: "VenueEscrow",
|
|
125
|
+
version: "1",
|
|
126
|
+
chainId: 56,
|
|
127
|
+
allowlistKey: "venue",
|
|
128
|
+
};
|
|
129
|
+
const SCHEMAS = {
|
|
130
|
+
polymarket_buy: {
|
|
131
|
+
primaryType: "OrderParams",
|
|
132
|
+
domain: PREFUNDED_DOMAIN,
|
|
133
|
+
fields: ORDER_PARAMS_FIELDS,
|
|
134
|
+
messageKeys: messageKeysFromFields(ORDER_PARAMS_FIELDS),
|
|
135
|
+
walletField: "user",
|
|
136
|
+
},
|
|
137
|
+
polymarket_sell: {
|
|
138
|
+
primaryType: "SellOrderParams",
|
|
139
|
+
domain: PREFUNDED_DOMAIN,
|
|
140
|
+
fields: SELL_ORDER_PARAMS_FIELDS,
|
|
141
|
+
messageKeys: messageKeysFromFields(SELL_ORDER_PARAMS_FIELDS),
|
|
142
|
+
walletField: "user",
|
|
143
|
+
},
|
|
144
|
+
opinion_buy: {
|
|
145
|
+
primaryType: "CrossChainOrderParams",
|
|
146
|
+
domain: PREFUNDED_DOMAIN,
|
|
147
|
+
fields: CROSS_CHAIN_ORDER_PARAMS_FIELDS,
|
|
148
|
+
messageKeys: messageKeysFromFields(CROSS_CHAIN_ORDER_PARAMS_FIELDS),
|
|
149
|
+
walletField: "user",
|
|
150
|
+
},
|
|
151
|
+
opinion_sell_polygon: {
|
|
152
|
+
primaryType: "CrossChainSellPayParams",
|
|
153
|
+
domain: PREFUNDED_DOMAIN,
|
|
154
|
+
fields: CROSS_CHAIN_SELL_PAY_PARAMS_FIELDS,
|
|
155
|
+
messageKeys: messageKeysFromFields(CROSS_CHAIN_SELL_PAY_PARAMS_FIELDS),
|
|
156
|
+
walletField: "user",
|
|
157
|
+
},
|
|
158
|
+
opinion_sell_bsc_pull: {
|
|
159
|
+
primaryType: "CrossChainSellPullParams",
|
|
160
|
+
domain: VENUE_DOMAIN,
|
|
161
|
+
fields: CROSS_CHAIN_SELL_PULL_PARAMS_FIELDS,
|
|
162
|
+
messageKeys: messageKeysFromFields(CROSS_CHAIN_SELL_PULL_PARAMS_FIELDS),
|
|
163
|
+
walletField: "user",
|
|
164
|
+
},
|
|
165
|
+
cancel_polymarket: {
|
|
166
|
+
primaryType: "CancelOrder",
|
|
167
|
+
domain: PREFUNDED_DOMAIN,
|
|
168
|
+
fields: CANCEL_ORDER_FIELDS,
|
|
169
|
+
messageKeys: messageKeysFromFields(CANCEL_ORDER_FIELDS),
|
|
170
|
+
walletField: "user",
|
|
171
|
+
},
|
|
172
|
+
cancel_opinion_polygon: {
|
|
173
|
+
primaryType: "CancelOrder",
|
|
174
|
+
domain: PREFUNDED_DOMAIN,
|
|
175
|
+
fields: CANCEL_ORDER_FIELDS,
|
|
176
|
+
messageKeys: messageKeysFromFields(CANCEL_ORDER_FIELDS),
|
|
177
|
+
walletField: "user",
|
|
178
|
+
},
|
|
179
|
+
cancel_opinion_bsc_pull: {
|
|
180
|
+
primaryType: "CancelPull",
|
|
181
|
+
domain: VENUE_DOMAIN,
|
|
182
|
+
fields: CANCEL_PULL_FIELDS,
|
|
183
|
+
messageKeys: messageKeysFromFields(CANCEL_PULL_FIELDS),
|
|
184
|
+
walletField: "user",
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
// secp256k1 group order / 2, for canonical low-s check.
|
|
188
|
+
exports.SECP256K1_HALF_N = 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0n;
|
|
189
|
+
const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
190
|
+
const SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Layer 1 — Schema validation
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
/**
|
|
195
|
+
* Validate the structural shape of a typed-data payload before signing.
|
|
196
|
+
* Throws {@link InvalidSignature} on any mismatch.
|
|
197
|
+
*/
|
|
198
|
+
function validateTypedData(typedData, route, walletAddress) {
|
|
199
|
+
const schema = schemaFor(route);
|
|
200
|
+
if (typedData === null || typeof typedData !== "object") {
|
|
201
|
+
schemaFail("typed_data must be an object");
|
|
202
|
+
}
|
|
203
|
+
if (typedData.primaryType !== schema.primaryType) {
|
|
204
|
+
schemaFail(`primaryType expected '${schema.primaryType}' got '${typedData.primaryType}'`);
|
|
205
|
+
}
|
|
206
|
+
const types = typedData.types;
|
|
207
|
+
if (types === null || typeof types !== "object") {
|
|
208
|
+
schemaFail("types must be an object");
|
|
209
|
+
}
|
|
210
|
+
const domain = typedData.domain;
|
|
211
|
+
if (domain === null || typeof domain !== "object") {
|
|
212
|
+
schemaFail("domain must be an object");
|
|
213
|
+
}
|
|
214
|
+
const message = typedData.message;
|
|
215
|
+
if (message === null || typeof message !== "object") {
|
|
216
|
+
schemaFail("message must be an object");
|
|
217
|
+
}
|
|
218
|
+
validateDomain(domain, schema.domain);
|
|
219
|
+
validateTypes(types, schema);
|
|
220
|
+
validateMessage(message, schema, walletAddress);
|
|
221
|
+
}
|
|
222
|
+
function validateDomain(domain, expected) {
|
|
223
|
+
const actualKeys = Object.keys(domain).sort();
|
|
224
|
+
const expectedKeys = ["chainId", "name", "verifyingContract", "version"];
|
|
225
|
+
if (actualKeys.length !== expectedKeys.length ||
|
|
226
|
+
actualKeys.some((k, i) => k !== expectedKeys[i])) {
|
|
227
|
+
schemaFail(`domain keys expected ${JSON.stringify(expectedKeys)} got ${JSON.stringify(actualKeys)}`);
|
|
228
|
+
}
|
|
229
|
+
if (domain.name !== expected.name) {
|
|
230
|
+
schemaFail(`domain.name expected '${expected.name}' got '${domain.name}'`);
|
|
231
|
+
}
|
|
232
|
+
if (domain.version !== expected.version) {
|
|
233
|
+
schemaFail(`domain.version expected '${expected.version}' got '${domain.version}'`);
|
|
234
|
+
}
|
|
235
|
+
const chainId = asInt(domain.chainId, "domain.chainId");
|
|
236
|
+
if (chainId !== expected.chainId) {
|
|
237
|
+
schemaFail(`domain.chainId expected ${expected.chainId} got ${chainId}`);
|
|
238
|
+
}
|
|
239
|
+
const verifyingContract = normalizeAddress(domain.verifyingContract);
|
|
240
|
+
if (verifyingContract === null) {
|
|
241
|
+
schemaFail("domain.verifyingContract must be an EVM address");
|
|
242
|
+
}
|
|
243
|
+
const allowlist = allowedAddresses(expected.allowlistKey, expected.chainId);
|
|
244
|
+
if (allowlist.size === 0) {
|
|
245
|
+
schemaFail(`no allowlisted verifyingContract configured for chain ${expected.chainId}`);
|
|
246
|
+
}
|
|
247
|
+
if (!allowlist.has(verifyingContract)) {
|
|
248
|
+
schemaFail("domain.verifyingContract is not allowlisted");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function validateTypes(types, schema) {
|
|
252
|
+
const typeNames = new Set(Object.keys(types));
|
|
253
|
+
const allowed = new Set([schema.primaryType, "EIP712Domain"]);
|
|
254
|
+
for (const name of typeNames) {
|
|
255
|
+
if (!allowed.has(name)) {
|
|
256
|
+
schemaFail(`unexpected type entry: '${name}'`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (!typeNames.has("EIP712Domain")) {
|
|
260
|
+
schemaFail("types.EIP712Domain is required");
|
|
261
|
+
}
|
|
262
|
+
if (!typeNames.has(schema.primaryType)) {
|
|
263
|
+
schemaFail(`types.${schema.primaryType} is required`);
|
|
264
|
+
}
|
|
265
|
+
if (!fieldListsEqual(types["EIP712Domain"], EIP712_DOMAIN_FIELDS)) {
|
|
266
|
+
schemaFail("types.EIP712Domain field order mismatch");
|
|
267
|
+
}
|
|
268
|
+
if (!fieldListsEqual(types[schema.primaryType], schema.fields)) {
|
|
269
|
+
schemaFail(`types.${schema.primaryType} fields mismatch`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function validateMessage(message, schema, walletAddress) {
|
|
273
|
+
const actualKeys = new Set(Object.keys(message));
|
|
274
|
+
if (actualKeys.size !== schema.messageKeys.size ||
|
|
275
|
+
[...actualKeys].some((k) => !schema.messageKeys.has(k))) {
|
|
276
|
+
schemaFail(`message keys expected ${JSON.stringify([...schema.messageKeys].sort())} got ${JSON.stringify([...actualKeys].sort())}`);
|
|
277
|
+
}
|
|
278
|
+
const walletValue = message[schema.walletField];
|
|
279
|
+
if (!addressesEqual(walletValue, walletAddress)) {
|
|
280
|
+
schemaFail(`message.${schema.walletField} does not match wallet_address`);
|
|
281
|
+
}
|
|
282
|
+
const deadlineKey = "deadline" in message ? "deadline" : "expiry" in message ? "expiry" : null;
|
|
283
|
+
if (deadlineKey === null) {
|
|
284
|
+
schemaFail("message.deadline/expiry is required");
|
|
285
|
+
}
|
|
286
|
+
const deadline = asInt(message[deadlineKey], `message.${deadlineKey}`);
|
|
287
|
+
if (deadline <= Math.floor(Date.now() / 1000)) {
|
|
288
|
+
schemaFail(`message.${deadlineKey} is expired`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Layer 2 — Economic match
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
/**
|
|
295
|
+
* Reject typed-data whose economics don't agree with the user's original
|
|
296
|
+
* build request / build response. Guards against a compromised server
|
|
297
|
+
* returning valid-shape typed-data with altered amounts or wrong target.
|
|
298
|
+
*/
|
|
299
|
+
function validateEconomics(typedData, route, buildRequest, buildResponse) {
|
|
300
|
+
schemaFor(route); // assert known route
|
|
301
|
+
if (typedData === null || typeof typedData !== "object") {
|
|
302
|
+
economicFail("typed_data must be an object");
|
|
303
|
+
}
|
|
304
|
+
const message = typedData.message;
|
|
305
|
+
if (message === null || typeof message !== "object") {
|
|
306
|
+
economicFail("message must be an object");
|
|
307
|
+
}
|
|
308
|
+
if (route === "polymarket_buy") {
|
|
309
|
+
validatePolymarketBuyEconomics(message, buildRequest);
|
|
310
|
+
validateWorstPrice(message, route, buildRequest, buildResponse);
|
|
311
|
+
}
|
|
312
|
+
else if (route === "polymarket_sell") {
|
|
313
|
+
validatePolymarketSellEconomics(message, buildRequest);
|
|
314
|
+
validateWorstPrice(message, route, buildRequest, buildResponse);
|
|
315
|
+
}
|
|
316
|
+
else if (route === "opinion_buy" ||
|
|
317
|
+
route === "opinion_sell_polygon" ||
|
|
318
|
+
route === "opinion_sell_bsc_pull") {
|
|
319
|
+
validateOpinionMarketId(message, buildResponse);
|
|
320
|
+
}
|
|
321
|
+
// cancel_* routes: no economic check — chain enforces nonce.
|
|
322
|
+
}
|
|
323
|
+
function validatePolymarketBuyEconomics(message, buildRequest) {
|
|
324
|
+
const denom = getField(buildRequest, "denom");
|
|
325
|
+
if (denom !== "usdc") {
|
|
326
|
+
economicFail(`denom expected 'usdc' got ${JSON.stringify(denom)}`);
|
|
327
|
+
}
|
|
328
|
+
const amount = firstPresent(getField(buildRequest, "amount"), getField(buildRequest, "amount_usdc"), getField(buildRequest, "amountUsdc"));
|
|
329
|
+
if (amount === MISSING)
|
|
330
|
+
economicFail("amount missing");
|
|
331
|
+
const expected = to6decOrFail(amount, "max_cost_usdc");
|
|
332
|
+
const actual = messageBigInt(message, "max_cost_usdc", "maxCostUsdc");
|
|
333
|
+
if (actual !== expected) {
|
|
334
|
+
economicFail(`max_cost_usdc expected ${expected} got ${actual}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function validatePolymarketSellEconomics(message, buildRequest) {
|
|
338
|
+
const denom = getField(buildRequest, "denom");
|
|
339
|
+
if (denom !== "shares") {
|
|
340
|
+
economicFail(`denom expected 'shares' got ${JSON.stringify(denom)}`);
|
|
341
|
+
}
|
|
342
|
+
const amount = firstPresent(getField(buildRequest, "amount"), getField(buildRequest, "shares"));
|
|
343
|
+
if (amount === MISSING)
|
|
344
|
+
economicFail("amount missing");
|
|
345
|
+
const expected = to6decOrFail(amount, "shares_6dec");
|
|
346
|
+
const actual = messageBigInt(message, "shares_6dec", "shares6dec", "tokenAmount");
|
|
347
|
+
if (actual !== expected) {
|
|
348
|
+
economicFail(`shares_6dec expected ${expected} got ${actual}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const SIX_DEC_DIVISOR = 1_000_000;
|
|
352
|
+
function validateWorstPrice(message, route, buildRequest, buildResponse) {
|
|
353
|
+
const worstPriceMicro = messageBigInt(message, "worst_price", "worstPrice");
|
|
354
|
+
const worstPrice = Number(worstPriceMicro) / SIX_DEC_DIVISOR;
|
|
355
|
+
const slippagePctRaw = firstPresent(getField(buildRequest, "slippage_pct"), getField(buildRequest, "slippagePct"), getField(buildResponse, "slippage_pct"), getField(buildResponse, "slippagePct"), 20);
|
|
356
|
+
const slippagePct = toFiniteNumber(slippagePctRaw, "slippage_pct");
|
|
357
|
+
if (route === "polymarket_buy") {
|
|
358
|
+
const bestPrice = toFiniteNumber(firstPresent(getPath(buildResponse, "quote", "best_price"), getField(buildResponse, "best_price"), getField(buildResponse, "best_ask"), getField(buildResponse, "bestAsk")), "quote.best_price");
|
|
359
|
+
const upper = bestPrice * (1 + slippagePct / 100);
|
|
360
|
+
if (worstPrice > upper) {
|
|
361
|
+
economicFail(`worst_price expected <= ${upper} got ${worstPrice}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
const bestPrice = toFiniteNumber(firstPresent(getPath(buildResponse, "quote", "best_price"), getField(buildResponse, "best_price"), getField(buildResponse, "best_bid"), getField(buildResponse, "bestBid")), "quote.best_price");
|
|
366
|
+
const lower = bestPrice * (1 - slippagePct / 100);
|
|
367
|
+
if (worstPrice < lower) {
|
|
368
|
+
economicFail(`worst_price expected >= ${lower} got ${worstPrice}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function validateOpinionMarketId(message, buildResponse) {
|
|
373
|
+
const expected = firstPresent(getPath(buildResponse, "resolved", "opinion_market_id"), getPath(buildResponse, "resolved", "opinionMarketId"), getField(buildResponse, "opinion_market_id"), getField(buildResponse, "opinionMarketId"), getPath(buildResponse, "params", "opinion_market_id"), getPath(buildResponse, "params", "opinionMarketId"));
|
|
374
|
+
if (expected === MISSING)
|
|
375
|
+
economicFail("resolved.opinion_market_id missing");
|
|
376
|
+
const actual = firstPresent(getField(message, "opinion_market_id"), getField(message, "opinionMarketId"));
|
|
377
|
+
if (actual === MISSING)
|
|
378
|
+
economicFail("message.opinion_market_id missing");
|
|
379
|
+
if (idValue(actual) !== idValue(expected)) {
|
|
380
|
+
economicFail(`opinion_market_id expected ${expected} got ${actual}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Layer 3 — Post-sign verification
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
/**
|
|
387
|
+
* Verify a signature against typed-data and return the normalized signature.
|
|
388
|
+
*
|
|
389
|
+
* Performs:
|
|
390
|
+
* - exact 65-byte length (0x + 130 hex)
|
|
391
|
+
* - low-s canonical check
|
|
392
|
+
* - v ∈ {27, 28} (normalizes {0,1} → {27,28})
|
|
393
|
+
* - typed-data recovery, asserting recovered address === walletAddress
|
|
394
|
+
*
|
|
395
|
+
* Throws {@link InvalidSignature} on any failure.
|
|
396
|
+
*/
|
|
397
|
+
function verifySignature(typedData, signature, walletAddress) {
|
|
398
|
+
if (typeof signature !== "string" || !SIGNATURE_RE.test(signature)) {
|
|
399
|
+
throw new hosted_errors_1.InvalidSignature(0, "signature must be 0x-prefixed 65-byte hex");
|
|
400
|
+
}
|
|
401
|
+
const hex = signature.slice(2);
|
|
402
|
+
const sHex = hex.slice(64, 128);
|
|
403
|
+
const sValue = BigInt("0x" + sHex);
|
|
404
|
+
if (sValue > exports.SECP256K1_HALF_N) {
|
|
405
|
+
throw new hosted_errors_1.InvalidSignature(0, "non-canonical (high-s)");
|
|
406
|
+
}
|
|
407
|
+
let vByte = parseInt(hex.slice(128, 130), 16);
|
|
408
|
+
let normalized = signature;
|
|
409
|
+
if (vByte === 0 || vByte === 1) {
|
|
410
|
+
vByte += 27;
|
|
411
|
+
normalized = "0x" + hex.slice(0, 128) + vByte.toString(16).padStart(2, "0");
|
|
412
|
+
}
|
|
413
|
+
if (vByte !== 27 && vByte !== 28) {
|
|
414
|
+
throw new hosted_errors_1.InvalidSignature(0, `invalid recovery byte: ${vByte}`);
|
|
415
|
+
}
|
|
416
|
+
let ethers;
|
|
417
|
+
try {
|
|
418
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
419
|
+
ethers = require("ethers");
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
throw new hosted_errors_1.InvalidSignature(0, "ethers is required for hosted signature verification");
|
|
423
|
+
}
|
|
424
|
+
let recovered;
|
|
425
|
+
try {
|
|
426
|
+
// ethers expects `types` WITHOUT the EIP712Domain entry.
|
|
427
|
+
const types = { ...typedData.types };
|
|
428
|
+
delete types["EIP712Domain"];
|
|
429
|
+
recovered = ethers.verifyTypedData(typedData.domain, types, typedData.message, normalized);
|
|
430
|
+
}
|
|
431
|
+
catch (exc) {
|
|
432
|
+
throw new hosted_errors_1.InvalidSignature(0, `signature recovery failed: ${exc.message}`);
|
|
433
|
+
}
|
|
434
|
+
if (!addressesEqual(recovered, walletAddress)) {
|
|
435
|
+
throw new hosted_errors_1.InvalidSignature(0, `signature signer mismatch: expected ${walletAddress} got ${recovered}`);
|
|
436
|
+
}
|
|
437
|
+
return normalized;
|
|
438
|
+
}
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
// Internal helpers
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
const MISSING = Symbol("missing");
|
|
443
|
+
function schemaFor(route) {
|
|
444
|
+
const schema = SCHEMAS[route];
|
|
445
|
+
if (!schema)
|
|
446
|
+
schemaFail(`unknown typed-data route: '${route}'`);
|
|
447
|
+
return schema;
|
|
448
|
+
}
|
|
449
|
+
function schemaFail(message) {
|
|
450
|
+
throw new hosted_errors_1.InvalidSignature(0, `typed_data schema mismatch: ${message}`);
|
|
451
|
+
}
|
|
452
|
+
function economicFail(message) {
|
|
453
|
+
throw new hosted_errors_1.InvalidSignature(0, `economic mismatch: ${message}`);
|
|
454
|
+
}
|
|
455
|
+
function fieldListsEqual(actual, expected) {
|
|
456
|
+
if (!Array.isArray(actual) || actual.length !== expected.length)
|
|
457
|
+
return false;
|
|
458
|
+
for (let i = 0; i < expected.length; i++) {
|
|
459
|
+
const a = actual[i];
|
|
460
|
+
const e = expected[i];
|
|
461
|
+
if (a === null || typeof a !== "object")
|
|
462
|
+
return false;
|
|
463
|
+
if (a.name !== e.name || a.type !== e.type)
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
function asInt(value, label) {
|
|
469
|
+
if (typeof value === "boolean")
|
|
470
|
+
schemaFail(`${label} must be an integer`);
|
|
471
|
+
if (typeof value === "number" && Number.isInteger(value))
|
|
472
|
+
return value;
|
|
473
|
+
if (typeof value === "bigint")
|
|
474
|
+
return Number(value);
|
|
475
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
476
|
+
const parsed = Number(value);
|
|
477
|
+
if (Number.isInteger(parsed))
|
|
478
|
+
return parsed;
|
|
479
|
+
// Allow big numeric strings that fit safe integer range
|
|
480
|
+
try {
|
|
481
|
+
return Number(BigInt(value));
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
schemaFail(`${label} must be an integer`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
schemaFail(`${label} must be an integer`);
|
|
488
|
+
}
|
|
489
|
+
function normalizeAddress(value) {
|
|
490
|
+
if (typeof value !== "string")
|
|
491
|
+
return null;
|
|
492
|
+
const candidate = value.trim();
|
|
493
|
+
if (!ADDRESS_RE.test(candidate))
|
|
494
|
+
return null;
|
|
495
|
+
return candidate.toLowerCase();
|
|
496
|
+
}
|
|
497
|
+
function addressesEqual(left, right) {
|
|
498
|
+
const a = normalizeAddress(left);
|
|
499
|
+
const b = normalizeAddress(right);
|
|
500
|
+
return a !== null && a === b;
|
|
501
|
+
}
|
|
502
|
+
function allowedAddresses(key, chainId) {
|
|
503
|
+
const raw = key === "prefunded"
|
|
504
|
+
? constants.PREFUNDED_ESCROW_ADDRESSES
|
|
505
|
+
: constants.VENUE_ESCROW_ADDRESSES;
|
|
506
|
+
const list = [];
|
|
507
|
+
if (raw == null) {
|
|
508
|
+
// empty
|
|
509
|
+
}
|
|
510
|
+
else if (typeof raw === "string") {
|
|
511
|
+
list.push(raw);
|
|
512
|
+
}
|
|
513
|
+
else if (Array.isArray(raw)) {
|
|
514
|
+
list.push(...raw);
|
|
515
|
+
}
|
|
516
|
+
else if (raw instanceof Set) {
|
|
517
|
+
for (const v of raw)
|
|
518
|
+
list.push(v);
|
|
519
|
+
}
|
|
520
|
+
else if (typeof raw === "object") {
|
|
521
|
+
const lookup = raw[String(chainId)] ??
|
|
522
|
+
raw[chainId];
|
|
523
|
+
if (typeof lookup === "string")
|
|
524
|
+
list.push(lookup);
|
|
525
|
+
else if (Array.isArray(lookup))
|
|
526
|
+
list.push(...lookup);
|
|
527
|
+
}
|
|
528
|
+
const out = new Set();
|
|
529
|
+
for (const v of list) {
|
|
530
|
+
const normalized = normalizeAddress(v);
|
|
531
|
+
if (normalized !== null)
|
|
532
|
+
out.add(normalized);
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
function getField(container, key) {
|
|
537
|
+
if (container === null || container === undefined)
|
|
538
|
+
return MISSING;
|
|
539
|
+
if (typeof container === "object") {
|
|
540
|
+
const obj = container;
|
|
541
|
+
if (key in obj)
|
|
542
|
+
return obj[key];
|
|
543
|
+
return MISSING;
|
|
544
|
+
}
|
|
545
|
+
return MISSING;
|
|
546
|
+
}
|
|
547
|
+
function getPath(container, ...keys) {
|
|
548
|
+
let current = container;
|
|
549
|
+
for (const key of keys) {
|
|
550
|
+
current = getField(current, key);
|
|
551
|
+
if (current === MISSING)
|
|
552
|
+
return MISSING;
|
|
553
|
+
}
|
|
554
|
+
return current;
|
|
555
|
+
}
|
|
556
|
+
function firstPresent(...values) {
|
|
557
|
+
for (const v of values) {
|
|
558
|
+
if (v !== MISSING && v !== null && v !== undefined)
|
|
559
|
+
return v;
|
|
560
|
+
}
|
|
561
|
+
return MISSING;
|
|
562
|
+
}
|
|
563
|
+
function to6decOrFail(amount, label) {
|
|
564
|
+
try {
|
|
565
|
+
return (0, hosted_mappers_1.to6dec)(amount);
|
|
566
|
+
}
|
|
567
|
+
catch (exc) {
|
|
568
|
+
throw new hosted_errors_1.InvalidSignature(0, `economic mismatch: ${label} must fit the 6-decimal grid (${exc.message})`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function messageBigInt(message, ...keys) {
|
|
572
|
+
for (const key of keys) {
|
|
573
|
+
if (key in message) {
|
|
574
|
+
const v = message[key];
|
|
575
|
+
if (typeof v === "bigint")
|
|
576
|
+
return v;
|
|
577
|
+
if (typeof v === "number" && Number.isInteger(v))
|
|
578
|
+
return BigInt(v);
|
|
579
|
+
if (typeof v === "string" && v.trim().length > 0) {
|
|
580
|
+
try {
|
|
581
|
+
return BigInt(v);
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
economicFail(`message.${key} must be an integer`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
economicFail(`message.${key} must be an integer`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
economicFail(`message.${keys[0]} missing`);
|
|
591
|
+
}
|
|
592
|
+
function toFiniteNumber(value, label) {
|
|
593
|
+
if (value === MISSING || value === null || value === undefined) {
|
|
594
|
+
economicFail(`${label} missing`);
|
|
595
|
+
}
|
|
596
|
+
if (typeof value === "number") {
|
|
597
|
+
if (!Number.isFinite(value))
|
|
598
|
+
economicFail(`${label} must be finite`);
|
|
599
|
+
return value;
|
|
600
|
+
}
|
|
601
|
+
if (typeof value === "bigint")
|
|
602
|
+
return Number(value);
|
|
603
|
+
if (typeof value === "string") {
|
|
604
|
+
const parsed = Number(value);
|
|
605
|
+
if (!Number.isFinite(parsed))
|
|
606
|
+
economicFail(`${label} must be a number`);
|
|
607
|
+
return parsed;
|
|
608
|
+
}
|
|
609
|
+
economicFail(`${label} must be a number`);
|
|
610
|
+
}
|
|
611
|
+
function idValue(value) {
|
|
612
|
+
if (typeof value === "string")
|
|
613
|
+
return value;
|
|
614
|
+
if (typeof value === "number")
|
|
615
|
+
return String(value);
|
|
616
|
+
if (typeof value === "bigint")
|
|
617
|
+
return value.toString();
|
|
618
|
+
return JSON.stringify(value);
|
|
619
|
+
}
|