@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.
- package/README.md +264 -0
- package/dist/accountKit.d.ts +265 -0
- package/dist/accountKit.js +638 -0
- package/dist/approve.d.ts +46 -0
- package/dist/approve.js +69 -0
- package/dist/auth.d.ts +77 -0
- package/dist/auth.js +76 -0
- package/dist/eip712.d.ts +216 -0
- package/dist/eip712.js +229 -0
- package/dist/http.d.ts +46 -0
- package/dist/http.js +120 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +15 -0
- package/dist/types.d.ts +235 -0
- package/dist/types.js +20 -0
- package/dist/ws.d.ts +118 -0
- package/dist/ws.js +224 -0
- package/package.json +62 -0
package/dist/approve.js
ADDED
|
@@ -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
|
+
};
|
package/dist/eip712.d.ts
ADDED
|
@@ -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
|
+
}
|