@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 CHANGED
@@ -1,8 +1,15 @@
1
- # Changelog
2
-
3
- ## 0.1.0-preview.1
4
-
5
- - Converts the SDK into a publishable preview package.
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.1`
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.1
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
- 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
- ```
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.1` is not a v1 stability promise. The stable preview shape is the
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
- const account = process.env.SENTICORE_ACCOUNT;
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 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
- }
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-${Date.now()}`,
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
- const account = process.env.SENTICORE_ACCOUNT;
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 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
- }
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-${Date.now()}`
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);
@@ -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.1";
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) {
@@ -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): `0x${string}`;
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;
@@ -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
- const canonical = new TextEncoder().encode(canonicalActionPayloadJson(payload));
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
- const hash = actionSigningHash(payload);
145
- let hex = "";
146
- for (const byte of hash)
147
- hex += byte.toString(16).padStart(2, "0");
148
- return `0x${hex}`;
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.1",
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
  }