@usepraxis/sdk 0.1.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 +106 -0
- package/dist/index.cjs +350 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +372 -0
- package/dist/index.d.ts +372 -0
- package/dist/index.js +334 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signs the wallet-ownership challenge. Implement this to bridge any wallet:
|
|
3
|
+
* - Node / backend agents: use {@link keypairSigner} with a secret key.
|
|
4
|
+
* - Browser: wrap a wallet-adapter, e.g.
|
|
5
|
+
* `{ address: wallet.publicKey.toBase58(), signMessage: (m) => wallet.signMessage(m) }`.
|
|
6
|
+
*
|
|
7
|
+
* `signMessage` must return the raw 64-byte Ed25519 signature; the SDK base58-
|
|
8
|
+
* encodes it for the API.
|
|
9
|
+
*/
|
|
10
|
+
interface PraxisSigner {
|
|
11
|
+
/** base58-encoded Solana public key. */
|
|
12
|
+
readonly address: string;
|
|
13
|
+
signMessage(message: Uint8Array): Promise<Uint8Array> | Uint8Array;
|
|
14
|
+
}
|
|
15
|
+
type SecretKeyInput = Uint8Array | number[] | string;
|
|
16
|
+
/**
|
|
17
|
+
* Build a {@link PraxisSigner} from a Solana secret key — a 64-byte keypair or a
|
|
18
|
+
* 32-byte seed, as raw bytes, a number array, or a base58 string (Phantom /
|
|
19
|
+
* `solana-keygen` export). Node and browser friendly; no `@solana/web3.js`
|
|
20
|
+
* dependency.
|
|
21
|
+
*/
|
|
22
|
+
declare function keypairSigner(secret: SecretKeyInput): PraxisSigner;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Praxis SDK wire types — the JSON contract exposed by `/api/praxis/*`.
|
|
26
|
+
*
|
|
27
|
+
* MONEY RULE: every monetary value crosses the wire as a **decimal string of
|
|
28
|
+
* integer base units** (lamports for SOL, token base units for SPL) — never a
|
|
29
|
+
* float. On the server these are `bigint`; the API serializes them to strings.
|
|
30
|
+
* The SDK keeps them as strings and leaves bigint conversion to the caller
|
|
31
|
+
* (see `toBaseUnits` / `fromBaseUnits` in `units.ts`). This file intentionally
|
|
32
|
+
* mirrors `@praxis/shared` but with `string` money, so the SDK ships a
|
|
33
|
+
* self-contained type contract with no internal workspace dependency.
|
|
34
|
+
*/
|
|
35
|
+
/** A base58-encoded Solana public key. */
|
|
36
|
+
type Address = string;
|
|
37
|
+
/** Integer base units as a decimal string (e.g. "500000000" = 0.5 SOL). */
|
|
38
|
+
type BaseUnitString = string;
|
|
39
|
+
/** Mirrors the on-chain `ActionKind` discriminants. */
|
|
40
|
+
declare enum ActionKind {
|
|
41
|
+
Transfer = 0,
|
|
42
|
+
TransferSpl = 1
|
|
43
|
+
}
|
|
44
|
+
/** Why Aegis rejected an agent action. Mirrors the on-chain enum order. */
|
|
45
|
+
declare enum RejectReason {
|
|
46
|
+
Unauthorized = 0,
|
|
47
|
+
Paused = 1,
|
|
48
|
+
Expired = 2,
|
|
49
|
+
OverPerTx = 3,
|
|
50
|
+
OverDaily = 4,
|
|
51
|
+
RecipientNotAllowed = 5,
|
|
52
|
+
Overflow = 6,
|
|
53
|
+
MintNotAllowed = 7
|
|
54
|
+
}
|
|
55
|
+
/** Returned by `POST /auth/challenge`; sign `message` to prove wallet ownership. */
|
|
56
|
+
interface WalletChallenge {
|
|
57
|
+
address: Address;
|
|
58
|
+
/** Opaque signed token echoed back to `/auth/verify`. */
|
|
59
|
+
nonce: string;
|
|
60
|
+
/** The exact UTF-8 string the wallet must sign. */
|
|
61
|
+
message: string;
|
|
62
|
+
/** ISO-8601 expiry of the challenge. */
|
|
63
|
+
expiresAt: string;
|
|
64
|
+
}
|
|
65
|
+
interface SessionInfo {
|
|
66
|
+
authenticated: boolean;
|
|
67
|
+
walletAddress: Address;
|
|
68
|
+
}
|
|
69
|
+
interface TokenInfo {
|
|
70
|
+
symbol: string;
|
|
71
|
+
mint: Address;
|
|
72
|
+
decimals: number;
|
|
73
|
+
verified: boolean;
|
|
74
|
+
}
|
|
75
|
+
interface AddressBookEntry {
|
|
76
|
+
label: string;
|
|
77
|
+
name: string;
|
|
78
|
+
address: Address;
|
|
79
|
+
note?: string;
|
|
80
|
+
}
|
|
81
|
+
interface PolicyView {
|
|
82
|
+
address: Address;
|
|
83
|
+
owner: Address;
|
|
84
|
+
/** `11111…1111` (default) once the agent key is revoked. */
|
|
85
|
+
agentAuthority: Address;
|
|
86
|
+
maxPerTx: BaseUnitString;
|
|
87
|
+
dailyLimit: BaseUnitString;
|
|
88
|
+
spentToday: BaseUnitString;
|
|
89
|
+
dayStartTs: number;
|
|
90
|
+
allowedPrograms: Address[];
|
|
91
|
+
/** Empty array means "any recipient allowed". */
|
|
92
|
+
allowedRecipients: Address[];
|
|
93
|
+
allowedMints: Address[];
|
|
94
|
+
expiryTs: number;
|
|
95
|
+
paused: boolean;
|
|
96
|
+
vaultBalance: BaseUnitString;
|
|
97
|
+
tokenMint: Address;
|
|
98
|
+
tokenMaxPerTx: BaseUnitString;
|
|
99
|
+
tokenDailyLimit: BaseUnitString;
|
|
100
|
+
tokenSpentToday: BaseUnitString;
|
|
101
|
+
tokenDayStartTs: number;
|
|
102
|
+
}
|
|
103
|
+
interface PolicyCheckResult {
|
|
104
|
+
allowed: boolean;
|
|
105
|
+
reason?: string;
|
|
106
|
+
reasonCode?: RejectReason;
|
|
107
|
+
spentToday: BaseUnitString;
|
|
108
|
+
dailyLimit: BaseUnitString;
|
|
109
|
+
remaining: BaseUnitString;
|
|
110
|
+
}
|
|
111
|
+
interface PolicyUpdate {
|
|
112
|
+
maxPerTx?: BaseUnitString;
|
|
113
|
+
dailyLimit?: BaseUnitString;
|
|
114
|
+
expiryTs?: number;
|
|
115
|
+
paused?: boolean;
|
|
116
|
+
}
|
|
117
|
+
interface TokenEnvelopeConfig {
|
|
118
|
+
tokenMint: Address;
|
|
119
|
+
tokenMaxPerTx: BaseUnitString;
|
|
120
|
+
tokenDailyLimit: BaseUnitString;
|
|
121
|
+
}
|
|
122
|
+
type AllowListKind = "programs" | "recipients" | "mints";
|
|
123
|
+
interface TransferDetail {
|
|
124
|
+
kind: "transfer";
|
|
125
|
+
amount: BaseUnitString;
|
|
126
|
+
asset: TokenInfo;
|
|
127
|
+
recipientName: string;
|
|
128
|
+
recipientAddress: Address;
|
|
129
|
+
recipientNote?: string;
|
|
130
|
+
}
|
|
131
|
+
interface SwapDetail {
|
|
132
|
+
kind: "swap";
|
|
133
|
+
amountIn: BaseUnitString;
|
|
134
|
+
assetIn: TokenInfo;
|
|
135
|
+
estAmountOut: BaseUnitString;
|
|
136
|
+
assetOut: TokenInfo;
|
|
137
|
+
route: string;
|
|
138
|
+
priceImpactBps: number;
|
|
139
|
+
}
|
|
140
|
+
type ProposalDetail = TransferDetail | SwapDetail;
|
|
141
|
+
type ProposalState = "pending" | "signing" | "signed" | "blocked" | "cancelled";
|
|
142
|
+
interface ActionProposal {
|
|
143
|
+
id: string;
|
|
144
|
+
detail: ProposalDetail;
|
|
145
|
+
networkFee: BaseUnitString;
|
|
146
|
+
simulation: string;
|
|
147
|
+
check: PolicyCheckResult;
|
|
148
|
+
state: ProposalState;
|
|
149
|
+
sig?: string;
|
|
150
|
+
}
|
|
151
|
+
interface ClarifyOption {
|
|
152
|
+
label: string;
|
|
153
|
+
value: string;
|
|
154
|
+
hint?: string;
|
|
155
|
+
}
|
|
156
|
+
interface ResearchMetric {
|
|
157
|
+
label: string;
|
|
158
|
+
value: string;
|
|
159
|
+
trend?: "up" | "down" | "flat";
|
|
160
|
+
}
|
|
161
|
+
interface ResearchData {
|
|
162
|
+
token: string;
|
|
163
|
+
mint: Address;
|
|
164
|
+
metrics: ResearchMetric[];
|
|
165
|
+
summary: string;
|
|
166
|
+
}
|
|
167
|
+
type AgentBlock = {
|
|
168
|
+
type: "prose";
|
|
169
|
+
text: string;
|
|
170
|
+
} | {
|
|
171
|
+
type: "clarify";
|
|
172
|
+
text: string;
|
|
173
|
+
options: ClarifyOption[];
|
|
174
|
+
} | {
|
|
175
|
+
type: "proposal";
|
|
176
|
+
text: string;
|
|
177
|
+
proposalId: string;
|
|
178
|
+
} | {
|
|
179
|
+
type: "research";
|
|
180
|
+
text: string;
|
|
181
|
+
data: ResearchData;
|
|
182
|
+
};
|
|
183
|
+
type UserMessage = {
|
|
184
|
+
id: string;
|
|
185
|
+
role: "user";
|
|
186
|
+
ts: number;
|
|
187
|
+
text: string;
|
|
188
|
+
};
|
|
189
|
+
type AgentMessage = {
|
|
190
|
+
id: string;
|
|
191
|
+
role: "agent";
|
|
192
|
+
ts: number;
|
|
193
|
+
blocks: AgentBlock[];
|
|
194
|
+
};
|
|
195
|
+
type Message = UserMessage | AgentMessage;
|
|
196
|
+
interface Thread {
|
|
197
|
+
id: string;
|
|
198
|
+
title: string;
|
|
199
|
+
messages: Message[];
|
|
200
|
+
updatedAt: number;
|
|
201
|
+
}
|
|
202
|
+
interface ActivityEntry {
|
|
203
|
+
id: string;
|
|
204
|
+
kind: "transfer" | "swap";
|
|
205
|
+
label: string;
|
|
206
|
+
asset: string;
|
|
207
|
+
amount: BaseUnitString;
|
|
208
|
+
decimals: number;
|
|
209
|
+
result: "allowed" | "rejected";
|
|
210
|
+
reason?: string;
|
|
211
|
+
reasonCode?: RejectReason;
|
|
212
|
+
ts: number;
|
|
213
|
+
sig?: string;
|
|
214
|
+
}
|
|
215
|
+
interface UnsignedOwnerTransaction {
|
|
216
|
+
/** base64-encoded unsigned transaction for the owner wallet to sign. */
|
|
217
|
+
transaction: string;
|
|
218
|
+
blockhash: string;
|
|
219
|
+
lastValidBlockHeight: number;
|
|
220
|
+
}
|
|
221
|
+
/** Typed owner action accepted by `POST /owner/build`. */
|
|
222
|
+
type OwnerAction = {
|
|
223
|
+
kind: "bootstrapPolicy";
|
|
224
|
+
fundLamports?: BaseUnitString;
|
|
225
|
+
} | {
|
|
226
|
+
kind: "fundVault";
|
|
227
|
+
amount: BaseUnitString;
|
|
228
|
+
} | {
|
|
229
|
+
kind: "withdrawVault";
|
|
230
|
+
amount: BaseUnitString;
|
|
231
|
+
} | {
|
|
232
|
+
kind: "closePolicy";
|
|
233
|
+
} | {
|
|
234
|
+
kind: "revoke";
|
|
235
|
+
} | {
|
|
236
|
+
kind: "rotate";
|
|
237
|
+
} | {
|
|
238
|
+
kind: "updatePolicy";
|
|
239
|
+
patch: PolicyUpdate;
|
|
240
|
+
} | {
|
|
241
|
+
kind: "allowList";
|
|
242
|
+
listKind: AllowListKind;
|
|
243
|
+
address: Address;
|
|
244
|
+
mode: "add" | "remove";
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
248
|
+
interface PraxisClientOptions {
|
|
249
|
+
/** Base origin of the Praxis deployment, e.g. "http://localhost:3000". */
|
|
250
|
+
baseUrl: string;
|
|
251
|
+
/** Wallet signer for `connect()`. Optional if you only call public reads after an external login. */
|
|
252
|
+
signer?: PraxisSigner;
|
|
253
|
+
/** Custom fetch (defaults to global fetch). Required in runtimes without one. */
|
|
254
|
+
fetch?: FetchLike;
|
|
255
|
+
/** Per-request timeout in ms (default 20_000). */
|
|
256
|
+
timeoutMs?: number;
|
|
257
|
+
}
|
|
258
|
+
/** Result of {@link PraxisClient.ask} — the agent's reply, distilled. */
|
|
259
|
+
interface AskResult {
|
|
260
|
+
threadId: string;
|
|
261
|
+
/** The agent's reply message (prose / clarify / proposal / research blocks). */
|
|
262
|
+
message: AgentMessage;
|
|
263
|
+
/** Any action proposals the agent produced, hydrated for convenience. */
|
|
264
|
+
proposals: ActionProposal[];
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Typed client for a hosted Praxis agent. The agent's LLM, scoped agent key, and
|
|
268
|
+
* Aegis policy enforcement all live server-side; this SDK is an authenticated
|
|
269
|
+
* client of the `/api/praxis/*` surface.
|
|
270
|
+
*
|
|
271
|
+
* ```ts
|
|
272
|
+
* const praxis = new PraxisClient({ baseUrl, signer: keypairSigner(secret) });
|
|
273
|
+
* await praxis.connect();
|
|
274
|
+
* const { proposals } = await praxis.ask("send 0.5 SOL to maya");
|
|
275
|
+
* if (proposals[0]?.check.allowed) await praxis.signProposal(proposals[0].id);
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
declare class PraxisClient {
|
|
279
|
+
private readonly baseUrl;
|
|
280
|
+
private readonly signer?;
|
|
281
|
+
private readonly fetchImpl;
|
|
282
|
+
private readonly timeoutMs;
|
|
283
|
+
/** Manual cookie jar — Node's fetch does not persist Set-Cookie across calls. */
|
|
284
|
+
private sessionCookie?;
|
|
285
|
+
constructor(options: PraxisClientOptions);
|
|
286
|
+
/** The signer's wallet address, if a signer was provided. */
|
|
287
|
+
get address(): string | undefined;
|
|
288
|
+
/**
|
|
289
|
+
* Run the wallet-ownership handshake: request a challenge, sign its message,
|
|
290
|
+
* verify it, and store the resulting session cookie. Idempotent.
|
|
291
|
+
*/
|
|
292
|
+
connect(): Promise<SessionInfo>;
|
|
293
|
+
/** Current session, or `null` if not signed in. */
|
|
294
|
+
session(): Promise<SessionInfo | null>;
|
|
295
|
+
/** Clear the session (server-side cookie + local jar). */
|
|
296
|
+
logout(): Promise<void>;
|
|
297
|
+
/** Send a line to the agent. Creates a thread when `threadId` is omitted. */
|
|
298
|
+
send(text: string, threadId?: string | null): Promise<{
|
|
299
|
+
threadId: string;
|
|
300
|
+
}>;
|
|
301
|
+
/**
|
|
302
|
+
* Send a line and return the agent's reply in one call. The API resolves
|
|
303
|
+
* `send` only after the agent has finished, so this needs no polling.
|
|
304
|
+
*/
|
|
305
|
+
ask(text: string, threadId?: string | null): Promise<AskResult>;
|
|
306
|
+
newThread(threadId?: string): Promise<{
|
|
307
|
+
threadId: string;
|
|
308
|
+
}>;
|
|
309
|
+
signProposal(proposalId: string): Promise<void>;
|
|
310
|
+
cancelProposal(proposalId: string): Promise<void>;
|
|
311
|
+
getThreads(): Promise<Thread[]>;
|
|
312
|
+
getThread(id: string): Promise<Thread>;
|
|
313
|
+
getProposal(id: string): Promise<ActionProposal>;
|
|
314
|
+
getPolicy(): Promise<PolicyView>;
|
|
315
|
+
getActivity(): Promise<ActivityEntry[]>;
|
|
316
|
+
getAddressBook(): Promise<AddressBookEntry[]>;
|
|
317
|
+
isThinking(threadId: string): Promise<boolean>;
|
|
318
|
+
getVersion(): Promise<number>;
|
|
319
|
+
bootstrapPolicy(fundLamports?: BaseUnitString): Promise<void>;
|
|
320
|
+
updatePolicy(patch: PolicyUpdate): Promise<void>;
|
|
321
|
+
configureToken(config: TokenEnvelopeConfig): Promise<void>;
|
|
322
|
+
prepareTokenAccounts(recipientAddresses?: string[]): Promise<void>;
|
|
323
|
+
revokeAgent(): Promise<void>;
|
|
324
|
+
rotateAgent(): Promise<void>;
|
|
325
|
+
addToAllowList(kind: AllowListKind, address: string): Promise<void>;
|
|
326
|
+
removeFromAllowList(kind: AllowListKind, address: string): Promise<void>;
|
|
327
|
+
/** Build an unsigned owner transaction for the wallet to sign. */
|
|
328
|
+
buildOwnerTransaction(action: OwnerAction): Promise<UnsignedOwnerTransaction>;
|
|
329
|
+
/** Submit a wallet-signed owner transaction. */
|
|
330
|
+
submitOwnerTransaction(signed: UnsignedOwnerTransaction): Promise<{
|
|
331
|
+
sig: string;
|
|
332
|
+
}>;
|
|
333
|
+
private get;
|
|
334
|
+
private post;
|
|
335
|
+
private request;
|
|
336
|
+
private captureCookie;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Thrown when the Praxis API returns a non-2xx response. The backend's error
|
|
341
|
+
* envelope is `{ error: string, type: string }` with a meaningful HTTP status
|
|
342
|
+
* (400 input, 401 auth, 404 not-found, 429 rate-limit, 503 config, 500 other).
|
|
343
|
+
*/
|
|
344
|
+
declare class PraxisApiError extends Error {
|
|
345
|
+
readonly status: number;
|
|
346
|
+
/** The backend error class name, e.g. "PraxisAuthError", "PraxisRateLimitError". */
|
|
347
|
+
readonly type: string;
|
|
348
|
+
constructor(status: number, type: string, message: string);
|
|
349
|
+
get isAuth(): boolean;
|
|
350
|
+
get isRateLimited(): boolean;
|
|
351
|
+
get isInput(): boolean;
|
|
352
|
+
}
|
|
353
|
+
/** Thrown for SDK-side misconfiguration (no fetch, no signer, bad key, …). */
|
|
354
|
+
declare class PraxisConfigError extends Error {
|
|
355
|
+
constructor(message: string);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Money helpers. Praxis money crosses the wire as decimal strings of integer
|
|
360
|
+
* base units (lamports / token base units). These convert to/from `bigint` and
|
|
361
|
+
* human decimal amounts without floats.
|
|
362
|
+
*/
|
|
363
|
+
/** Parse a base-unit string (or bigint) into a bigint. */
|
|
364
|
+
declare function toBaseUnits(value: string | bigint): bigint;
|
|
365
|
+
/** Serialize a bigint (or number of whole base units) into a base-unit string. */
|
|
366
|
+
declare function fromBaseUnits(value: bigint | number): string;
|
|
367
|
+
/** Convert a human decimal amount ("0.5") into base units for `decimals` places. */
|
|
368
|
+
declare function humanToBaseUnits(amount: string, decimals: number): string;
|
|
369
|
+
/** Convert base units into a human decimal string for `decimals` places. */
|
|
370
|
+
declare function baseUnitsToHuman(value: string | bigint, decimals: number): string;
|
|
371
|
+
|
|
372
|
+
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 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 SwapDetail, type Thread, type TokenEnvelopeConfig, type TokenInfo, type TransferDetail, type UnsignedOwnerTransaction, type UserMessage, type WalletChallenge, baseUnitsToHuman, fromBaseUnits, humanToBaseUnits, keypairSigner, toBaseUnits };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import bs582 from 'bs58';
|
|
2
|
+
import nacl from 'tweetnacl';
|
|
3
|
+
|
|
4
|
+
// src/client.ts
|
|
5
|
+
|
|
6
|
+
// src/errors.ts
|
|
7
|
+
var PraxisApiError = class _PraxisApiError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
/** The backend error class name, e.g. "PraxisAuthError", "PraxisRateLimitError". */
|
|
10
|
+
type;
|
|
11
|
+
constructor(status, type, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "PraxisApiError";
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.type = type;
|
|
16
|
+
Object.setPrototypeOf(this, _PraxisApiError.prototype);
|
|
17
|
+
}
|
|
18
|
+
get isAuth() {
|
|
19
|
+
return this.status === 401;
|
|
20
|
+
}
|
|
21
|
+
get isRateLimited() {
|
|
22
|
+
return this.status === 429;
|
|
23
|
+
}
|
|
24
|
+
get isInput() {
|
|
25
|
+
return this.status === 400;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var PraxisConfigError = class _PraxisConfigError extends Error {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "PraxisConfigError";
|
|
32
|
+
Object.setPrototypeOf(this, _PraxisConfigError.prototype);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// src/client.ts
|
|
37
|
+
var API_PREFIX = "/api/praxis";
|
|
38
|
+
var PraxisClient = class {
|
|
39
|
+
baseUrl;
|
|
40
|
+
signer;
|
|
41
|
+
fetchImpl;
|
|
42
|
+
timeoutMs;
|
|
43
|
+
/** Manual cookie jar — Node's fetch does not persist Set-Cookie across calls. */
|
|
44
|
+
sessionCookie;
|
|
45
|
+
constructor(options) {
|
|
46
|
+
if (!options.baseUrl) throw new PraxisConfigError("baseUrl is required");
|
|
47
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
48
|
+
this.signer = options.signer;
|
|
49
|
+
const resolvedFetch = options.fetch ?? globalThis.fetch;
|
|
50
|
+
if (!resolvedFetch) {
|
|
51
|
+
throw new PraxisConfigError("No fetch available; pass options.fetch (Node <18 or non-browser runtime).");
|
|
52
|
+
}
|
|
53
|
+
this.fetchImpl = resolvedFetch;
|
|
54
|
+
this.timeoutMs = options.timeoutMs ?? 2e4;
|
|
55
|
+
}
|
|
56
|
+
// --- auth ----------------------------------------------------------------
|
|
57
|
+
/** The signer's wallet address, if a signer was provided. */
|
|
58
|
+
get address() {
|
|
59
|
+
return this.signer?.address;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Run the wallet-ownership handshake: request a challenge, sign its message,
|
|
63
|
+
* verify it, and store the resulting session cookie. Idempotent.
|
|
64
|
+
*/
|
|
65
|
+
async connect() {
|
|
66
|
+
if (!this.signer) {
|
|
67
|
+
throw new PraxisConfigError("connect() requires a signer. Pass one to the constructor.");
|
|
68
|
+
}
|
|
69
|
+
const challenge = await this.post("/auth/challenge", {
|
|
70
|
+
address: this.signer.address
|
|
71
|
+
});
|
|
72
|
+
const signature = await this.signer.signMessage(new TextEncoder().encode(challenge.message));
|
|
73
|
+
return this.post("/auth/verify", {
|
|
74
|
+
address: this.signer.address,
|
|
75
|
+
nonce: challenge.nonce,
|
|
76
|
+
signature: bs582.encode(signature)
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** Current session, or `null` if not signed in. */
|
|
80
|
+
async session() {
|
|
81
|
+
try {
|
|
82
|
+
return await this.get("/auth/session");
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error instanceof PraxisApiError && error.isAuth) return null;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Clear the session (server-side cookie + local jar). */
|
|
89
|
+
async logout() {
|
|
90
|
+
await this.request("DELETE", "/auth/session");
|
|
91
|
+
this.sessionCookie = void 0;
|
|
92
|
+
}
|
|
93
|
+
// --- conversation --------------------------------------------------------
|
|
94
|
+
/** Send a line to the agent. Creates a thread when `threadId` is omitted. */
|
|
95
|
+
send(text, threadId = null) {
|
|
96
|
+
return this.post("/send", { text, threadId });
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Send a line and return the agent's reply in one call. The API resolves
|
|
100
|
+
* `send` only after the agent has finished, so this needs no polling.
|
|
101
|
+
*/
|
|
102
|
+
async ask(text, threadId = null) {
|
|
103
|
+
const { threadId: tid } = await this.send(text, threadId);
|
|
104
|
+
const thread = await this.getThread(tid);
|
|
105
|
+
const message = lastAgentMessage(thread);
|
|
106
|
+
if (!message) {
|
|
107
|
+
throw new PraxisApiError(500, "Error", "Agent produced no reply message.");
|
|
108
|
+
}
|
|
109
|
+
const proposalIds = message.blocks.filter((b) => b.type === "proposal").map((b) => b.proposalId);
|
|
110
|
+
const proposals = await Promise.all(proposalIds.map((id) => this.getProposal(id)));
|
|
111
|
+
return { threadId: tid, message, proposals };
|
|
112
|
+
}
|
|
113
|
+
newThread(threadId) {
|
|
114
|
+
return this.post("/new-thread", threadId ? { threadId } : {});
|
|
115
|
+
}
|
|
116
|
+
signProposal(proposalId) {
|
|
117
|
+
return this.post("/sign-proposal", { proposalId });
|
|
118
|
+
}
|
|
119
|
+
cancelProposal(proposalId) {
|
|
120
|
+
return this.post("/cancel-proposal", { proposalId });
|
|
121
|
+
}
|
|
122
|
+
// --- reads ---------------------------------------------------------------
|
|
123
|
+
getThreads() {
|
|
124
|
+
return this.get("/get-threads");
|
|
125
|
+
}
|
|
126
|
+
getThread(id) {
|
|
127
|
+
return this.get("/get-thread", { id });
|
|
128
|
+
}
|
|
129
|
+
getProposal(id) {
|
|
130
|
+
return this.get("/get-proposal", { id });
|
|
131
|
+
}
|
|
132
|
+
getPolicy() {
|
|
133
|
+
return this.get("/get-policy");
|
|
134
|
+
}
|
|
135
|
+
getActivity() {
|
|
136
|
+
return this.get("/get-activity");
|
|
137
|
+
}
|
|
138
|
+
getAddressBook() {
|
|
139
|
+
return this.get("/get-address-book");
|
|
140
|
+
}
|
|
141
|
+
isThinking(threadId) {
|
|
142
|
+
return this.get("/is-thinking", { threadId });
|
|
143
|
+
}
|
|
144
|
+
getVersion() {
|
|
145
|
+
return this.get("/get-version");
|
|
146
|
+
}
|
|
147
|
+
// --- policy / owner mutations (server-key mode) --------------------------
|
|
148
|
+
bootstrapPolicy(fundLamports) {
|
|
149
|
+
return this.post("/bootstrap-policy", fundLamports ? { fundLamports } : {});
|
|
150
|
+
}
|
|
151
|
+
updatePolicy(patch) {
|
|
152
|
+
return this.post("/update-policy", { patch });
|
|
153
|
+
}
|
|
154
|
+
configureToken(config) {
|
|
155
|
+
return this.post("/configure-token", { config });
|
|
156
|
+
}
|
|
157
|
+
prepareTokenAccounts(recipientAddresses) {
|
|
158
|
+
return this.post("/prepare-token-accounts", recipientAddresses ? { recipientAddresses } : {});
|
|
159
|
+
}
|
|
160
|
+
revokeAgent() {
|
|
161
|
+
return this.post("/revoke-agent", {});
|
|
162
|
+
}
|
|
163
|
+
rotateAgent() {
|
|
164
|
+
return this.post("/rotate-agent", {});
|
|
165
|
+
}
|
|
166
|
+
addToAllowList(kind, address) {
|
|
167
|
+
return this.post("/add-to-allow-list", { kind, address });
|
|
168
|
+
}
|
|
169
|
+
removeFromAllowList(kind, address) {
|
|
170
|
+
return this.post("/remove-from-allow-list", { kind, address });
|
|
171
|
+
}
|
|
172
|
+
// --- owner wallet-signed transaction path --------------------------------
|
|
173
|
+
/** Build an unsigned owner transaction for the wallet to sign. */
|
|
174
|
+
buildOwnerTransaction(action) {
|
|
175
|
+
return this.post("/owner/build", { action });
|
|
176
|
+
}
|
|
177
|
+
/** Submit a wallet-signed owner transaction. */
|
|
178
|
+
submitOwnerTransaction(signed) {
|
|
179
|
+
return this.post("/owner/submit", signed);
|
|
180
|
+
}
|
|
181
|
+
// --- transport -----------------------------------------------------------
|
|
182
|
+
get(path, query) {
|
|
183
|
+
return this.request("GET", path, { query });
|
|
184
|
+
}
|
|
185
|
+
post(path, body) {
|
|
186
|
+
return this.request("POST", path, { body });
|
|
187
|
+
}
|
|
188
|
+
async request(method, path, opts = {}) {
|
|
189
|
+
const url = new URL(this.baseUrl + API_PREFIX + path);
|
|
190
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) url.searchParams.set(k, v);
|
|
191
|
+
const headers = { accept: "application/json" };
|
|
192
|
+
if (opts.body !== void 0) headers["content-type"] = "application/json";
|
|
193
|
+
if (this.sessionCookie) headers["cookie"] = this.sessionCookie;
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
196
|
+
let res;
|
|
197
|
+
try {
|
|
198
|
+
res = await this.fetchImpl(url.toString(), {
|
|
199
|
+
method,
|
|
200
|
+
headers,
|
|
201
|
+
body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body),
|
|
202
|
+
// Browser same-origin: let the HttpOnly cookie ride along.
|
|
203
|
+
credentials: "include",
|
|
204
|
+
signal: controller.signal
|
|
205
|
+
});
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
208
|
+
throw new PraxisApiError(0, "TimeoutError", `Praxis request timed out after ${this.timeoutMs}ms`);
|
|
209
|
+
}
|
|
210
|
+
throw error;
|
|
211
|
+
} finally {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
}
|
|
214
|
+
this.captureCookie(res);
|
|
215
|
+
const text = await res.text();
|
|
216
|
+
const parsed = text ? safeJsonParse(text) : void 0;
|
|
217
|
+
if (!res.ok) {
|
|
218
|
+
const message = (parsed && typeof parsed === "object" && "error" in parsed && typeof parsed.error === "string" ? parsed.error : void 0) ?? `Praxis API error ${res.status}`;
|
|
219
|
+
const type = parsed && typeof parsed === "object" && "type" in parsed && typeof parsed.type === "string" ? parsed.type : "Error";
|
|
220
|
+
throw new PraxisApiError(res.status, type, message);
|
|
221
|
+
}
|
|
222
|
+
return parsed;
|
|
223
|
+
}
|
|
224
|
+
captureCookie(res) {
|
|
225
|
+
const anyHeaders = res.headers;
|
|
226
|
+
const cookies = anyHeaders.getSetCookie?.() ?? splitSetCookie(res.headers.get("set-cookie"));
|
|
227
|
+
for (const cookie of cookies) {
|
|
228
|
+
const [pair] = cookie.split(";");
|
|
229
|
+
if (pair && pair.trim().startsWith("praxis_session=")) {
|
|
230
|
+
this.sessionCookie = pair.trim();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
function lastAgentMessage(thread) {
|
|
236
|
+
for (let i = thread.messages.length - 1; i >= 0; i--) {
|
|
237
|
+
const m = thread.messages[i];
|
|
238
|
+
if (m.role === "agent") return m;
|
|
239
|
+
}
|
|
240
|
+
return void 0;
|
|
241
|
+
}
|
|
242
|
+
function safeJsonParse(text) {
|
|
243
|
+
try {
|
|
244
|
+
return JSON.parse(text);
|
|
245
|
+
} catch {
|
|
246
|
+
return text;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function splitSetCookie(header) {
|
|
250
|
+
if (!header) return [];
|
|
251
|
+
return header.split(/,(?=\s*[^;,\s]+=)/);
|
|
252
|
+
}
|
|
253
|
+
function keypairSigner(secret) {
|
|
254
|
+
const bytes = normalizeSecret(secret);
|
|
255
|
+
const pair = bytes.length === 64 ? nacl.sign.keyPair.fromSecretKey(bytes) : nacl.sign.keyPair.fromSeed(bytes);
|
|
256
|
+
const address = bs582.encode(pair.publicKey);
|
|
257
|
+
const secretKey = pair.secretKey;
|
|
258
|
+
return {
|
|
259
|
+
address,
|
|
260
|
+
signMessage: (message) => nacl.sign.detached(message, secretKey)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function normalizeSecret(secret) {
|
|
264
|
+
let bytes;
|
|
265
|
+
if (typeof secret === "string") {
|
|
266
|
+
try {
|
|
267
|
+
bytes = bs582.decode(secret.trim());
|
|
268
|
+
} catch {
|
|
269
|
+
throw new PraxisConfigError("secret key string must be base58-encoded");
|
|
270
|
+
}
|
|
271
|
+
} else if (Array.isArray(secret)) {
|
|
272
|
+
bytes = Uint8Array.from(secret);
|
|
273
|
+
} else {
|
|
274
|
+
bytes = secret;
|
|
275
|
+
}
|
|
276
|
+
if (bytes.length !== 64 && bytes.length !== 32) {
|
|
277
|
+
throw new PraxisConfigError(
|
|
278
|
+
`secret key must be 64 bytes (keypair) or 32 bytes (seed), got ${bytes.length}`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return bytes;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/units.ts
|
|
285
|
+
function toBaseUnits(value) {
|
|
286
|
+
return typeof value === "bigint" ? value : BigInt(value.trim());
|
|
287
|
+
}
|
|
288
|
+
function fromBaseUnits(value) {
|
|
289
|
+
return BigInt(value).toString();
|
|
290
|
+
}
|
|
291
|
+
function humanToBaseUnits(amount, decimals) {
|
|
292
|
+
const trimmed = amount.trim();
|
|
293
|
+
if (!/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
294
|
+
throw new Error(`invalid decimal amount: "${amount}"`);
|
|
295
|
+
}
|
|
296
|
+
const [whole, fraction = ""] = trimmed.split(".");
|
|
297
|
+
if (fraction.length > decimals) {
|
|
298
|
+
throw new Error(`"${amount}" has more than ${decimals} decimal places`);
|
|
299
|
+
}
|
|
300
|
+
const padded = fraction.padEnd(decimals, "0");
|
|
301
|
+
return (BigInt(whole) * 10n ** BigInt(decimals) + BigInt(padded || "0")).toString();
|
|
302
|
+
}
|
|
303
|
+
function baseUnitsToHuman(value, decimals) {
|
|
304
|
+
const units = toBaseUnits(value);
|
|
305
|
+
const negative = units < 0n;
|
|
306
|
+
const abs = negative ? -units : units;
|
|
307
|
+
const divisor = 10n ** BigInt(decimals);
|
|
308
|
+
const whole = abs / divisor;
|
|
309
|
+
const fraction = (abs % divisor).toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
310
|
+
const sign = negative ? "-" : "";
|
|
311
|
+
return fraction ? `${sign}${whole}.${fraction}` : `${sign}${whole}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/types.ts
|
|
315
|
+
var ActionKind = /* @__PURE__ */ ((ActionKind2) => {
|
|
316
|
+
ActionKind2[ActionKind2["Transfer"] = 0] = "Transfer";
|
|
317
|
+
ActionKind2[ActionKind2["TransferSpl"] = 1] = "TransferSpl";
|
|
318
|
+
return ActionKind2;
|
|
319
|
+
})(ActionKind || {});
|
|
320
|
+
var RejectReason = /* @__PURE__ */ ((RejectReason2) => {
|
|
321
|
+
RejectReason2[RejectReason2["Unauthorized"] = 0] = "Unauthorized";
|
|
322
|
+
RejectReason2[RejectReason2["Paused"] = 1] = "Paused";
|
|
323
|
+
RejectReason2[RejectReason2["Expired"] = 2] = "Expired";
|
|
324
|
+
RejectReason2[RejectReason2["OverPerTx"] = 3] = "OverPerTx";
|
|
325
|
+
RejectReason2[RejectReason2["OverDaily"] = 4] = "OverDaily";
|
|
326
|
+
RejectReason2[RejectReason2["RecipientNotAllowed"] = 5] = "RecipientNotAllowed";
|
|
327
|
+
RejectReason2[RejectReason2["Overflow"] = 6] = "Overflow";
|
|
328
|
+
RejectReason2[RejectReason2["MintNotAllowed"] = 7] = "MintNotAllowed";
|
|
329
|
+
return RejectReason2;
|
|
330
|
+
})(RejectReason || {});
|
|
331
|
+
|
|
332
|
+
export { ActionKind, PraxisApiError, PraxisClient, PraxisConfigError, RejectReason, baseUnitsToHuman, fromBaseUnits, humanToBaseUnits, keypairSigner, toBaseUnits };
|
|
333
|
+
//# sourceMappingURL=index.js.map
|
|
334
|
+
//# sourceMappingURL=index.js.map
|