@aetherwealth/sdk 0.1.32
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/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/client.d.ts +130 -0
- package/dist/client.js +429 -0
- package/dist/errors.d.ts +131 -0
- package/dist/errors.js +178 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/resources/accounts.d.ts +8 -0
- package/dist/resources/accounts.js +84 -0
- package/dist/resources/alerts.d.ts +8 -0
- package/dist/resources/alerts.js +178 -0
- package/dist/resources/diary.d.ts +8 -0
- package/dist/resources/diary.js +75 -0
- package/dist/resources/envelope.d.ts +7 -0
- package/dist/resources/envelope.js +15 -0
- package/dist/resources/idempotency.d.ts +15 -0
- package/dist/resources/idempotency.js +19 -0
- package/dist/resources/market.d.ts +8 -0
- package/dist/resources/market.js +90 -0
- package/dist/resources/paginate.d.ts +15 -0
- package/dist/resources/paginate.js +41 -0
- package/dist/resources/stats.d.ts +8 -0
- package/dist/resources/stats.js +28 -0
- package/dist/resources/trades.d.ts +8 -0
- package/dist/resources/trades.js +113 -0
- package/dist/resources/validate.d.ts +12 -0
- package/dist/resources/validate.js +22 -0
- package/dist/retry.d.ts +40 -0
- package/dist/retry.js +110 -0
- package/dist/schemas.d.ts +286 -0
- package/dist/schemas.js +239 -0
- package/dist/types.d.ts +528 -0
- package/dist/types.js +12 -0
- package/package.json +55 -0
package/dist/errors.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error hierarchy for the AetherWealth SDK.
|
|
3
|
+
*
|
|
4
|
+
* All HTTP errors thrown by `AetherClient` are subclasses of
|
|
5
|
+
* `AetherApiError`, so callers can do
|
|
6
|
+
* `catch (err) { if (err instanceof AetherApiError) … }`. The 4xx
|
|
7
|
+
* subtypes let callers branch without sniffing status codes.
|
|
8
|
+
*
|
|
9
|
+
* `AetherNetworkError` is intentionally NOT a subclass of
|
|
10
|
+
* `AetherApiError` — it has no HTTP status because the request never
|
|
11
|
+
* reached the server (DNS failure, connection refused, TLS rejection).
|
|
12
|
+
*/
|
|
13
|
+
export class AetherApiError extends Error {
|
|
14
|
+
status;
|
|
15
|
+
code;
|
|
16
|
+
path;
|
|
17
|
+
method;
|
|
18
|
+
responseBody;
|
|
19
|
+
constructor(message, opts) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'AetherApiError';
|
|
22
|
+
this.status = opts.status;
|
|
23
|
+
this.code = opts.code;
|
|
24
|
+
this.path = opts.path;
|
|
25
|
+
this.method = opts.method;
|
|
26
|
+
this.responseBody = opts.responseBody;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class AetherAuthError extends AetherApiError {
|
|
30
|
+
status = 401;
|
|
31
|
+
constructor(opts) {
|
|
32
|
+
super(opts.message, {
|
|
33
|
+
status: 401,
|
|
34
|
+
code: opts.code,
|
|
35
|
+
path: opts.path,
|
|
36
|
+
method: opts.method,
|
|
37
|
+
responseBody: opts.responseBody
|
|
38
|
+
});
|
|
39
|
+
this.name = 'AetherAuthError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export class AetherForbiddenError extends AetherApiError {
|
|
43
|
+
status = 403;
|
|
44
|
+
constructor(opts) {
|
|
45
|
+
super(opts.message, {
|
|
46
|
+
status: 403,
|
|
47
|
+
code: opts.code,
|
|
48
|
+
path: opts.path,
|
|
49
|
+
method: opts.method,
|
|
50
|
+
responseBody: opts.responseBody
|
|
51
|
+
});
|
|
52
|
+
this.name = 'AetherForbiddenError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class AetherNotFoundError extends AetherApiError {
|
|
56
|
+
status = 404;
|
|
57
|
+
constructor(opts) {
|
|
58
|
+
super(opts.message, {
|
|
59
|
+
status: 404,
|
|
60
|
+
code: opts.code,
|
|
61
|
+
path: opts.path,
|
|
62
|
+
method: opts.method,
|
|
63
|
+
responseBody: opts.responseBody
|
|
64
|
+
});
|
|
65
|
+
this.name = 'AetherNotFoundError';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export class AetherValidationError extends AetherApiError {
|
|
69
|
+
constructor(opts) {
|
|
70
|
+
super(opts.message, {
|
|
71
|
+
status: opts.status ?? 400,
|
|
72
|
+
code: opts.code,
|
|
73
|
+
path: opts.path,
|
|
74
|
+
method: opts.method,
|
|
75
|
+
responseBody: opts.responseBody
|
|
76
|
+
});
|
|
77
|
+
this.name = 'AetherValidationError';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export class AetherRateLimitError extends AetherApiError {
|
|
81
|
+
status = 429;
|
|
82
|
+
retryAfterSeconds;
|
|
83
|
+
constructor(opts) {
|
|
84
|
+
super(opts.message, {
|
|
85
|
+
status: 429,
|
|
86
|
+
code: opts.code,
|
|
87
|
+
path: opts.path,
|
|
88
|
+
method: opts.method,
|
|
89
|
+
responseBody: opts.responseBody
|
|
90
|
+
});
|
|
91
|
+
this.name = 'AetherRateLimitError';
|
|
92
|
+
this.retryAfterSeconds = opts.retryAfterSeconds;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export class AetherNetworkError extends Error {
|
|
96
|
+
cause;
|
|
97
|
+
path;
|
|
98
|
+
method;
|
|
99
|
+
constructor(opts) {
|
|
100
|
+
super(opts.message);
|
|
101
|
+
this.name = 'AetherNetworkError';
|
|
102
|
+
this.cause = opts.cause;
|
|
103
|
+
this.path = opts.path;
|
|
104
|
+
this.method = opts.method;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Thrown when a request is aborted by the client's own timeout (not by a
|
|
109
|
+
* caller-supplied signal). Subclasses `AetherNetworkError` so existing
|
|
110
|
+
* `catch (e) { if (e instanceof AetherNetworkError) … }` blocks — and
|
|
111
|
+
* `withRetry` — treat a timeout as the transient network condition it is.
|
|
112
|
+
*
|
|
113
|
+
* `timeoutMs` is the configured budget; `elapsedMs` is how long the request
|
|
114
|
+
* actually ran before the abort fired (useful for logging/telemetry).
|
|
115
|
+
*/
|
|
116
|
+
export class AetherTimeoutError extends AetherNetworkError {
|
|
117
|
+
timeoutMs;
|
|
118
|
+
elapsedMs;
|
|
119
|
+
constructor(opts) {
|
|
120
|
+
super({ message: opts.message, cause: opts.cause, path: opts.path, method: opts.method });
|
|
121
|
+
this.name = 'AetherTimeoutError';
|
|
122
|
+
this.timeoutMs = opts.timeoutMs;
|
|
123
|
+
this.elapsedMs = opts.elapsedMs;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Parse the `Retry-After` header. Per RFC 7231 the value may be either:
|
|
128
|
+
* - a decimal number of seconds (`Retry-After: 120`), or
|
|
129
|
+
* - an HTTP-date (`Retry-After: Sat, 25 Apr 2026 12:00:00 GMT`).
|
|
130
|
+
* Returns the number of seconds from now, or null if absent/unparseable.
|
|
131
|
+
* The `now` argument is injectable for deterministic tests.
|
|
132
|
+
*/
|
|
133
|
+
export function parseRetryAfter(headerValue, now = Date.now) {
|
|
134
|
+
if (!headerValue)
|
|
135
|
+
return null;
|
|
136
|
+
const trimmed = headerValue.trim();
|
|
137
|
+
if (trimmed.length === 0)
|
|
138
|
+
return null;
|
|
139
|
+
const asInt = Number(trimmed);
|
|
140
|
+
if (Number.isFinite(asInt)) {
|
|
141
|
+
return Math.max(0, Math.floor(asInt));
|
|
142
|
+
}
|
|
143
|
+
const asDate = Date.parse(trimmed);
|
|
144
|
+
if (!Number.isFinite(asDate))
|
|
145
|
+
return null;
|
|
146
|
+
return Math.max(0, Math.floor((asDate - now()) / 1000));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Factory: map an HTTP response into the most specific subtype.
|
|
150
|
+
* Preserves the response envelope so callers can inspect `responseBody`,
|
|
151
|
+
* and reads `Retry-After` for 429s when headers are available.
|
|
152
|
+
*/
|
|
153
|
+
export function classifyError(args) {
|
|
154
|
+
const { status, body, path, method, headers } = args;
|
|
155
|
+
const envelope = body && typeof body === 'object' && body !== null ? body : {};
|
|
156
|
+
// Backend convention: {success: false, error: "CODE", message: "human"}.
|
|
157
|
+
// Some Express defaults flip these (error string is the human message)
|
|
158
|
+
// — fall back gracefully when message is missing.
|
|
159
|
+
const rawMessage = typeof envelope['message'] === 'string' ? envelope['message'] : null;
|
|
160
|
+
const rawCode = typeof envelope['error'] === 'string' ? envelope['error'] : null;
|
|
161
|
+
const message = rawMessage ?? rawCode ?? `Request failed with status ${status}`;
|
|
162
|
+
const code = rawCode ?? `HTTP_${status}`;
|
|
163
|
+
const opts = { message, code, path, method, responseBody: body };
|
|
164
|
+
if (status === 401)
|
|
165
|
+
return new AetherAuthError(opts);
|
|
166
|
+
if (status === 403)
|
|
167
|
+
return new AetherForbiddenError(opts);
|
|
168
|
+
if (status === 404)
|
|
169
|
+
return new AetherNotFoundError(opts);
|
|
170
|
+
if (status === 400 || status === 422) {
|
|
171
|
+
return new AetherValidationError({ ...opts, status: status });
|
|
172
|
+
}
|
|
173
|
+
if (status === 429) {
|
|
174
|
+
const retryAfterSeconds = parseRetryAfter(headers?.get('retry-after') ?? null, args.now);
|
|
175
|
+
return new AetherRateLimitError({ ...opts, retryAfterSeconds });
|
|
176
|
+
}
|
|
177
|
+
return new AetherApiError(message, { status, code, path, method, responseBody: body });
|
|
178
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @aetherwealth/sdk
|
|
3
|
+
*
|
|
4
|
+
* Typed TypeScript client for the AetherWealth **public API**
|
|
5
|
+
* (`/api/public/v1/<domain>/<action>`). Reusable in any Node/Bun/edge runtime
|
|
6
|
+
* that needs programmatic access to AetherWealth data.
|
|
7
|
+
*
|
|
8
|
+
* Auth — every public route is authenticated with a **public API key**
|
|
9
|
+
* (`aw_live_…`) sent as `Authorization: Bearer …`:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const client = new AetherClient({
|
|
13
|
+
* auth: {type: 'apiKey', apiKey: process.env.AETHER_API_KEY!}
|
|
14
|
+
* // baseUrl defaults to the production API; set it only for staging/local dev.
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* `auth` is a discriminated union (one member today, `apiKey`) so future
|
|
19
|
+
* modes can be added without breaking callers. See the README for setup.
|
|
20
|
+
*/
|
|
21
|
+
export type { AetherAuth, AetherClientConfig, AetherFetchInit, ApiKeyAuth } from './client.js';
|
|
22
|
+
export { AetherClient, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS } from './client.js';
|
|
23
|
+
export { AetherApiError, AetherAuthError, AetherForbiddenError, AetherNetworkError, AetherNotFoundError, AetherRateLimitError, AetherTimeoutError, AetherValidationError, classifyError, parseRetryAfter } from './errors.js';
|
|
24
|
+
export type { RetryOptions } from './retry.js';
|
|
25
|
+
export { computeDelay, withRetry } from './retry.js';
|
|
26
|
+
export { accountSchema, alertSchema, dedupModeSchema, diaryEntrySchema, economicEventSchema, indicatorAlertSchema, indicatorConditionSchema, macroSeriesPointSchema, macroSeriesResultSchema, marketConfigSchema, paginationSchema, parseAccount, parseAlert, parseDiaryEntry, parseEconomicEvent, parseIndicatorAlert, parseMacroSeriesResult, parseMarketConfig, parseTrade, parseTradingStats, priceConditionSchema, tradeDirectionSchema, tradeSchema, tradeStatusSchema, tradingStatsSchema, triggerTypeSchema } from './schemas.js';
|
|
27
|
+
export type { Account, AccountsResource, Alert, AlertsResource, AlertType, ApiEnvelope, CalendarQuery, CalendarResult, CloseTradeInput, CreateAccountInput, CreateIndicatorAlertInput, CreatePriceAlertInput, CreateTradeInput, CreateTrendlineAlertInput, DedupMode, DiaryEntry, DiaryListQuery, DiaryResource, EconomicEvent, EconomicEventMinimal, EconomicEventRich, IdempotencyOptions, IndicatorAlert, IndicatorCondition, ListAlertsQuery, MacroQuery, MacroRange, MacroSeriesQuery, MacroSeriesResult, MarketConfig, MarketResource, PaginatedResponse, PaginationMeta, PriceCondition, SeriesOp, StatsQuery, StatsResource, ThresholdOp, Trade, TradeDirection, TradeListQuery, TradeStatus, TradeStatusFilter, TradesResource, TradingStats, TriggerType, UpdateAccountInput, UpdateAlertInput, UpdateIndicatorAlertInput, UpdateTradeInput, UpsertDiaryInput } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @aetherwealth/sdk
|
|
3
|
+
*
|
|
4
|
+
* Typed TypeScript client for the AetherWealth **public API**
|
|
5
|
+
* (`/api/public/v1/<domain>/<action>`). Reusable in any Node/Bun/edge runtime
|
|
6
|
+
* that needs programmatic access to AetherWealth data.
|
|
7
|
+
*
|
|
8
|
+
* Auth — every public route is authenticated with a **public API key**
|
|
9
|
+
* (`aw_live_…`) sent as `Authorization: Bearer …`:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const client = new AetherClient({
|
|
13
|
+
* auth: {type: 'apiKey', apiKey: process.env.AETHER_API_KEY!}
|
|
14
|
+
* // baseUrl defaults to the production API; set it only for staging/local dev.
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* `auth` is a discriminated union (one member today, `apiKey`) so future
|
|
19
|
+
* modes can be added without breaking callers. See the README for setup.
|
|
20
|
+
*/
|
|
21
|
+
export { AetherClient, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS } from './client.js';
|
|
22
|
+
export { AetherApiError, AetherAuthError, AetherForbiddenError, AetherNetworkError, AetherNotFoundError, AetherRateLimitError, AetherTimeoutError, AetherValidationError, classifyError, parseRetryAfter } from './errors.js';
|
|
23
|
+
export { computeDelay, withRetry } from './retry.js';
|
|
24
|
+
export { accountSchema, alertSchema, dedupModeSchema, diaryEntrySchema, economicEventSchema, indicatorAlertSchema, indicatorConditionSchema, macroSeriesPointSchema, macroSeriesResultSchema, marketConfigSchema, paginationSchema, parseAccount, parseAlert, parseDiaryEntry, parseEconomicEvent, parseIndicatorAlert, parseMacroSeriesResult, parseMarketConfig, parseTrade, parseTradingStats, priceConditionSchema, tradeDirectionSchema, tradeSchema, tradeStatusSchema, tradingStatsSchema, triggerTypeSchema } from './schemas.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AetherClient } from '../client.js';
|
|
2
|
+
import type { AccountsResource } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* `accounts` — public API `POST /api/public/v1/accounts/*`. `trades()` is a
|
|
5
|
+
* convenience wrapper over the trades-list endpoint filtered by account (there
|
|
6
|
+
* is no dedicated per-account trades route).
|
|
7
|
+
*/
|
|
8
|
+
export declare function createAccountsResource(client: AetherClient): AccountsResource;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { parseAccount, parseTrade } from '../schemas.js';
|
|
2
|
+
import { pluck } from './envelope.js';
|
|
3
|
+
import { resolveIdempotencyKey } from './idempotency.js';
|
|
4
|
+
import { flatten, paginate } from './paginate.js';
|
|
5
|
+
import { assertEachShape, assertShape } from './validate.js';
|
|
6
|
+
/**
|
|
7
|
+
* `accounts` — public API `POST /api/public/v1/accounts/*`. `trades()` is a
|
|
8
|
+
* convenience wrapper over the trades-list endpoint filtered by account (there
|
|
9
|
+
* is no dedicated per-account trades route).
|
|
10
|
+
*/
|
|
11
|
+
export function createAccountsResource(client) {
|
|
12
|
+
const resource = {
|
|
13
|
+
async list() {
|
|
14
|
+
const env = await client.request('/api/public/v1/accounts/list', {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
body: {}
|
|
17
|
+
});
|
|
18
|
+
return assertEachShape(client.validateResponses, parseAccount, pluck(env, 'accounts'));
|
|
19
|
+
},
|
|
20
|
+
async create(input, opts) {
|
|
21
|
+
if (!input.name || input.name.trim().length === 0) {
|
|
22
|
+
throw new Error('accounts.create: name is required');
|
|
23
|
+
}
|
|
24
|
+
const env = await client.request('/api/public/v1/accounts/create', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
body: input,
|
|
27
|
+
idempotencyKey: resolveIdempotencyKey(opts)
|
|
28
|
+
});
|
|
29
|
+
return assertShape(client.validateResponses, parseAccount, pluck(env, 'account'));
|
|
30
|
+
},
|
|
31
|
+
async update(accountId, input) {
|
|
32
|
+
if (!accountId)
|
|
33
|
+
throw new Error('accounts.update: accountId is required');
|
|
34
|
+
const env = await client.request('/api/public/v1/accounts/update', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
body: { id: accountId, ...input }
|
|
37
|
+
});
|
|
38
|
+
return assertShape(client.validateResponses, parseAccount, pluck(env, 'account'));
|
|
39
|
+
},
|
|
40
|
+
async delete(accountId) {
|
|
41
|
+
if (!accountId)
|
|
42
|
+
throw new Error('accounts.delete: accountId is required');
|
|
43
|
+
// Resolves on 2xx; a missing account throws AetherNotFoundError upstream.
|
|
44
|
+
await client.request('/api/public/v1/accounts/delete', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: { id: accountId }
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
async trades(accountId, query) {
|
|
50
|
+
if (!accountId)
|
|
51
|
+
throw new Error('accounts.trades: accountId is required');
|
|
52
|
+
const body = { accountId };
|
|
53
|
+
// 'ALL' is a "no filter" sentinel; the backend rejects it (enum is
|
|
54
|
+
// OPEN|CLOSED|CANCELLED), so omit it.
|
|
55
|
+
if (query?.status !== undefined && query.status !== 'ALL') {
|
|
56
|
+
body['status'] = query.status;
|
|
57
|
+
}
|
|
58
|
+
if (query?.pair !== undefined)
|
|
59
|
+
body['pair'] = query.pair;
|
|
60
|
+
if (query?.from !== undefined)
|
|
61
|
+
body['from'] = query.from;
|
|
62
|
+
if (query?.to !== undefined)
|
|
63
|
+
body['to'] = query.to;
|
|
64
|
+
if (query?.page !== undefined)
|
|
65
|
+
body['page'] = query.page;
|
|
66
|
+
if (query?.limit !== undefined)
|
|
67
|
+
body['limit'] = query.limit;
|
|
68
|
+
const env = await client.request('/api/public/v1/trades/list', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
body
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
data: assertEachShape(client.validateResponses, parseTrade, pluck(env, 'trades')),
|
|
74
|
+
pagination: pluck(env, 'pagination')
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
tradesAll(accountId, query) {
|
|
78
|
+
if (!accountId)
|
|
79
|
+
throw new Error('accounts.tradesAll: accountId is required');
|
|
80
|
+
return flatten(paginate(page => resource.trades(accountId, { ...query, page }), query?.page ?? 1));
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return resource;
|
|
84
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AetherClient } from '../client.js';
|
|
2
|
+
import type { AlertsResource } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* `alerts` — public API `POST /api/public/v1/alerts/*`. Price and trendline
|
|
5
|
+
* alerts share the `alerts` surface; indicator alerts live under the
|
|
6
|
+
* `indicator/*` sub-paths and return the `indicatorAlerts`/`indicatorAlert` key.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createAlertsResource(client: AetherClient): AlertsResource;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { parseAlert, parseIndicatorAlert } from '../schemas.js';
|
|
2
|
+
import { pluck } from './envelope.js';
|
|
3
|
+
import { resolveIdempotencyKey } from './idempotency.js';
|
|
4
|
+
import { assertEachShape, assertShape } from './validate.js';
|
|
5
|
+
const PRICE_CONDITIONS = new Set(['above', 'below', 'crosses']);
|
|
6
|
+
const TRIGGER_TYPES = new Set(['close', 'wick']);
|
|
7
|
+
const DEDUP_MODES = new Set(['edge', 'continuous']);
|
|
8
|
+
const SERIES_OPS = new Set([
|
|
9
|
+
'gt_series',
|
|
10
|
+
'lt_series',
|
|
11
|
+
'crosses_above_series',
|
|
12
|
+
'crosses_below_series'
|
|
13
|
+
]);
|
|
14
|
+
function listBody(query) {
|
|
15
|
+
const body = {};
|
|
16
|
+
if (query?.includeArchived !== undefined)
|
|
17
|
+
body['includeArchived'] = query.includeArchived;
|
|
18
|
+
if (query?.pair !== undefined)
|
|
19
|
+
body['pair'] = query.pair;
|
|
20
|
+
return body;
|
|
21
|
+
}
|
|
22
|
+
/** Required scalar fields + enum checks shared by every indicator-alert create. */
|
|
23
|
+
function assertIndicatorBase(input) {
|
|
24
|
+
if (!input.pair)
|
|
25
|
+
throw new Error('alerts.createIndicator: pair is required');
|
|
26
|
+
if (!input.timeframe)
|
|
27
|
+
throw new Error('alerts.createIndicator: timeframe is required');
|
|
28
|
+
if (!input.indicatorType) {
|
|
29
|
+
throw new Error('alerts.createIndicator: indicatorType is required');
|
|
30
|
+
}
|
|
31
|
+
if (!input.outputSeries) {
|
|
32
|
+
throw new Error('alerts.createIndicator: outputSeries is required');
|
|
33
|
+
}
|
|
34
|
+
if (!TRIGGER_TYPES.has(input.triggerType)) {
|
|
35
|
+
throw new Error('alerts.createIndicator: triggerType must be close or wick');
|
|
36
|
+
}
|
|
37
|
+
if (!DEDUP_MODES.has(input.dedupMode)) {
|
|
38
|
+
throw new Error('alerts.createIndicator: dedupMode must be edge or continuous');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The threshold/series split is load-bearing: a series op with a stray
|
|
43
|
+
* `threshold` (or a threshold op missing one) makes the evaluator read an
|
|
44
|
+
* undefined value and the alert silently never fires.
|
|
45
|
+
*/
|
|
46
|
+
function assertIndicatorCondition(condition) {
|
|
47
|
+
const cond = condition;
|
|
48
|
+
if (!cond || !cond.op) {
|
|
49
|
+
throw new Error('alerts.createIndicator: condition.op is required');
|
|
50
|
+
}
|
|
51
|
+
if (SERIES_OPS.has(cond.op)) {
|
|
52
|
+
if (!cond.other) {
|
|
53
|
+
throw new Error(`alerts.createIndicator: series op "${cond.op}" requires a non-empty "other" series`);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!Number.isFinite(cond.threshold)) {
|
|
58
|
+
throw new Error(`alerts.createIndicator: threshold op "${cond.op}" requires a numeric "threshold"`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* `alerts` — public API `POST /api/public/v1/alerts/*`. Price and trendline
|
|
63
|
+
* alerts share the `alerts` surface; indicator alerts live under the
|
|
64
|
+
* `indicator/*` sub-paths and return the `indicatorAlerts`/`indicatorAlert` key.
|
|
65
|
+
*/
|
|
66
|
+
export function createAlertsResource(client) {
|
|
67
|
+
return {
|
|
68
|
+
async list(query) {
|
|
69
|
+
const env = await client.request('/api/public/v1/alerts/list', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
body: listBody(query)
|
|
72
|
+
});
|
|
73
|
+
return assertEachShape(client.validateResponses, parseAlert, pluck(env, 'alerts'));
|
|
74
|
+
},
|
|
75
|
+
async createPrice(input, opts) {
|
|
76
|
+
if (!input.pair)
|
|
77
|
+
throw new Error('alerts.createPrice: pair is required');
|
|
78
|
+
if (!input.timeframe)
|
|
79
|
+
throw new Error('alerts.createPrice: timeframe is required');
|
|
80
|
+
if (!Number.isFinite(input.price)) {
|
|
81
|
+
throw new Error('alerts.createPrice: price must be a finite number');
|
|
82
|
+
}
|
|
83
|
+
if (!PRICE_CONDITIONS.has(input.condition)) {
|
|
84
|
+
throw new Error('alerts.createPrice: condition must be above, below, or crosses');
|
|
85
|
+
}
|
|
86
|
+
if (!TRIGGER_TYPES.has(input.triggerType)) {
|
|
87
|
+
throw new Error('alerts.createPrice: triggerType must be close or wick');
|
|
88
|
+
}
|
|
89
|
+
const env = await client.request('/api/public/v1/alerts/price/create', {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
body: input,
|
|
92
|
+
idempotencyKey: resolveIdempotencyKey(opts)
|
|
93
|
+
});
|
|
94
|
+
return assertShape(client.validateResponses, parseAlert, pluck(env, 'alert'));
|
|
95
|
+
},
|
|
96
|
+
async createTrendline(input, opts) {
|
|
97
|
+
if (!input.pair)
|
|
98
|
+
throw new Error('alerts.createTrendline: pair is required');
|
|
99
|
+
if (!input.timeframe)
|
|
100
|
+
throw new Error('alerts.createTrendline: timeframe is required');
|
|
101
|
+
if (!Number.isFinite(input.price1) || !Number.isFinite(input.price2)) {
|
|
102
|
+
throw new Error('alerts.createTrendline: price1 and price2 must be finite numbers');
|
|
103
|
+
}
|
|
104
|
+
if (!input.time1 || !input.time2) {
|
|
105
|
+
throw new Error('alerts.createTrendline: time1 and time2 are required');
|
|
106
|
+
}
|
|
107
|
+
if (!PRICE_CONDITIONS.has(input.condition)) {
|
|
108
|
+
throw new Error('alerts.createTrendline: condition must be above, below, or crosses');
|
|
109
|
+
}
|
|
110
|
+
if (!TRIGGER_TYPES.has(input.triggerType)) {
|
|
111
|
+
throw new Error('alerts.createTrendline: triggerType must be close or wick');
|
|
112
|
+
}
|
|
113
|
+
const env = await client.request('/api/public/v1/alerts/trendline/create', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
body: input,
|
|
116
|
+
idempotencyKey: resolveIdempotencyKey(opts)
|
|
117
|
+
});
|
|
118
|
+
return assertShape(client.validateResponses, parseAlert, pluck(env, 'alert'));
|
|
119
|
+
},
|
|
120
|
+
async update(alertId, input) {
|
|
121
|
+
if (!alertId)
|
|
122
|
+
throw new Error('alerts.update: alertId is required');
|
|
123
|
+
const env = await client.request('/api/public/v1/alerts/update', {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
body: { id: alertId, ...input }
|
|
126
|
+
});
|
|
127
|
+
return assertShape(client.validateResponses, parseAlert, pluck(env, 'alert'));
|
|
128
|
+
},
|
|
129
|
+
async delete(alertId) {
|
|
130
|
+
if (!alertId)
|
|
131
|
+
throw new Error('alerts.delete: alertId is required');
|
|
132
|
+
// Resolves on 2xx; a missing alert throws AetherNotFoundError upstream.
|
|
133
|
+
await client.request('/api/public/v1/alerts/delete', {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
body: { id: alertId }
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
async listIndicator(query) {
|
|
139
|
+
const env = await client.request('/api/public/v1/alerts/indicator/list', {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
body: listBody(query)
|
|
142
|
+
});
|
|
143
|
+
return assertEachShape(client.validateResponses, parseIndicatorAlert, pluck(env, 'indicatorAlerts'));
|
|
144
|
+
},
|
|
145
|
+
async createIndicator(input, opts) {
|
|
146
|
+
assertIndicatorBase(input);
|
|
147
|
+
assertIndicatorCondition(input.condition);
|
|
148
|
+
const env = await client.request('/api/public/v1/alerts/indicator/create', {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
body: input,
|
|
151
|
+
idempotencyKey: resolveIdempotencyKey(opts)
|
|
152
|
+
});
|
|
153
|
+
return assertShape(client.validateResponses, parseIndicatorAlert, pluck(env, 'indicatorAlert'));
|
|
154
|
+
},
|
|
155
|
+
async updateIndicator(alertId, input) {
|
|
156
|
+
if (!alertId)
|
|
157
|
+
throw new Error('alerts.updateIndicator: alertId is required');
|
|
158
|
+
// Same load-bearing threshold/series split as create — but only when
|
|
159
|
+
// the caller is actually changing the condition (update is partial).
|
|
160
|
+
if (input.condition !== undefined)
|
|
161
|
+
assertIndicatorCondition(input.condition);
|
|
162
|
+
const env = await client.request('/api/public/v1/alerts/indicator/update', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
body: { id: alertId, ...input }
|
|
165
|
+
});
|
|
166
|
+
return assertShape(client.validateResponses, parseIndicatorAlert, pluck(env, 'indicatorAlert'));
|
|
167
|
+
},
|
|
168
|
+
async deleteIndicator(alertId) {
|
|
169
|
+
if (!alertId)
|
|
170
|
+
throw new Error('alerts.deleteIndicator: alertId is required');
|
|
171
|
+
// Resolves on 2xx; a missing alert throws AetherNotFoundError upstream.
|
|
172
|
+
await client.request('/api/public/v1/alerts/indicator/delete', {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
body: { id: alertId }
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AetherClient } from '../client.js';
|
|
2
|
+
import type { DiaryResource } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* `diary` — public API `POST /api/public/v1/diary/*`. Entries are keyed by
|
|
5
|
+
* calendar date (`YYYY-MM-DD`); `upsert` creates or replaces the entry for a
|
|
6
|
+
* date.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createDiaryResource(client: AetherClient): DiaryResource;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { parseDiaryEntry } from '../schemas.js';
|
|
2
|
+
import { pluck } from './envelope.js';
|
|
3
|
+
import { flatten, paginate } from './paginate.js';
|
|
4
|
+
import { assertEachShape, assertShape } from './validate.js';
|
|
5
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
6
|
+
/**
|
|
7
|
+
* `diary` — public API `POST /api/public/v1/diary/*`. Entries are keyed by
|
|
8
|
+
* calendar date (`YYYY-MM-DD`); `upsert` creates or replaces the entry for a
|
|
9
|
+
* date.
|
|
10
|
+
*/
|
|
11
|
+
export function createDiaryResource(client) {
|
|
12
|
+
const resource = {
|
|
13
|
+
async list(query) {
|
|
14
|
+
const body = {};
|
|
15
|
+
if (query?.from !== undefined)
|
|
16
|
+
body['from'] = query.from;
|
|
17
|
+
if (query?.to !== undefined)
|
|
18
|
+
body['to'] = query.to;
|
|
19
|
+
if (query?.page !== undefined)
|
|
20
|
+
body['page'] = query.page;
|
|
21
|
+
if (query?.limit !== undefined)
|
|
22
|
+
body['limit'] = query.limit;
|
|
23
|
+
const env = await client.request('/api/public/v1/diary/list', {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
body
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
data: assertEachShape(client.validateResponses, parseDiaryEntry, pluck(env, 'entries')),
|
|
29
|
+
pagination: pluck(env, 'pagination')
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
async get(date) {
|
|
33
|
+
if (!ISO_DATE.test(date))
|
|
34
|
+
throw new Error('diary.get: date must be YYYY-MM-DD');
|
|
35
|
+
const env = await client.request('/api/public/v1/diary/get', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
body: { date }
|
|
38
|
+
});
|
|
39
|
+
return assertShape(client.validateResponses, parseDiaryEntry, pluck(env, 'entry'));
|
|
40
|
+
},
|
|
41
|
+
async upsert(input) {
|
|
42
|
+
if (!ISO_DATE.test(input.date))
|
|
43
|
+
throw new Error('diary.upsert: date must be YYYY-MM-DD');
|
|
44
|
+
if (!input.content || input.content.trim().length === 0) {
|
|
45
|
+
throw new Error('diary.upsert: content is required');
|
|
46
|
+
}
|
|
47
|
+
if (input.rating !== undefined &&
|
|
48
|
+
input.rating !== null &&
|
|
49
|
+
(input.rating < 1 || input.rating > 5)) {
|
|
50
|
+
throw new Error('diary.upsert: rating must be between 1 and 5');
|
|
51
|
+
}
|
|
52
|
+
const env = await client.request('/api/public/v1/diary/upsert', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
body: input
|
|
55
|
+
});
|
|
56
|
+
return assertShape(client.validateResponses, parseDiaryEntry, pluck(env, 'entry'));
|
|
57
|
+
},
|
|
58
|
+
async delete(date) {
|
|
59
|
+
if (!ISO_DATE.test(date))
|
|
60
|
+
throw new Error('diary.delete: date must be YYYY-MM-DD');
|
|
61
|
+
// Resolves on 2xx; a missing entry throws AetherNotFoundError upstream.
|
|
62
|
+
await client.request('/api/public/v1/diary/delete', {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
body: { date }
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
pages(query) {
|
|
68
|
+
return paginate(page => resource.list({ ...query, page }), query?.page ?? 1);
|
|
69
|
+
},
|
|
70
|
+
listAll(query) {
|
|
71
|
+
return flatten(resource.pages(query));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return resource;
|
|
75
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aether-backend responses are `{success: true, <namedKey>: payload}` envelopes
|
|
3
|
+
* (e.g. `{success, alerts}`, `{success, trade}`, `{success, deleted}`) rather
|
|
4
|
+
* than a generic `{data}`. `client.request` returns the whole object for these
|
|
5
|
+
* (it only auto-unwraps a literal `data` key); resources pluck their named key.
|
|
6
|
+
*/
|
|
7
|
+
export declare function pluck<T>(envelope: unknown, key: string): T;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aether-backend responses are `{success: true, <namedKey>: payload}` envelopes
|
|
3
|
+
* (e.g. `{success, alerts}`, `{success, trade}`, `{success, deleted}`) rather
|
|
4
|
+
* than a generic `{data}`. `client.request` returns the whole object for these
|
|
5
|
+
* (it only auto-unwraps a literal `data` key); resources pluck their named key.
|
|
6
|
+
*/
|
|
7
|
+
export function pluck(envelope, key) {
|
|
8
|
+
// `Object.hasOwn` (not `key in envelope`) so an unwrapped array can't match
|
|
9
|
+
// an `Array.prototype` method — e.g. `'entries' in [x]` is `true`, which
|
|
10
|
+
// would otherwise return `Array.prototype.entries` instead of throwing.
|
|
11
|
+
if (typeof envelope !== 'object' || envelope === null || !Object.hasOwn(envelope, key)) {
|
|
12
|
+
throw new Error(`AetherWealth SDK: malformed response — expected "${key}" in the response envelope`);
|
|
13
|
+
}
|
|
14
|
+
return envelope[key];
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IdempotencyOptions } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the `Idempotency-Key` for a create call.
|
|
4
|
+
*
|
|
5
|
+
* - Caller-supplied key → used **verbatim**. Pass a STABLE key to make a retry
|
|
6
|
+
* safe: every attempt then dedups to the same backend record
|
|
7
|
+
* (`withRetry(() => trades.create(input, {idempotencyKey: key}))`).
|
|
8
|
+
* - No key → a fresh `crypto.randomUUID()` per call. This protects a single
|
|
9
|
+
* call (a network hiccup after the server committed won't create a duplicate
|
|
10
|
+
* on a manual re-invoke), but is NOT retry-safe on its own — do not generate
|
|
11
|
+
* inside a retried closure, or each attempt gets a new key and dedup is lost.
|
|
12
|
+
*
|
|
13
|
+
* Empty strings are treated as "absent" (a blank key can never dedup).
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveIdempotencyKey(opts?: IdempotencyOptions): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the `Idempotency-Key` for a create call.
|
|
3
|
+
*
|
|
4
|
+
* - Caller-supplied key → used **verbatim**. Pass a STABLE key to make a retry
|
|
5
|
+
* safe: every attempt then dedups to the same backend record
|
|
6
|
+
* (`withRetry(() => trades.create(input, {idempotencyKey: key}))`).
|
|
7
|
+
* - No key → a fresh `crypto.randomUUID()` per call. This protects a single
|
|
8
|
+
* call (a network hiccup after the server committed won't create a duplicate
|
|
9
|
+
* on a manual re-invoke), but is NOT retry-safe on its own — do not generate
|
|
10
|
+
* inside a retried closure, or each attempt gets a new key and dedup is lost.
|
|
11
|
+
*
|
|
12
|
+
* Empty strings are treated as "absent" (a blank key can never dedup).
|
|
13
|
+
*/
|
|
14
|
+
export function resolveIdempotencyKey(opts) {
|
|
15
|
+
if (opts?.idempotencyKey && opts.idempotencyKey.length > 0) {
|
|
16
|
+
return opts.idempotencyKey;
|
|
17
|
+
}
|
|
18
|
+
return crypto.randomUUID();
|
|
19
|
+
}
|