@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/ws.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client with auto-reconnect + PR-19 auth handshake.
|
|
3
|
+
*
|
|
4
|
+
* Two paths:
|
|
5
|
+
*
|
|
6
|
+
* - `connectPublic(channels)` — public channels only (`markets`,
|
|
7
|
+
* `orderbook:*`, `trades:*`). Sends `{type:'subscribe', channels}`
|
|
8
|
+
* immediately on open, no auth.
|
|
9
|
+
*
|
|
10
|
+
* - `connectAuthed({ signAuth, channels })` — sends
|
|
11
|
+
* `{type:'auth', ...}` first, awaits `auth_ok`, then sends
|
|
12
|
+
* `{type:'subscribe', channels}`. Required for `orders:<wallet>` +
|
|
13
|
+
* `balance:<wallet>` (PR-19 gates these behind a verified WsAuth
|
|
14
|
+
* signature). On reconnect within the 24h token TTL, replays the
|
|
15
|
+
* cached token via `{type:'auth', token}` — only re-prompts the
|
|
16
|
+
* `signAuth` callback if the server rejects the cached token.
|
|
17
|
+
*
|
|
18
|
+
* Server message envelope (mirrors `src/ws/WebSocketServer.ts:235-293`):
|
|
19
|
+
*
|
|
20
|
+
* client → server { type: 'auth', wallet, timestamp, sessionId, signature }
|
|
21
|
+
* server → client { type: 'auth_ok', wallet, token, expiresAt }
|
|
22
|
+
* or { type: 'auth_error' } (no reason echoed — anti-probe)
|
|
23
|
+
*
|
|
24
|
+
* client → server { type: 'auth', token } (token replay)
|
|
25
|
+
* server → client { type: 'auth_ok', wallet, token } (token re-validated)
|
|
26
|
+
*
|
|
27
|
+
* State machine:
|
|
28
|
+
* idle → authenticating → subscribing → live
|
|
29
|
+
* → unauthed-failure (signAuth rejected)
|
|
30
|
+
*
|
|
31
|
+
* The legacy `connect(SubscribePayload)` API is retained as a deprecated
|
|
32
|
+
* shim that routes to `connectPublic` so existing public-only callers
|
|
33
|
+
* keep working without code changes. The `wallet` field on the legacy
|
|
34
|
+
* payload is now silently ignored (it was never authenticated and
|
|
35
|
+
* post-PR-19 the server drops private subs from anonymous clients).
|
|
36
|
+
*/
|
|
37
|
+
export class UpDownWsClient {
|
|
38
|
+
url;
|
|
39
|
+
onMessage;
|
|
40
|
+
ws = null;
|
|
41
|
+
reconnect = 0;
|
|
42
|
+
timer = null;
|
|
43
|
+
closed = false;
|
|
44
|
+
mode = null;
|
|
45
|
+
state = "idle";
|
|
46
|
+
/** Cached token + expiry from the last successful auth_ok. Replayed on
|
|
47
|
+
* reconnect; cleared if the server returns auth_error against it. */
|
|
48
|
+
cachedToken = null;
|
|
49
|
+
cachedTokenExpiresAt = 0;
|
|
50
|
+
constructor(url, onMessage) {
|
|
51
|
+
this.url = url;
|
|
52
|
+
this.onMessage = onMessage;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Subscribe to public channels only (`markets`, `orderbook:*`,
|
|
56
|
+
* `trades:*`). No signature required.
|
|
57
|
+
*/
|
|
58
|
+
connectPublic(channels) {
|
|
59
|
+
this.closed = false;
|
|
60
|
+
this.mode = { kind: "public", channels: channels.slice() };
|
|
61
|
+
this.openSocket();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Authenticate via the PR-19 EIP-712 handshake, then subscribe.
|
|
65
|
+
* Required for `orders:*` and `balance:*` channels.
|
|
66
|
+
*/
|
|
67
|
+
connectAuthed(opts) {
|
|
68
|
+
this.closed = false;
|
|
69
|
+
this.mode = {
|
|
70
|
+
kind: "authed",
|
|
71
|
+
opts: { ...opts, channels: opts.channels.slice() },
|
|
72
|
+
};
|
|
73
|
+
this.openSocket();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* @deprecated Use `connectPublic(channels)` or `connectAuthed({ ... })`.
|
|
77
|
+
* Pre-PR-19 the WS server accepted `{type:'subscribe', wallet}` and
|
|
78
|
+
* auto-attached `orders:<wallet>` to the anonymous client; that path
|
|
79
|
+
* is now closed. The `wallet` field on the payload is ignored — if
|
|
80
|
+
* the channel list contains private channels they will be silently
|
|
81
|
+
* dropped by the server because the connection is unauthenticated.
|
|
82
|
+
*/
|
|
83
|
+
connect(subscribe) {
|
|
84
|
+
this.connectPublic(subscribe.channels);
|
|
85
|
+
}
|
|
86
|
+
disconnect() {
|
|
87
|
+
this.closed = true;
|
|
88
|
+
this.state = "idle";
|
|
89
|
+
if (this.timer) {
|
|
90
|
+
clearTimeout(this.timer);
|
|
91
|
+
this.timer = null;
|
|
92
|
+
}
|
|
93
|
+
this.ws?.close();
|
|
94
|
+
this.ws = null;
|
|
95
|
+
}
|
|
96
|
+
openSocket() {
|
|
97
|
+
if (this.closed)
|
|
98
|
+
return;
|
|
99
|
+
const ws = new WebSocket(this.url);
|
|
100
|
+
this.ws = ws;
|
|
101
|
+
this.state = "idle";
|
|
102
|
+
ws.onopen = () => {
|
|
103
|
+
this.reconnect = 0;
|
|
104
|
+
void this.startHandshake();
|
|
105
|
+
};
|
|
106
|
+
ws.onmessage = (ev) => {
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(String(ev.data));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
void this.handleIncoming(parsed);
|
|
115
|
+
};
|
|
116
|
+
ws.onclose = () => {
|
|
117
|
+
if (this.closed)
|
|
118
|
+
return;
|
|
119
|
+
if (this.state === "unauthed-failure") {
|
|
120
|
+
// signAuth was rejected — don't reconnect into a hot loop.
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const attempt = this.reconnect++;
|
|
124
|
+
const delay = attempt > 12 ? 30_000 : Math.min(30_000, 1000 * 2 ** Math.min(attempt, 5));
|
|
125
|
+
this.timer = setTimeout(() => this.openSocket(), delay);
|
|
126
|
+
};
|
|
127
|
+
ws.onerror = () => ws.close();
|
|
128
|
+
}
|
|
129
|
+
async startHandshake() {
|
|
130
|
+
if (!this.mode || !this.ws)
|
|
131
|
+
return;
|
|
132
|
+
if (this.mode.kind === "public") {
|
|
133
|
+
this.state = "subscribing";
|
|
134
|
+
this.send({ type: "subscribe", channels: this.mode.channels });
|
|
135
|
+
// Public path: the `subscribed` reply marks us live, but for
|
|
136
|
+
// simplicity we mark live optimistically — onMessage delivery
|
|
137
|
+
// doesn't gate on it.
|
|
138
|
+
this.state = "live";
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Authed path. Try token replay first if we have a non-expired cache.
|
|
142
|
+
this.state = "authenticating";
|
|
143
|
+
if (this.cachedToken && this.cachedTokenExpiresAt > Date.now()) {
|
|
144
|
+
this.send({ type: "auth", token: this.cachedToken });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
await this.runSignAndSendAuth();
|
|
148
|
+
}
|
|
149
|
+
async runSignAndSendAuth() {
|
|
150
|
+
if (!this.mode || this.mode.kind !== "authed")
|
|
151
|
+
return;
|
|
152
|
+
let creds;
|
|
153
|
+
try {
|
|
154
|
+
creds = await this.mode.opts.signAuth();
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
this.state = "unauthed-failure";
|
|
158
|
+
this.mode.opts.onAuthError?.(err instanceof Error ? err.message : "signAuth threw");
|
|
159
|
+
this.ws?.close();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (creds.token) {
|
|
163
|
+
// Caller pre-cached a token — try replay first.
|
|
164
|
+
this.cachedToken = creds.token;
|
|
165
|
+
// Caller didn't supply expiry, so trust it for this attempt; if
|
|
166
|
+
// the server rejects we'll re-sign on auth_error.
|
|
167
|
+
this.cachedTokenExpiresAt = Math.max(this.cachedTokenExpiresAt, Date.now() + 60_000);
|
|
168
|
+
this.send({ type: "auth", token: creds.token });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.send({
|
|
172
|
+
type: "auth",
|
|
173
|
+
wallet: creds.wallet,
|
|
174
|
+
timestamp: creds.timestamp.toString(),
|
|
175
|
+
sessionId: creds.sessionId,
|
|
176
|
+
signature: creds.signature,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async handleIncoming(msg) {
|
|
180
|
+
if (msg.type === "auth_ok") {
|
|
181
|
+
const m = msg;
|
|
182
|
+
if (typeof m.token === "string") {
|
|
183
|
+
this.cachedToken = m.token;
|
|
184
|
+
// Server-supplied expiresAt is the source of truth (24h TTL).
|
|
185
|
+
// Fall back to TTL-from-now if the server omits it (token
|
|
186
|
+
// replay path doesn't echo expiresAt).
|
|
187
|
+
this.cachedTokenExpiresAt =
|
|
188
|
+
typeof m.expiresAt === "number"
|
|
189
|
+
? m.expiresAt
|
|
190
|
+
: Math.max(this.cachedTokenExpiresAt, Date.now() + 23 * 60 * 60 * 1000);
|
|
191
|
+
}
|
|
192
|
+
if (this.mode?.kind === "authed") {
|
|
193
|
+
this.state = "subscribing";
|
|
194
|
+
this.send({ type: "subscribe", channels: this.mode.opts.channels });
|
|
195
|
+
this.state = "live";
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (msg.type === "auth_error") {
|
|
200
|
+
// Cached token is bad. Drop it and try a fresh sign — but only
|
|
201
|
+
// ONCE per connect cycle to avoid a sign loop if the user's
|
|
202
|
+
// wallet keeps rejecting.
|
|
203
|
+
if (this.cachedToken) {
|
|
204
|
+
this.cachedToken = null;
|
|
205
|
+
this.cachedTokenExpiresAt = 0;
|
|
206
|
+
await this.runSignAndSendAuth();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Fresh signature was rejected — terminal failure.
|
|
210
|
+
this.state = "unauthed-failure";
|
|
211
|
+
if (this.mode?.kind === "authed") {
|
|
212
|
+
this.mode.opts.onAuthError?.("auth_error");
|
|
213
|
+
}
|
|
214
|
+
this.ws?.close();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this.onMessage(msg);
|
|
218
|
+
}
|
|
219
|
+
send(payload) {
|
|
220
|
+
if (this.ws && this.ws.readyState === 1 /* OPEN */) {
|
|
221
|
+
this.ws.send(JSON.stringify(payload));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pulsepairs/sdk",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Standalone SDK for the UpDown (PulsePairs) up/down prediction markets — matcher REST/WS client, EIP-712 order signing, trade-math, and Alchemy Account Kit (smart-account) order signing for rain.trade integration.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"scripts": {
|
|
22
|
+
"clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
|
|
23
|
+
"build": "npm run clean && tsc",
|
|
24
|
+
"test": "node scripts/eip712-golden.test.mjs",
|
|
25
|
+
"prepublishOnly": "npm run build && npm test",
|
|
26
|
+
"example:taker": "npx tsx examples/simple-taker.ts",
|
|
27
|
+
"example:maker": "npx tsx examples/simple-maker.ts",
|
|
28
|
+
"example:dmm": "npx tsx examples/full-dmm-bot.ts"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@aa-sdk/core": "^4.88.0",
|
|
32
|
+
"@account-kit/infra": "^4.88.0",
|
|
33
|
+
"@account-kit/smart-contracts": "^4.88.0",
|
|
34
|
+
"@account-kit/wallet-client": "^4.88.0",
|
|
35
|
+
"viem": "^2.21.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"@account-kit/smart-contracts": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"@account-kit/wallet-client": {
|
|
42
|
+
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"@account-kit/infra": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"@aa-sdk/core": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.10.0",
|
|
53
|
+
"typescript": "^5.9.3",
|
|
54
|
+
"viem": "^2.47.0"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18"
|
|
61
|
+
}
|
|
62
|
+
}
|