@cesto/sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/dist/index.cjs +394 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +573 -0
- package/dist/index.d.ts +573 -0
- package/dist/index.js +377 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cesto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# @cesto/sdk
|
|
2
|
+
|
|
3
|
+
Typed, server-side TypeScript client for the Cesto read-only API.
|
|
4
|
+
|
|
5
|
+
> **Server-side only.** Authentication uses a **secret** API key (`cesto_sk_…`).
|
|
6
|
+
> Never use it in a browser — the client throws if it detects one.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm i @cesto/sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Requires Node 18+ (uses the global `fetch`).
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { Cesto } from '@cesto/sdk';
|
|
20
|
+
|
|
21
|
+
const cesto = new Cesto({ apiKey: process.env.CESTO_API_KEY! });
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Pass your secret key as `apiKey` (read it from your environment, e.g. `process.env.CESTO_API_KEY`).
|
|
25
|
+
The constructor throws if it's missing.
|
|
26
|
+
|
|
27
|
+
### Products
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// List products
|
|
31
|
+
const products = await cesto.products.list();
|
|
32
|
+
const defi = await cesto.products.list({ category: 'defi' });
|
|
33
|
+
|
|
34
|
+
// List + backtested performance (one extra request, merged per product)
|
|
35
|
+
const withPerf = await cesto.products.list({ includeBacktest: true });
|
|
36
|
+
withPerf[0].backtest; // ProductBacktest | null
|
|
37
|
+
|
|
38
|
+
// Get one product by slug
|
|
39
|
+
const product = await cesto.products.get('my-basket-slug');
|
|
40
|
+
|
|
41
|
+
// Get + backtested value chart (default 1y window)
|
|
42
|
+
const detailed = await cesto.products.get('my-basket-slug', {
|
|
43
|
+
includeBacktestChart: true,
|
|
44
|
+
chartTimeRange: '1y',
|
|
45
|
+
});
|
|
46
|
+
detailed.backtestChart; // BacktestChart | PredictionMarketChart | null
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The chart is a union — discriminate it with the shipped guard:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { isPredictionMarketChart } from '@cesto/sdk';
|
|
53
|
+
|
|
54
|
+
if (detailed.backtestChart && isPredictionMarketChart(detailed.backtestChart)) {
|
|
55
|
+
detailed.backtestChart.markets; // PredictionMarketEvent[]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> `includeBacktest` / `includeBacktestChart` make a second request. If it fails, the
|
|
60
|
+
> folded-in field is `null` — your primary data is never lost to a flaky secondary call.
|
|
61
|
+
|
|
62
|
+
### Positions (by external Solana wallet)
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const { positions, pendingClosePositions } = await cesto.positions.list({
|
|
66
|
+
wallet: 'EXTERNAL_SOLANA_ADDRESS',
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A wallet with no Cesto account returns an empty result (not an error).
|
|
71
|
+
|
|
72
|
+
## Full example
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { Cesto, isPredictionMarketChart } from '@cesto/sdk';
|
|
76
|
+
|
|
77
|
+
async function main() {
|
|
78
|
+
// Read the key from your environment — never hard-code a secret key.
|
|
79
|
+
const cesto = new Cesto({
|
|
80
|
+
apiKey: process.env.CESTO_API_KEY!,
|
|
81
|
+
environment: 'PRODUCTION', // 'PRODUCTION' | 'BETA'
|
|
82
|
+
timeout: 30_000,
|
|
83
|
+
maxRetries: 2,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// List products + backtested performance (one extra request, merged per product).
|
|
87
|
+
// A failed analytics call degrades gracefully → backtest: null.
|
|
88
|
+
const products = await cesto.products.list({ includeBacktest: true });
|
|
89
|
+
console.log(`${products.length} products`);
|
|
90
|
+
|
|
91
|
+
// Get one product by slug, with its backtested value chart.
|
|
92
|
+
const slug = products[0]?.slug;
|
|
93
|
+
if (slug) {
|
|
94
|
+
const detail = await cesto.products.get(slug, {
|
|
95
|
+
includeBacktestChart: true,
|
|
96
|
+
chartTimeRange: '1y',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// The chart is a union — discriminate with the shipped type guard.
|
|
100
|
+
const chart = detail.backtestChart;
|
|
101
|
+
if (chart && !isPredictionMarketChart(chart)) {
|
|
102
|
+
console.log(`${detail.name}: ${chart.timeSeries.length} chart points`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Positions by external Solana wallet. An unknown wallet returns empty.
|
|
107
|
+
const { positions } = await cesto.positions.list({
|
|
108
|
+
wallet: 'EXTERNAL_SOLANA_ADDRESS',
|
|
109
|
+
});
|
|
110
|
+
for (const pos of positions) {
|
|
111
|
+
console.log(`• ${pos.product.name} — ${pos.investments.length} investment(s)`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
main().catch((err) => {
|
|
116
|
+
// Typed error handling — narrow with the classes hung off `Cesto`.
|
|
117
|
+
if (err instanceof Cesto.AuthenticationError) {
|
|
118
|
+
console.error('Auth failed — check your CESTO_API_KEY.');
|
|
119
|
+
} else if (err instanceof Cesto.RateLimitError) {
|
|
120
|
+
console.error(`Rate limited. Retry after ~${err.retryAfter ?? '?'}ms.`);
|
|
121
|
+
} else if (err instanceof Cesto.APIError) {
|
|
122
|
+
console.error(`API error ${err.status}: ${err.message} [${err.code ?? 'no-code'}]`);
|
|
123
|
+
} else {
|
|
124
|
+
console.error('Unexpected error:', err);
|
|
125
|
+
}
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
new Cesto({
|
|
134
|
+
apiKey: 'cesto_sk_…', // required
|
|
135
|
+
environment: 'PRODUCTION', // 'PRODUCTION' | 'BETA', default 'PRODUCTION'
|
|
136
|
+
timeout: 60_000, // ms, default 60s
|
|
137
|
+
maxRetries: 2, // default 2
|
|
138
|
+
fetch: customFetch, // optional fetch override
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The backend URL is **not a free-form option**. `NODE_ENV` decides local-vs-deployed,
|
|
143
|
+
and `environment` selects which deployed backend:
|
|
144
|
+
|
|
145
|
+
| NODE_ENV | environment | URL |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `production` | `PRODUCTION` (default) | `https://backend.cesto.co` |
|
|
148
|
+
| `production` | `BETA` | `https://dev.backend.cesto.co` |
|
|
149
|
+
|
|
150
|
+
Keys are environment-specific (each backend has its own) — use the `PRODUCTION` key with
|
|
151
|
+
`PRODUCTION`, the `BETA` key with `BETA`.
|
|
152
|
+
|
|
153
|
+
Per-request overrides (second/third argument on every method):
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
await cesto.products.list({ category: 'defi' }, { timeout: 5_000, maxRetries: 0, signal });
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Errors
|
|
160
|
+
|
|
161
|
+
All errors extend `CestoError`. Server responses become typed `APIError` subclasses;
|
|
162
|
+
transport problems become `APIConnectionError` / `APIConnectionTimeoutError`; caller
|
|
163
|
+
cancellation becomes `APIUserAbortError`.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { Cesto } from '@cesto/sdk';
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
await cesto.products.get('missing');
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (err instanceof Cesto.NotFoundError) {
|
|
172
|
+
// 404 — err.code, err.requestId, err.details available
|
|
173
|
+
} else if (err instanceof Cesto.RateLimitError) {
|
|
174
|
+
// 429 — err.retryAfter (ms)
|
|
175
|
+
} else if (err instanceof Cesto.APIConnectionError) {
|
|
176
|
+
// network / timeout
|
|
177
|
+
} else {
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Transient failures (429, 5xx, network, timeouts) are retried automatically with
|
|
184
|
+
exponential backoff + jitter, honoring `Retry-After`.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/errors.ts
|
|
4
|
+
var CestoError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = new.target.name;
|
|
8
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var APIError = class extends CestoError {
|
|
12
|
+
/** HTTP status code. */
|
|
13
|
+
status;
|
|
14
|
+
/** Machine-readable error code from the API envelope (e.g. `RESOURCE_NOT_FOUND`). */
|
|
15
|
+
code;
|
|
16
|
+
/** Correlation id from the API envelope, useful when reporting issues. */
|
|
17
|
+
requestId;
|
|
18
|
+
/** Structured error details from the API envelope. */
|
|
19
|
+
details;
|
|
20
|
+
/** Response headers. */
|
|
21
|
+
headers;
|
|
22
|
+
constructor(status, message, opts = {}) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.code = opts.code;
|
|
26
|
+
this.requestId = opts.requestId;
|
|
27
|
+
this.details = opts.details;
|
|
28
|
+
this.headers = opts.headers;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var BadRequestError = class extends APIError {
|
|
32
|
+
};
|
|
33
|
+
var AuthenticationError = class extends APIError {
|
|
34
|
+
};
|
|
35
|
+
var PermissionDeniedError = class extends APIError {
|
|
36
|
+
};
|
|
37
|
+
var NotFoundError = class extends APIError {
|
|
38
|
+
};
|
|
39
|
+
var ConflictError = class extends APIError {
|
|
40
|
+
};
|
|
41
|
+
var UnprocessableEntityError = class extends APIError {
|
|
42
|
+
};
|
|
43
|
+
var RateLimitError = class extends APIError {
|
|
44
|
+
/** Milliseconds to wait before retrying, derived from `Retry-After`. */
|
|
45
|
+
retryAfter;
|
|
46
|
+
constructor(status, message, opts = {}) {
|
|
47
|
+
super(status, message, opts);
|
|
48
|
+
this.retryAfter = opts.retryAfter;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var InternalServerError = class extends APIError {
|
|
52
|
+
};
|
|
53
|
+
var APIConnectionError = class extends CestoError {
|
|
54
|
+
cause;
|
|
55
|
+
constructor(message = "Connection error", opts = {}) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.cause = opts.cause;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var APIConnectionTimeoutError = class extends APIConnectionError {
|
|
61
|
+
constructor(message = "Request timed out") {
|
|
62
|
+
super(message);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var APIUserAbortError = class extends CestoError {
|
|
66
|
+
constructor(message = "Request was aborted") {
|
|
67
|
+
super(message);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
function makeAPIError(status, envelope, headers, retryAfter) {
|
|
71
|
+
const message = envelope.message || `HTTP ${status}`;
|
|
72
|
+
const opts = {
|
|
73
|
+
code: envelope.code,
|
|
74
|
+
requestId: envelope.requestId,
|
|
75
|
+
details: envelope.details,
|
|
76
|
+
headers
|
|
77
|
+
};
|
|
78
|
+
switch (status) {
|
|
79
|
+
case 400:
|
|
80
|
+
return new BadRequestError(status, message, opts);
|
|
81
|
+
case 401:
|
|
82
|
+
return new AuthenticationError(status, message, opts);
|
|
83
|
+
case 403:
|
|
84
|
+
return new PermissionDeniedError(status, message, opts);
|
|
85
|
+
case 404:
|
|
86
|
+
return new NotFoundError(status, message, opts);
|
|
87
|
+
case 409:
|
|
88
|
+
return new ConflictError(status, message, opts);
|
|
89
|
+
case 422:
|
|
90
|
+
return new UnprocessableEntityError(status, message, opts);
|
|
91
|
+
case 429:
|
|
92
|
+
return new RateLimitError(status, message, { ...opts, retryAfter });
|
|
93
|
+
default:
|
|
94
|
+
if (status >= 500) return new InternalServerError(status, message, opts);
|
|
95
|
+
return new APIError(status, message, opts);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/core/options.ts
|
|
100
|
+
var PROD_BASE_URL = "https://backend.cesto.co";
|
|
101
|
+
var BETA_BASE_URL = "https://dev.backend.cesto.co";
|
|
102
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
103
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
104
|
+
function isBrowser() {
|
|
105
|
+
return typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
106
|
+
}
|
|
107
|
+
function resolveBaseURL(environment) {
|
|
108
|
+
return environment === "BETA" ? BETA_BASE_URL : PROD_BASE_URL;
|
|
109
|
+
}
|
|
110
|
+
function resolveOptions(options) {
|
|
111
|
+
if (isBrowser()) {
|
|
112
|
+
throw new CestoError(
|
|
113
|
+
"The Cesto SDK is server-side only. It must not run in a browser \u2014 a secret key (cesto_sk_\u2026) would be exposed to end users."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const apiKey = options.apiKey;
|
|
117
|
+
if (!apiKey) {
|
|
118
|
+
throw new CestoError("Missing API key. Pass { apiKey } when constructing the client.");
|
|
119
|
+
}
|
|
120
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
121
|
+
if (typeof fetchImpl !== "function") {
|
|
122
|
+
throw new CestoError(
|
|
123
|
+
"No fetch implementation found. Use Node 18+ or pass a custom { fetch } in ClientOptions."
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
127
|
+
if (!Number.isFinite(timeout) || timeout < 0) {
|
|
128
|
+
throw new CestoError("`timeout` must be a finite number >= 0.");
|
|
129
|
+
}
|
|
130
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
131
|
+
if (!Number.isInteger(maxRetries) || maxRetries < 0) {
|
|
132
|
+
throw new CestoError("`maxRetries` must be an integer >= 0.");
|
|
133
|
+
}
|
|
134
|
+
const environment = options.environment ?? "PRODUCTION";
|
|
135
|
+
if (environment !== "PRODUCTION" && environment !== "BETA") {
|
|
136
|
+
throw new CestoError(`Invalid environment: ${environment}. Use 'PRODUCTION' or 'BETA'.`);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
apiKey,
|
|
140
|
+
baseURL: resolveBaseURL(environment),
|
|
141
|
+
timeout,
|
|
142
|
+
maxRetries,
|
|
143
|
+
fetch: options.fetch ?? fetchImpl.bind(globalThis)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/core/version.ts
|
|
148
|
+
var VERSION = "0.0.1";
|
|
149
|
+
|
|
150
|
+
// src/core/request.ts
|
|
151
|
+
var RETRY_BASE_MS = 500;
|
|
152
|
+
var RETRY_CAP_MS = 8e3;
|
|
153
|
+
var APIClientCore = class {
|
|
154
|
+
constructor(opts) {
|
|
155
|
+
this.opts = opts;
|
|
156
|
+
}
|
|
157
|
+
get(path, options = {}) {
|
|
158
|
+
return this.request("GET", path, options);
|
|
159
|
+
}
|
|
160
|
+
async request(method, path, options) {
|
|
161
|
+
const url = this.buildURL(path, options.query);
|
|
162
|
+
const timeout = options.timeout ?? this.opts.timeout;
|
|
163
|
+
const maxRetries = options.maxRetries ?? this.opts.maxRetries;
|
|
164
|
+
let attempt = 0;
|
|
165
|
+
for (; ; ) {
|
|
166
|
+
const timer = withTimeout(options.signal, timeout);
|
|
167
|
+
let response;
|
|
168
|
+
try {
|
|
169
|
+
response = await this.opts.fetch(url, {
|
|
170
|
+
method,
|
|
171
|
+
headers: buildHeaders(this.opts.apiKey, attempt),
|
|
172
|
+
signal: timer.signal
|
|
173
|
+
});
|
|
174
|
+
} catch (err) {
|
|
175
|
+
timer.cleanup();
|
|
176
|
+
const mapped = mapFetchError(err, timer, options.signal);
|
|
177
|
+
if (mapped instanceof APIUserAbortError) throw mapped;
|
|
178
|
+
if (attempt < maxRetries) {
|
|
179
|
+
attempt += 1;
|
|
180
|
+
await sleep(backoff(attempt));
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
throw mapped;
|
|
184
|
+
}
|
|
185
|
+
timer.cleanup();
|
|
186
|
+
if (response.ok) {
|
|
187
|
+
return parseJSON(response);
|
|
188
|
+
}
|
|
189
|
+
if (shouldRetryStatus(response.status) && attempt < maxRetries) {
|
|
190
|
+
attempt += 1;
|
|
191
|
+
await sleep(parseRetryAfter(response.headers) ?? backoff(attempt));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
throw await errorFromResponse(response);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
buildURL(path, query) {
|
|
198
|
+
const url = new URL(`${this.opts.baseURL}${path}`);
|
|
199
|
+
if (query) {
|
|
200
|
+
for (const [key, value] of Object.entries(query)) {
|
|
201
|
+
if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return url.toString();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
function buildHeaders(apiKey, attempt) {
|
|
208
|
+
const headers = {
|
|
209
|
+
"X-API-Key": apiKey,
|
|
210
|
+
Accept: "application/json"
|
|
211
|
+
};
|
|
212
|
+
const ua = userAgent();
|
|
213
|
+
if (ua) headers["User-Agent"] = ua;
|
|
214
|
+
if (attempt > 0) headers["x-cesto-retry-count"] = String(attempt);
|
|
215
|
+
return headers;
|
|
216
|
+
}
|
|
217
|
+
function userAgent() {
|
|
218
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
219
|
+
return `cesto-sdk/${VERSION} (node/${process.versions.node})`;
|
|
220
|
+
}
|
|
221
|
+
return `cesto-sdk/${VERSION}`;
|
|
222
|
+
}
|
|
223
|
+
function withTimeout(userSignal, timeoutMs) {
|
|
224
|
+
const controller = new AbortController();
|
|
225
|
+
let timedOut = false;
|
|
226
|
+
const timer = setTimeout(() => {
|
|
227
|
+
timedOut = true;
|
|
228
|
+
controller.abort(new Error("timeout"));
|
|
229
|
+
}, timeoutMs);
|
|
230
|
+
const onUserAbort = () => controller.abort(userSignal?.reason);
|
|
231
|
+
if (userSignal) {
|
|
232
|
+
if (userSignal.aborted) controller.abort(userSignal.reason);
|
|
233
|
+
else userSignal.addEventListener("abort", onUserAbort, { once: true });
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
signal: controller.signal,
|
|
237
|
+
didTimeout: () => timedOut,
|
|
238
|
+
cleanup: () => {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
userSignal?.removeEventListener("abort", onUserAbort);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function mapFetchError(err, timer, userSignal) {
|
|
245
|
+
if (userSignal?.aborted) return new APIUserAbortError();
|
|
246
|
+
if (timer.didTimeout()) return new APIConnectionTimeoutError();
|
|
247
|
+
return new APIConnectionError("Connection error", { cause: err });
|
|
248
|
+
}
|
|
249
|
+
function shouldRetryStatus(status) {
|
|
250
|
+
return status === 408 || status === 409 || status === 429 || status >= 500;
|
|
251
|
+
}
|
|
252
|
+
async function parseJSON(response) {
|
|
253
|
+
const text = await response.text();
|
|
254
|
+
if (!text) return void 0;
|
|
255
|
+
try {
|
|
256
|
+
return JSON.parse(text);
|
|
257
|
+
} catch {
|
|
258
|
+
throw new CestoError("Failed to parse response body as JSON.");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function errorFromResponse(response) {
|
|
262
|
+
let envelope = {};
|
|
263
|
+
try {
|
|
264
|
+
const text = await response.text();
|
|
265
|
+
if (text) {
|
|
266
|
+
const parsed = JSON.parse(text);
|
|
267
|
+
if (parsed && typeof parsed === "object") envelope = parsed;
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
return makeAPIError(
|
|
272
|
+
response.status,
|
|
273
|
+
envelope,
|
|
274
|
+
response.headers,
|
|
275
|
+
parseRetryAfter(response.headers)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
function parseRetryAfter(headers) {
|
|
279
|
+
const value = headers.get("retry-after");
|
|
280
|
+
if (!value) return void 0;
|
|
281
|
+
const seconds = Number(value);
|
|
282
|
+
if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1e3);
|
|
283
|
+
const dateMs = Date.parse(value);
|
|
284
|
+
if (!Number.isNaN(dateMs)) return Math.max(0, dateMs - Date.now());
|
|
285
|
+
return void 0;
|
|
286
|
+
}
|
|
287
|
+
function backoff(attempt) {
|
|
288
|
+
const ceiling = Math.min(RETRY_CAP_MS, RETRY_BASE_MS * 2 ** (attempt - 1));
|
|
289
|
+
return Math.random() * ceiling;
|
|
290
|
+
}
|
|
291
|
+
function sleep(ms) {
|
|
292
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/resources/resource.ts
|
|
296
|
+
var APIResource = class {
|
|
297
|
+
constructor(client) {
|
|
298
|
+
this.client = client;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// src/resources/positions.ts
|
|
303
|
+
var Positions = class extends APIResource {
|
|
304
|
+
/**
|
|
305
|
+
* List a user's open/closing positions, resolved from their external Solana
|
|
306
|
+
* wallet. A wallet with no Cesto account returns an empty result (not an error).
|
|
307
|
+
*/
|
|
308
|
+
list(params, options) {
|
|
309
|
+
if (!params?.wallet) throw new CestoError("wallet is required");
|
|
310
|
+
return this.client.get("/positions/by-wallet", {
|
|
311
|
+
query: { wallet: params.wallet },
|
|
312
|
+
...options
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// src/resources/products.ts
|
|
318
|
+
var DEFAULT_CHART_TIME_RANGE = "1y";
|
|
319
|
+
var Products = class extends APIResource {
|
|
320
|
+
async list(params = {}, options) {
|
|
321
|
+
const query = params.category ? { category: params.category } : void 0;
|
|
322
|
+
if (!params.includeBacktest) {
|
|
323
|
+
return this.client.get("/products", { query, ...options });
|
|
324
|
+
}
|
|
325
|
+
const [items, backtests] = await Promise.all([
|
|
326
|
+
this.client.get("/products", { query, ...options }),
|
|
327
|
+
this.client.get("/products/analytics", { query, ...options }).catch(() => ({}))
|
|
328
|
+
]);
|
|
329
|
+
return items.map((item) => ({ ...item, backtest: backtests[item.id] ?? null }));
|
|
330
|
+
}
|
|
331
|
+
async get(slug, params = {}, options) {
|
|
332
|
+
const product = await this.client.get(
|
|
333
|
+
`/products/${encodeURIComponent(slug)}`,
|
|
334
|
+
options
|
|
335
|
+
);
|
|
336
|
+
if (!params.includeBacktestChart) return product;
|
|
337
|
+
const backtestChart = await this.client.get(`/products/${encodeURIComponent(product.id)}/graph`, {
|
|
338
|
+
query: { timeRange: params.chartTimeRange ?? DEFAULT_CHART_TIME_RANGE },
|
|
339
|
+
...options
|
|
340
|
+
}).catch(() => null);
|
|
341
|
+
return { ...product, backtestChart };
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/client.ts
|
|
346
|
+
var Cesto = class {
|
|
347
|
+
products;
|
|
348
|
+
positions;
|
|
349
|
+
core;
|
|
350
|
+
constructor(options = {}) {
|
|
351
|
+
this.core = new APIClientCore(resolveOptions(options));
|
|
352
|
+
this.products = new Products(this.core);
|
|
353
|
+
this.positions = new Positions(this.core);
|
|
354
|
+
}
|
|
355
|
+
// Error classes re-exposed for ergonomic `instanceof` checks:
|
|
356
|
+
// catch (e) { if (e instanceof Cesto.NotFoundError) … }
|
|
357
|
+
static CestoError = CestoError;
|
|
358
|
+
static APIError = APIError;
|
|
359
|
+
static BadRequestError = BadRequestError;
|
|
360
|
+
static AuthenticationError = AuthenticationError;
|
|
361
|
+
static PermissionDeniedError = PermissionDeniedError;
|
|
362
|
+
static NotFoundError = NotFoundError;
|
|
363
|
+
static ConflictError = ConflictError;
|
|
364
|
+
static UnprocessableEntityError = UnprocessableEntityError;
|
|
365
|
+
static RateLimitError = RateLimitError;
|
|
366
|
+
static InternalServerError = InternalServerError;
|
|
367
|
+
static APIConnectionError = APIConnectionError;
|
|
368
|
+
static APIConnectionTimeoutError = APIConnectionTimeoutError;
|
|
369
|
+
static APIUserAbortError = APIUserAbortError;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// src/types/products.ts
|
|
373
|
+
function isPredictionMarketChart(chart) {
|
|
374
|
+
return chart.predictionMarket === true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
exports.APIConnectionError = APIConnectionError;
|
|
378
|
+
exports.APIConnectionTimeoutError = APIConnectionTimeoutError;
|
|
379
|
+
exports.APIError = APIError;
|
|
380
|
+
exports.APIUserAbortError = APIUserAbortError;
|
|
381
|
+
exports.AuthenticationError = AuthenticationError;
|
|
382
|
+
exports.BadRequestError = BadRequestError;
|
|
383
|
+
exports.Cesto = Cesto;
|
|
384
|
+
exports.CestoError = CestoError;
|
|
385
|
+
exports.ConflictError = ConflictError;
|
|
386
|
+
exports.InternalServerError = InternalServerError;
|
|
387
|
+
exports.NotFoundError = NotFoundError;
|
|
388
|
+
exports.PermissionDeniedError = PermissionDeniedError;
|
|
389
|
+
exports.RateLimitError = RateLimitError;
|
|
390
|
+
exports.UnprocessableEntityError = UnprocessableEntityError;
|
|
391
|
+
exports.VERSION = VERSION;
|
|
392
|
+
exports.isPredictionMarketChart = isPredictionMarketChart;
|
|
393
|
+
//# sourceMappingURL=index.cjs.map
|
|
394
|
+
//# sourceMappingURL=index.cjs.map
|