@pulsepairs/sdk 0.2.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.
@@ -0,0 +1,69 @@
1
+ const ERC20_ABI = [
2
+ {
3
+ type: "function",
4
+ name: "allowance",
5
+ stateMutability: "view",
6
+ inputs: [
7
+ { name: "owner", type: "address" },
8
+ { name: "spender", type: "address" },
9
+ ],
10
+ outputs: [{ name: "", type: "uint256" }],
11
+ },
12
+ {
13
+ type: "function",
14
+ name: "approve",
15
+ stateMutability: "nonpayable",
16
+ inputs: [
17
+ { name: "spender", type: "address" },
18
+ { name: "value", type: "uint256" },
19
+ ],
20
+ outputs: [{ name: "", type: "bool" }],
21
+ },
22
+ ];
23
+ export const MAX_UINT256 = (1n << 256n) - 1n;
24
+ /** Default refresh threshold — re-approve if allowance falls below 10k USDT.
25
+ * MaxUint256 effectively never decreases with USDT, but if a partial
26
+ * approval was set in some flow this guards against drift. */
27
+ const DEFAULT_THRESHOLD = 10000n * 1000000n;
28
+ /**
29
+ * Ensure the maker has enough USDT allowance on the settlement contract.
30
+ *
31
+ * @param args.publicClient — viem `createPublicClient(...)` for reads.
32
+ * @param args.walletClient — viem `createWalletClient({ account, ... })`
33
+ * with the maker's account (must have ETH for gas).
34
+ * @param args.usdt — USDT token address (from `cfg.usdtAddress`).
35
+ * @param args.settlement — Settlement contract address (from
36
+ * `cfg.pairs[0].settlementAddress` for today's
37
+ * single-settlement deploy).
38
+ * @param args.threshold — Re-approve when current allowance is below this
39
+ * atomic-USDT amount. Default 10,000 USDT.
40
+ *
41
+ * Returns `already_ok` if nothing needed to be done, `approved` with the
42
+ * tx hash if a new approval was sent. Caller can `await
43
+ * publicClient.waitForTransactionReceipt({ hash })` if it wants to confirm
44
+ * before placing orders.
45
+ */
46
+ export async function ensureSettlementAllowance(args) {
47
+ const account = args.walletClient.account;
48
+ if (!account)
49
+ throw new Error("walletClient has no account");
50
+ const owner = account.address;
51
+ const threshold = args.threshold ?? DEFAULT_THRESHOLD;
52
+ const current = (await args.publicClient.readContract({
53
+ address: args.usdt,
54
+ abi: ERC20_ABI,
55
+ functionName: "allowance",
56
+ args: [owner, args.settlement],
57
+ }));
58
+ if (current >= threshold)
59
+ return { status: "already_ok", allowance: current };
60
+ const txHash = await args.walletClient.writeContract({
61
+ account,
62
+ chain: args.walletClient.chain ?? null,
63
+ address: args.usdt,
64
+ abi: ERC20_ABI,
65
+ functionName: "approve",
66
+ args: [args.settlement, MAX_UINT256],
67
+ });
68
+ return { status: "approved", txHash, allowance: MAX_UINT256 };
69
+ }
package/dist/auth.d.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Phase 3 / Gate 1 — L2 HMAC auth helpers for the TypeScript SDK.
3
+ *
4
+ * Two surfaces:
5
+ * - `buildHmacSignature(secret, timestamp, method, path, body?)` — exact
6
+ * bytes-level match with the backend's `lib/hmacAuth.ts` and with
7
+ * Polymarket's `clob-client/src/signing/hmac.ts`. The pre-merge
8
+ * structural diff in the gate-1 commit body side-by-sides both.
9
+ * - `clobAuthTypedData(domain, address, timestamp, nonce)` — builds the
10
+ * EIP-712 ClobAuth payload bots sign once at session open.
11
+ *
12
+ * Bot quickstart:
13
+ *
14
+ * const issued = await fetch('/auth/credentials', { ... });
15
+ * const { apiKey, apiSecret, passphrase } = await issued.json();
16
+ * // store apiSecret + passphrase out-of-band (env var, KMS, etc).
17
+ * // for every subsequent request:
18
+ * const ts = Math.floor(Date.now() / 1000);
19
+ * const sig = buildHmacSignature(apiSecret, ts, 'DELETE', '/orders/abc');
20
+ * await fetch('/orders/abc', {
21
+ * method: 'DELETE',
22
+ * headers: {
23
+ * 'X-PP-ADDRESS': address,
24
+ * 'X-PP-API-KEY': apiKey,
25
+ * 'X-PP-PASSPHRASE': passphrase,
26
+ * 'X-PP-TIMESTAMP': String(ts),
27
+ * 'X-PP-SIGNATURE': sig,
28
+ * },
29
+ * });
30
+ *
31
+ * Browser callers should NEVER hold apiSecret in client-side code. Doc 1.5#2.
32
+ */
33
+ export declare const CLOB_AUTH_TYPES: {
34
+ ClobAuth: {
35
+ name: string;
36
+ type: string;
37
+ }[];
38
+ };
39
+ export declare const CLOB_AUTH_MESSAGE = "This message attests that I control the given wallet";
40
+ export interface ClobAuthDomain {
41
+ name: string;
42
+ version: string;
43
+ chainId: number;
44
+ }
45
+ export interface ClobAuthMessage {
46
+ address: `0x${string}`;
47
+ timestamp: string;
48
+ nonce: bigint;
49
+ message: string;
50
+ }
51
+ export declare function buildClobAuthTypedData(domain: ClobAuthDomain, address: `0x${string}`, timestamp: string, nonce: bigint): {
52
+ domain: ClobAuthDomain;
53
+ types: typeof CLOB_AUTH_TYPES;
54
+ primaryType: "ClobAuth";
55
+ message: ClobAuthMessage;
56
+ };
57
+ /**
58
+ * Build the L2 HMAC signature header value.
59
+ *
60
+ * @param secretBase64 - the apiSecret returned by POST /auth/credentials
61
+ * @param timestamp - unix SECONDS (NOT ms — matches Polymarket clob-client)
62
+ * @param method - HTTP method, uppercase
63
+ * @param requestPath - path with query string, no host (e.g. "/orders/abc?foo=1")
64
+ * @param body - raw JSON-stringified body, or undefined for GET/DELETE
65
+ *
66
+ * Bytes-level identical to:
67
+ * Polymarket/clob-client/src/signing/hmac.ts ::buildPolyHmacSignature
68
+ */
69
+ export declare function buildHmacSignature(secretBase64: string, timestamp: number, method: string, requestPath: string, body?: string): string;
70
+ /** Header names. SDK consumers should use these constants rather than literal strings. */
71
+ export declare const HMAC_HEADERS: {
72
+ readonly ADDRESS: "X-PP-ADDRESS";
73
+ readonly API_KEY: "X-PP-API-KEY";
74
+ readonly PASSPHRASE: "X-PP-PASSPHRASE";
75
+ readonly TIMESTAMP: "X-PP-TIMESTAMP";
76
+ readonly SIGNATURE: "X-PP-SIGNATURE";
77
+ };
package/dist/auth.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Phase 3 / Gate 1 — L2 HMAC auth helpers for the TypeScript SDK.
3
+ *
4
+ * Two surfaces:
5
+ * - `buildHmacSignature(secret, timestamp, method, path, body?)` — exact
6
+ * bytes-level match with the backend's `lib/hmacAuth.ts` and with
7
+ * Polymarket's `clob-client/src/signing/hmac.ts`. The pre-merge
8
+ * structural diff in the gate-1 commit body side-by-sides both.
9
+ * - `clobAuthTypedData(domain, address, timestamp, nonce)` — builds the
10
+ * EIP-712 ClobAuth payload bots sign once at session open.
11
+ *
12
+ * Bot quickstart:
13
+ *
14
+ * const issued = await fetch('/auth/credentials', { ... });
15
+ * const { apiKey, apiSecret, passphrase } = await issued.json();
16
+ * // store apiSecret + passphrase out-of-band (env var, KMS, etc).
17
+ * // for every subsequent request:
18
+ * const ts = Math.floor(Date.now() / 1000);
19
+ * const sig = buildHmacSignature(apiSecret, ts, 'DELETE', '/orders/abc');
20
+ * await fetch('/orders/abc', {
21
+ * method: 'DELETE',
22
+ * headers: {
23
+ * 'X-PP-ADDRESS': address,
24
+ * 'X-PP-API-KEY': apiKey,
25
+ * 'X-PP-PASSPHRASE': passphrase,
26
+ * 'X-PP-TIMESTAMP': String(ts),
27
+ * 'X-PP-SIGNATURE': sig,
28
+ * },
29
+ * });
30
+ *
31
+ * Browser callers should NEVER hold apiSecret in client-side code. Doc 1.5#2.
32
+ */
33
+ import { createHmac } from "crypto";
34
+ export const CLOB_AUTH_TYPES = {
35
+ ClobAuth: [
36
+ { name: "address", type: "address" },
37
+ { name: "timestamp", type: "string" },
38
+ { name: "nonce", type: "uint256" },
39
+ { name: "message", type: "string" },
40
+ ],
41
+ };
42
+ export const CLOB_AUTH_MESSAGE = "This message attests that I control the given wallet";
43
+ export function buildClobAuthTypedData(domain, address, timestamp, nonce) {
44
+ return {
45
+ domain,
46
+ types: CLOB_AUTH_TYPES,
47
+ primaryType: "ClobAuth",
48
+ message: { address, timestamp, nonce, message: CLOB_AUTH_MESSAGE },
49
+ };
50
+ }
51
+ /**
52
+ * Build the L2 HMAC signature header value.
53
+ *
54
+ * @param secretBase64 - the apiSecret returned by POST /auth/credentials
55
+ * @param timestamp - unix SECONDS (NOT ms — matches Polymarket clob-client)
56
+ * @param method - HTTP method, uppercase
57
+ * @param requestPath - path with query string, no host (e.g. "/orders/abc?foo=1")
58
+ * @param body - raw JSON-stringified body, or undefined for GET/DELETE
59
+ *
60
+ * Bytes-level identical to:
61
+ * Polymarket/clob-client/src/signing/hmac.ts ::buildPolyHmacSignature
62
+ */
63
+ export function buildHmacSignature(secretBase64, timestamp, method, requestPath, body) {
64
+ const secretBytes = Buffer.from(secretBase64, "base64");
65
+ const message = `${timestamp}${method}${requestPath}${body ?? ""}`;
66
+ const sigB64 = createHmac("sha256", secretBytes).update(message, "utf8").digest("base64");
67
+ return sigB64.split("+").join("-").split("/").join("_");
68
+ }
69
+ /** Header names. SDK consumers should use these constants rather than literal strings. */
70
+ export const HMAC_HEADERS = {
71
+ ADDRESS: "X-PP-ADDRESS",
72
+ API_KEY: "X-PP-API-KEY",
73
+ PASSPHRASE: "X-PP-PASSPHRASE",
74
+ TIMESTAMP: "X-PP-TIMESTAMP",
75
+ SIGNATURE: "X-PP-SIGNATURE",
76
+ };
@@ -0,0 +1,216 @@
1
+ import type { ApiConfig, Eip712Domain, PairConfig } from "./types.js";
2
+ /**
3
+ * EIP-712 type schema for the WebSocket auth handshake. Mirrors the
4
+ * backend's `WS_AUTH_TYPES` in `src/ws/wsAuth.ts` exactly. If these drift,
5
+ * the server's `verifyTypedData` rejects the signature and gates
6
+ * `orders:<wallet>` + `balance:<wallet>` shut.
7
+ *
8
+ * Distinct domain from order signing (name "PulsePairs WebSocket Auth")
9
+ * so an order signature can never be replayed as a WS-auth signature
10
+ * and vice versa.
11
+ */
12
+ export declare const WS_AUTH_TYPES: {
13
+ readonly WsAuth: readonly [{
14
+ readonly name: "wallet";
15
+ readonly type: "address";
16
+ }, {
17
+ readonly name: "timestamp";
18
+ readonly type: "uint256";
19
+ }, {
20
+ readonly name: "sessionId";
21
+ readonly type: "bytes32";
22
+ }];
23
+ };
24
+ export type WsAuthMessage = {
25
+ wallet: `0x${string}`;
26
+ /** Unix seconds. Server window is ±60s of its clock. */
27
+ timestamp: bigint;
28
+ /** 32 random bytes, hex-encoded. Server rejects reuse within the
29
+ * timestamp window. Use `freshSessionId()`. */
30
+ sessionId: `0x${string}`;
31
+ };
32
+ /**
33
+ * Build typed-data for the WS auth handshake. Domain is fixed
34
+ * (`PulsePairs WebSocket Auth`, version `1`, chainId from config,
35
+ * verifyingContract = zero) — does NOT depend on settlement address.
36
+ *
37
+ * Pass the result straight to `walletClient.signTypedData({ ...result })`.
38
+ * Then send `{ type: 'auth', wallet, timestamp, sessionId, signature }`
39
+ * over the WS. Server replies `{ type: 'auth_ok', token, expiresAt }`
40
+ * (token TTL 24h) or `{ type: 'auth_error' }`.
41
+ */
42
+ export declare function buildWsAuthTypedData(args: {
43
+ cfg: Pick<ApiConfig, "chainId">;
44
+ wallet: `0x${string}`;
45
+ timestamp: bigint;
46
+ sessionId: `0x${string}`;
47
+ }): {
48
+ domain: Eip712Domain;
49
+ types: typeof WS_AUTH_TYPES;
50
+ primaryType: "WsAuth";
51
+ message: WsAuthMessage;
52
+ };
53
+ /**
54
+ * Generate a fresh 32-byte sessionId (bytes32 hex) for a WS auth
55
+ * handshake. Uses `globalThis.crypto.getRandomValues` — present on
56
+ * browsers and Node 18+ (the SDK's documented runtime baseline).
57
+ *
58
+ * Server rejects sessionId reuse within the timestamp window — generate
59
+ * a fresh one for every initial sign (NOT for token replay, which has
60
+ * no sessionId).
61
+ */
62
+ export declare function freshSessionId(): `0x${string}`;
63
+ /**
64
+ * EIP-712 type schemas. Mirrors the backend's `EIP712_ORDER_TYPES` /
65
+ * `EIP712_CANCEL_TYPES` exactly — if these drift, signatures stop being
66
+ * accepted by the on-chain `SignatureChecker`.
67
+ */
68
+ export declare const ORDER_TYPES: {
69
+ readonly Order: readonly [{
70
+ readonly name: "maker";
71
+ readonly type: "address";
72
+ }, {
73
+ readonly name: "market";
74
+ readonly type: "uint256";
75
+ }, {
76
+ readonly name: "option";
77
+ readonly type: "uint256";
78
+ }, {
79
+ readonly name: "side";
80
+ readonly type: "uint8";
81
+ }, {
82
+ readonly name: "type";
83
+ readonly type: "uint8";
84
+ }, {
85
+ readonly name: "price";
86
+ readonly type: "uint256";
87
+ }, {
88
+ readonly name: "amount";
89
+ readonly type: "uint256";
90
+ }, {
91
+ readonly name: "maxFee";
92
+ readonly type: "uint256";
93
+ }, {
94
+ readonly name: "nonce";
95
+ readonly type: "uint256";
96
+ }, {
97
+ readonly name: "expiry";
98
+ readonly type: "uint256";
99
+ }];
100
+ };
101
+ export declare const CANCEL_TYPES: {
102
+ readonly Cancel: readonly [{
103
+ readonly name: "maker";
104
+ readonly type: "address";
105
+ }, {
106
+ readonly name: "orderId";
107
+ readonly type: "string";
108
+ }, {
109
+ readonly name: "nonce";
110
+ readonly type: "uint256";
111
+ }, {
112
+ readonly name: "expiry";
113
+ readonly type: "uint256";
114
+ }];
115
+ };
116
+ export type OrderSignMessage = {
117
+ maker: `0x${string}`;
118
+ /** On-chain market id (uint256), bare number from the composite key
119
+ * suffix — NOT the composite string. */
120
+ market: bigint;
121
+ option: bigint;
122
+ side: number;
123
+ type: number;
124
+ price: bigint;
125
+ amount: bigint;
126
+ /** F-2026-17731: signed fee cap (atomic USDT). */
127
+ maxFee: bigint;
128
+ nonce: bigint;
129
+ expiry: bigint;
130
+ };
131
+ export type CancelSignMessage = {
132
+ maker: `0x${string}`;
133
+ orderId: string;
134
+ /** uint256 nonce — unique per cancel; backend replay-caches for 5 min. */
135
+ nonce: bigint;
136
+ /** uint256 unix-sec expiry — backend rejects past-expiry cancels. */
137
+ expiry: bigint;
138
+ };
139
+ /**
140
+ * Look up the pair config that owns a settlement contract. The single-
141
+ * settlement deployment makes this a degenerate one-entry array today, but
142
+ * code that hard-codes `pairs[0]` will silently break the moment a second
143
+ * settlement deploys. Always look up by address.
144
+ */
145
+ export declare function findPairBySettlement(pairs: readonly PairConfig[], settlementAddress: string): PairConfig | null;
146
+ /**
147
+ * Resolve the EIP-712 domain a maker should sign against for a given
148
+ * settlement address. Uses `cfg.pairs[]` (preferred), falls back to the
149
+ * legacy top-level `cfg.eip712.domain` only if no pair matches and the
150
+ * legacy field's `verifyingContract` matches the requested settlement —
151
+ * otherwise throws so a misconfiguration produces a clear error instead
152
+ * of a silently-wrong signature.
153
+ */
154
+ export declare function domainForSettlement(cfg: ApiConfig, settlementAddress: string): Eip712Domain;
155
+ /**
156
+ * Build typed-data for `POST /orders`. Pass the settlement address from
157
+ * the market the order is for; the helper picks the right domain.
158
+ */
159
+ export declare function buildOrderTypedData(args: {
160
+ cfg: ApiConfig;
161
+ settlementAddress: string;
162
+ message: OrderSignMessage;
163
+ }): {
164
+ domain: Eip712Domain;
165
+ types: typeof ORDER_TYPES;
166
+ primaryType: "Order";
167
+ message: OrderSignMessage;
168
+ };
169
+ /**
170
+ * Build typed-data for `DELETE /orders/:id`. The cancel signature uses
171
+ * the same per-pair domain as the original order — derive from the
172
+ * order's market composite key.
173
+ */
174
+ export declare function buildCancelTypedData(args: {
175
+ cfg: ApiConfig;
176
+ settlementAddress: string;
177
+ message: CancelSignMessage;
178
+ }): {
179
+ domain: Eip712Domain;
180
+ types: typeof CANCEL_TYPES;
181
+ primaryType: "Cancel";
182
+ message: CancelSignMessage;
183
+ };
184
+ export type ParsedComposite = {
185
+ settlementAddress: `0x${string}`;
186
+ marketId: string;
187
+ };
188
+ /** Parse a `<settlementAddress>-<marketId>` composite key. */
189
+ export declare function parseCompositeMarketKey(market: string): ParsedComposite | null;
190
+ /**
191
+ * Cents → bps (1¢ = 100 bps; 50¢ = 5000 bps). Throws on out-of-range.
192
+ * Accepts integers or half-integer cents (e.g. 50.5 → 5050) for partial-cent
193
+ * pricing. UI typically clamps to whole cents.
194
+ */
195
+ export declare function centsToBps(cents: number): number;
196
+ /** Bps → cents (5500 → 55). Inverse of `centsToBps`. */
197
+ export declare function bpsToCents(bps: number): number;
198
+ /** Atomic-USDT denomination of the documented $5–$500 stake window. */
199
+ export declare const MIN_STAKE_ATOMIC = 5000000n;
200
+ export declare const MAX_STAKE_ATOMIC = 500000000n;
201
+ /** Parse a USD string (e.g. "5", "5.50", "12.345") into atomic USDT.
202
+ * Rejects negatives + non-finite. */
203
+ export declare function parseStake(usd: string | number): bigint;
204
+ /**
205
+ * Defense-in-depth stake clamp. Throws if `amountAtomic` is outside the
206
+ * documented `$5 ≤ stake ≤ $500` window. Frontend gates these too; backend
207
+ * enforces only the upper bound today (BUG-S2.1 — see PULSEPAIRS_BACKLOG).
208
+ * SDK callers should validate before signing so a bad stake never produces
209
+ * a signed payload at all.
210
+ */
211
+ export declare function assertStakeBounds(amountAtomic: bigint): void;
212
+ /**
213
+ * Probability-weighted fee in atomic USDT. Mirrors the backend / contract
214
+ * formula. Returns the fee for `notionalAtomic` filled at `priceBps`.
215
+ */
216
+ export declare function feeAtomic(notionalAtomic: bigint, priceBps: number, cfg: Pick<ApiConfig, "platformFeeBps" | "makerFeeBps" | "feeModel">): bigint;
package/dist/eip712.js ADDED
@@ -0,0 +1,229 @@
1
+ /* ───────────────────────── WS auth (PR-19) ───────────────────────── */
2
+ /**
3
+ * EIP-712 type schema for the WebSocket auth handshake. Mirrors the
4
+ * backend's `WS_AUTH_TYPES` in `src/ws/wsAuth.ts` exactly. If these drift,
5
+ * the server's `verifyTypedData` rejects the signature and gates
6
+ * `orders:<wallet>` + `balance:<wallet>` shut.
7
+ *
8
+ * Distinct domain from order signing (name "PulsePairs WebSocket Auth")
9
+ * so an order signature can never be replayed as a WS-auth signature
10
+ * and vice versa.
11
+ */
12
+ export const WS_AUTH_TYPES = {
13
+ WsAuth: [
14
+ { name: "wallet", type: "address" },
15
+ { name: "timestamp", type: "uint256" },
16
+ { name: "sessionId", type: "bytes32" },
17
+ ],
18
+ };
19
+ const WS_AUTH_VERIFYING_CONTRACT = "0x0000000000000000000000000000000000000000";
20
+ /**
21
+ * Build typed-data for the WS auth handshake. Domain is fixed
22
+ * (`PulsePairs WebSocket Auth`, version `1`, chainId from config,
23
+ * verifyingContract = zero) — does NOT depend on settlement address.
24
+ *
25
+ * Pass the result straight to `walletClient.signTypedData({ ...result })`.
26
+ * Then send `{ type: 'auth', wallet, timestamp, sessionId, signature }`
27
+ * over the WS. Server replies `{ type: 'auth_ok', token, expiresAt }`
28
+ * (token TTL 24h) or `{ type: 'auth_error' }`.
29
+ */
30
+ export function buildWsAuthTypedData(args) {
31
+ return {
32
+ domain: {
33
+ name: "PulsePairs WebSocket Auth",
34
+ version: "1",
35
+ chainId: args.cfg.chainId,
36
+ verifyingContract: WS_AUTH_VERIFYING_CONTRACT,
37
+ },
38
+ types: WS_AUTH_TYPES,
39
+ primaryType: "WsAuth",
40
+ message: {
41
+ wallet: args.wallet,
42
+ timestamp: args.timestamp,
43
+ sessionId: args.sessionId,
44
+ },
45
+ };
46
+ }
47
+ /**
48
+ * Generate a fresh 32-byte sessionId (bytes32 hex) for a WS auth
49
+ * handshake. Uses `globalThis.crypto.getRandomValues` — present on
50
+ * browsers and Node 18+ (the SDK's documented runtime baseline).
51
+ *
52
+ * Server rejects sessionId reuse within the timestamp window — generate
53
+ * a fresh one for every initial sign (NOT for token replay, which has
54
+ * no sessionId).
55
+ */
56
+ export function freshSessionId() {
57
+ const bytes = new Uint8Array(32);
58
+ const g = globalThis;
59
+ if (!g.crypto || typeof g.crypto.getRandomValues !== "function") {
60
+ throw new Error("globalThis.crypto.getRandomValues unavailable; use Node 18+ or a modern browser");
61
+ }
62
+ g.crypto.getRandomValues(bytes);
63
+ let hex = "0x";
64
+ for (let i = 0; i < bytes.length; i++) {
65
+ hex += bytes[i].toString(16).padStart(2, "0");
66
+ }
67
+ return hex;
68
+ }
69
+ /**
70
+ * EIP-712 type schemas. Mirrors the backend's `EIP712_ORDER_TYPES` /
71
+ * `EIP712_CANCEL_TYPES` exactly — if these drift, signatures stop being
72
+ * accepted by the on-chain `SignatureChecker`.
73
+ */
74
+ export const ORDER_TYPES = {
75
+ Order: [
76
+ { name: "maker", type: "address" },
77
+ { name: "market", type: "uint256" },
78
+ { name: "option", type: "uint256" },
79
+ { name: "side", type: "uint8" },
80
+ { name: "type", type: "uint8" },
81
+ { name: "price", type: "uint256" },
82
+ { name: "amount", type: "uint256" },
83
+ // F-2026-17731 (Hacken remediation V2): signed fee cap — max total fee (platform + maker)
84
+ // this order pays WHEN FILLED AS THE TAKER. Must match ORDER_TYPEHASH in UpDownSettlement.sol.
85
+ { name: "maxFee", type: "uint256" },
86
+ { name: "nonce", type: "uint256" },
87
+ { name: "expiry", type: "uint256" },
88
+ ],
89
+ };
90
+ // PR-13 (P1-4): Cancel sigs gained `nonce` + `expiry` so a captured signature
91
+ // can't be replayed forever. Drift here = backend rejects with 404. Mirrors
92
+ // the backend's `EIP712_CANCEL_TYPES` exactly.
93
+ export const CANCEL_TYPES = {
94
+ Cancel: [
95
+ { name: "maker", type: "address" },
96
+ { name: "orderId", type: "string" },
97
+ { name: "nonce", type: "uint256" },
98
+ { name: "expiry", type: "uint256" },
99
+ ],
100
+ };
101
+ const ZERO_ADDR = "0x0000000000000000000000000000000000000000";
102
+ /**
103
+ * Look up the pair config that owns a settlement contract. The single-
104
+ * settlement deployment makes this a degenerate one-entry array today, but
105
+ * code that hard-codes `pairs[0]` will silently break the moment a second
106
+ * settlement deploys. Always look up by address.
107
+ */
108
+ export function findPairBySettlement(pairs, settlementAddress) {
109
+ const target = settlementAddress.trim().toLowerCase();
110
+ if (!target || target === ZERO_ADDR)
111
+ return null;
112
+ return pairs.find((p) => p.settlementAddress.toLowerCase() === target) ?? null;
113
+ }
114
+ /**
115
+ * Resolve the EIP-712 domain a maker should sign against for a given
116
+ * settlement address. Uses `cfg.pairs[]` (preferred), falls back to the
117
+ * legacy top-level `cfg.eip712.domain` only if no pair matches and the
118
+ * legacy field's `verifyingContract` matches the requested settlement —
119
+ * otherwise throws so a misconfiguration produces a clear error instead
120
+ * of a silently-wrong signature.
121
+ */
122
+ export function domainForSettlement(cfg, settlementAddress) {
123
+ const pair = findPairBySettlement(cfg.pairs, settlementAddress);
124
+ if (pair)
125
+ return pair.eip712.domain;
126
+ const legacy = cfg.eip712?.domain;
127
+ if (legacy &&
128
+ legacy.verifyingContract.toLowerCase() === settlementAddress.trim().toLowerCase()) {
129
+ return legacy;
130
+ }
131
+ throw new Error(`No EIP-712 domain found for settlement ${settlementAddress}. ` +
132
+ `Available pairs: ${cfg.pairs.map((p) => p.pairId + "@" + p.settlementAddress).join(", ")}`);
133
+ }
134
+ /**
135
+ * Build typed-data for `POST /orders`. Pass the settlement address from
136
+ * the market the order is for; the helper picks the right domain.
137
+ */
138
+ export function buildOrderTypedData(args) {
139
+ return {
140
+ domain: domainForSettlement(args.cfg, args.settlementAddress),
141
+ types: ORDER_TYPES,
142
+ primaryType: "Order",
143
+ message: args.message,
144
+ };
145
+ }
146
+ /**
147
+ * Build typed-data for `DELETE /orders/:id`. The cancel signature uses
148
+ * the same per-pair domain as the original order — derive from the
149
+ * order's market composite key.
150
+ */
151
+ export function buildCancelTypedData(args) {
152
+ return {
153
+ domain: domainForSettlement(args.cfg, args.settlementAddress),
154
+ types: CANCEL_TYPES,
155
+ primaryType: "Cancel",
156
+ message: args.message,
157
+ };
158
+ }
159
+ const COMPOSITE_RE = /^(0x[a-fA-F0-9]{40})-(\d+)$/;
160
+ /** Parse a `<settlementAddress>-<marketId>` composite key. */
161
+ export function parseCompositeMarketKey(market) {
162
+ const m = market.trim().match(COMPOSITE_RE);
163
+ if (!m)
164
+ return null;
165
+ return {
166
+ settlementAddress: m[1].toLowerCase(),
167
+ marketId: m[2],
168
+ };
169
+ }
170
+ /* ───────────────────────── Price + stake helpers ───────────────────────── */
171
+ /**
172
+ * Cents → bps (1¢ = 100 bps; 50¢ = 5000 bps). Throws on out-of-range.
173
+ * Accepts integers or half-integer cents (e.g. 50.5 → 5050) for partial-cent
174
+ * pricing. UI typically clamps to whole cents.
175
+ */
176
+ export function centsToBps(cents) {
177
+ if (!Number.isFinite(cents) || cents <= 0 || cents >= 100) {
178
+ throw new Error(`cents out of range (0,100): ${cents}`);
179
+ }
180
+ return Math.round(cents * 100);
181
+ }
182
+ /** Bps → cents (5500 → 55). Inverse of `centsToBps`. */
183
+ export function bpsToCents(bps) {
184
+ if (!Number.isFinite(bps) || bps <= 0 || bps >= 10000) {
185
+ throw new Error(`bps out of range (0,10000): ${bps}`);
186
+ }
187
+ return bps / 100;
188
+ }
189
+ /** Atomic-USDT denomination of the documented $5–$500 stake window. */
190
+ export const MIN_STAKE_ATOMIC = 5000000n;
191
+ export const MAX_STAKE_ATOMIC = 500000000n;
192
+ /** Parse a USD string (e.g. "5", "5.50", "12.345") into atomic USDT.
193
+ * Rejects negatives + non-finite. */
194
+ export function parseStake(usd) {
195
+ const s = typeof usd === "number" ? String(usd) : usd.trim();
196
+ const n = Number(s);
197
+ if (!Number.isFinite(n) || n < 0)
198
+ throw new Error(`invalid stake: ${usd}`);
199
+ return BigInt(Math.round(n * 1e6));
200
+ }
201
+ /**
202
+ * Defense-in-depth stake clamp. Throws if `amountAtomic` is outside the
203
+ * documented `$5 ≤ stake ≤ $500` window. Frontend gates these too; backend
204
+ * enforces only the upper bound today (BUG-S2.1 — see PULSEPAIRS_BACKLOG).
205
+ * SDK callers should validate before signing so a bad stake never produces
206
+ * a signed payload at all.
207
+ */
208
+ export function assertStakeBounds(amountAtomic) {
209
+ if (amountAtomic < MIN_STAKE_ATOMIC) {
210
+ throw new Error(`stake below $${Number(MIN_STAKE_ATOMIC) / 1e6} minimum`);
211
+ }
212
+ if (amountAtomic > MAX_STAKE_ATOMIC) {
213
+ throw new Error(`stake above $${Number(MAX_STAKE_ATOMIC) / 1e6} maximum`);
214
+ }
215
+ }
216
+ /**
217
+ * Probability-weighted fee in atomic USDT. Mirrors the backend / contract
218
+ * formula. Returns the fee for `notionalAtomic` filled at `priceBps`.
219
+ */
220
+ export function feeAtomic(notionalAtomic, priceBps, cfg) {
221
+ const totalBps = BigInt(cfg.platformFeeBps + cfg.makerFeeBps);
222
+ if (cfg.feeModel === "probability-weighted" || cfg.feeModel == null) {
223
+ const p = BigInt(priceBps);
224
+ const weightNumerator = 4n * p * (10000n - p);
225
+ const effectiveBps = (totalBps * weightNumerator) / (10000n * 10000n);
226
+ return (notionalAtomic * effectiveBps) / 10000n;
227
+ }
228
+ return (notionalAtomic * totalBps) / 10000n;
229
+ }