@temple-digital-group/temple-canton-js 1.0.37 → 1.0.38-beta
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 +361 -361
- package/index.js +12 -12
- package/package.json +47 -47
- package/src/api/config.d.ts +20 -20
- package/src/api/index.ts +355 -355
- package/src/api/tokenStore.ts +87 -87
- package/src/api/types.ts +149 -149
- package/src/auth0/index.js +50 -50
- package/src/canton/index.js +3825 -3825
- package/src/canton/instrumentCatalog.js +174 -174
- package/src/canton/request_schemas/cancel_orders_amulet.json +77 -77
- package/src/canton/request_schemas/cancel_orders_utility.json +68 -68
- package/src/canton/request_schemas/create_order_proposal_amulet.json +94 -94
- package/src/canton/request_schemas/create_order_proposal_utility.json +121 -121
- package/src/canton/request_schemas/create_utility_credential.json +31 -31
- package/src/canton/request_schemas/execute_transfer_factory.json +43 -43
- package/src/canton/request_schemas/get_allocation_factory.json +21 -21
- package/src/canton/request_schemas/get_amulet_holdings.json +21 -21
- package/src/canton/request_schemas/get_instrument_configurations.json +21 -21
- package/src/canton/request_schemas/get_locked_amulet_holdings.json +21 -21
- package/src/canton/request_schemas/get_order_proposals.json +21 -21
- package/src/canton/request_schemas/get_orders.json +21 -21
- package/src/canton/request_schemas/get_sender_credentials.json +22 -22
- package/src/canton/request_schemas/get_transfer_factory.json +28 -28
- package/src/canton/request_schemas/get_utility_holdings.json +21 -21
- package/src/canton/request_schemas/unlock_amulet.json +38 -38
- package/src/canton/walletAdapter.js +89 -89
- package/src/config/index.d.ts +57 -57
- package/src/config/index.js +154 -154
package/src/api/index.ts
CHANGED
|
@@ -1,355 +1,355 @@
|
|
|
1
|
-
import axios, { type AxiosRequestConfig } from "axios";
|
|
2
|
-
import config, { initializeConfig, setWalletAdapter } from "../../src/config/index.js";
|
|
3
|
-
import {
|
|
4
|
-
setTokens,
|
|
5
|
-
getAccessToken,
|
|
6
|
-
getRefreshToken,
|
|
7
|
-
getApiKey,
|
|
8
|
-
setApiKey as setApiKeyFromConfig,
|
|
9
|
-
isTokenExpired,
|
|
10
|
-
clearTokens,
|
|
11
|
-
getAuthHeader,
|
|
12
|
-
} from "./tokenStore.js";
|
|
13
|
-
import type {
|
|
14
|
-
ApiError,
|
|
15
|
-
LoginUser,
|
|
16
|
-
LoginResponse,
|
|
17
|
-
RefreshResponse,
|
|
18
|
-
Ticker,
|
|
19
|
-
OrderBook,
|
|
20
|
-
OrderBookOptions,
|
|
21
|
-
SymbolConfig,
|
|
22
|
-
OpenInterest,
|
|
23
|
-
Trade,
|
|
24
|
-
RecentTradesOptions,
|
|
25
|
-
ActiveOrder,
|
|
26
|
-
ActiveOrdersOptions,
|
|
27
|
-
CancelOrderResponse,
|
|
28
|
-
CancelAllOrdersOptions,
|
|
29
|
-
CancelAllOrdersResponse,
|
|
30
|
-
DisclosuresResponse,
|
|
31
|
-
} from "./types.js";
|
|
32
|
-
|
|
33
|
-
// Re-export types and token utilities consumers may need
|
|
34
|
-
export type {
|
|
35
|
-
ApiError,
|
|
36
|
-
LoginUser,
|
|
37
|
-
LoginResponse,
|
|
38
|
-
RefreshResponse,
|
|
39
|
-
Ticker,
|
|
40
|
-
OrderBook,
|
|
41
|
-
OrderBookOptions,
|
|
42
|
-
SymbolConfig,
|
|
43
|
-
OpenInterest,
|
|
44
|
-
Trade,
|
|
45
|
-
RecentTradesOptions,
|
|
46
|
-
ActiveOrder,
|
|
47
|
-
ActiveOrdersOptions,
|
|
48
|
-
CancelOrderResponse,
|
|
49
|
-
CancelAllOrdersOptions,
|
|
50
|
-
CancelAllOrdersResponse,
|
|
51
|
-
DisclosuresResponse,
|
|
52
|
-
};
|
|
53
|
-
export { setApiKey, getUserId } from "./tokenStore.js";
|
|
54
|
-
export { setWalletAdapter } from "../../src/config/index.js";
|
|
55
|
-
|
|
56
|
-
// ── Initialization ──
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Initialize the Temple SDK — sets config and optionally authenticates with the REST API.
|
|
60
|
-
* Pass `API_EMAIL`/`API_PASSWORD` or `API_KEY` to authenticate eagerly at init time.
|
|
61
|
-
*
|
|
62
|
-
* @returns The login response on success, or an ApiError on failure.
|
|
63
|
-
* Returns `null` if no API credentials were provided (config-only init).
|
|
64
|
-
*/
|
|
65
|
-
export async function initialize(cfg: Record<string, unknown>): Promise<LoginResponse | ApiError | null> {
|
|
66
|
-
initializeConfig(cfg);
|
|
67
|
-
|
|
68
|
-
if (cfg.WALLET_ADAPTER) {
|
|
69
|
-
setWalletAdapter(cfg.WALLET_ADAPTER);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const apiKey = cfg.API_KEY as string | undefined;
|
|
73
|
-
if (apiKey) {
|
|
74
|
-
setApiKeyFromConfig(apiKey);
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const email = cfg.API_EMAIL as string | undefined;
|
|
79
|
-
const password = cfg.API_PASSWORD as string | undefined;
|
|
80
|
-
if (email && password) {
|
|
81
|
-
return login(email, password);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ── Internal helpers ──
|
|
88
|
-
|
|
89
|
-
/** Pending auto-login promise for dedup. */
|
|
90
|
-
let autoLoginPromise: Promise<LoginResponse | ApiError> | null = null;
|
|
91
|
-
|
|
92
|
-
function handleApiError(error: unknown, context: string): ApiError {
|
|
93
|
-
const err = error as { response?: { status?: number; data?: { message?: string; error?: string; code?: string } }; message?: string };
|
|
94
|
-
const status = err.response?.status ?? null;
|
|
95
|
-
const message =
|
|
96
|
-
err.response?.data?.message ??
|
|
97
|
-
err.response?.data?.error ??
|
|
98
|
-
err.message ??
|
|
99
|
-
"Unknown error";
|
|
100
|
-
const code = err.response?.data?.code ?? null;
|
|
101
|
-
|
|
102
|
-
console.error(`[Temple API] ${context}: ${message}${status ? ` (HTTP ${status})` : ""}`);
|
|
103
|
-
|
|
104
|
-
return { error: true, status, code, message };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Pending refresh promise for dedup. */
|
|
108
|
-
let refreshPromise: Promise<RefreshResponse | ApiError> | null = null;
|
|
109
|
-
|
|
110
|
-
async function ensureAuthenticated(): Promise<ApiError | null> {
|
|
111
|
-
// API key takes priority — no login needed
|
|
112
|
-
const configApiKey = (config as Record<string, unknown>).API_KEY as string | undefined;
|
|
113
|
-
if (!getApiKey() && configApiKey) {
|
|
114
|
-
setApiKeyFromConfig(configApiKey);
|
|
115
|
-
}
|
|
116
|
-
if (getApiKey()) return null;
|
|
117
|
-
|
|
118
|
-
// Auto-login from config if no token exists
|
|
119
|
-
if (!getAccessToken()) {
|
|
120
|
-
const email = (config as Record<string, unknown>).API_EMAIL as string | undefined;
|
|
121
|
-
const password = (config as Record<string, unknown>).API_PASSWORD as string | undefined;
|
|
122
|
-
if (email && password) {
|
|
123
|
-
if (!autoLoginPromise) {
|
|
124
|
-
autoLoginPromise = login(email, password).finally(() => {
|
|
125
|
-
autoLoginPromise = null;
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
const result = await autoLoginPromise;
|
|
129
|
-
if ("error" in result && result.error) return result as ApiError;
|
|
130
|
-
} else {
|
|
131
|
-
return { error: true, status: 401, code: "NOT_AUTHENTICATED", message: "Not logged in. Call login() or provide API_EMAIL/API_PASSWORD in config." };
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Auto-refresh if expired
|
|
136
|
-
if (isTokenExpired()) {
|
|
137
|
-
if (!refreshPromise) {
|
|
138
|
-
refreshPromise = refreshAccessToken().finally(() => {
|
|
139
|
-
refreshPromise = null;
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
const result = await refreshPromise;
|
|
143
|
-
if ("error" in result && result.error) return result as ApiError;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function authenticatedGet<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T | ApiError> {
|
|
150
|
-
const authError = await ensureAuthenticated();
|
|
151
|
-
if (authError) return authError;
|
|
152
|
-
|
|
153
|
-
const headers = {
|
|
154
|
-
...getAuthHeader(),
|
|
155
|
-
"Content-Type": "application/json",
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// Filter out undefined params
|
|
159
|
-
const cleanParams: Record<string, string | number> = {};
|
|
160
|
-
if (params) {
|
|
161
|
-
for (const [key, value] of Object.entries(params)) {
|
|
162
|
-
if (value !== undefined) cleanParams[key] = value;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const response = await axios.get<T>(`${config.API_BASE_URL}${path}`, {
|
|
168
|
-
headers,
|
|
169
|
-
params: Object.keys(cleanParams).length > 0 ? cleanParams : undefined,
|
|
170
|
-
} as AxiosRequestConfig);
|
|
171
|
-
return response.data;
|
|
172
|
-
} catch (error) {
|
|
173
|
-
return handleApiError(error, `GET ${path}`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function authenticatedPost<T>(path: string, body?: Record<string, unknown>): Promise<T | ApiError> {
|
|
178
|
-
const authError = await ensureAuthenticated();
|
|
179
|
-
if (authError) return authError;
|
|
180
|
-
|
|
181
|
-
const headers = {
|
|
182
|
-
...getAuthHeader(),
|
|
183
|
-
"Content-Type": "application/json",
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
const response = await axios.post<T>(`${config.API_BASE_URL}${path}`, body ?? {}, { headers } as AxiosRequestConfig);
|
|
188
|
-
return response.data;
|
|
189
|
-
} catch (error) {
|
|
190
|
-
return handleApiError(error, `POST ${path}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ── Auth ──
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Login to the Temple REST API with email and password.
|
|
198
|
-
* Stores tokens automatically for subsequent API calls.
|
|
199
|
-
* @param email - User email
|
|
200
|
-
* @param password - User password
|
|
201
|
-
*/
|
|
202
|
-
export async function login(email: string, password: string): Promise<LoginResponse | ApiError> {
|
|
203
|
-
if (!email || !password) {
|
|
204
|
-
return { error: true, status: null, code: "INVALID_PARAMS", message: "Email and password are required." };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
const response = await axios.post<LoginResponse>(`${config.API_BASE_URL}/auth/login`, {
|
|
209
|
-
email,
|
|
210
|
-
password,
|
|
211
|
-
});
|
|
212
|
-
setTokens(response.data);
|
|
213
|
-
return response.data;
|
|
214
|
-
} catch (error) {
|
|
215
|
-
return handleApiError(error, "login");
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Refresh the access token using the stored or provided refresh token.
|
|
221
|
-
* Updates the token store automatically on success.
|
|
222
|
-
* @param refreshTokenOverride - Optional explicit refresh token (uses stored one if omitted)
|
|
223
|
-
*/
|
|
224
|
-
export async function refreshAccessToken(refreshTokenOverride?: string): Promise<RefreshResponse | ApiError> {
|
|
225
|
-
const token = refreshTokenOverride ?? getRefreshToken();
|
|
226
|
-
if (!token) {
|
|
227
|
-
return { error: true, status: null, code: "NO_REFRESH_TOKEN", message: "No refresh token available. Call login() first." };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
const response = await axios.post<RefreshResponse>(`${config.API_BASE_URL}/auth/refresh`, {
|
|
232
|
-
refresh_token: token,
|
|
233
|
-
});
|
|
234
|
-
setTokens(response.data);
|
|
235
|
-
return response.data;
|
|
236
|
-
} catch (error) {
|
|
237
|
-
return handleApiError(error, "refreshAccessToken");
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Logout: clear all stored REST API tokens.
|
|
243
|
-
*/
|
|
244
|
-
export function logout(): void {
|
|
245
|
-
clearTokens();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ── Market Data ──
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Get ticker data for one or all trading pairs.
|
|
252
|
-
* @param symbol - Optional symbol filter (e.g., "Amulet/USDCx")
|
|
253
|
-
*/
|
|
254
|
-
export async function getTicker(symbol?: string): Promise<Ticker | Ticker[] | ApiError> {
|
|
255
|
-
return authenticatedGet("/api/v1/market/ticker", { symbol });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Get the order book for a trading pair.
|
|
260
|
-
* @param symbol - Trading pair symbol (required)
|
|
261
|
-
* @param options - Optional levels and precision parameters
|
|
262
|
-
*/
|
|
263
|
-
export async function getOrderBook(symbol: string, options: OrderBookOptions = {}): Promise<OrderBook | ApiError> {
|
|
264
|
-
if (!symbol) {
|
|
265
|
-
return { error: true, status: null, code: "INVALID_PARAMS", message: "Symbol is required." };
|
|
266
|
-
}
|
|
267
|
-
return authenticatedGet("/api/v1/market/orderbook", {
|
|
268
|
-
symbol,
|
|
269
|
-
levels: options.levels,
|
|
270
|
-
precision: options.precision,
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Get symbol configuration (paused status, decimals, minimum quantity).
|
|
276
|
-
* @param symbol - Trading pair symbol (required)
|
|
277
|
-
*/
|
|
278
|
-
export async function getSymbolConfig(symbol: string): Promise<SymbolConfig | ApiError> {
|
|
279
|
-
if (!symbol) {
|
|
280
|
-
return { error: true, status: null, code: "INVALID_PARAMS", message: "Symbol is required." };
|
|
281
|
-
}
|
|
282
|
-
return authenticatedGet("/api/v1/trading/symbol-config", { symbol });
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Get open interest for a trading pair.
|
|
287
|
-
* @param symbol - Trading pair symbol (required)
|
|
288
|
-
*/
|
|
289
|
-
export async function getOpenInterest(symbol: string): Promise<OpenInterest | ApiError> {
|
|
290
|
-
if (!symbol) {
|
|
291
|
-
return { error: true, status: null, code: "INVALID_PARAMS", message: "Symbol is required." };
|
|
292
|
-
}
|
|
293
|
-
return authenticatedGet("/api/v1/market/open-interest", { symbol });
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Get recent trades for a trading pair.
|
|
298
|
-
* @param symbol - Trading pair symbol (required)
|
|
299
|
-
* @param options - Optional limit (max 500)
|
|
300
|
-
*/
|
|
301
|
-
export async function getRecentTrades(symbol: string, options: RecentTradesOptions = {}): Promise<Trade[] | ApiError> {
|
|
302
|
-
if (!symbol) {
|
|
303
|
-
return { error: true, status: null, code: "INVALID_PARAMS", message: "Symbol is required." };
|
|
304
|
-
}
|
|
305
|
-
return authenticatedGet("/api/v1/market/trades", {
|
|
306
|
-
symbol,
|
|
307
|
-
limit: options.limit,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// ── Orders ──
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Get active orders, optionally filtered by symbol.
|
|
315
|
-
* @param options - Optional symbol and limit filters
|
|
316
|
-
*/
|
|
317
|
-
export async function getActiveOrders(options: ActiveOrdersOptions = {}): Promise<ActiveOrder[] | ApiError> {
|
|
318
|
-
return authenticatedGet("/api/trading/orders/active", {
|
|
319
|
-
symbol: options.symbol,
|
|
320
|
-
limit: options.limit,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Cancel a specific order by ID.
|
|
326
|
-
* @param orderId - The order ID to cancel
|
|
327
|
-
*/
|
|
328
|
-
export async function cancelOrder(orderId: string): Promise<CancelOrderResponse | ApiError> {
|
|
329
|
-
if (!orderId) {
|
|
330
|
-
return { error: true, status: null, code: "INVALID_PARAMS", message: "Order ID is required." };
|
|
331
|
-
}
|
|
332
|
-
return authenticatedPost(`/api/trading/orders/${encodeURIComponent(orderId)}/cancel`);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Cancel all active orders, optionally filtered by symbol.
|
|
337
|
-
* @param options - Optional symbol filter
|
|
338
|
-
*/
|
|
339
|
-
export async function cancelAllOrders(options: CancelAllOrdersOptions = {}): Promise<CancelAllOrdersResponse | ApiError> {
|
|
340
|
-
return authenticatedPost("/api/v1/trading/orders/cancel-all", options.symbol ? { symbol: options.symbol } : undefined);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// ── Disclosures ──
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Get amulet disclosure data for a party from the Temple REST API.
|
|
347
|
-
* Returns protocol disclosures (amulet rules, open mining rounds, external party amulet rules).
|
|
348
|
-
* @param partyId - Canton party ID
|
|
349
|
-
*/
|
|
350
|
-
export async function getDisclosures(partyId: string): Promise<DisclosuresResponse | ApiError> {
|
|
351
|
-
if (!partyId) {
|
|
352
|
-
return { error: true, status: null, code: "INVALID_PARAMS", message: "Party ID is required." };
|
|
353
|
-
}
|
|
354
|
-
return authenticatedGet("/api/amulet/disclosures", { partyId });
|
|
355
|
-
}
|
|
1
|
+
import axios, { type AxiosRequestConfig } from "axios";
|
|
2
|
+
import config, { initializeConfig, setWalletAdapter } from "../../src/config/index.js";
|
|
3
|
+
import {
|
|
4
|
+
setTokens,
|
|
5
|
+
getAccessToken,
|
|
6
|
+
getRefreshToken,
|
|
7
|
+
getApiKey,
|
|
8
|
+
setApiKey as setApiKeyFromConfig,
|
|
9
|
+
isTokenExpired,
|
|
10
|
+
clearTokens,
|
|
11
|
+
getAuthHeader,
|
|
12
|
+
} from "./tokenStore.js";
|
|
13
|
+
import type {
|
|
14
|
+
ApiError,
|
|
15
|
+
LoginUser,
|
|
16
|
+
LoginResponse,
|
|
17
|
+
RefreshResponse,
|
|
18
|
+
Ticker,
|
|
19
|
+
OrderBook,
|
|
20
|
+
OrderBookOptions,
|
|
21
|
+
SymbolConfig,
|
|
22
|
+
OpenInterest,
|
|
23
|
+
Trade,
|
|
24
|
+
RecentTradesOptions,
|
|
25
|
+
ActiveOrder,
|
|
26
|
+
ActiveOrdersOptions,
|
|
27
|
+
CancelOrderResponse,
|
|
28
|
+
CancelAllOrdersOptions,
|
|
29
|
+
CancelAllOrdersResponse,
|
|
30
|
+
DisclosuresResponse,
|
|
31
|
+
} from "./types.js";
|
|
32
|
+
|
|
33
|
+
// Re-export types and token utilities consumers may need
|
|
34
|
+
export type {
|
|
35
|
+
ApiError,
|
|
36
|
+
LoginUser,
|
|
37
|
+
LoginResponse,
|
|
38
|
+
RefreshResponse,
|
|
39
|
+
Ticker,
|
|
40
|
+
OrderBook,
|
|
41
|
+
OrderBookOptions,
|
|
42
|
+
SymbolConfig,
|
|
43
|
+
OpenInterest,
|
|
44
|
+
Trade,
|
|
45
|
+
RecentTradesOptions,
|
|
46
|
+
ActiveOrder,
|
|
47
|
+
ActiveOrdersOptions,
|
|
48
|
+
CancelOrderResponse,
|
|
49
|
+
CancelAllOrdersOptions,
|
|
50
|
+
CancelAllOrdersResponse,
|
|
51
|
+
DisclosuresResponse,
|
|
52
|
+
};
|
|
53
|
+
export { setApiKey, getUserId } from "./tokenStore.js";
|
|
54
|
+
export { setWalletAdapter } from "../../src/config/index.js";
|
|
55
|
+
|
|
56
|
+
// ── Initialization ──
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Initialize the Temple SDK — sets config and optionally authenticates with the REST API.
|
|
60
|
+
* Pass `API_EMAIL`/`API_PASSWORD` or `API_KEY` to authenticate eagerly at init time.
|
|
61
|
+
*
|
|
62
|
+
* @returns The login response on success, or an ApiError on failure.
|
|
63
|
+
* Returns `null` if no API credentials were provided (config-only init).
|
|
64
|
+
*/
|
|
65
|
+
export async function initialize(cfg: Record<string, unknown>): Promise<LoginResponse | ApiError | null> {
|
|
66
|
+
initializeConfig(cfg);
|
|
67
|
+
|
|
68
|
+
if (cfg.WALLET_ADAPTER) {
|
|
69
|
+
setWalletAdapter(cfg.WALLET_ADAPTER);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const apiKey = cfg.API_KEY as string | undefined;
|
|
73
|
+
if (apiKey) {
|
|
74
|
+
setApiKeyFromConfig(apiKey);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const email = cfg.API_EMAIL as string | undefined;
|
|
79
|
+
const password = cfg.API_PASSWORD as string | undefined;
|
|
80
|
+
if (email && password) {
|
|
81
|
+
return login(email, password);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Internal helpers ──
|
|
88
|
+
|
|
89
|
+
/** Pending auto-login promise for dedup. */
|
|
90
|
+
let autoLoginPromise: Promise<LoginResponse | ApiError> | null = null;
|
|
91
|
+
|
|
92
|
+
function handleApiError(error: unknown, context: string): ApiError {
|
|
93
|
+
const err = error as { response?: { status?: number; data?: { message?: string; error?: string; code?: string } }; message?: string };
|
|
94
|
+
const status = err.response?.status ?? null;
|
|
95
|
+
const message =
|
|
96
|
+
err.response?.data?.message ??
|
|
97
|
+
err.response?.data?.error ??
|
|
98
|
+
err.message ??
|
|
99
|
+
"Unknown error";
|
|
100
|
+
const code = err.response?.data?.code ?? null;
|
|
101
|
+
|
|
102
|
+
console.error(`[Temple API] ${context}: ${message}${status ? ` (HTTP ${status})` : ""}`);
|
|
103
|
+
|
|
104
|
+
return { error: true, status, code, message };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Pending refresh promise for dedup. */
|
|
108
|
+
let refreshPromise: Promise<RefreshResponse | ApiError> | null = null;
|
|
109
|
+
|
|
110
|
+
async function ensureAuthenticated(): Promise<ApiError | null> {
|
|
111
|
+
// API key takes priority — no login needed
|
|
112
|
+
const configApiKey = (config as Record<string, unknown>).API_KEY as string | undefined;
|
|
113
|
+
if (!getApiKey() && configApiKey) {
|
|
114
|
+
setApiKeyFromConfig(configApiKey);
|
|
115
|
+
}
|
|
116
|
+
if (getApiKey()) return null;
|
|
117
|
+
|
|
118
|
+
// Auto-login from config if no token exists
|
|
119
|
+
if (!getAccessToken()) {
|
|
120
|
+
const email = (config as Record<string, unknown>).API_EMAIL as string | undefined;
|
|
121
|
+
const password = (config as Record<string, unknown>).API_PASSWORD as string | undefined;
|
|
122
|
+
if (email && password) {
|
|
123
|
+
if (!autoLoginPromise) {
|
|
124
|
+
autoLoginPromise = login(email, password).finally(() => {
|
|
125
|
+
autoLoginPromise = null;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const result = await autoLoginPromise;
|
|
129
|
+
if ("error" in result && result.error) return result as ApiError;
|
|
130
|
+
} else {
|
|
131
|
+
return { error: true, status: 401, code: "NOT_AUTHENTICATED", message: "Not logged in. Call login() or provide API_EMAIL/API_PASSWORD in config." };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Auto-refresh if expired
|
|
136
|
+
if (isTokenExpired()) {
|
|
137
|
+
if (!refreshPromise) {
|
|
138
|
+
refreshPromise = refreshAccessToken().finally(() => {
|
|
139
|
+
refreshPromise = null;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const result = await refreshPromise;
|
|
143
|
+
if ("error" in result && result.error) return result as ApiError;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function authenticatedGet<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T | ApiError> {
|
|
150
|
+
const authError = await ensureAuthenticated();
|
|
151
|
+
if (authError) return authError;
|
|
152
|
+
|
|
153
|
+
const headers = {
|
|
154
|
+
...getAuthHeader(),
|
|
155
|
+
"Content-Type": "application/json",
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Filter out undefined params
|
|
159
|
+
const cleanParams: Record<string, string | number> = {};
|
|
160
|
+
if (params) {
|
|
161
|
+
for (const [key, value] of Object.entries(params)) {
|
|
162
|
+
if (value !== undefined) cleanParams[key] = value;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const response = await axios.get<T>(`${config.API_BASE_URL}${path}`, {
|
|
168
|
+
headers,
|
|
169
|
+
params: Object.keys(cleanParams).length > 0 ? cleanParams : undefined,
|
|
170
|
+
} as AxiosRequestConfig);
|
|
171
|
+
return response.data;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return handleApiError(error, `GET ${path}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function authenticatedPost<T>(path: string, body?: Record<string, unknown>): Promise<T | ApiError> {
|
|
178
|
+
const authError = await ensureAuthenticated();
|
|
179
|
+
if (authError) return authError;
|
|
180
|
+
|
|
181
|
+
const headers = {
|
|
182
|
+
...getAuthHeader(),
|
|
183
|
+
"Content-Type": "application/json",
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const response = await axios.post<T>(`${config.API_BASE_URL}${path}`, body ?? {}, { headers } as AxiosRequestConfig);
|
|
188
|
+
return response.data;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return handleApiError(error, `POST ${path}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Auth ──
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Login to the Temple REST API with email and password.
|
|
198
|
+
* Stores tokens automatically for subsequent API calls.
|
|
199
|
+
* @param email - User email
|
|
200
|
+
* @param password - User password
|
|
201
|
+
*/
|
|
202
|
+
export async function login(email: string, password: string): Promise<LoginResponse | ApiError> {
|
|
203
|
+
if (!email || !password) {
|
|
204
|
+
return { error: true, status: null, code: "INVALID_PARAMS", message: "Email and password are required." };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const response = await axios.post<LoginResponse>(`${config.API_BASE_URL}/auth/login`, {
|
|
209
|
+
email,
|
|
210
|
+
password,
|
|
211
|
+
});
|
|
212
|
+
setTokens(response.data);
|
|
213
|
+
return response.data;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return handleApiError(error, "login");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Refresh the access token using the stored or provided refresh token.
|
|
221
|
+
* Updates the token store automatically on success.
|
|
222
|
+
* @param refreshTokenOverride - Optional explicit refresh token (uses stored one if omitted)
|
|
223
|
+
*/
|
|
224
|
+
export async function refreshAccessToken(refreshTokenOverride?: string): Promise<RefreshResponse | ApiError> {
|
|
225
|
+
const token = refreshTokenOverride ?? getRefreshToken();
|
|
226
|
+
if (!token) {
|
|
227
|
+
return { error: true, status: null, code: "NO_REFRESH_TOKEN", message: "No refresh token available. Call login() first." };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const response = await axios.post<RefreshResponse>(`${config.API_BASE_URL}/auth/refresh`, {
|
|
232
|
+
refresh_token: token,
|
|
233
|
+
});
|
|
234
|
+
setTokens(response.data);
|
|
235
|
+
return response.data;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return handleApiError(error, "refreshAccessToken");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Logout: clear all stored REST API tokens.
|
|
243
|
+
*/
|
|
244
|
+
export function logout(): void {
|
|
245
|
+
clearTokens();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Market Data ──
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get ticker data for one or all trading pairs.
|
|
252
|
+
* @param symbol - Optional symbol filter (e.g., "Amulet/USDCx")
|
|
253
|
+
*/
|
|
254
|
+
export async function getTicker(symbol?: string): Promise<Ticker | Ticker[] | ApiError> {
|
|
255
|
+
return authenticatedGet("/api/v1/market/ticker", { symbol });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get the order book for a trading pair.
|
|
260
|
+
* @param symbol - Trading pair symbol (required)
|
|
261
|
+
* @param options - Optional levels and precision parameters
|
|
262
|
+
*/
|
|
263
|
+
export async function getOrderBook(symbol: string, options: OrderBookOptions = {}): Promise<OrderBook | ApiError> {
|
|
264
|
+
if (!symbol) {
|
|
265
|
+
return { error: true, status: null, code: "INVALID_PARAMS", message: "Symbol is required." };
|
|
266
|
+
}
|
|
267
|
+
return authenticatedGet("/api/v1/market/orderbook", {
|
|
268
|
+
symbol,
|
|
269
|
+
levels: options.levels,
|
|
270
|
+
precision: options.precision,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get symbol configuration (paused status, decimals, minimum quantity).
|
|
276
|
+
* @param symbol - Trading pair symbol (required)
|
|
277
|
+
*/
|
|
278
|
+
export async function getSymbolConfig(symbol: string): Promise<SymbolConfig | ApiError> {
|
|
279
|
+
if (!symbol) {
|
|
280
|
+
return { error: true, status: null, code: "INVALID_PARAMS", message: "Symbol is required." };
|
|
281
|
+
}
|
|
282
|
+
return authenticatedGet("/api/v1/trading/symbol-config", { symbol });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get open interest for a trading pair.
|
|
287
|
+
* @param symbol - Trading pair symbol (required)
|
|
288
|
+
*/
|
|
289
|
+
export async function getOpenInterest(symbol: string): Promise<OpenInterest | ApiError> {
|
|
290
|
+
if (!symbol) {
|
|
291
|
+
return { error: true, status: null, code: "INVALID_PARAMS", message: "Symbol is required." };
|
|
292
|
+
}
|
|
293
|
+
return authenticatedGet("/api/v1/market/open-interest", { symbol });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get recent trades for a trading pair.
|
|
298
|
+
* @param symbol - Trading pair symbol (required)
|
|
299
|
+
* @param options - Optional limit (max 500)
|
|
300
|
+
*/
|
|
301
|
+
export async function getRecentTrades(symbol: string, options: RecentTradesOptions = {}): Promise<Trade[] | ApiError> {
|
|
302
|
+
if (!symbol) {
|
|
303
|
+
return { error: true, status: null, code: "INVALID_PARAMS", message: "Symbol is required." };
|
|
304
|
+
}
|
|
305
|
+
return authenticatedGet("/api/v1/market/trades", {
|
|
306
|
+
symbol,
|
|
307
|
+
limit: options.limit,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Orders ──
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get active orders, optionally filtered by symbol.
|
|
315
|
+
* @param options - Optional symbol and limit filters
|
|
316
|
+
*/
|
|
317
|
+
export async function getActiveOrders(options: ActiveOrdersOptions = {}): Promise<ActiveOrder[] | ApiError> {
|
|
318
|
+
return authenticatedGet("/api/trading/orders/active", {
|
|
319
|
+
symbol: options.symbol,
|
|
320
|
+
limit: options.limit,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Cancel a specific order by ID.
|
|
326
|
+
* @param orderId - The order ID to cancel
|
|
327
|
+
*/
|
|
328
|
+
export async function cancelOrder(orderId: string): Promise<CancelOrderResponse | ApiError> {
|
|
329
|
+
if (!orderId) {
|
|
330
|
+
return { error: true, status: null, code: "INVALID_PARAMS", message: "Order ID is required." };
|
|
331
|
+
}
|
|
332
|
+
return authenticatedPost(`/api/trading/orders/${encodeURIComponent(orderId)}/cancel`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Cancel all active orders, optionally filtered by symbol.
|
|
337
|
+
* @param options - Optional symbol filter
|
|
338
|
+
*/
|
|
339
|
+
export async function cancelAllOrders(options: CancelAllOrdersOptions = {}): Promise<CancelAllOrdersResponse | ApiError> {
|
|
340
|
+
return authenticatedPost("/api/v1/trading/orders/cancel-all", options.symbol ? { symbol: options.symbol } : undefined);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── Disclosures ──
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get amulet disclosure data for a party from the Temple REST API.
|
|
347
|
+
* Returns protocol disclosures (amulet rules, open mining rounds, external party amulet rules).
|
|
348
|
+
* @param partyId - Canton party ID
|
|
349
|
+
*/
|
|
350
|
+
export async function getDisclosures(partyId: string): Promise<DisclosuresResponse | ApiError> {
|
|
351
|
+
if (!partyId) {
|
|
352
|
+
return { error: true, status: null, code: "INVALID_PARAMS", message: "Party ID is required." };
|
|
353
|
+
}
|
|
354
|
+
return authenticatedGet("/api/amulet/disclosures", { partyId });
|
|
355
|
+
}
|