@sentico-labs/sdk 0.1.0-preview.1 → 0.1.0-preview.2
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 +12 -5
- package/README.md +68 -45
- package/dist/examples/market-maker-batch.js +21 -21
- package/dist/examples/submit-order.js +29 -34
- package/dist/src/config.js +1 -1
- package/dist/src/signing.d.ts +36 -5
- package/dist/src/signing.js +221 -11
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.1.0-preview.
|
|
4
|
-
|
|
5
|
-
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0-preview.2
|
|
4
|
+
|
|
5
|
+
- Adds local secp256k1 action signing helpers (`signAction`, `signActionHash`) for direct Trading HTTP and Order Entry submission.
|
|
6
|
+
- Adds deterministic order-id helpers for place orders and quote-replace child legs.
|
|
7
|
+
- Replaces placeholder example signatures with submit-ready locally signed actions.
|
|
8
|
+
- Rejects submit-ready JSON payloads that would lose integer precision in JavaScript.
|
|
9
|
+
|
|
10
|
+
## 0.1.0-preview.1
|
|
11
|
+
|
|
12
|
+
- Converts the SDK into a publishable preview package.
|
|
6
13
|
- Adds `SenticoreClient` with explicit `public`, `trading`, `orderEntry`, `raw`, and `ws` planes.
|
|
7
14
|
- Adds typed `ApiResponse`, rate-limit metadata, request-id propagation, timeouts, and retries.
|
|
8
15
|
- Adds typed API errors for auth, validation, conflict, rate-limit, payload-too-large, and server failures.
|
package/README.md
CHANGED
|
@@ -4,12 +4,12 @@ Official TypeScript SDK preview for Senticore HTTP, WebSocket, and
|
|
|
4
4
|
Institutional Order Entry.
|
|
5
5
|
|
|
6
6
|
Package name: `@sentico-labs/sdk`
|
|
7
|
-
Preview version: `0.1.0-preview.
|
|
7
|
+
Preview version: `0.1.0-preview.2`
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm install @sentico-labs/sdk@0.1.0-preview.
|
|
12
|
+
npm install @sentico-labs/sdk@0.1.0-preview.2
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
For local development from the repository:
|
|
@@ -59,48 +59,71 @@ const book = await client.public.getMarketOrderbook(1, { book: "YES", depth: 20
|
|
|
59
59
|
const trades = await client.public.listMarketTrades(1, { limit: 50 });
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
## Trading HTTP
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
62
|
+
## Trading HTTP
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import {
|
|
66
|
+
actionSigningHashHex,
|
|
67
|
+
deriveOrderIdHex,
|
|
68
|
+
signAction,
|
|
69
|
+
type LocalActionPayload
|
|
70
|
+
} from "@sentico-labs/sdk";
|
|
71
|
+
|
|
72
|
+
const account = "0x00000000000000000000000000000000000000d3" as const;
|
|
73
|
+
const privateKey = process.env.SENTICORE_PRIVATE_KEY!;
|
|
74
|
+
|
|
75
|
+
const payload: LocalActionPayload = {
|
|
76
|
+
account,
|
|
77
|
+
nonce: 4810,
|
|
78
|
+
ts: Date.now(),
|
|
79
|
+
action: {
|
|
80
|
+
kind: "SpotPlaceOrder",
|
|
81
|
+
market: 7,
|
|
82
|
+
side: "Bid",
|
|
83
|
+
price: 998400,
|
|
84
|
+
qty: 1000,
|
|
85
|
+
timeInForce: "post_only"
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const signedAction = signAction(payload, privateKey);
|
|
90
|
+
|
|
91
|
+
const accepted = await client.trading.submitSignedAction(signedAction, {
|
|
92
|
+
idempotencyKey: "client-order-1"
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log({
|
|
96
|
+
signingHash: actionSigningHashHex(payload),
|
|
97
|
+
derivedOrderId: deriveOrderIdHex(payload),
|
|
98
|
+
accepted: accepted.data
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
If you use nonce reservations, include the returned token in the payload before
|
|
103
|
+
signing:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const reserved = await client.trading.reserveNonce(account, {
|
|
107
|
+
count: 1,
|
|
108
|
+
ttlMs: 30_000,
|
|
109
|
+
idempotencyKey: "nonce-1"
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const reservedPayload: LocalActionPayload = {
|
|
113
|
+
account,
|
|
114
|
+
nonce: Number((reserved.data as { startNonce: number }).startNonce),
|
|
115
|
+
nonceReservationId: (reserved.data as { reservationId: string }).reservationId,
|
|
116
|
+
ts: Date.now(),
|
|
117
|
+
action: {
|
|
118
|
+
kind: "SpotPlaceOrder",
|
|
119
|
+
market: 7,
|
|
120
|
+
side: "Bid",
|
|
121
|
+
price: 998400,
|
|
122
|
+
qty: 1000,
|
|
123
|
+
timeInForce: "post_only"
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
```
|
|
104
127
|
|
|
105
128
|
## Institutional Order Entry
|
|
106
129
|
|
|
@@ -185,6 +208,6 @@ try {
|
|
|
185
208
|
|
|
186
209
|
## Preview Compatibility
|
|
187
210
|
|
|
188
|
-
`0.1.0-preview.
|
|
211
|
+
`0.1.0-preview.2` is not a v1 stability promise. The stable preview shape is the
|
|
189
212
|
top-level namespace split: `public`, `trading`, `orderEntry`, `raw`, and `ws`.
|
|
190
213
|
`mm` remains as a deprecated compatibility alias.
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { SenticoreClient } from "../src/index.js";
|
|
2
|
-
|
|
1
|
+
import { SenticoreClient, signAction } from "../src/index.js";
|
|
2
|
+
function requireEnv(name) {
|
|
3
|
+
const value = process.env[name];
|
|
4
|
+
if (!value)
|
|
5
|
+
throw new Error(`${name} is required`);
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
const account = requireEnv("SENTICORE_ACCOUNT");
|
|
9
|
+
const privateKey = requireEnv("SENTICORE_PRIVATE_KEY");
|
|
3
10
|
const client = new SenticoreClient({
|
|
4
11
|
publicHttpBaseUrl: process.env.SENTICORE_PUBLIC_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
5
12
|
tradingHttpBaseUrl: process.env.SENTICORE_TRADING_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
@@ -8,28 +15,21 @@ const client = new SenticoreClient({
|
|
|
8
15
|
"wss://api.sentico-labs.xyz/api/v1/ws/private/{account}",
|
|
9
16
|
orderEntryHttpBaseUrl: process.env.SENTICORE_ORDER_ENTRY_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
10
17
|
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
|
|
18
|
+
orderEntryApiKey: process.env.SENTICORE_ORDER_ENTRY_API_KEY ?? process.env.SENTICORE_MM_API_KEY,
|
|
12
19
|
});
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
order_id: process.env.SENTICORE_ORDER_ID ??
|
|
22
|
-
"0x0101010101010101010101010101010101010101010101010101010101010101"
|
|
23
|
-
}
|
|
24
|
-
}
|
|
20
|
+
const payload = {
|
|
21
|
+
account,
|
|
22
|
+
nonce: Number(requireEnv("SENTICORE_NONCE")),
|
|
23
|
+
ts: Date.now(),
|
|
24
|
+
action: {
|
|
25
|
+
kind: "Cancel",
|
|
26
|
+
orderId: process.env.SENTICORE_ORDER_ID ??
|
|
27
|
+
"0x0101010101010101010101010101010101010101010101010101010101010101",
|
|
25
28
|
},
|
|
26
|
-
signature: {
|
|
27
|
-
scheme: "EcdsaSecp256k1",
|
|
28
|
-
bytes: new Uint8Array(65)
|
|
29
|
-
}
|
|
30
29
|
};
|
|
30
|
+
const action = signAction(payload, privateKey);
|
|
31
31
|
const response = await client.orderEntry.submitActions([action], {
|
|
32
|
-
idempotencyKey: `order-entry-${
|
|
33
|
-
responseMode: "summary"
|
|
32
|
+
idempotencyKey: `order-entry-${account}-${payload.nonce}`,
|
|
33
|
+
responseMode: "summary",
|
|
34
34
|
});
|
|
35
35
|
console.log(response.data);
|
|
@@ -1,45 +1,40 @@
|
|
|
1
|
-
import { SenticoreClient } from "../src/index.js";
|
|
2
|
-
|
|
1
|
+
import { SenticoreClient, actionSigningHashHex, deriveOrderIdHex, signAction, } from "../src/index.js";
|
|
2
|
+
function requireEnv(name) {
|
|
3
|
+
const value = process.env[name];
|
|
4
|
+
if (!value)
|
|
5
|
+
throw new Error(`${name} is required`);
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
const account = requireEnv("SENTICORE_ACCOUNT");
|
|
9
|
+
const privateKey = requireEnv("SENTICORE_PRIVATE_KEY");
|
|
10
|
+
const nonce = Number(requireEnv("SENTICORE_NONCE"));
|
|
3
11
|
const client = new SenticoreClient({
|
|
4
12
|
publicHttpBaseUrl: process.env.SENTICORE_PUBLIC_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
5
13
|
tradingHttpBaseUrl: process.env.SENTICORE_TRADING_HTTP_URL ?? "https://api.sentico-labs.xyz",
|
|
6
14
|
publicWsUrl: process.env.SENTICORE_PUBLIC_WS_URL ?? "wss://api.sentico-labs.xyz/api/v1/ws/public",
|
|
7
15
|
privateWsUrl: process.env.SENTICORE_PRIVATE_WS_URL ??
|
|
8
16
|
"wss://api.sentico-labs.xyz/api/v1/ws/private/{account}",
|
|
9
|
-
bearerToken: process.env.SENTICORE_BEARER_TOKEN
|
|
17
|
+
bearerToken: process.env.SENTICORE_BEARER_TOKEN,
|
|
10
18
|
});
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
}
|
|
19
|
+
const payload = {
|
|
20
|
+
account,
|
|
21
|
+
nonce,
|
|
22
|
+
ts: Date.now(),
|
|
23
|
+
action: {
|
|
24
|
+
kind: "SpotPlaceOrder",
|
|
25
|
+
market: Number(process.env.SENTICORE_MARKET_ID ?? "7"),
|
|
26
|
+
side: "Bid",
|
|
27
|
+
price: Number(process.env.SENTICORE_PRICE_MICROS ?? "998400"),
|
|
28
|
+
qty: Number(process.env.SENTICORE_QTY_ATOMS ?? "1000"),
|
|
29
|
+
timeInForce: "post_only",
|
|
36
30
|
},
|
|
37
|
-
signature: {
|
|
38
|
-
scheme: "EcdsaSecp256k1",
|
|
39
|
-
bytes: new Uint8Array(65)
|
|
40
|
-
}
|
|
41
31
|
};
|
|
32
|
+
const signedAction = signAction(payload, privateKey);
|
|
42
33
|
const response = await client.trading.submitSignedAction(signedAction, {
|
|
43
|
-
idempotencyKey: `order-${
|
|
34
|
+
idempotencyKey: `order-${account}-${nonce}`,
|
|
35
|
+
});
|
|
36
|
+
console.log({
|
|
37
|
+
signingHash: actionSigningHashHex(payload),
|
|
38
|
+
derivedOrderId: deriveOrderIdHex(payload),
|
|
39
|
+
response: response.data,
|
|
44
40
|
});
|
|
45
|
-
console.log(response.data);
|
package/dist/src/config.js
CHANGED
|
@@ -2,7 +2,7 @@ const DEFAULT_TIMEOUT_MS = 10_000;
|
|
|
2
2
|
const DEFAULT_MAX_RETRIES = 2;
|
|
3
3
|
const DEFAULT_RETRY_BACKOFF_MS = 250;
|
|
4
4
|
const DEFAULT_ORDER_ENTRY_BINARY_PATH = "/api/order-entry/binary";
|
|
5
|
-
const DEFAULT_USER_AGENT = "@sentico-labs/sdk/0.1.0-preview.
|
|
5
|
+
const DEFAULT_USER_AGENT = "@sentico-labs/sdk/0.1.0-preview.2";
|
|
6
6
|
export function normalizeConfig(config) {
|
|
7
7
|
const fetchImpl = config.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
8
8
|
if (!fetchImpl) {
|
package/dist/src/signing.d.ts
CHANGED
|
@@ -25,8 +25,27 @@
|
|
|
25
25
|
* Golden vectors pinning compatibility with the backend live in
|
|
26
26
|
* `tests/signing.test.ts` and in `senticore-types::tests::signing_hash_golden_vectors`.
|
|
27
27
|
*/
|
|
28
|
+
import type { SignedAction } from "./types.js";
|
|
28
29
|
export declare const ACTION_PAYLOAD_DOMAIN_V1 = "SENTICORE/ACTION_PAYLOAD/v1";
|
|
30
|
+
export declare const ORDER_ID_DOMAIN_V1 = "SENTICORE/ORDER_ID/v1";
|
|
29
31
|
export type UintLike = number | bigint;
|
|
32
|
+
export type HexString = `0x${string}`;
|
|
33
|
+
export type PrivateKeyLike = Uint8Array | HexString | string;
|
|
34
|
+
export type SignatureLike = Uint8Array | number[] | HexString | string;
|
|
35
|
+
export type ActionSigningMode = "raw" | "personal";
|
|
36
|
+
export interface SignActionOptions {
|
|
37
|
+
/**
|
|
38
|
+
* `raw` signs the 32-byte BLAKE3 action hash directly.
|
|
39
|
+
* `personal` signs keccak256("\x19Ethereum Signed Message:\n32" || hash).
|
|
40
|
+
* The backend accepts both forms.
|
|
41
|
+
*/
|
|
42
|
+
mode?: ActionSigningMode;
|
|
43
|
+
/**
|
|
44
|
+
* Recovery byte format. The backend accepts both 0/1 and 27/28.
|
|
45
|
+
* 0/1 is the default to avoid Ethereum-specific post-processing.
|
|
46
|
+
*/
|
|
47
|
+
recoveryId?: "recovery" | "ethereum";
|
|
48
|
+
}
|
|
30
49
|
export type LocalSide = "Bid" | "Ask";
|
|
31
50
|
export type LocalBook = "YES" | "NO";
|
|
32
51
|
export type LocalTimeInForce = "gtc" | "ioc" | "fok" | "post_only";
|
|
@@ -90,12 +109,24 @@ export interface LocalActionPayload {
|
|
|
90
109
|
ts: UintLike;
|
|
91
110
|
action: LocalAction;
|
|
92
111
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Canonical declaration-order JSON for an action payload — the EXACT bytes
|
|
95
|
-
* the backend hashes and persists.
|
|
96
|
-
*/
|
|
97
112
|
export declare function canonicalActionPayloadJson(payload: LocalActionPayload): string;
|
|
113
|
+
/** Wire payload object suitable for submitSignedAction/order-entry JSON. */
|
|
114
|
+
export declare function actionPayloadToWirePayload(payload: LocalActionPayload): Record<string, unknown>;
|
|
98
115
|
/** blake3(domain || canonical_bytes) as raw bytes. */
|
|
99
116
|
export declare function actionSigningHash(payload: LocalActionPayload): Uint8Array;
|
|
100
117
|
/** blake3 signing hash as 0x-prefixed lowercase hex. */
|
|
101
|
-
export declare function actionSigningHashHex(payload: LocalActionPayload):
|
|
118
|
+
export declare function actionSigningHashHex(payload: LocalActionPayload): HexString;
|
|
119
|
+
/** Deterministic order id for SpotPlaceOrder/OutcomePlaceOrder payloads. */
|
|
120
|
+
export declare function deriveOrderIdHex(payload: LocalActionPayload): HexString | null;
|
|
121
|
+
/** Deterministic child order id for QuoteReplace/SpotQuoteReplace place legs. */
|
|
122
|
+
export declare function deriveQuoteReplaceOrderIdHex(payload: LocalActionPayload, legIndex: number): HexString | null;
|
|
123
|
+
/** EIP-191 personal_sign digest for a 32-byte action hash. */
|
|
124
|
+
export declare function personalSignDigestHash(hash32: Uint8Array): Uint8Array;
|
|
125
|
+
/** Sign a 32-byte action hash and return 65 bytes (r || s || v). */
|
|
126
|
+
export declare function signActionHash(hash32: Uint8Array, privateKey: PrivateKeyLike, options?: SignActionOptions): Uint8Array;
|
|
127
|
+
/** Hex helper for logging, fixtures, or wallet interop. */
|
|
128
|
+
export declare function signatureToHex(signature: SignatureLike): HexString;
|
|
129
|
+
/** Build a SignedAction from a local payload and externally produced signature. */
|
|
130
|
+
export declare function signedActionFromLocalPayload(payload: LocalActionPayload, signature: SignatureLike): SignedAction;
|
|
131
|
+
/** Compute, sign, and wrap a local action payload for direct submission. */
|
|
132
|
+
export declare function signAction(payload: LocalActionPayload, privateKey: PrivateKeyLike, options?: SignActionOptions): SignedAction;
|
package/dist/src/signing.js
CHANGED
|
@@ -25,8 +25,12 @@
|
|
|
25
25
|
* Golden vectors pinning compatibility with the backend live in
|
|
26
26
|
* `tests/signing.test.ts` and in `senticore-types::tests::signing_hash_golden_vectors`.
|
|
27
27
|
*/
|
|
28
|
+
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
28
29
|
import { blake3 } from "@noble/hashes/blake3";
|
|
30
|
+
import { keccak_256 } from "@noble/hashes/sha3";
|
|
31
|
+
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
|
29
32
|
export const ACTION_PAYLOAD_DOMAIN_V1 = "SENTICORE/ACTION_PAYLOAD/v1";
|
|
33
|
+
export const ORDER_ID_DOMAIN_V1 = "SENTICORE/ORDER_ID/v1";
|
|
30
34
|
// -----------------------------
|
|
31
35
|
// Canonical JSON writer
|
|
32
36
|
// -----------------------------
|
|
@@ -123,6 +127,158 @@ function writeAction(action) {
|
|
|
123
127
|
* Canonical declaration-order JSON for an action payload — the EXACT bytes
|
|
124
128
|
* the backend hashes and persists.
|
|
125
129
|
*/
|
|
130
|
+
function encodeWithDomain(domain, canonicalJson) {
|
|
131
|
+
const canonical = new TextEncoder().encode(canonicalJson);
|
|
132
|
+
const domainBytes = new TextEncoder().encode(domain);
|
|
133
|
+
const joined = new Uint8Array(domainBytes.length + canonical.length);
|
|
134
|
+
joined.set(domainBytes, 0);
|
|
135
|
+
joined.set(canonical, domainBytes.length);
|
|
136
|
+
return blake3(joined);
|
|
137
|
+
}
|
|
138
|
+
function bytesTo0xHex(bytes) {
|
|
139
|
+
return `0x${bytesToHex(bytes)}`;
|
|
140
|
+
}
|
|
141
|
+
function fixedHexToBytes(value, bytes, field) {
|
|
142
|
+
const raw = value.startsWith("0x") || value.startsWith("0X") ? value.slice(2) : value;
|
|
143
|
+
if (!/^[0-9a-fA-F]+$/.test(raw) || raw.length !== bytes * 2) {
|
|
144
|
+
throw new Error(`${field} must be ${bytes} bytes of hex`);
|
|
145
|
+
}
|
|
146
|
+
return hexToBytes(raw);
|
|
147
|
+
}
|
|
148
|
+
function normalizePrivateKey(privateKey) {
|
|
149
|
+
const bytes = typeof privateKey === "string"
|
|
150
|
+
? fixedHexToBytes(privateKey, 32, "privateKey")
|
|
151
|
+
: privateKey;
|
|
152
|
+
if (bytes.length !== 32) {
|
|
153
|
+
throw new Error(`privateKey must be 32 bytes, got ${bytes.length}`);
|
|
154
|
+
}
|
|
155
|
+
return bytes;
|
|
156
|
+
}
|
|
157
|
+
function normalizeSignatureBytes(signature) {
|
|
158
|
+
const bytes = typeof signature === "string"
|
|
159
|
+
? fixedHexToBytes(signature, 65, "signature")
|
|
160
|
+
: signature instanceof Uint8Array
|
|
161
|
+
? signature
|
|
162
|
+
: Uint8Array.from(signature);
|
|
163
|
+
if (bytes.length !== 65) {
|
|
164
|
+
throw new Error(`signature must be 65 bytes, got ${bytes.length}`);
|
|
165
|
+
}
|
|
166
|
+
return [...bytes];
|
|
167
|
+
}
|
|
168
|
+
function isPlaceOrderPayload(payload) {
|
|
169
|
+
return (payload.action.kind === "SpotPlaceOrder" ||
|
|
170
|
+
payload.action.kind === "OutcomePlaceOrder");
|
|
171
|
+
}
|
|
172
|
+
function isZeroUint(value, field) {
|
|
173
|
+
return writeUint(value, field) === "0";
|
|
174
|
+
}
|
|
175
|
+
function quoteReplaceChildPayload(payload, legIndex) {
|
|
176
|
+
if (!Number.isSafeInteger(legIndex) || legIndex < 0) {
|
|
177
|
+
throw new Error("legIndex must be a non-negative safe integer");
|
|
178
|
+
}
|
|
179
|
+
if (payload.action.kind === "SpotQuoteReplace") {
|
|
180
|
+
const leg = payload.action.legs[legIndex];
|
|
181
|
+
if (!leg || isZeroUint(leg.qty, "qty"))
|
|
182
|
+
return null;
|
|
183
|
+
return {
|
|
184
|
+
account: payload.account,
|
|
185
|
+
nonce: payload.nonce,
|
|
186
|
+
nonceReservationId: `spot_quote_replace:${legIndex}`,
|
|
187
|
+
ts: payload.ts,
|
|
188
|
+
action: {
|
|
189
|
+
kind: "SpotPlaceOrder",
|
|
190
|
+
market: payload.action.market,
|
|
191
|
+
side: leg.side,
|
|
192
|
+
price: leg.price,
|
|
193
|
+
qty: leg.qty,
|
|
194
|
+
stpMode: leg.stpMode,
|
|
195
|
+
timeInForce: leg.timeInForce,
|
|
196
|
+
isMarket: leg.isMarket,
|
|
197
|
+
reduceOnly: leg.reduceOnly,
|
|
198
|
+
expiresAt: leg.expiresAt,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (payload.action.kind === "QuoteReplace") {
|
|
203
|
+
const leg = payload.action.legs[legIndex];
|
|
204
|
+
if (!leg || isZeroUint(leg.qty, "qty"))
|
|
205
|
+
return null;
|
|
206
|
+
return {
|
|
207
|
+
account: payload.account,
|
|
208
|
+
nonce: payload.nonce,
|
|
209
|
+
nonceReservationId: `quote_replace:${legIndex}`,
|
|
210
|
+
ts: payload.ts,
|
|
211
|
+
action: {
|
|
212
|
+
kind: "OutcomePlaceOrder",
|
|
213
|
+
market: payload.action.market,
|
|
214
|
+
book: leg.book,
|
|
215
|
+
side: leg.side,
|
|
216
|
+
price: leg.price,
|
|
217
|
+
qty: leg.qty,
|
|
218
|
+
stpMode: leg.stpMode,
|
|
219
|
+
timeInForce: leg.timeInForce,
|
|
220
|
+
isMarket: leg.isMarket,
|
|
221
|
+
reduceOnly: leg.reduceOnly,
|
|
222
|
+
expiresAt: leg.expiresAt,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
function assertJsonSafeUint(value, field) {
|
|
229
|
+
if (value === null || value === undefined)
|
|
230
|
+
return;
|
|
231
|
+
const decimal = writeUint(value, field);
|
|
232
|
+
if (BigInt(decimal) > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
233
|
+
throw new Error(`${field} exceeds Number.MAX_SAFE_INTEGER; JSON SignedAction submission would lose precision`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function assertJsonSafeFlags(flags) {
|
|
237
|
+
assertJsonSafeUint(flags.expiresAt, "expiresAt");
|
|
238
|
+
}
|
|
239
|
+
function assertJsonSafePayload(payload) {
|
|
240
|
+
assertJsonSafeUint(payload.nonce, "nonce");
|
|
241
|
+
assertJsonSafeUint(payload.ts, "ts");
|
|
242
|
+
switch (payload.action.kind) {
|
|
243
|
+
case "SpotPlaceOrder":
|
|
244
|
+
assertJsonSafeUint(payload.action.market, "market");
|
|
245
|
+
assertJsonSafeUint(payload.action.price, "price");
|
|
246
|
+
assertJsonSafeUint(payload.action.qty, "qty");
|
|
247
|
+
assertJsonSafeFlags(payload.action);
|
|
248
|
+
return;
|
|
249
|
+
case "OutcomePlaceOrder":
|
|
250
|
+
assertJsonSafeUint(payload.action.market, "market");
|
|
251
|
+
assertJsonSafeUint(payload.action.price, "price");
|
|
252
|
+
assertJsonSafeUint(payload.action.qty, "qty");
|
|
253
|
+
assertJsonSafeFlags(payload.action);
|
|
254
|
+
return;
|
|
255
|
+
case "Cancel":
|
|
256
|
+
return;
|
|
257
|
+
case "AmendOrder":
|
|
258
|
+
assertJsonSafeUint(payload.action.newQty, "newQty");
|
|
259
|
+
return;
|
|
260
|
+
case "SpotQuoteReplace":
|
|
261
|
+
assertJsonSafeUint(payload.action.market, "market");
|
|
262
|
+
for (const [index, leg] of payload.action.legs.entries()) {
|
|
263
|
+
assertJsonSafeUint(leg.price, `legs[${index}].price`);
|
|
264
|
+
assertJsonSafeUint(leg.qty, `legs[${index}].qty`);
|
|
265
|
+
assertJsonSafeFlags(leg);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
case "QuoteReplace":
|
|
269
|
+
assertJsonSafeUint(payload.action.market, "market");
|
|
270
|
+
for (const [index, leg] of payload.action.legs.entries()) {
|
|
271
|
+
assertJsonSafeUint(leg.price, `legs[${index}].price`);
|
|
272
|
+
assertJsonSafeUint(leg.qty, `legs[${index}].qty`);
|
|
273
|
+
assertJsonSafeFlags(leg);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
default: {
|
|
277
|
+
const exhaustive = payload.action;
|
|
278
|
+
throw new Error(`unsupported action ${exhaustive.kind}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
126
282
|
export function canonicalActionPayloadJson(payload) {
|
|
127
283
|
return (`{"account":${writeHexBytes(payload.account, 20, "account")},` +
|
|
128
284
|
`"nonce":${writeUint(payload.nonce, "nonce")},` +
|
|
@@ -130,20 +286,74 @@ export function canonicalActionPayloadJson(payload) {
|
|
|
130
286
|
`"ts":${writeUint(payload.ts, "ts")},` +
|
|
131
287
|
`"action":${writeAction(payload.action)}}`);
|
|
132
288
|
}
|
|
289
|
+
/** Wire payload object suitable for submitSignedAction/order-entry JSON. */
|
|
290
|
+
export function actionPayloadToWirePayload(payload) {
|
|
291
|
+
assertJsonSafePayload(payload);
|
|
292
|
+
return JSON.parse(canonicalActionPayloadJson(payload));
|
|
293
|
+
}
|
|
133
294
|
/** blake3(domain || canonical_bytes) as raw bytes. */
|
|
134
295
|
export function actionSigningHash(payload) {
|
|
135
|
-
|
|
136
|
-
const domain = new TextEncoder().encode(ACTION_PAYLOAD_DOMAIN_V1);
|
|
137
|
-
const joined = new Uint8Array(domain.length + canonical.length);
|
|
138
|
-
joined.set(domain, 0);
|
|
139
|
-
joined.set(canonical, domain.length);
|
|
140
|
-
return blake3(joined);
|
|
296
|
+
return encodeWithDomain(ACTION_PAYLOAD_DOMAIN_V1, canonicalActionPayloadJson(payload));
|
|
141
297
|
}
|
|
142
298
|
/** blake3 signing hash as 0x-prefixed lowercase hex. */
|
|
143
299
|
export function actionSigningHashHex(payload) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
300
|
+
return bytesTo0xHex(actionSigningHash(payload));
|
|
301
|
+
}
|
|
302
|
+
/** Deterministic order id for SpotPlaceOrder/OutcomePlaceOrder payloads. */
|
|
303
|
+
export function deriveOrderIdHex(payload) {
|
|
304
|
+
if (!isPlaceOrderPayload(payload))
|
|
305
|
+
return null;
|
|
306
|
+
return bytesTo0xHex(encodeWithDomain(ORDER_ID_DOMAIN_V1, canonicalActionPayloadJson(payload)));
|
|
307
|
+
}
|
|
308
|
+
/** Deterministic child order id for QuoteReplace/SpotQuoteReplace place legs. */
|
|
309
|
+
export function deriveQuoteReplaceOrderIdHex(payload, legIndex) {
|
|
310
|
+
const child = quoteReplaceChildPayload(payload, legIndex);
|
|
311
|
+
return child ? deriveOrderIdHex(child) : null;
|
|
312
|
+
}
|
|
313
|
+
/** EIP-191 personal_sign digest for a 32-byte action hash. */
|
|
314
|
+
export function personalSignDigestHash(hash32) {
|
|
315
|
+
if (hash32.length !== 32) {
|
|
316
|
+
throw new Error(`hash32 must be 32 bytes, got ${hash32.length}`);
|
|
317
|
+
}
|
|
318
|
+
const prefix = new TextEncoder().encode("\x19Ethereum Signed Message:\n32");
|
|
319
|
+
const joined = new Uint8Array(prefix.length + hash32.length);
|
|
320
|
+
joined.set(prefix, 0);
|
|
321
|
+
joined.set(hash32, prefix.length);
|
|
322
|
+
return keccak_256(joined);
|
|
323
|
+
}
|
|
324
|
+
/** Sign a 32-byte action hash and return 65 bytes (r || s || v). */
|
|
325
|
+
export function signActionHash(hash32, privateKey, options = {}) {
|
|
326
|
+
if (hash32.length !== 32) {
|
|
327
|
+
throw new Error(`hash32 must be 32 bytes, got ${hash32.length}`);
|
|
328
|
+
}
|
|
329
|
+
const digest = options.mode === "personal" ? personalSignDigestHash(hash32) : hash32;
|
|
330
|
+
const signature = secp256k1.sign(digest, normalizePrivateKey(privateKey));
|
|
331
|
+
const compact = signature.toCompactRawBytes();
|
|
332
|
+
const recovery = signature.recovery;
|
|
333
|
+
if (recovery !== 0 && recovery !== 1) {
|
|
334
|
+
throw new Error(`invalid recovery id ${recovery}`);
|
|
335
|
+
}
|
|
336
|
+
const out = new Uint8Array(65);
|
|
337
|
+
out.set(compact, 0);
|
|
338
|
+
out[64] = options.recoveryId === "ethereum" ? recovery + 27 : recovery;
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
/** Hex helper for logging, fixtures, or wallet interop. */
|
|
342
|
+
export function signatureToHex(signature) {
|
|
343
|
+
return bytesTo0xHex(Uint8Array.from(normalizeSignatureBytes(signature)));
|
|
344
|
+
}
|
|
345
|
+
/** Build a SignedAction from a local payload and externally produced signature. */
|
|
346
|
+
export function signedActionFromLocalPayload(payload, signature) {
|
|
347
|
+
return {
|
|
348
|
+
payload: actionPayloadToWirePayload(payload),
|
|
349
|
+
signature: {
|
|
350
|
+
scheme: "EcdsaSecp256k1",
|
|
351
|
+
bytes: normalizeSignatureBytes(signature),
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/** Compute, sign, and wrap a local action payload for direct submission. */
|
|
356
|
+
export function signAction(payload, privateKey, options = {}) {
|
|
357
|
+
const signature = signActionHash(actionSigningHash(payload), privateKey, options);
|
|
358
|
+
return signedActionFromLocalPayload(payload, signature);
|
|
149
359
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "@sentico-labs/sdk",
|
|
3
|
-
"version": "0.1.0-preview.
|
|
2
|
+
"name": "@sentico-labs/sdk",
|
|
3
|
+
"version": "0.1.0-preview.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Institutional TypeScript SDK for Senticore HTTP, WebSocket, and Order Entry.",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"typescript": "^5.6.3"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@noble/curves": "^1.9.7",
|
|
35
36
|
"@noble/hashes": "^1.8.0"
|
|
36
37
|
}
|
|
37
38
|
}
|