@sentico-labs/sdk 0.1.0-preview.1
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/CHANGELOG.md +13 -0
- package/README.md +190 -0
- package/dist/examples/market-maker-batch.d.ts +1 -0
- package/dist/examples/market-maker-batch.js +35 -0
- package/dist/examples/private-user-stream.d.ts +1 -0
- package/dist/examples/private-user-stream.js +21 -0
- package/dist/examples/public-market-stream.d.ts +1 -0
- package/dist/examples/public-market-stream.js +18 -0
- package/dist/examples/recover-from-gap.d.ts +1 -0
- package/dist/examples/recover-from-gap.js +35 -0
- package/dist/examples/submit-order.d.ts +1 -0
- package/dist/examples/submit-order.js +45 -0
- package/dist/src/client.d.ts +15 -0
- package/dist/src/client.js +24 -0
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +41 -0
- package/dist/src/errors.d.ts +38 -0
- package/dist/src/errors.js +106 -0
- package/dist/src/http.d.ts +88 -0
- package/dist/src/http.js +313 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +8 -0
- package/dist/src/mm.d.ts +32 -0
- package/dist/src/mm.js +74 -0
- package/dist/src/signing.d.ts +101 -0
- package/dist/src/signing.js +149 -0
- package/dist/src/types.d.ts +156 -0
- package/dist/src/types.js +1 -0
- package/dist/src/ws.d.ts +13 -0
- package/dist/src/ws.js +78 -0
- package/package.json +37 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0-preview.1
|
|
4
|
+
|
|
5
|
+
- Converts the SDK into a publishable preview package.
|
|
6
|
+
- Adds `SenticoreClient` with explicit `public`, `trading`, `orderEntry`, `raw`, and `ws` planes.
|
|
7
|
+
- Adds typed `ApiResponse`, rate-limit metadata, request-id propagation, timeouts, and retries.
|
|
8
|
+
- Adds typed API errors for auth, validation, conflict, rate-limit, payload-too-large, and server failures.
|
|
9
|
+
- Adds Institutional Order Entry batch encoding for the current cold JSON payload over
|
|
10
|
+
`application/x-senticore-order-entry-batch`.
|
|
11
|
+
- Keeps `client.mm`, `mmApiKey`, and `SenticoreMarketMakerClient` as deprecated compatibility aliases.
|
|
12
|
+
- Keeps legacy `SenticoreHttpClient` and `SenticoreWsClient` exports for existing examples.
|
|
13
|
+
- Adds Node test coverage and npm packaging verification.
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Senticore TypeScript SDK
|
|
2
|
+
|
|
3
|
+
Official TypeScript SDK preview for Senticore HTTP, WebSocket, and
|
|
4
|
+
Institutional Order Entry.
|
|
5
|
+
|
|
6
|
+
Package name: `@sentico-labs/sdk`
|
|
7
|
+
Preview version: `0.1.0-preview.1`
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @sentico-labs/sdk@0.1.0-preview.1
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For local development from the repository:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm ci
|
|
19
|
+
npm run verify
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { SenticoreClient } from "@sentico-labs/sdk";
|
|
26
|
+
|
|
27
|
+
const client = new SenticoreClient({
|
|
28
|
+
publicHttpBaseUrl: "https://api.sentico-labs.xyz",
|
|
29
|
+
tradingHttpBaseUrl: "https://api.sentico-labs.xyz",
|
|
30
|
+
publicWsUrl: "wss://api.sentico-labs.xyz/api/v1/ws/public",
|
|
31
|
+
privateWsUrl: "wss://api.sentico-labs.xyz/api/v1/ws/private/{account}",
|
|
32
|
+
orderEntryHttpBaseUrl: "https://api.sentico-labs.xyz",
|
|
33
|
+
orderEntryBinaryPath: "/api/order-entry/binary",
|
|
34
|
+
bearerToken: process.env.SENTICORE_BEARER_TOKEN,
|
|
35
|
+
orderEntryApiKey: process.env.SENTICORE_ORDER_ENTRY_API_KEY
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const markets = await client.public.listMarkets();
|
|
39
|
+
console.log(markets.data, markets.requestId, markets.rateLimit);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Client Planes
|
|
43
|
+
|
|
44
|
+
- `client.public`: anonymous public HTTP reads.
|
|
45
|
+
- `client.trading`: authenticated private reads and trading writes.
|
|
46
|
+
- `client.orderEntry`: Institutional Order Entry.
|
|
47
|
+
- `client.mm`: deprecated alias for `client.orderEntry`.
|
|
48
|
+
- `client.raw`: escape hatch for endpoints not promoted to stable helpers yet.
|
|
49
|
+
- `client.ws`: public and private WebSocket helpers.
|
|
50
|
+
|
|
51
|
+
Legacy `SenticoreHttpClient` and `SenticoreWsClient` exports are still present
|
|
52
|
+
for existing integrations. New integrations should use `SenticoreClient`.
|
|
53
|
+
|
|
54
|
+
## Public Market Data
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const markets = await client.public.listMarkets();
|
|
58
|
+
const book = await client.public.getMarketOrderbook(1, { book: "YES", depth: 20 });
|
|
59
|
+
const trades = await client.public.listMarketTrades(1, { limit: 50 });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Trading HTTP
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const account = "0x00000000000000000000000000000000000000d3";
|
|
66
|
+
const nonce = await client.trading.reserveNonce(account, {
|
|
67
|
+
count: 1,
|
|
68
|
+
ttlMs: 30_000,
|
|
69
|
+
idempotencyKey: "nonce-1"
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const signedAction = {
|
|
73
|
+
payload: {
|
|
74
|
+
account,
|
|
75
|
+
nonce: nonce.data.startNonce,
|
|
76
|
+
nonceReservationId: nonce.data.reservationId,
|
|
77
|
+
ts: Date.now(),
|
|
78
|
+
action: {
|
|
79
|
+
PlaceOrder: {
|
|
80
|
+
market: 1,
|
|
81
|
+
book: "YES",
|
|
82
|
+
side: "Bid",
|
|
83
|
+
price: 500000,
|
|
84
|
+
qty: 100000,
|
|
85
|
+
is_market: false,
|
|
86
|
+
reduce_only: false,
|
|
87
|
+
time_in_force: null,
|
|
88
|
+
expires_at: null,
|
|
89
|
+
stp_mode: null
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
signature: {
|
|
94
|
+
scheme: "EcdsaSecp256k1",
|
|
95
|
+
bytes: new Uint8Array(65)
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const accepted = await client.trading.submitSignedAction(signedAction, {
|
|
100
|
+
idempotencyKey: "client-order-1"
|
|
101
|
+
});
|
|
102
|
+
console.log(accepted.data);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Institutional Order Entry
|
|
106
|
+
|
|
107
|
+
The current Senticore cold binary path uses compact JSON bytes on the wire:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{"version":1,"actions":[...],"idempotencyKey":"..."}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The SDK sends this payload with
|
|
114
|
+
`Content-Type: application/x-senticore-order-entry-batch` and
|
|
115
|
+
`X-Senticore-Order-Entry-Key`.
|
|
116
|
+
|
|
117
|
+
The canonical private-beta endpoint is `POST /api/order-entry/binary`. The live
|
|
118
|
+
edge maps that alias to the trading plane's `/api/v1/mm/orders/batch.bin`
|
|
119
|
+
handler. If you connect directly to a trading-plane service without the edge
|
|
120
|
+
alias, set `orderEntryBinaryPath: "/api/v1/mm/orders/batch.bin"`.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const response = await client.orderEntry.submitActions([signedAction], {
|
|
124
|
+
idempotencyKey: "order-entry-batch-1",
|
|
125
|
+
responseMode: "summary"
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
console.log(response.data.ok, response.data.seqs, response.data.ackMode);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Low-latency clients that already produce venue payload bytes can call:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
await client.orderEntry.submitEncoded(encodedPayload, {
|
|
135
|
+
idempotencyKey: "order-entry-raw-1"
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## WebSocket
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const socket = client.ws.connectPublic((frame) => {
|
|
143
|
+
console.log(frame);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
socket.addEventListener("open", () => {
|
|
147
|
+
client.ws.subscribe(socket, {
|
|
148
|
+
type: "l2Book",
|
|
149
|
+
marketId: 1,
|
|
150
|
+
depth: 20
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Private user streams:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const token = await client.trading.issuePrivateWsToken(account, {
|
|
159
|
+
ttlMs: 60_000
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const socket = client.ws.connectPrivate(account, token.data.token, (frame) => {
|
|
163
|
+
console.log(frame);
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Replay gaps raise `ReplayGapError` during frame parsing. Production clients
|
|
168
|
+
should resync from an HTTP snapshot and resubscribe from a known sequence.
|
|
169
|
+
|
|
170
|
+
## Error Handling
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { RateLimitError, SenticoreApiError } from "@sentico-labs/sdk";
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await client.public.listMarkets();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error instanceof RateLimitError) {
|
|
179
|
+
console.log("retry after", error.retryAfter);
|
|
180
|
+
} else if (error instanceof SenticoreApiError) {
|
|
181
|
+
console.log(error.status, error.requestId, error.payload);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Preview Compatibility
|
|
187
|
+
|
|
188
|
+
`0.1.0-preview.1` is not a v1 stability promise. The stable preview shape is the
|
|
189
|
+
top-level namespace split: `public`, `trading`, `orderEntry`, `raw`, and `ws`.
|
|
190
|
+
`mm` remains as a deprecated compatibility alias.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { SenticoreClient } from "../src/index.js";
|
|
2
|
+
const account = process.env.SENTICORE_ACCOUNT;
|
|
3
|
+
const client = new SenticoreClient({
|
|
4
|
+
publicHttpBaseUrl: process.env.SENTICORE_PUBLIC_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
5
|
+
tradingHttpBaseUrl: process.env.SENTICORE_TRADING_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
6
|
+
publicWsUrl: process.env.SENTICORE_PUBLIC_WS_URL ?? "wss://api.sentico-labs.xyz/api/v1/ws/public",
|
|
7
|
+
privateWsUrl: process.env.SENTICORE_PRIVATE_WS_URL ??
|
|
8
|
+
"wss://api.sentico-labs.xyz/api/v1/ws/private/{account}",
|
|
9
|
+
orderEntryHttpBaseUrl: process.env.SENTICORE_ORDER_ENTRY_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
10
|
+
orderEntryBinaryPath: process.env.SENTICORE_ORDER_ENTRY_BINARY_PATH ?? "/api/order-entry/binary",
|
|
11
|
+
orderEntryApiKey: process.env.SENTICORE_ORDER_ENTRY_API_KEY ?? process.env.SENTICORE_MM_API_KEY
|
|
12
|
+
});
|
|
13
|
+
const action = {
|
|
14
|
+
payload: {
|
|
15
|
+
account,
|
|
16
|
+
nonce: Number(process.env.SENTICORE_NONCE ?? "0"),
|
|
17
|
+
nonceReservationId: null,
|
|
18
|
+
ts: Date.now(),
|
|
19
|
+
action: {
|
|
20
|
+
Cancel: {
|
|
21
|
+
order_id: process.env.SENTICORE_ORDER_ID ??
|
|
22
|
+
"0x0101010101010101010101010101010101010101010101010101010101010101"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
signature: {
|
|
27
|
+
scheme: "EcdsaSecp256k1",
|
|
28
|
+
bytes: new Uint8Array(65)
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const response = await client.orderEntry.submitActions([action], {
|
|
32
|
+
idempotencyKey: `order-entry-${Date.now()}`,
|
|
33
|
+
responseMode: "summary"
|
|
34
|
+
});
|
|
35
|
+
console.log(response.data);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SenticoreClient } from "../src/index.js";
|
|
2
|
+
const account = process.env.SENTICORE_ACCOUNT;
|
|
3
|
+
const client = new SenticoreClient({
|
|
4
|
+
publicHttpBaseUrl: process.env.SENTICORE_PUBLIC_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
5
|
+
tradingHttpBaseUrl: process.env.SENTICORE_TRADING_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
6
|
+
publicWsUrl: process.env.SENTICORE_PUBLIC_WS_URL ?? "wss://api.sentico-labs.xyz/api/v1/ws/public",
|
|
7
|
+
privateWsUrl: process.env.SENTICORE_PRIVATE_WS_URL ??
|
|
8
|
+
"wss://api.sentico-labs.xyz/api/v1/ws/private/{account}",
|
|
9
|
+
bearerToken: process.env.SENTICORE_BEARER_TOKEN
|
|
10
|
+
});
|
|
11
|
+
const token = await client.trading.issuePrivateWsToken(account, {
|
|
12
|
+
ttlMs: 120_000,
|
|
13
|
+
clientId: "algo-desk-1"
|
|
14
|
+
});
|
|
15
|
+
const socket = client.ws.connectPrivate(account, token.data.token, (frame) => {
|
|
16
|
+
console.log("private frame", frame);
|
|
17
|
+
});
|
|
18
|
+
socket.addEventListener("open", () => {
|
|
19
|
+
client.ws.subscribe(socket, { type: "account" });
|
|
20
|
+
client.ws.subscribe(socket, { type: "userFills" });
|
|
21
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SenticoreClient } from "../src/index.js";
|
|
2
|
+
const client = new SenticoreClient({
|
|
3
|
+
publicHttpBaseUrl: process.env.SENTICORE_PUBLIC_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
4
|
+
tradingHttpBaseUrl: process.env.SENTICORE_TRADING_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
5
|
+
publicWsUrl: process.env.SENTICORE_PUBLIC_WS_URL ?? "wss://api.sentico-labs.xyz/api/v1/ws/public",
|
|
6
|
+
privateWsUrl: process.env.SENTICORE_PRIVATE_WS_URL ??
|
|
7
|
+
"wss://api.sentico-labs.xyz/api/v1/ws/private/{account}"
|
|
8
|
+
});
|
|
9
|
+
const socket = client.ws.connectPublic((frame) => {
|
|
10
|
+
console.log("public frame", frame);
|
|
11
|
+
});
|
|
12
|
+
socket.addEventListener("open", () => {
|
|
13
|
+
client.ws.subscribe(socket, {
|
|
14
|
+
type: "marketSnapshot",
|
|
15
|
+
marketId: 1,
|
|
16
|
+
cursor: 0
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ReplayGapError, SenticoreClient } from "../src/index.js";
|
|
2
|
+
const client = new SenticoreClient({
|
|
3
|
+
publicHttpBaseUrl: process.env.SENTICORE_PUBLIC_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
4
|
+
tradingHttpBaseUrl: process.env.SENTICORE_TRADING_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
5
|
+
publicWsUrl: process.env.SENTICORE_PUBLIC_WS_URL ?? "wss://api.sentico-labs.xyz/api/v1/ws/public",
|
|
6
|
+
privateWsUrl: process.env.SENTICORE_PRIVATE_WS_URL ??
|
|
7
|
+
"wss://api.sentico-labs.xyz/api/v1/ws/private/{account}"
|
|
8
|
+
});
|
|
9
|
+
let lastCursor = 0;
|
|
10
|
+
function handle(frame) {
|
|
11
|
+
if (typeof frame.seq === "number") {
|
|
12
|
+
lastCursor = frame.seq;
|
|
13
|
+
}
|
|
14
|
+
console.log(frame);
|
|
15
|
+
}
|
|
16
|
+
const socket = client.ws.connectPublic((frame) => {
|
|
17
|
+
try {
|
|
18
|
+
handle(frame);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof ReplayGapError) {
|
|
22
|
+
void client.public.getMarketSnapshot(1, { depth: 20 }).then((snapshot) => {
|
|
23
|
+
console.log("resynced snapshot", snapshot.data);
|
|
24
|
+
lastCursor = 0;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
socket.addEventListener("open", () => {
|
|
30
|
+
client.ws.subscribe(socket, {
|
|
31
|
+
type: "marketSnapshot",
|
|
32
|
+
marketId: 1,
|
|
33
|
+
cursor: lastCursor
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { SenticoreClient } from "../src/index.js";
|
|
2
|
+
const account = process.env.SENTICORE_ACCOUNT;
|
|
3
|
+
const client = new SenticoreClient({
|
|
4
|
+
publicHttpBaseUrl: process.env.SENTICORE_PUBLIC_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
5
|
+
tradingHttpBaseUrl: process.env.SENTICORE_TRADING_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
6
|
+
publicWsUrl: process.env.SENTICORE_PUBLIC_WS_URL ?? "wss://api.sentico-labs.xyz/api/v1/ws/public",
|
|
7
|
+
privateWsUrl: process.env.SENTICORE_PRIVATE_WS_URL ??
|
|
8
|
+
"wss://api.sentico-labs.xyz/api/v1/ws/private/{account}",
|
|
9
|
+
bearerToken: process.env.SENTICORE_BEARER_TOKEN
|
|
10
|
+
});
|
|
11
|
+
const nonce = await client.trading.reserveNonce(account, {
|
|
12
|
+
count: 1,
|
|
13
|
+
ttlMs: 30_000,
|
|
14
|
+
idempotencyKey: `nonce-${Date.now()}`
|
|
15
|
+
});
|
|
16
|
+
const signedAction = {
|
|
17
|
+
payload: {
|
|
18
|
+
account,
|
|
19
|
+
nonce: Number(nonce.data.startNonce),
|
|
20
|
+
nonceReservationId: nonce.data.reservationId,
|
|
21
|
+
ts: Date.now(),
|
|
22
|
+
action: {
|
|
23
|
+
PlaceOrder: {
|
|
24
|
+
market: 1,
|
|
25
|
+
book: "YES",
|
|
26
|
+
side: "Bid",
|
|
27
|
+
price: 500000,
|
|
28
|
+
qty: 100000,
|
|
29
|
+
is_market: false,
|
|
30
|
+
reduce_only: false,
|
|
31
|
+
time_in_force: null,
|
|
32
|
+
expires_at: null,
|
|
33
|
+
stp_mode: null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
signature: {
|
|
38
|
+
scheme: "EcdsaSecp256k1",
|
|
39
|
+
bytes: new Uint8Array(65)
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const response = await client.trading.submitSignedAction(signedAction, {
|
|
43
|
+
idempotencyKey: `order-${Date.now()}`
|
|
44
|
+
});
|
|
45
|
+
console.log(response.data);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SenticorePublicHttpClient, SenticoreRawHttpClient, SenticoreTradingHttpClient } from "./http.js";
|
|
2
|
+
import { SenticoreOrderEntryClient } from "./mm.js";
|
|
3
|
+
import type { NormalizedSenticoreSdkConfig, SenticoreSdkConfig } from "./types.js";
|
|
4
|
+
import { SenticoreWsClient } from "./ws.js";
|
|
5
|
+
export declare class SenticoreClient {
|
|
6
|
+
readonly config: NormalizedSenticoreSdkConfig;
|
|
7
|
+
readonly public: SenticorePublicHttpClient;
|
|
8
|
+
readonly trading: SenticoreTradingHttpClient;
|
|
9
|
+
readonly orderEntry: SenticoreOrderEntryClient;
|
|
10
|
+
/** @deprecated Use orderEntry. */
|
|
11
|
+
readonly mm: SenticoreOrderEntryClient;
|
|
12
|
+
readonly raw: SenticoreRawHttpClient;
|
|
13
|
+
readonly ws: SenticoreWsClient;
|
|
14
|
+
constructor(config: SenticoreSdkConfig);
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { normalizeConfig } from "./config.js";
|
|
2
|
+
import { SenticoreHttpTransport, SenticorePublicHttpClient, SenticoreRawHttpClient, SenticoreTradingHttpClient } from "./http.js";
|
|
3
|
+
import { SenticoreOrderEntryClient } from "./mm.js";
|
|
4
|
+
import { SenticoreWsClient } from "./ws.js";
|
|
5
|
+
export class SenticoreClient {
|
|
6
|
+
config;
|
|
7
|
+
public;
|
|
8
|
+
trading;
|
|
9
|
+
orderEntry;
|
|
10
|
+
/** @deprecated Use orderEntry. */
|
|
11
|
+
mm;
|
|
12
|
+
raw;
|
|
13
|
+
ws;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = normalizeConfig(config);
|
|
16
|
+
const transport = new SenticoreHttpTransport(this.config);
|
|
17
|
+
this.public = new SenticorePublicHttpClient(transport);
|
|
18
|
+
this.trading = new SenticoreTradingHttpClient(transport);
|
|
19
|
+
this.orderEntry = new SenticoreOrderEntryClient(transport);
|
|
20
|
+
this.mm = this.orderEntry;
|
|
21
|
+
this.raw = new SenticoreRawHttpClient(transport);
|
|
22
|
+
this.ws = new SenticoreWsClient(this.config);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
2
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
3
|
+
const DEFAULT_RETRY_BACKOFF_MS = 250;
|
|
4
|
+
const DEFAULT_ORDER_ENTRY_BINARY_PATH = "/api/order-entry/binary";
|
|
5
|
+
const DEFAULT_USER_AGENT = "@sentico-labs/sdk/0.1.0-preview.1";
|
|
6
|
+
export function normalizeConfig(config) {
|
|
7
|
+
const fetchImpl = config.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
8
|
+
if (!fetchImpl) {
|
|
9
|
+
throw new Error("Senticore SDK requires fetch or config.fetch");
|
|
10
|
+
}
|
|
11
|
+
const WebSocketCtor = config.WebSocketCtor ?? globalThis.WebSocket;
|
|
12
|
+
const orderEntryHttpBaseUrl = trimTrailingSlash(config.orderEntryHttpBaseUrl ?? config.mmHttpBaseUrl ?? config.tradingHttpBaseUrl);
|
|
13
|
+
const orderEntryBinaryPath = normalizePath(config.orderEntryBinaryPath ?? config.mmBinaryPath ?? DEFAULT_ORDER_ENTRY_BINARY_PATH);
|
|
14
|
+
const orderEntryApiKey = config.orderEntryApiKey ?? config.mmApiKey;
|
|
15
|
+
return {
|
|
16
|
+
publicHttpBaseUrl: trimTrailingSlash(config.publicHttpBaseUrl),
|
|
17
|
+
tradingHttpBaseUrl: trimTrailingSlash(config.tradingHttpBaseUrl),
|
|
18
|
+
publicWsUrl: config.publicWsUrl,
|
|
19
|
+
privateWsUrl: config.privateWsUrl,
|
|
20
|
+
orderEntryHttpBaseUrl,
|
|
21
|
+
orderEntryBinaryPath,
|
|
22
|
+
orderEntryApiKey,
|
|
23
|
+
mmHttpBaseUrl: orderEntryHttpBaseUrl,
|
|
24
|
+
mmBinaryPath: orderEntryBinaryPath,
|
|
25
|
+
bearerToken: config.bearerToken,
|
|
26
|
+
mmApiKey: orderEntryApiKey,
|
|
27
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
28
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
29
|
+
retryBackoffMs: config.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS,
|
|
30
|
+
userAgent: config.userAgent ?? DEFAULT_USER_AGENT,
|
|
31
|
+
headers: config.headers ?? {},
|
|
32
|
+
fetch: fetchImpl,
|
|
33
|
+
WebSocketCtor
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function trimTrailingSlash(value) {
|
|
37
|
+
return value.replace(/\/+$/, "");
|
|
38
|
+
}
|
|
39
|
+
function normalizePath(path) {
|
|
40
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare class SenticoreError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class TransportError extends SenticoreError {
|
|
5
|
+
readonly cause?: unknown;
|
|
6
|
+
constructor(message: string, cause?: unknown);
|
|
7
|
+
}
|
|
8
|
+
export declare class ReplayGapError extends SenticoreError {
|
|
9
|
+
readonly frame?: unknown;
|
|
10
|
+
constructor(message: string, frame?: unknown);
|
|
11
|
+
}
|
|
12
|
+
export declare class SenticoreApiError extends SenticoreError {
|
|
13
|
+
readonly status: number;
|
|
14
|
+
readonly payload: unknown;
|
|
15
|
+
readonly headers: Headers;
|
|
16
|
+
readonly requestId?: string | undefined;
|
|
17
|
+
constructor(message: string, status: number, payload: unknown, headers: Headers, requestId?: string | undefined);
|
|
18
|
+
}
|
|
19
|
+
export declare class AuthenticationError extends SenticoreApiError {
|
|
20
|
+
}
|
|
21
|
+
export declare class PermissionDeniedError extends SenticoreApiError {
|
|
22
|
+
}
|
|
23
|
+
export declare class NotFoundError extends SenticoreApiError {
|
|
24
|
+
}
|
|
25
|
+
export declare class ConflictError extends SenticoreApiError {
|
|
26
|
+
}
|
|
27
|
+
export declare class ValidationError extends SenticoreApiError {
|
|
28
|
+
}
|
|
29
|
+
export declare class PayloadTooLargeError extends SenticoreApiError {
|
|
30
|
+
}
|
|
31
|
+
export declare class ServerError extends SenticoreApiError {
|
|
32
|
+
}
|
|
33
|
+
export declare class RateLimitError extends SenticoreApiError {
|
|
34
|
+
readonly retryAfter?: number | undefined;
|
|
35
|
+
constructor(message: string, status: number, payload: unknown, headers: Headers, requestId?: string, retryAfter?: number | undefined);
|
|
36
|
+
}
|
|
37
|
+
export declare function requestIdFromHeaders(headers: Headers): string | undefined;
|
|
38
|
+
export declare function apiErrorFromStatus(response: Response, payload: unknown): SenticoreApiError;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export class SenticoreError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = new.target.name;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class TransportError extends SenticoreError {
|
|
8
|
+
cause;
|
|
9
|
+
constructor(message, cause) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.cause = cause;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class ReplayGapError extends SenticoreError {
|
|
15
|
+
frame;
|
|
16
|
+
constructor(message, frame) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.frame = frame;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class SenticoreApiError extends SenticoreError {
|
|
22
|
+
status;
|
|
23
|
+
payload;
|
|
24
|
+
headers;
|
|
25
|
+
requestId;
|
|
26
|
+
constructor(message, status, payload, headers, requestId) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.status = status;
|
|
29
|
+
this.payload = payload;
|
|
30
|
+
this.headers = headers;
|
|
31
|
+
this.requestId = requestId;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class AuthenticationError extends SenticoreApiError {
|
|
35
|
+
}
|
|
36
|
+
export class PermissionDeniedError extends SenticoreApiError {
|
|
37
|
+
}
|
|
38
|
+
export class NotFoundError extends SenticoreApiError {
|
|
39
|
+
}
|
|
40
|
+
export class ConflictError extends SenticoreApiError {
|
|
41
|
+
}
|
|
42
|
+
export class ValidationError extends SenticoreApiError {
|
|
43
|
+
}
|
|
44
|
+
export class PayloadTooLargeError extends SenticoreApiError {
|
|
45
|
+
}
|
|
46
|
+
export class ServerError extends SenticoreApiError {
|
|
47
|
+
}
|
|
48
|
+
export class RateLimitError extends SenticoreApiError {
|
|
49
|
+
retryAfter;
|
|
50
|
+
constructor(message, status, payload, headers, requestId, retryAfter) {
|
|
51
|
+
super(message, status, payload, headers, requestId);
|
|
52
|
+
this.retryAfter = retryAfter;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function requestIdFromHeaders(headers) {
|
|
56
|
+
return (headers.get("x-request-id") ??
|
|
57
|
+
headers.get("x-correlation-id") ??
|
|
58
|
+
headers.get("x-senticore-request-id") ??
|
|
59
|
+
undefined);
|
|
60
|
+
}
|
|
61
|
+
export function apiErrorFromStatus(response, payload) {
|
|
62
|
+
const requestId = requestIdFromHeaders(response.headers);
|
|
63
|
+
const message = messageFromPayload(response.status, payload);
|
|
64
|
+
if (response.status === 401) {
|
|
65
|
+
return new AuthenticationError(message, response.status, payload, response.headers, requestId);
|
|
66
|
+
}
|
|
67
|
+
if (response.status === 403) {
|
|
68
|
+
return new PermissionDeniedError(message, response.status, payload, response.headers, requestId);
|
|
69
|
+
}
|
|
70
|
+
if (response.status === 404) {
|
|
71
|
+
return new NotFoundError(message, response.status, payload, response.headers, requestId);
|
|
72
|
+
}
|
|
73
|
+
if (response.status === 409) {
|
|
74
|
+
return new ConflictError(message, response.status, payload, response.headers, requestId);
|
|
75
|
+
}
|
|
76
|
+
if (response.status === 400 || response.status === 422) {
|
|
77
|
+
return new ValidationError(message, response.status, payload, response.headers, requestId);
|
|
78
|
+
}
|
|
79
|
+
if (response.status === 413) {
|
|
80
|
+
return new PayloadTooLargeError(message, response.status, payload, response.headers, requestId);
|
|
81
|
+
}
|
|
82
|
+
if (response.status === 429) {
|
|
83
|
+
return new RateLimitError(message, response.status, payload, response.headers, requestId, parseRetryAfter(response.headers.get("retry-after")));
|
|
84
|
+
}
|
|
85
|
+
if (response.status >= 500) {
|
|
86
|
+
return new ServerError(message, response.status, payload, response.headers, requestId);
|
|
87
|
+
}
|
|
88
|
+
return new SenticoreApiError(message, response.status, payload, response.headers, requestId);
|
|
89
|
+
}
|
|
90
|
+
function messageFromPayload(status, payload) {
|
|
91
|
+
if (payload && typeof payload === "object") {
|
|
92
|
+
const record = payload;
|
|
93
|
+
for (const key of ["error", "message", "detail"]) {
|
|
94
|
+
if (typeof record[key] === "string" && record[key]) {
|
|
95
|
+
return `Senticore HTTP ${status}: ${record[key]}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return `Senticore HTTP ${status}`;
|
|
100
|
+
}
|
|
101
|
+
function parseRetryAfter(value) {
|
|
102
|
+
if (!value)
|
|
103
|
+
return undefined;
|
|
104
|
+
const parsed = Number(value);
|
|
105
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
106
|
+
}
|