@usepraxis/sdk 0.1.1 → 0.5.0
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 +37 -4
- package/dist/index.cjs +189 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +239 -14
- package/dist/index.d.ts +239 -14
- package/dist/index.js +189 -24
- package/dist/index.js.map +1 -1
- package/package.json +16 -4
package/README.md
CHANGED
|
@@ -33,6 +33,12 @@ for (const p of proposals) {
|
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
A proposal stays signable for a week. Its amount is fixed when it is built and
|
|
37
|
+
Aegis enforces the envelope live at submit, so an older card still moves
|
|
38
|
+
exactly what it says — what drifts are the readings on it (fee, simulated
|
|
39
|
+
outcome, remaining daily envelope). Past a week the backend refuses it and you
|
|
40
|
+
ask again.
|
|
41
|
+
|
|
36
42
|
`ask()` returns once the agent has finished — the API resolves `send` only after
|
|
37
43
|
the reply is ready, so there is no polling.
|
|
38
44
|
|
|
@@ -47,6 +53,12 @@ the reply is ready, so there is no polling.
|
|
|
47
53
|
|
|
48
54
|
The signed-in wallet is the **owner** whose Aegis policy PDA scopes everything.
|
|
49
55
|
|
|
56
|
+
Sessions are short on purpose — holding one is enough to move value *within*
|
|
57
|
+
the Aegis envelope, with no further wallet signature — so a long-running
|
|
58
|
+
process will outlive its cookie. When a signer is configured, the SDK runs the
|
|
59
|
+
handshake again on a `401` and retries the call once; you do not need your own
|
|
60
|
+
reconnect loop. Without a signer, the `401` is returned as-is.
|
|
61
|
+
|
|
50
62
|
## Signers
|
|
51
63
|
|
|
52
64
|
Provide any `PraxisSigner` — `{ address, signMessage(bytes) }`.
|
|
@@ -83,14 +95,35 @@ toBaseUnits("500000000"); // 500000000n
|
|
|
83
95
|
|------|---------|
|
|
84
96
|
| Auth | `connect()`, `session()`, `logout()` |
|
|
85
97
|
| Conversation | `ask()`, `send()`, `newThread()`, `signProposal()`, `cancelProposal()` |
|
|
86
|
-
| Reads | `getPolicy()`, `getThreads()`, `getThread()`, `getProposal()`, `getActivity()`, `getAddressBook()`, `
|
|
87
|
-
|
|
|
98
|
+
| Reads | `getPolicy()`, `getThreads()`, `getThread()`, `getProposal()`, `getProposals()`, `getActivity()`, `getAddressBook()`, `getSchedules()`, `getVersion()` |
|
|
99
|
+
| Contacts | `addContact()`, `removeContact()` — labels only, no signing power |
|
|
100
|
+
| Recurring | `cancelSchedule()` — stop a recurring buy (fires only ever emit proposals) |
|
|
101
|
+
| Policy (server-key) | `bootstrapPolicy()`, `fundVault()`, `withdrawVault()`, `updatePolicy()`, `configureToken()`, `prepareTokenAccounts()`, `revokeAgent()`, `rotateAgent()`, `addToAllowList()`, `removeFromAllowList()`, `deleteAgent()` |
|
|
88
102
|
| Owner (wallet-signed) | `buildOwnerTransaction()`, `submitOwnerTransaction()` |
|
|
103
|
+
| Stocks (PreStocks) | `getTokenUniverse()`, `getStockResearch()` — see `examples/stocks-dca.ts` |
|
|
104
|
+
|
|
105
|
+
`session()` returns the current `SessionInfo` or `null` when signed out.
|
|
106
|
+
|
|
107
|
+
> **Owner wallet-signed path.** `buildOwnerTransaction()` returns an *unsigned*
|
|
108
|
+
> transaction; you sign it with a transaction-capable wallet (a browser wallet
|
|
109
|
+
> adapter or `@solana/web3.js`) and submit the result with
|
|
110
|
+
> `submitOwnerTransaction()`. The SDK's `keypairSigner` signs the sign-in
|
|
111
|
+
> *message* only, not transactions — so a pure-Node owner-action flow must bring
|
|
112
|
+
> its own transaction signer.
|
|
113
|
+
>
|
|
114
|
+
> Pass the whole draft back. Since **0.5.0** it carries a `draft` token: the
|
|
115
|
+
> backend refuses to relay a transaction it did not build, which is what keeps
|
|
116
|
+
> the server-side checks on an action (a token balance still in the vault, a
|
|
117
|
+
> mint Aegis cannot drive) from being skippable by assembling your own bytes.
|
|
118
|
+
> Mutate `transaction` only; leave the other fields alone.
|
|
89
119
|
|
|
90
120
|
## Errors
|
|
91
121
|
|
|
92
|
-
Non-2xx responses throw `PraxisApiError` with `.status`, `.type`,
|
|
93
|
-
`.isAuth` / `.isRateLimited` / `.isInput
|
|
122
|
+
Non-2xx responses throw `PraxisApiError` with `.status`, `.type`, a stable
|
|
123
|
+
`.code` to branch on, and helpers `.isAuth` / `.isRateLimited` / `.isInput` /
|
|
124
|
+
`.isNotFound` / `.isConfig` / `.isConflict` / `.isPolicyNotFound` / `.isServer`. A client-side timeout or connection failure throws
|
|
125
|
+
`PraxisApiError` with `.isTimeout` / `.isNetwork` (and `.status === 0`, with the
|
|
126
|
+
original error on `.cause`). SDK-side misconfiguration throws `PraxisConfigError`.
|
|
94
127
|
|
|
95
128
|
```ts
|
|
96
129
|
import { PraxisApiError } from "@usepraxis/sdk";
|
package/dist/index.cjs
CHANGED
|
@@ -15,11 +15,17 @@ var PraxisApiError = class _PraxisApiError extends Error {
|
|
|
15
15
|
status;
|
|
16
16
|
/** The backend error class name, e.g. "PraxisAuthError", "PraxisRateLimitError". */
|
|
17
17
|
type;
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/** Stable error classification — the field to branch on. */
|
|
19
|
+
code;
|
|
20
|
+
/** Structured facts about the failure, e.g. `{ policyAddress }` for `policy_not_found`. */
|
|
21
|
+
details;
|
|
22
|
+
constructor(status, type, message, options) {
|
|
23
|
+
super(message, options);
|
|
20
24
|
this.name = "PraxisApiError";
|
|
21
25
|
this.status = status;
|
|
22
26
|
this.type = type;
|
|
27
|
+
this.code = options?.code ?? codeFromStatus(status);
|
|
28
|
+
this.details = options?.details;
|
|
23
29
|
Object.setPrototypeOf(this, _PraxisApiError.prototype);
|
|
24
30
|
}
|
|
25
31
|
get isAuth() {
|
|
@@ -31,10 +37,48 @@ var PraxisApiError = class _PraxisApiError extends Error {
|
|
|
31
37
|
get isInput() {
|
|
32
38
|
return this.status === 400;
|
|
33
39
|
}
|
|
40
|
+
/** Resource not found (404). */
|
|
41
|
+
get isNotFound() {
|
|
42
|
+
return this.status === 404;
|
|
43
|
+
}
|
|
44
|
+
/** Server reported a configuration problem (503) — usually transient. */
|
|
45
|
+
get isConfig() {
|
|
46
|
+
return this.status === 503;
|
|
47
|
+
}
|
|
48
|
+
/** The request timed out client-side before any HTTP response. */
|
|
49
|
+
get isTimeout() {
|
|
50
|
+
return this.status === 0 && this.type === "TimeoutError";
|
|
51
|
+
}
|
|
52
|
+
/** A connection-level failure (DNS, refused, TLS) before any HTTP response. */
|
|
53
|
+
get isNetwork() {
|
|
54
|
+
return this.status === 0 && this.type === "NetworkError";
|
|
55
|
+
}
|
|
56
|
+
/** Any server-side failure (HTTP >= 500). */
|
|
57
|
+
get isServer() {
|
|
58
|
+
return this.status >= 500;
|
|
59
|
+
}
|
|
60
|
+
/** The wallet has no Aegis policy yet — the first-run state, not a fault. */
|
|
61
|
+
get isPolicyNotFound() {
|
|
62
|
+
return this.code === "policy_not_found";
|
|
63
|
+
}
|
|
64
|
+
/** A concurrent writer won; the call is safe to retry after a reload. */
|
|
65
|
+
get isConflict() {
|
|
66
|
+
return this.code === "conflict";
|
|
67
|
+
}
|
|
34
68
|
};
|
|
69
|
+
function codeFromStatus(status) {
|
|
70
|
+
if (status === 400) return "invalid_input";
|
|
71
|
+
if (status === 401) return "unauthorized";
|
|
72
|
+
if (status === 404) return "not_found";
|
|
73
|
+
if (status === 409) return "conflict";
|
|
74
|
+
if (status === 429) return "rate_limited";
|
|
75
|
+
if (status === 503) return "config_error";
|
|
76
|
+
if (status === 0) return "client_error";
|
|
77
|
+
return "internal_error";
|
|
78
|
+
}
|
|
35
79
|
var PraxisConfigError = class _PraxisConfigError extends Error {
|
|
36
|
-
constructor(message) {
|
|
37
|
-
super(message);
|
|
80
|
+
constructor(message, options) {
|
|
81
|
+
super(message, options);
|
|
38
82
|
this.name = "PraxisConfigError";
|
|
39
83
|
Object.setPrototypeOf(this, _PraxisConfigError.prototype);
|
|
40
84
|
}
|
|
@@ -47,6 +91,7 @@ var PraxisClient = class {
|
|
|
47
91
|
signer;
|
|
48
92
|
fetchImpl;
|
|
49
93
|
timeoutMs;
|
|
94
|
+
agentTimeoutMs;
|
|
50
95
|
/** Manual cookie jar — Node's fetch does not persist Set-Cookie across calls. */
|
|
51
96
|
sessionCookie;
|
|
52
97
|
constructor(options) {
|
|
@@ -59,6 +104,7 @@ var PraxisClient = class {
|
|
|
59
104
|
}
|
|
60
105
|
this.fetchImpl = resolvedFetch;
|
|
61
106
|
this.timeoutMs = options.timeoutMs ?? 2e4;
|
|
107
|
+
this.agentTimeoutMs = Math.max(options.agentTimeoutMs ?? 6e4, this.timeoutMs);
|
|
62
108
|
}
|
|
63
109
|
// --- auth ----------------------------------------------------------------
|
|
64
110
|
/** The signer's wallet address, if a signer was provided. */
|
|
@@ -67,7 +113,8 @@ var PraxisClient = class {
|
|
|
67
113
|
}
|
|
68
114
|
/**
|
|
69
115
|
* Run the wallet-ownership handshake: request a challenge, sign its message,
|
|
70
|
-
* verify it, and store the resulting session cookie.
|
|
116
|
+
* verify it, and store the resulting session cookie. Safe to call again to
|
|
117
|
+
* refresh the session (each call issues a new challenge + cookie).
|
|
71
118
|
*/
|
|
72
119
|
async connect() {
|
|
73
120
|
if (!this.signer) {
|
|
@@ -83,24 +130,37 @@ var PraxisClient = class {
|
|
|
83
130
|
signature: bs582__default.default.encode(signature)
|
|
84
131
|
});
|
|
85
132
|
}
|
|
86
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Current session, or `null` if not signed in. The endpoint answers `200`
|
|
135
|
+
* with `{ authenticated: false }` when signed out, so this normalizes both
|
|
136
|
+
* that shape and a `401` to `null`.
|
|
137
|
+
*/
|
|
87
138
|
async session() {
|
|
88
139
|
try {
|
|
89
|
-
|
|
140
|
+
const info = await this.get("/auth/session");
|
|
141
|
+
return info && info.authenticated ? info : null;
|
|
90
142
|
} catch (error) {
|
|
91
143
|
if (error instanceof PraxisApiError && error.isAuth) return null;
|
|
92
144
|
throw error;
|
|
93
145
|
}
|
|
94
146
|
}
|
|
95
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* Clear the session (server-side cookie + local jar). Idempotent: if there is
|
|
149
|
+
* no active session, the local jar is still cleared and no error is thrown.
|
|
150
|
+
*/
|
|
96
151
|
async logout() {
|
|
97
|
-
|
|
98
|
-
|
|
152
|
+
try {
|
|
153
|
+
await this.request("DELETE", "/auth/session");
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (!(error instanceof PraxisApiError && error.isAuth)) throw error;
|
|
156
|
+
} finally {
|
|
157
|
+
this.sessionCookie = void 0;
|
|
158
|
+
}
|
|
99
159
|
}
|
|
100
160
|
// --- conversation --------------------------------------------------------
|
|
101
161
|
/** Send a line to the agent. Creates a thread when `threadId` is omitted. */
|
|
102
162
|
send(text, threadId = null) {
|
|
103
|
-
return this.post("/send", { text, threadId });
|
|
163
|
+
return this.post("/send", { text, threadId }, this.agentTimeoutMs);
|
|
104
164
|
}
|
|
105
165
|
/**
|
|
106
166
|
* Send a line and return the agent's reply in one call. The API resolves
|
|
@@ -114,7 +174,9 @@ var PraxisClient = class {
|
|
|
114
174
|
throw new PraxisApiError(500, "Error", "Agent produced no reply message.");
|
|
115
175
|
}
|
|
116
176
|
const proposalIds = message.blocks.filter((b) => b.type === "proposal").map((b) => b.proposalId);
|
|
117
|
-
|
|
177
|
+
if (proposalIds.length === 0) return { threadId: tid, message, proposals: [] };
|
|
178
|
+
const byId = new Map((await this.getProposals()).map((p) => [p.id, p]));
|
|
179
|
+
const proposals = proposalIds.map((id) => byId.get(id)).filter((p) => p !== void 0);
|
|
118
180
|
return { threadId: tid, message, proposals };
|
|
119
181
|
}
|
|
120
182
|
newThread(threadId) {
|
|
@@ -126,6 +188,22 @@ var PraxisClient = class {
|
|
|
126
188
|
cancelProposal(proposalId) {
|
|
127
189
|
return this.post("/cancel-proposal", { proposalId });
|
|
128
190
|
}
|
|
191
|
+
/** List recurring-buy schedules (each fire emits one proposal; never signs). */
|
|
192
|
+
getSchedules() {
|
|
193
|
+
return this.get("/get-schedules");
|
|
194
|
+
}
|
|
195
|
+
/** Stop a recurring-buy schedule. Unknown ids are a no-op (idempotent). */
|
|
196
|
+
cancelSchedule(scheduleId) {
|
|
197
|
+
return this.post("/cancel-schedule", { scheduleId });
|
|
198
|
+
}
|
|
199
|
+
/** Save (or rename) an address-book contact. Labels have no signing power. */
|
|
200
|
+
addContact(label, address) {
|
|
201
|
+
return this.post("/add-contact", { label, address });
|
|
202
|
+
}
|
|
203
|
+
/** Remove a contact by address or label (case-insensitive, idempotent). */
|
|
204
|
+
removeContact(key) {
|
|
205
|
+
return this.post("/remove-contact", { key });
|
|
206
|
+
}
|
|
129
207
|
// --- reads ---------------------------------------------------------------
|
|
130
208
|
getThreads() {
|
|
131
209
|
return this.get("/get-threads");
|
|
@@ -136,6 +214,14 @@ var PraxisClient = class {
|
|
|
136
214
|
getProposal(id) {
|
|
137
215
|
return this.get("/get-proposal", { id });
|
|
138
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Every proposal this wallet holds, in one request. Prefer this over a loop
|
|
219
|
+
* of {@link getProposal}: a per-id fetch is what trips the read rate limit
|
|
220
|
+
* on a busy thread.
|
|
221
|
+
*/
|
|
222
|
+
getProposals() {
|
|
223
|
+
return this.get("/get-proposals");
|
|
224
|
+
}
|
|
139
225
|
getPolicy() {
|
|
140
226
|
return this.get("/get-policy");
|
|
141
227
|
}
|
|
@@ -145,16 +231,47 @@ var PraxisClient = class {
|
|
|
145
231
|
getAddressBook() {
|
|
146
232
|
return this.get("/get-address-book");
|
|
147
233
|
}
|
|
148
|
-
isThinking(threadId) {
|
|
149
|
-
return this.get("/is-thinking", { threadId });
|
|
150
|
-
}
|
|
151
234
|
getVersion() {
|
|
152
235
|
return this.get("/get-version");
|
|
153
236
|
}
|
|
237
|
+
// --- stocks (PreStocks universe; empty unless the server enables it) ------
|
|
238
|
+
/**
|
|
239
|
+
* The server's stock universe (`[]` when `PRAXIS_STOCKS_ENABLED` is off).
|
|
240
|
+
* Read-only; symbols/mints here are the only pre-IPO stocks Praxis will
|
|
241
|
+
* touch (bounty exclusivity is enforced server-side).
|
|
242
|
+
*/
|
|
243
|
+
getTokenUniverse() {
|
|
244
|
+
return this.get("/get-stock-universe");
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Read-only research for a stock symbol, via the agent (`research <symbol>`).
|
|
248
|
+
* Returns neutral market data — never advice. Throws when the agent has no
|
|
249
|
+
* research to show (unknown symbol or unavailable quotes).
|
|
250
|
+
*/
|
|
251
|
+
async getStockResearch(symbol) {
|
|
252
|
+
const { message } = await this.ask(`research ${symbol}`);
|
|
253
|
+
const block = message.blocks.find((b) => b.type === "research");
|
|
254
|
+
if (!block || block.type !== "research") {
|
|
255
|
+
throw new PraxisApiError(404, "NotFound", `No research available for ${symbol}.`);
|
|
256
|
+
}
|
|
257
|
+
return block.data;
|
|
258
|
+
}
|
|
154
259
|
// --- policy / owner mutations (server-key mode) --------------------------
|
|
155
260
|
bootstrapPolicy(fundLamports) {
|
|
156
261
|
return this.post("/bootstrap-policy", fundLamports ? { fundLamports } : {});
|
|
157
262
|
}
|
|
263
|
+
/** Deposit SOL (lamports, base-unit string) from the owner into the vault. */
|
|
264
|
+
fundVault(amount) {
|
|
265
|
+
return this.post("/fund-vault", { amount });
|
|
266
|
+
}
|
|
267
|
+
/** Withdraw SOL (lamports, base-unit string) from the vault to the owner. */
|
|
268
|
+
withdrawVault(amount) {
|
|
269
|
+
return this.post("/withdraw-vault", { amount });
|
|
270
|
+
}
|
|
271
|
+
/** Tear the agent down — drain the vault and close the policy. Irreversible. */
|
|
272
|
+
deleteAgent() {
|
|
273
|
+
return this.post("/delete-agent", {});
|
|
274
|
+
}
|
|
158
275
|
updatePolicy(patch) {
|
|
159
276
|
return this.post("/update-policy", { patch });
|
|
160
277
|
}
|
|
@@ -177,11 +294,16 @@ var PraxisClient = class {
|
|
|
177
294
|
return this.post("/remove-from-allow-list", { kind, address });
|
|
178
295
|
}
|
|
179
296
|
// --- owner wallet-signed transaction path --------------------------------
|
|
180
|
-
/**
|
|
297
|
+
/**
|
|
298
|
+
* Build an unsigned owner transaction for the wallet to sign. The caller signs
|
|
299
|
+
* the returned base64 `transaction` with a transaction-capable wallet, then
|
|
300
|
+
* passes the result to {@link submitOwnerTransaction}. (The SDK's
|
|
301
|
+
* `keypairSigner` signs sign-in messages only, not transactions.)
|
|
302
|
+
*/
|
|
181
303
|
buildOwnerTransaction(action) {
|
|
182
304
|
return this.post("/owner/build", { action });
|
|
183
305
|
}
|
|
184
|
-
/** Submit a wallet-signed owner transaction. */
|
|
306
|
+
/** Submit a wallet-signed owner transaction; resolves with its signature. */
|
|
185
307
|
submitOwnerTransaction(signed) {
|
|
186
308
|
return this.post("/owner/submit", signed);
|
|
187
309
|
}
|
|
@@ -189,17 +311,41 @@ var PraxisClient = class {
|
|
|
189
311
|
get(path, query) {
|
|
190
312
|
return this.request("GET", path, { query });
|
|
191
313
|
}
|
|
192
|
-
post(path, body) {
|
|
193
|
-
return this.request("POST", path, { body });
|
|
314
|
+
post(path, body, timeoutMs) {
|
|
315
|
+
return this.request("POST", path, { body, timeoutMs });
|
|
194
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Run a request, and if the session has expired, sign in again and retry it
|
|
319
|
+
* once.
|
|
320
|
+
*
|
|
321
|
+
* Sessions are deliberately short — holding one is enough to move value
|
|
322
|
+
* within the Aegis envelope — so a long-lived agent process WILL outlive its
|
|
323
|
+
* cookie. Without this, every caller writes the same catch-401-and-reconnect
|
|
324
|
+
* block, and the ones who do not simply stop working after a day. Retried
|
|
325
|
+
* once only, never for the auth endpoints themselves, and only when a signer
|
|
326
|
+
* is configured; without one there is nothing to re-authenticate with and the
|
|
327
|
+
* 401 is the honest answer.
|
|
328
|
+
*/
|
|
195
329
|
async request(method, path, opts = {}) {
|
|
330
|
+
try {
|
|
331
|
+
return await this.send1(method, path, opts);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
const recoverable = error instanceof PraxisApiError && error.isAuth && Boolean(this.signer) && !path.startsWith("/auth/");
|
|
334
|
+
if (!recoverable) throw error;
|
|
335
|
+
this.sessionCookie = void 0;
|
|
336
|
+
await this.connect();
|
|
337
|
+
return this.send1(method, path, opts);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async send1(method, path, opts = {}) {
|
|
196
341
|
const url = new URL(this.baseUrl + API_PREFIX + path);
|
|
197
342
|
for (const [k, v] of Object.entries(opts.query ?? {})) url.searchParams.set(k, v);
|
|
198
343
|
const headers = { accept: "application/json" };
|
|
199
344
|
if (opts.body !== void 0) headers["content-type"] = "application/json";
|
|
200
345
|
if (this.sessionCookie) headers["cookie"] = this.sessionCookie;
|
|
346
|
+
const timeoutMs = opts.timeoutMs ?? this.timeoutMs;
|
|
201
347
|
const controller = new AbortController();
|
|
202
|
-
const timer = setTimeout(() => controller.abort(),
|
|
348
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
203
349
|
let res;
|
|
204
350
|
try {
|
|
205
351
|
res = await this.fetchImpl(url.toString(), {
|
|
@@ -212,9 +358,13 @@ var PraxisClient = class {
|
|
|
212
358
|
});
|
|
213
359
|
} catch (error) {
|
|
214
360
|
if (error instanceof Error && error.name === "AbortError") {
|
|
215
|
-
throw new PraxisApiError(0, "TimeoutError", `Praxis request timed out after ${
|
|
361
|
+
throw new PraxisApiError(0, "TimeoutError", `Praxis request timed out after ${timeoutMs}ms`, {
|
|
362
|
+
cause: error
|
|
363
|
+
});
|
|
216
364
|
}
|
|
217
|
-
throw error;
|
|
365
|
+
if (error instanceof PraxisApiError) throw error;
|
|
366
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
367
|
+
throw new PraxisApiError(0, "NetworkError", `Praxis request failed: ${detail}`, { cause: error });
|
|
218
368
|
} finally {
|
|
219
369
|
clearTimeout(timer);
|
|
220
370
|
}
|
|
@@ -224,7 +374,10 @@ var PraxisClient = class {
|
|
|
224
374
|
if (!res.ok) {
|
|
225
375
|
const message = (parsed && typeof parsed === "object" && "error" in parsed && typeof parsed.error === "string" ? parsed.error : void 0) ?? `Praxis API error ${res.status}`;
|
|
226
376
|
const type = parsed && typeof parsed === "object" && "type" in parsed && typeof parsed.type === "string" ? parsed.type : "Error";
|
|
227
|
-
|
|
377
|
+
const record = parsed && typeof parsed === "object" ? parsed : void 0;
|
|
378
|
+
const code = typeof record?.code === "string" ? record.code : void 0;
|
|
379
|
+
const details = record?.details && typeof record.details === "object" && !Array.isArray(record.details) ? record.details : void 0;
|
|
380
|
+
throw new PraxisApiError(res.status, type, message, { code, details });
|
|
228
381
|
}
|
|
229
382
|
return parsed;
|
|
230
383
|
}
|
|
@@ -276,6 +429,9 @@ function normalizeSecret(secret) {
|
|
|
276
429
|
throw new PraxisConfigError("secret key string must be base58-encoded");
|
|
277
430
|
}
|
|
278
431
|
} else if (Array.isArray(secret)) {
|
|
432
|
+
if (!secret.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)) {
|
|
433
|
+
throw new PraxisConfigError("secret key array must contain only byte values (integers 0\u2013255)");
|
|
434
|
+
}
|
|
279
435
|
bytes = Uint8Array.from(secret);
|
|
280
436
|
} else {
|
|
281
437
|
bytes = secret;
|
|
@@ -289,10 +445,19 @@ function normalizeSecret(secret) {
|
|
|
289
445
|
}
|
|
290
446
|
|
|
291
447
|
// src/units.ts
|
|
448
|
+
var INTEGER_RE = /^-?\d+$/;
|
|
292
449
|
function toBaseUnits(value) {
|
|
293
|
-
|
|
450
|
+
if (typeof value === "bigint") return value;
|
|
451
|
+
const trimmed = value.trim();
|
|
452
|
+
if (!INTEGER_RE.test(trimmed)) {
|
|
453
|
+
throw new Error(`toBaseUnits: expected an integer base-unit string, got "${value}"`);
|
|
454
|
+
}
|
|
455
|
+
return BigInt(trimmed);
|
|
294
456
|
}
|
|
295
457
|
function fromBaseUnits(value) {
|
|
458
|
+
if (typeof value === "number" && !Number.isSafeInteger(value)) {
|
|
459
|
+
throw new Error(`fromBaseUnits: number must be a safe integer, got ${value}`);
|
|
460
|
+
}
|
|
296
461
|
return BigInt(value).toString();
|
|
297
462
|
}
|
|
298
463
|
function humanToBaseUnits(amount, decimals) {
|