@usepraxis/sdk 0.2.0 → 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/dist/index.d.cts CHANGED
@@ -130,6 +130,14 @@ interface TransferDetail {
130
130
  recipientName: string;
131
131
  recipientAddress: Address;
132
132
  recipientNote?: string;
133
+ /** The destination is the owner's own wallet (derived from the address). */
134
+ toSelf?: true;
135
+ /**
136
+ * Server-computed USD value of `amount`, as a decimal string, when a real
137
+ * price is available. Absent means "not known" — display only, and never an
138
+ * input to base-unit math.
139
+ */
140
+ usdEstimate?: string;
133
141
  }
134
142
  interface SwapDetail {
135
143
  kind: "swap";
@@ -144,6 +152,14 @@ type ProposalDetail = TransferDetail | SwapDetail;
144
152
  type ProposalState = "pending" | "signing" | "signed" | "blocked" | "cancelled";
145
153
  interface ActionProposal {
146
154
  id: string;
155
+ /**
156
+ * Unix seconds when the proposal was produced. The signature gate refuses a
157
+ * proposal older than a week: the amount is fixed and Aegis enforces the
158
+ * envelope live, but the readings on it (fee, simulation, remaining
159
+ * envelope, USD figure) drift. Ask again for a fresh one. Absent on
160
+ * proposals created before this field existed.
161
+ */
162
+ createdAt?: number;
147
163
  detail: ProposalDetail;
148
164
  networkFee: BaseUnitString;
149
165
  simulation: string;
@@ -158,15 +174,46 @@ interface ClarifyOption {
158
174
  }
159
175
  interface ResearchMetric {
160
176
  label: string;
177
+ /** Already display-formatted, and abbreviated where a raw figure is unreadable. */
161
178
  value: string;
179
+ /** The full-precision figure behind an abbreviated `value`, when one was cut. */
180
+ exact?: string;
181
+ /** Why the metric is missing, or what it measures. */
182
+ note?: string;
162
183
  trend?: "up" | "down" | "flat";
163
184
  }
185
+ /**
186
+ * One step in how a research card was produced. A card whose whole claim is
187
+ * "data, no advice" has to be able to show its working: "unavailable" means
188
+ * something different when an indexer has no pair than when an RPC refused
189
+ * the query.
190
+ */
191
+ interface ResearchSource {
192
+ /** "Solana RPC", "Market data (…)", "PreStocks", "Token resolution". */
193
+ label: string;
194
+ status: "ok" | "partial" | "unavailable";
195
+ detail: string;
196
+ }
164
197
  interface ResearchData {
165
198
  token: string;
199
+ /** Project name from the indexer, when it differs from the ticker. */
200
+ name?: string;
166
201
  mint: Address;
167
202
  metrics: ResearchMetric[];
203
+ /** How the card was produced, in the order the steps ran. */
204
+ sources?: ResearchSource[];
168
205
  summary: string;
169
206
  }
207
+ /**
208
+ * Stocklana C07: one PreStocks entry from `GET /get-stock-universe`.
209
+ * Empty unless the server runs with `PRAXIS_STOCKS_ENABLED=1`.
210
+ */
211
+ interface StockUniverseEntry {
212
+ symbol: string;
213
+ name: string;
214
+ mint: Address;
215
+ decimals: number;
216
+ }
170
217
  interface PolicyChangeRow {
171
218
  label: string;
172
219
  from: string;
@@ -221,6 +268,8 @@ interface ActivityEntry {
221
268
  id: string;
222
269
  kind: "transfer" | "swap";
223
270
  label: string;
271
+ /** Destination address for a transfer. Stable where `label` is a lookup. */
272
+ target?: Address;
224
273
  asset: string;
225
274
  amount: BaseUnitString;
226
275
  decimals: number;
@@ -230,11 +279,46 @@ interface ActivityEntry {
230
279
  ts: number;
231
280
  sig?: string;
232
281
  }
282
+ /** When a recurring buy fires. `weekday`: 0=Sunday..6=Saturday (UTC). */
283
+ type DcaCadence = {
284
+ type: "daily";
285
+ } | {
286
+ type: "weekly";
287
+ weekday: number;
288
+ } | {
289
+ type: "monthly";
290
+ day: number;
291
+ };
292
+ /**
293
+ * One recurring-buy schedule from `GET /get-schedules`. Each fire emits one
294
+ * transfer proposal through the same policy checks as a one-off buy — nothing
295
+ * ever auto-signs. Money is a base-unit string per the money rule above.
296
+ */
297
+ interface DcaSchedule {
298
+ id: string;
299
+ asset: string;
300
+ amount: BaseUnitString;
301
+ decimals: number;
302
+ recipientAddress: Address;
303
+ recipientName: string;
304
+ cadence: DcaCadence;
305
+ /** Unix milliseconds of the next fire. */
306
+ nextFireTs: number;
307
+ createdAt: number;
308
+ threadId: string;
309
+ }
233
310
  interface UnsignedOwnerTransaction {
234
311
  /** base64-encoded unsigned transaction for the owner wallet to sign. */
235
312
  transaction: string;
236
313
  blockhash: string;
237
314
  lastValidBlockHeight: number;
315
+ /**
316
+ * Opaque, backend-signed fingerprint of this draft. Echo it back verbatim in
317
+ * {@link SignedOwnerTransaction}: the relay refuses any transaction that is
318
+ * not the one Praxis built, which is what keeps the server-side checks for
319
+ * this action from being skippable.
320
+ */
321
+ draft: string;
238
322
  }
239
323
  /**
240
324
  * An {@link UnsignedOwnerTransaction} after the owner wallet has signed it — the
@@ -268,6 +352,14 @@ type OwnerAction = {
268
352
  listKind: AllowListKind;
269
353
  address: Address;
270
354
  mode: "add" | "remove";
355
+ } | {
356
+ kind: "configureToken";
357
+ tokenMint: Address;
358
+ tokenMaxPerTx: BaseUnitString;
359
+ tokenDailyLimit: BaseUnitString;
360
+ } | {
361
+ kind: "prepareTokenAccounts";
362
+ recipientAddresses?: Address[];
271
363
  };
272
364
 
273
365
  type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
@@ -349,14 +441,39 @@ declare class PraxisClient {
349
441
  }>;
350
442
  signProposal(proposalId: string): Promise<void>;
351
443
  cancelProposal(proposalId: string): Promise<void>;
444
+ /** List recurring-buy schedules (each fire emits one proposal; never signs). */
445
+ getSchedules(): Promise<DcaSchedule[]>;
446
+ /** Stop a recurring-buy schedule. Unknown ids are a no-op (idempotent). */
447
+ cancelSchedule(scheduleId: string): Promise<void>;
448
+ /** Save (or rename) an address-book contact. Labels have no signing power. */
449
+ addContact(label: string, address: string): Promise<void>;
450
+ /** Remove a contact by address or label (case-insensitive, idempotent). */
451
+ removeContact(key: string): Promise<void>;
352
452
  getThreads(): Promise<Thread[]>;
353
453
  getThread(id: string): Promise<Thread>;
354
454
  getProposal(id: string): Promise<ActionProposal>;
455
+ /**
456
+ * Every proposal this wallet holds, in one request. Prefer this over a loop
457
+ * of {@link getProposal}: a per-id fetch is what trips the read rate limit
458
+ * on a busy thread.
459
+ */
460
+ getProposals(): Promise<ActionProposal[]>;
355
461
  getPolicy(): Promise<PolicyView>;
356
462
  getActivity(): Promise<ActivityEntry[]>;
357
463
  getAddressBook(): Promise<AddressBookEntry[]>;
358
- isThinking(threadId: string): Promise<boolean>;
359
464
  getVersion(): Promise<number>;
465
+ /**
466
+ * The server's stock universe (`[]` when `PRAXIS_STOCKS_ENABLED` is off).
467
+ * Read-only; symbols/mints here are the only pre-IPO stocks Praxis will
468
+ * touch (bounty exclusivity is enforced server-side).
469
+ */
470
+ getTokenUniverse(): Promise<StockUniverseEntry[]>;
471
+ /**
472
+ * Read-only research for a stock symbol, via the agent (`research <symbol>`).
473
+ * Returns neutral market data — never advice. Throws when the agent has no
474
+ * research to show (unknown symbol or unavailable quotes).
475
+ */
476
+ getStockResearch(symbol: string): Promise<ResearchData>;
360
477
  bootstrapPolicy(fundLamports?: BaseUnitString): Promise<void>;
361
478
  /** Deposit SOL (lamports, base-unit string) from the owner into the vault. */
362
479
  fundVault(amount: BaseUnitString): Promise<void>;
@@ -384,21 +501,51 @@ declare class PraxisClient {
384
501
  }>;
385
502
  private get;
386
503
  private post;
504
+ /**
505
+ * Run a request, and if the session has expired, sign in again and retry it
506
+ * once.
507
+ *
508
+ * Sessions are deliberately short — holding one is enough to move value
509
+ * within the Aegis envelope — so a long-lived agent process WILL outlive its
510
+ * cookie. Without this, every caller writes the same catch-401-and-reconnect
511
+ * block, and the ones who do not simply stop working after a day. Retried
512
+ * once only, never for the auth endpoints themselves, and only when a signer
513
+ * is configured; without one there is nothing to re-authenticate with and the
514
+ * 401 is the honest answer.
515
+ */
387
516
  private request;
517
+ private send1;
388
518
  private captureCookie;
389
519
  }
390
520
 
521
+ /**
522
+ * A stable, machine-readable classification of a backend failure. Branch on
523
+ * this rather than on `message`, which is human prose and free to change.
524
+ * `client_error` is synthesized SDK-side for failures with no HTTP response
525
+ * (timeout, DNS, TLS).
526
+ */
527
+ type PraxisErrorCode = "config_error" | "unauthorized" | "invalid_input" | "not_found"
528
+ /** The wallet has no Aegis policy account yet — call `bootstrapPolicy`. */
529
+ | "policy_not_found" | "rate_limited"
530
+ /** A concurrent writer changed this wallet's state first; reload and retry. */
531
+ | "conflict" | "internal_error" | "client_error";
391
532
  /**
392
533
  * Thrown when the Praxis API returns a non-2xx response. The backend's error
393
- * envelope is `{ error: string, type: string }` with a meaningful HTTP status
534
+ * envelope is `{ error, type, code, details? }` with a meaningful HTTP status
394
535
  * (400 input, 401 auth, 404 not-found, 429 rate-limit, 503 config, 500 other).
395
536
  */
396
537
  declare class PraxisApiError extends Error {
397
538
  readonly status: number;
398
539
  /** The backend error class name, e.g. "PraxisAuthError", "PraxisRateLimitError". */
399
540
  readonly type: string;
541
+ /** Stable error classification — the field to branch on. */
542
+ readonly code: PraxisErrorCode;
543
+ /** Structured facts about the failure, e.g. `{ policyAddress }` for `policy_not_found`. */
544
+ readonly details?: Record<string, string | number | boolean>;
400
545
  constructor(status: number, type: string, message: string, options?: {
401
546
  cause?: unknown;
547
+ code?: PraxisErrorCode;
548
+ details?: Record<string, string | number | boolean>;
402
549
  });
403
550
  get isAuth(): boolean;
404
551
  get isRateLimited(): boolean;
@@ -413,6 +560,10 @@ declare class PraxisApiError extends Error {
413
560
  get isNetwork(): boolean;
414
561
  /** Any server-side failure (HTTP >= 500). */
415
562
  get isServer(): boolean;
563
+ /** The wallet has no Aegis policy yet — the first-run state, not a fault. */
564
+ get isPolicyNotFound(): boolean;
565
+ /** A concurrent writer won; the call is safe to retry after a reload. */
566
+ get isConflict(): boolean;
416
567
  }
417
568
  /** Thrown for SDK-side misconfiguration (no fetch, no signer, bad key, …). */
418
569
  declare class PraxisConfigError extends Error {
@@ -443,4 +594,4 @@ declare function humanToBaseUnits(amount: string, decimals: number): string;
443
594
  /** Convert base units into a human decimal string for `decimals` places. */
444
595
  declare function baseUnitsToHuman(value: string | bigint, decimals: number): string;
445
596
 
446
- export { ActionKind, type ActionProposal, type ActivityEntry, type Address, type AddressBookEntry, type AgentBlock, type AgentMessage, type AllowListKind, type AskResult, type BaseUnitString, type ClarifyOption, type FetchLike, type Message, type OwnerAction, type PolicyChangeRow, type PolicyCheckResult, type PolicyUpdate, type PolicyView, PraxisApiError, PraxisClient, type PraxisClientOptions, PraxisConfigError, type PraxisSigner, type ProposalDetail, type ProposalState, RejectReason, type ResearchData, type ResearchMetric, type SecretKeyInput, type SessionInfo, type SignedOwnerTransaction, type SwapDetail, type Thread, type TokenEnvelopeConfig, type TokenInfo, type TransferDetail, type UnsignedOwnerTransaction, type UserMessage, type WalletChallenge, baseUnitsToHuman, fromBaseUnits, humanToBaseUnits, keypairSigner, toBaseUnits };
597
+ export { ActionKind, type ActionProposal, type ActivityEntry, type Address, type AddressBookEntry, type AgentBlock, type AgentMessage, type AllowListKind, type AskResult, type BaseUnitString, type ClarifyOption, type DcaCadence, type DcaSchedule, type FetchLike, type Message, type OwnerAction, type PolicyChangeRow, type PolicyCheckResult, type PolicyUpdate, type PolicyView, PraxisApiError, PraxisClient, type PraxisClientOptions, PraxisConfigError, type PraxisErrorCode, type PraxisSigner, type ProposalDetail, type ProposalState, RejectReason, type ResearchData, type ResearchMetric, type ResearchSource, type SecretKeyInput, type SessionInfo, type SignedOwnerTransaction, type StockUniverseEntry, type SwapDetail, type Thread, type TokenEnvelopeConfig, type TokenInfo, type TransferDetail, type UnsignedOwnerTransaction, type UserMessage, type WalletChallenge, baseUnitsToHuman, fromBaseUnits, humanToBaseUnits, keypairSigner, toBaseUnits };
package/dist/index.d.ts CHANGED
@@ -130,6 +130,14 @@ interface TransferDetail {
130
130
  recipientName: string;
131
131
  recipientAddress: Address;
132
132
  recipientNote?: string;
133
+ /** The destination is the owner's own wallet (derived from the address). */
134
+ toSelf?: true;
135
+ /**
136
+ * Server-computed USD value of `amount`, as a decimal string, when a real
137
+ * price is available. Absent means "not known" — display only, and never an
138
+ * input to base-unit math.
139
+ */
140
+ usdEstimate?: string;
133
141
  }
134
142
  interface SwapDetail {
135
143
  kind: "swap";
@@ -144,6 +152,14 @@ type ProposalDetail = TransferDetail | SwapDetail;
144
152
  type ProposalState = "pending" | "signing" | "signed" | "blocked" | "cancelled";
145
153
  interface ActionProposal {
146
154
  id: string;
155
+ /**
156
+ * Unix seconds when the proposal was produced. The signature gate refuses a
157
+ * proposal older than a week: the amount is fixed and Aegis enforces the
158
+ * envelope live, but the readings on it (fee, simulation, remaining
159
+ * envelope, USD figure) drift. Ask again for a fresh one. Absent on
160
+ * proposals created before this field existed.
161
+ */
162
+ createdAt?: number;
147
163
  detail: ProposalDetail;
148
164
  networkFee: BaseUnitString;
149
165
  simulation: string;
@@ -158,15 +174,46 @@ interface ClarifyOption {
158
174
  }
159
175
  interface ResearchMetric {
160
176
  label: string;
177
+ /** Already display-formatted, and abbreviated where a raw figure is unreadable. */
161
178
  value: string;
179
+ /** The full-precision figure behind an abbreviated `value`, when one was cut. */
180
+ exact?: string;
181
+ /** Why the metric is missing, or what it measures. */
182
+ note?: string;
162
183
  trend?: "up" | "down" | "flat";
163
184
  }
185
+ /**
186
+ * One step in how a research card was produced. A card whose whole claim is
187
+ * "data, no advice" has to be able to show its working: "unavailable" means
188
+ * something different when an indexer has no pair than when an RPC refused
189
+ * the query.
190
+ */
191
+ interface ResearchSource {
192
+ /** "Solana RPC", "Market data (…)", "PreStocks", "Token resolution". */
193
+ label: string;
194
+ status: "ok" | "partial" | "unavailable";
195
+ detail: string;
196
+ }
164
197
  interface ResearchData {
165
198
  token: string;
199
+ /** Project name from the indexer, when it differs from the ticker. */
200
+ name?: string;
166
201
  mint: Address;
167
202
  metrics: ResearchMetric[];
203
+ /** How the card was produced, in the order the steps ran. */
204
+ sources?: ResearchSource[];
168
205
  summary: string;
169
206
  }
207
+ /**
208
+ * Stocklana C07: one PreStocks entry from `GET /get-stock-universe`.
209
+ * Empty unless the server runs with `PRAXIS_STOCKS_ENABLED=1`.
210
+ */
211
+ interface StockUniverseEntry {
212
+ symbol: string;
213
+ name: string;
214
+ mint: Address;
215
+ decimals: number;
216
+ }
170
217
  interface PolicyChangeRow {
171
218
  label: string;
172
219
  from: string;
@@ -221,6 +268,8 @@ interface ActivityEntry {
221
268
  id: string;
222
269
  kind: "transfer" | "swap";
223
270
  label: string;
271
+ /** Destination address for a transfer. Stable where `label` is a lookup. */
272
+ target?: Address;
224
273
  asset: string;
225
274
  amount: BaseUnitString;
226
275
  decimals: number;
@@ -230,11 +279,46 @@ interface ActivityEntry {
230
279
  ts: number;
231
280
  sig?: string;
232
281
  }
282
+ /** When a recurring buy fires. `weekday`: 0=Sunday..6=Saturday (UTC). */
283
+ type DcaCadence = {
284
+ type: "daily";
285
+ } | {
286
+ type: "weekly";
287
+ weekday: number;
288
+ } | {
289
+ type: "monthly";
290
+ day: number;
291
+ };
292
+ /**
293
+ * One recurring-buy schedule from `GET /get-schedules`. Each fire emits one
294
+ * transfer proposal through the same policy checks as a one-off buy — nothing
295
+ * ever auto-signs. Money is a base-unit string per the money rule above.
296
+ */
297
+ interface DcaSchedule {
298
+ id: string;
299
+ asset: string;
300
+ amount: BaseUnitString;
301
+ decimals: number;
302
+ recipientAddress: Address;
303
+ recipientName: string;
304
+ cadence: DcaCadence;
305
+ /** Unix milliseconds of the next fire. */
306
+ nextFireTs: number;
307
+ createdAt: number;
308
+ threadId: string;
309
+ }
233
310
  interface UnsignedOwnerTransaction {
234
311
  /** base64-encoded unsigned transaction for the owner wallet to sign. */
235
312
  transaction: string;
236
313
  blockhash: string;
237
314
  lastValidBlockHeight: number;
315
+ /**
316
+ * Opaque, backend-signed fingerprint of this draft. Echo it back verbatim in
317
+ * {@link SignedOwnerTransaction}: the relay refuses any transaction that is
318
+ * not the one Praxis built, which is what keeps the server-side checks for
319
+ * this action from being skippable.
320
+ */
321
+ draft: string;
238
322
  }
239
323
  /**
240
324
  * An {@link UnsignedOwnerTransaction} after the owner wallet has signed it — the
@@ -268,6 +352,14 @@ type OwnerAction = {
268
352
  listKind: AllowListKind;
269
353
  address: Address;
270
354
  mode: "add" | "remove";
355
+ } | {
356
+ kind: "configureToken";
357
+ tokenMint: Address;
358
+ tokenMaxPerTx: BaseUnitString;
359
+ tokenDailyLimit: BaseUnitString;
360
+ } | {
361
+ kind: "prepareTokenAccounts";
362
+ recipientAddresses?: Address[];
271
363
  };
272
364
 
273
365
  type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
@@ -349,14 +441,39 @@ declare class PraxisClient {
349
441
  }>;
350
442
  signProposal(proposalId: string): Promise<void>;
351
443
  cancelProposal(proposalId: string): Promise<void>;
444
+ /** List recurring-buy schedules (each fire emits one proposal; never signs). */
445
+ getSchedules(): Promise<DcaSchedule[]>;
446
+ /** Stop a recurring-buy schedule. Unknown ids are a no-op (idempotent). */
447
+ cancelSchedule(scheduleId: string): Promise<void>;
448
+ /** Save (or rename) an address-book contact. Labels have no signing power. */
449
+ addContact(label: string, address: string): Promise<void>;
450
+ /** Remove a contact by address or label (case-insensitive, idempotent). */
451
+ removeContact(key: string): Promise<void>;
352
452
  getThreads(): Promise<Thread[]>;
353
453
  getThread(id: string): Promise<Thread>;
354
454
  getProposal(id: string): Promise<ActionProposal>;
455
+ /**
456
+ * Every proposal this wallet holds, in one request. Prefer this over a loop
457
+ * of {@link getProposal}: a per-id fetch is what trips the read rate limit
458
+ * on a busy thread.
459
+ */
460
+ getProposals(): Promise<ActionProposal[]>;
355
461
  getPolicy(): Promise<PolicyView>;
356
462
  getActivity(): Promise<ActivityEntry[]>;
357
463
  getAddressBook(): Promise<AddressBookEntry[]>;
358
- isThinking(threadId: string): Promise<boolean>;
359
464
  getVersion(): Promise<number>;
465
+ /**
466
+ * The server's stock universe (`[]` when `PRAXIS_STOCKS_ENABLED` is off).
467
+ * Read-only; symbols/mints here are the only pre-IPO stocks Praxis will
468
+ * touch (bounty exclusivity is enforced server-side).
469
+ */
470
+ getTokenUniverse(): Promise<StockUniverseEntry[]>;
471
+ /**
472
+ * Read-only research for a stock symbol, via the agent (`research <symbol>`).
473
+ * Returns neutral market data — never advice. Throws when the agent has no
474
+ * research to show (unknown symbol or unavailable quotes).
475
+ */
476
+ getStockResearch(symbol: string): Promise<ResearchData>;
360
477
  bootstrapPolicy(fundLamports?: BaseUnitString): Promise<void>;
361
478
  /** Deposit SOL (lamports, base-unit string) from the owner into the vault. */
362
479
  fundVault(amount: BaseUnitString): Promise<void>;
@@ -384,21 +501,51 @@ declare class PraxisClient {
384
501
  }>;
385
502
  private get;
386
503
  private post;
504
+ /**
505
+ * Run a request, and if the session has expired, sign in again and retry it
506
+ * once.
507
+ *
508
+ * Sessions are deliberately short — holding one is enough to move value
509
+ * within the Aegis envelope — so a long-lived agent process WILL outlive its
510
+ * cookie. Without this, every caller writes the same catch-401-and-reconnect
511
+ * block, and the ones who do not simply stop working after a day. Retried
512
+ * once only, never for the auth endpoints themselves, and only when a signer
513
+ * is configured; without one there is nothing to re-authenticate with and the
514
+ * 401 is the honest answer.
515
+ */
387
516
  private request;
517
+ private send1;
388
518
  private captureCookie;
389
519
  }
390
520
 
521
+ /**
522
+ * A stable, machine-readable classification of a backend failure. Branch on
523
+ * this rather than on `message`, which is human prose and free to change.
524
+ * `client_error` is synthesized SDK-side for failures with no HTTP response
525
+ * (timeout, DNS, TLS).
526
+ */
527
+ type PraxisErrorCode = "config_error" | "unauthorized" | "invalid_input" | "not_found"
528
+ /** The wallet has no Aegis policy account yet — call `bootstrapPolicy`. */
529
+ | "policy_not_found" | "rate_limited"
530
+ /** A concurrent writer changed this wallet's state first; reload and retry. */
531
+ | "conflict" | "internal_error" | "client_error";
391
532
  /**
392
533
  * Thrown when the Praxis API returns a non-2xx response. The backend's error
393
- * envelope is `{ error: string, type: string }` with a meaningful HTTP status
534
+ * envelope is `{ error, type, code, details? }` with a meaningful HTTP status
394
535
  * (400 input, 401 auth, 404 not-found, 429 rate-limit, 503 config, 500 other).
395
536
  */
396
537
  declare class PraxisApiError extends Error {
397
538
  readonly status: number;
398
539
  /** The backend error class name, e.g. "PraxisAuthError", "PraxisRateLimitError". */
399
540
  readonly type: string;
541
+ /** Stable error classification — the field to branch on. */
542
+ readonly code: PraxisErrorCode;
543
+ /** Structured facts about the failure, e.g. `{ policyAddress }` for `policy_not_found`. */
544
+ readonly details?: Record<string, string | number | boolean>;
400
545
  constructor(status: number, type: string, message: string, options?: {
401
546
  cause?: unknown;
547
+ code?: PraxisErrorCode;
548
+ details?: Record<string, string | number | boolean>;
402
549
  });
403
550
  get isAuth(): boolean;
404
551
  get isRateLimited(): boolean;
@@ -413,6 +560,10 @@ declare class PraxisApiError extends Error {
413
560
  get isNetwork(): boolean;
414
561
  /** Any server-side failure (HTTP >= 500). */
415
562
  get isServer(): boolean;
563
+ /** The wallet has no Aegis policy yet — the first-run state, not a fault. */
564
+ get isPolicyNotFound(): boolean;
565
+ /** A concurrent writer won; the call is safe to retry after a reload. */
566
+ get isConflict(): boolean;
416
567
  }
417
568
  /** Thrown for SDK-side misconfiguration (no fetch, no signer, bad key, …). */
418
569
  declare class PraxisConfigError extends Error {
@@ -443,4 +594,4 @@ declare function humanToBaseUnits(amount: string, decimals: number): string;
443
594
  /** Convert base units into a human decimal string for `decimals` places. */
444
595
  declare function baseUnitsToHuman(value: string | bigint, decimals: number): string;
445
596
 
446
- export { ActionKind, type ActionProposal, type ActivityEntry, type Address, type AddressBookEntry, type AgentBlock, type AgentMessage, type AllowListKind, type AskResult, type BaseUnitString, type ClarifyOption, type FetchLike, type Message, type OwnerAction, type PolicyChangeRow, type PolicyCheckResult, type PolicyUpdate, type PolicyView, PraxisApiError, PraxisClient, type PraxisClientOptions, PraxisConfigError, type PraxisSigner, type ProposalDetail, type ProposalState, RejectReason, type ResearchData, type ResearchMetric, type SecretKeyInput, type SessionInfo, type SignedOwnerTransaction, type SwapDetail, type Thread, type TokenEnvelopeConfig, type TokenInfo, type TransferDetail, type UnsignedOwnerTransaction, type UserMessage, type WalletChallenge, baseUnitsToHuman, fromBaseUnits, humanToBaseUnits, keypairSigner, toBaseUnits };
597
+ export { ActionKind, type ActionProposal, type ActivityEntry, type Address, type AddressBookEntry, type AgentBlock, type AgentMessage, type AllowListKind, type AskResult, type BaseUnitString, type ClarifyOption, type DcaCadence, type DcaSchedule, type FetchLike, type Message, type OwnerAction, type PolicyChangeRow, type PolicyCheckResult, type PolicyUpdate, type PolicyView, PraxisApiError, PraxisClient, type PraxisClientOptions, PraxisConfigError, type PraxisErrorCode, type PraxisSigner, type ProposalDetail, type ProposalState, RejectReason, type ResearchData, type ResearchMetric, type ResearchSource, type SecretKeyInput, type SessionInfo, type SignedOwnerTransaction, type StockUniverseEntry, type SwapDetail, type Thread, type TokenEnvelopeConfig, type TokenInfo, type TransferDetail, type UnsignedOwnerTransaction, type UserMessage, type WalletChallenge, baseUnitsToHuman, fromBaseUnits, humanToBaseUnits, keypairSigner, toBaseUnits };
package/dist/index.js CHANGED
@@ -8,11 +8,17 @@ var PraxisApiError = class _PraxisApiError extends Error {
8
8
  status;
9
9
  /** The backend error class name, e.g. "PraxisAuthError", "PraxisRateLimitError". */
10
10
  type;
11
+ /** Stable error classification — the field to branch on. */
12
+ code;
13
+ /** Structured facts about the failure, e.g. `{ policyAddress }` for `policy_not_found`. */
14
+ details;
11
15
  constructor(status, type, message, options) {
12
16
  super(message, options);
13
17
  this.name = "PraxisApiError";
14
18
  this.status = status;
15
19
  this.type = type;
20
+ this.code = options?.code ?? codeFromStatus(status);
21
+ this.details = options?.details;
16
22
  Object.setPrototypeOf(this, _PraxisApiError.prototype);
17
23
  }
18
24
  get isAuth() {
@@ -44,7 +50,25 @@ var PraxisApiError = class _PraxisApiError extends Error {
44
50
  get isServer() {
45
51
  return this.status >= 500;
46
52
  }
53
+ /** The wallet has no Aegis policy yet — the first-run state, not a fault. */
54
+ get isPolicyNotFound() {
55
+ return this.code === "policy_not_found";
56
+ }
57
+ /** A concurrent writer won; the call is safe to retry after a reload. */
58
+ get isConflict() {
59
+ return this.code === "conflict";
60
+ }
47
61
  };
62
+ function codeFromStatus(status) {
63
+ if (status === 400) return "invalid_input";
64
+ if (status === 401) return "unauthorized";
65
+ if (status === 404) return "not_found";
66
+ if (status === 409) return "conflict";
67
+ if (status === 429) return "rate_limited";
68
+ if (status === 503) return "config_error";
69
+ if (status === 0) return "client_error";
70
+ return "internal_error";
71
+ }
48
72
  var PraxisConfigError = class _PraxisConfigError extends Error {
49
73
  constructor(message, options) {
50
74
  super(message, options);
@@ -143,7 +167,9 @@ var PraxisClient = class {
143
167
  throw new PraxisApiError(500, "Error", "Agent produced no reply message.");
144
168
  }
145
169
  const proposalIds = message.blocks.filter((b) => b.type === "proposal").map((b) => b.proposalId);
146
- const proposals = await Promise.all(proposalIds.map((id) => this.getProposal(id)));
170
+ if (proposalIds.length === 0) return { threadId: tid, message, proposals: [] };
171
+ const byId = new Map((await this.getProposals()).map((p) => [p.id, p]));
172
+ const proposals = proposalIds.map((id) => byId.get(id)).filter((p) => p !== void 0);
147
173
  return { threadId: tid, message, proposals };
148
174
  }
149
175
  newThread(threadId) {
@@ -155,6 +181,22 @@ var PraxisClient = class {
155
181
  cancelProposal(proposalId) {
156
182
  return this.post("/cancel-proposal", { proposalId });
157
183
  }
184
+ /** List recurring-buy schedules (each fire emits one proposal; never signs). */
185
+ getSchedules() {
186
+ return this.get("/get-schedules");
187
+ }
188
+ /** Stop a recurring-buy schedule. Unknown ids are a no-op (idempotent). */
189
+ cancelSchedule(scheduleId) {
190
+ return this.post("/cancel-schedule", { scheduleId });
191
+ }
192
+ /** Save (or rename) an address-book contact. Labels have no signing power. */
193
+ addContact(label, address) {
194
+ return this.post("/add-contact", { label, address });
195
+ }
196
+ /** Remove a contact by address or label (case-insensitive, idempotent). */
197
+ removeContact(key) {
198
+ return this.post("/remove-contact", { key });
199
+ }
158
200
  // --- reads ---------------------------------------------------------------
159
201
  getThreads() {
160
202
  return this.get("/get-threads");
@@ -165,6 +207,14 @@ var PraxisClient = class {
165
207
  getProposal(id) {
166
208
  return this.get("/get-proposal", { id });
167
209
  }
210
+ /**
211
+ * Every proposal this wallet holds, in one request. Prefer this over a loop
212
+ * of {@link getProposal}: a per-id fetch is what trips the read rate limit
213
+ * on a busy thread.
214
+ */
215
+ getProposals() {
216
+ return this.get("/get-proposals");
217
+ }
168
218
  getPolicy() {
169
219
  return this.get("/get-policy");
170
220
  }
@@ -174,12 +224,31 @@ var PraxisClient = class {
174
224
  getAddressBook() {
175
225
  return this.get("/get-address-book");
176
226
  }
177
- isThinking(threadId) {
178
- return this.get("/is-thinking", { threadId });
179
- }
180
227
  getVersion() {
181
228
  return this.get("/get-version");
182
229
  }
230
+ // --- stocks (PreStocks universe; empty unless the server enables it) ------
231
+ /**
232
+ * The server's stock universe (`[]` when `PRAXIS_STOCKS_ENABLED` is off).
233
+ * Read-only; symbols/mints here are the only pre-IPO stocks Praxis will
234
+ * touch (bounty exclusivity is enforced server-side).
235
+ */
236
+ getTokenUniverse() {
237
+ return this.get("/get-stock-universe");
238
+ }
239
+ /**
240
+ * Read-only research for a stock symbol, via the agent (`research <symbol>`).
241
+ * Returns neutral market data — never advice. Throws when the agent has no
242
+ * research to show (unknown symbol or unavailable quotes).
243
+ */
244
+ async getStockResearch(symbol) {
245
+ const { message } = await this.ask(`research ${symbol}`);
246
+ const block = message.blocks.find((b) => b.type === "research");
247
+ if (!block || block.type !== "research") {
248
+ throw new PraxisApiError(404, "NotFound", `No research available for ${symbol}.`);
249
+ }
250
+ return block.data;
251
+ }
183
252
  // --- policy / owner mutations (server-key mode) --------------------------
184
253
  bootstrapPolicy(fundLamports) {
185
254
  return this.post("/bootstrap-policy", fundLamports ? { fundLamports } : {});
@@ -238,7 +307,30 @@ var PraxisClient = class {
238
307
  post(path, body, timeoutMs) {
239
308
  return this.request("POST", path, { body, timeoutMs });
240
309
  }
310
+ /**
311
+ * Run a request, and if the session has expired, sign in again and retry it
312
+ * once.
313
+ *
314
+ * Sessions are deliberately short — holding one is enough to move value
315
+ * within the Aegis envelope — so a long-lived agent process WILL outlive its
316
+ * cookie. Without this, every caller writes the same catch-401-and-reconnect
317
+ * block, and the ones who do not simply stop working after a day. Retried
318
+ * once only, never for the auth endpoints themselves, and only when a signer
319
+ * is configured; without one there is nothing to re-authenticate with and the
320
+ * 401 is the honest answer.
321
+ */
241
322
  async request(method, path, opts = {}) {
323
+ try {
324
+ return await this.send1(method, path, opts);
325
+ } catch (error) {
326
+ const recoverable = error instanceof PraxisApiError && error.isAuth && Boolean(this.signer) && !path.startsWith("/auth/");
327
+ if (!recoverable) throw error;
328
+ this.sessionCookie = void 0;
329
+ await this.connect();
330
+ return this.send1(method, path, opts);
331
+ }
332
+ }
333
+ async send1(method, path, opts = {}) {
242
334
  const url = new URL(this.baseUrl + API_PREFIX + path);
243
335
  for (const [k, v] of Object.entries(opts.query ?? {})) url.searchParams.set(k, v);
244
336
  const headers = { accept: "application/json" };
@@ -275,7 +367,10 @@ var PraxisClient = class {
275
367
  if (!res.ok) {
276
368
  const message = (parsed && typeof parsed === "object" && "error" in parsed && typeof parsed.error === "string" ? parsed.error : void 0) ?? `Praxis API error ${res.status}`;
277
369
  const type = parsed && typeof parsed === "object" && "type" in parsed && typeof parsed.type === "string" ? parsed.type : "Error";
278
- throw new PraxisApiError(res.status, type, message);
370
+ const record = parsed && typeof parsed === "object" ? parsed : void 0;
371
+ const code = typeof record?.code === "string" ? record.code : void 0;
372
+ const details = record?.details && typeof record.details === "object" && !Array.isArray(record.details) ? record.details : void 0;
373
+ throw new PraxisApiError(res.status, type, message, { code, details });
279
374
  }
280
375
  return parsed;
281
376
  }