@bondedhq/sdk 0.0.0-stage → 0.1.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +483 -2
  3. package/dist/actions.d.ts +76 -0
  4. package/dist/actions.js +115 -0
  5. package/dist/agent-url.d.ts +101 -0
  6. package/dist/agent-url.js +234 -0
  7. package/dist/agentkit.d.ts +104 -0
  8. package/dist/agentkit.js +206 -0
  9. package/dist/ai-sdk.d.ts +39 -0
  10. package/dist/ai-sdk.js +86 -0
  11. package/dist/breaches.d.ts +50 -0
  12. package/dist/breaches.js +57 -0
  13. package/dist/done.d.ts +14 -0
  14. package/dist/done.js +16 -0
  15. package/dist/eliza.d.ts +118 -0
  16. package/dist/eliza.js +281 -0
  17. package/dist/episode.d.ts +108 -0
  18. package/dist/episode.js +65 -0
  19. package/dist/errors.d.ts +93 -0
  20. package/dist/errors.js +97 -0
  21. package/dist/guarded.d.ts +80 -0
  22. package/dist/guarded.js +97 -0
  23. package/dist/index.d.ts +20 -0
  24. package/dist/index.js +19 -0
  25. package/dist/json-schema-zod3.d.ts +9 -0
  26. package/dist/json-schema-zod3.js +54 -0
  27. package/dist/langchain.d.ts +39 -0
  28. package/dist/langchain.js +53 -0
  29. package/dist/mandate.d.ts +36 -0
  30. package/dist/mandate.js +26 -0
  31. package/dist/mcp-config-cli.d.ts +3 -0
  32. package/dist/mcp-config-cli.js +44 -0
  33. package/dist/mcp.d.ts +131 -0
  34. package/dist/mcp.js +111 -0
  35. package/dist/module.d.ts +37 -0
  36. package/dist/module.js +32 -0
  37. package/dist/revert.d.ts +12 -0
  38. package/dist/revert.js +71 -0
  39. package/dist/sentinel.d.ts +43 -0
  40. package/dist/sentinel.js +94 -0
  41. package/dist/signature.d.ts +34 -0
  42. package/dist/signature.js +52 -0
  43. package/dist/solana-agent-kit.d.ts +141 -0
  44. package/dist/solana-agent-kit.js +333 -0
  45. package/dist/vault-tools.d.ts +67 -0
  46. package/dist/vault-tools.js +335 -0
  47. package/dist/vault.d.ts +116 -0
  48. package/dist/vault.js +326 -0
  49. package/package.json +134 -3
@@ -0,0 +1,333 @@
1
+ import { breachEventsFromLogs, executeInstruction, fetchVault, TOKEN_PROGRAM, vaultError, } from "@bondedhq/solana";
2
+ import { PublicKey, SystemProgram, TransactionInstruction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js";
3
+ import { z } from "zod/v3";
4
+ import { BondedError } from "./errors.js";
5
+ /** An error from the Solana vault, with the breach when the vault recorded one. */
6
+ export class SolanaVaultError extends BondedError {
7
+ breach;
8
+ signature;
9
+ name = "SolanaVaultError";
10
+ constructor(code, message, breach, signature, options) {
11
+ super(code, message, options);
12
+ this.breach = breach;
13
+ this.signature = signature;
14
+ }
15
+ }
16
+ /** Solana Agent Kit validates with zod 3; `zod/v3` is that API, typed at this boundary. */
17
+ const schema = (shape) => z.object(shape);
18
+ /** Reads and confirmations use "confirmed": a vault's state must be current to judge an action. */
19
+ const COMMITMENT = "confirmed";
20
+ const ATA_PROGRAM = new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
21
+ const MINT_DECIMALS_OFFSET = 44;
22
+ /** The associated token account of `owner` for `mint` (the vault, a PDA, included). */
23
+ export function associatedTokenAddress(owner, mint) {
24
+ return PublicKey.findProgramAddressSync([owner.toBuffer(), TOKEN_PROGRAM.toBuffer(), mint.toBuffer()], ATA_PROGRAM)[0];
25
+ }
26
+ /**
27
+ * A Bonded vault on Solana, from the agent's side. Every action is the vault program's
28
+ * `execute`, signed by the agent key.
29
+ */
30
+ export class SolanaBondedVault {
31
+ connection;
32
+ agent;
33
+ address;
34
+ constructor(connection, vault, agent) {
35
+ this.connection = connection;
36
+ this.agent = agent;
37
+ this.address = new PublicKey(vault);
38
+ }
39
+ async status() {
40
+ const vault = await fetchVault(this.connection, this.address, COMMITMENT);
41
+ if (!vault)
42
+ throw new BondedError("NOT_FOUND", `no Bonded vault at ${this.address.toBase58()}`);
43
+ return vault;
44
+ }
45
+ /**
46
+ * Send `amount` (base units) of `mint` from the vault to `to`'s associated token account,
47
+ * creating it (paid by the agent key) if it doesn't exist. Only a transfer to the vault's own
48
+ * account keeps to the mandate on Solana.
49
+ */
50
+ async transfer(params) {
51
+ const plan = await this.transferPlan(params);
52
+ return this.execute(plan.instruction, plan.setup);
53
+ }
54
+ /** Runs `inner` through the vault's `execute`, after any `setup` the agent key runs itself. */
55
+ async execute(inner, setup = [], lookupTables = []) {
56
+ const { transaction, blockhash, lastValidBlockHeight } = await this.build(inner, setup, lookupTables);
57
+ const signed = await this.agent.signTransaction(transaction);
58
+ let signature;
59
+ try {
60
+ signature = await this.connection.sendRawTransaction(signed.serialize(), {
61
+ preflightCommitment: COMMITMENT,
62
+ });
63
+ }
64
+ catch (error) {
65
+ throw this.programError(error);
66
+ }
67
+ const confirmed = await this.connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, COMMITMENT);
68
+ if (confirmed.value.err)
69
+ throw this.programError(confirmed.value.err, `transaction ${signature} failed`);
70
+ const tx = await this.connection.getTransaction(signature, {
71
+ commitment: COMMITMENT,
72
+ maxSupportedTransactionVersion: 0,
73
+ });
74
+ const breaches = breachEventsFromLogs(tx?.meta?.logMessages ?? []);
75
+ const blocked = breaches.find((b) => b.blocked);
76
+ if (blocked)
77
+ throw new SolanaVaultError("BLOCKED_BY_MANDATE", `the vault blocked this action (${blocked.type}); the attempt is recorded as breach ${blocked.breachId}`, blocked, signature);
78
+ return { signature, breaches };
79
+ }
80
+ /** What the vault would do with `inner`: simulates `execute` against the cluster's state. */
81
+ async dryRun(inner, setup = [], lookupTables = []) {
82
+ const { transaction } = await this.build(inner, setup, lookupTables);
83
+ const { value } = await this.connection.simulateTransaction(transaction, {
84
+ sigVerify: false,
85
+ replaceRecentBlockhash: true,
86
+ commitment: COMMITMENT,
87
+ });
88
+ if (value.err)
89
+ throw this.programError(value.err, "the simulation failed", value.logs);
90
+ const breaches = breachEventsFromLogs(value.logs ?? []);
91
+ return {
92
+ ok: breaches.length === 0,
93
+ blocked: breaches.some((b) => b.blocked),
94
+ ...(breaches[0] ? { breach: breaches[0] } : {}),
95
+ breaches,
96
+ };
97
+ }
98
+ /**
99
+ * The SPL `TransferChecked` the vault would sign for `transfer`, and the account creation the
100
+ * agent key runs first if needed. Pass both to `dryRun` to preview a transfer.
101
+ */
102
+ async transferPlan(params) {
103
+ const vault = await this.status();
104
+ const source = params.mint.equals(vault.baseMint)
105
+ ? vault.baseAccount
106
+ : associatedTokenAddress(this.address, params.mint);
107
+ const destination = associatedTokenAddress(params.to, params.mint);
108
+ const setup = (await this.connection.getAccountInfo(destination, COMMITMENT))
109
+ ? []
110
+ : [createAtaIdempotent(this.agent.publicKey, destination, params.to, params.mint)];
111
+ const decimals = await mintDecimals(this.connection, params.mint);
112
+ const data = Buffer.alloc(10);
113
+ data.writeUInt8(12, 0); // TransferChecked
114
+ data.writeBigUInt64LE(params.amount, 1);
115
+ data.writeUInt8(decimals, 9);
116
+ const instruction = new TransactionInstruction({
117
+ programId: TOKEN_PROGRAM,
118
+ data,
119
+ keys: [
120
+ { pubkey: source, isSigner: false, isWritable: true },
121
+ { pubkey: params.mint, isSigner: false, isWritable: false },
122
+ { pubkey: destination, isSigner: false, isWritable: true },
123
+ { pubkey: this.address, isSigner: true, isWritable: false },
124
+ ],
125
+ });
126
+ return { instruction, setup };
127
+ }
128
+ async build(inner, setup, lookupTables) {
129
+ const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash(COMMITMENT);
130
+ const execute = executeInstruction({ address: this.address, agent: this.agent.publicKey }, inner);
131
+ const message = new TransactionMessage({
132
+ payerKey: this.agent.publicKey,
133
+ recentBlockhash: blockhash,
134
+ instructions: [...setup, execute],
135
+ }).compileToV0Message(lookupTables);
136
+ return { transaction: new VersionedTransaction(message), blockhash, lastValidBlockHeight };
137
+ }
138
+ /** Names the vault program's error when the failure is one, e.g. a frozen vault. */
139
+ programError(error, context, logs) {
140
+ const code = customErrorCode(error);
141
+ const named = code !== undefined ? vaultError(code) : undefined;
142
+ if (named)
143
+ return new SolanaVaultError(named.name === "Frozen"
144
+ ? "VAULT_FROZEN"
145
+ : named.name === "NotAgent"
146
+ ? "NOT_AGENT"
147
+ : "VAULT_ERROR", named.message, undefined, undefined, { cause: error });
148
+ const detail = logs?.length ? `\n${logs.join("\n")}` : "";
149
+ const what = error instanceof Error ? error.message : JSON.stringify(error);
150
+ return new BondedError("CALL_REVERTED", `${context ?? "the transaction failed"}: ${what}${detail}`, {
151
+ cause: error,
152
+ });
153
+ }
154
+ }
155
+ /** The custom program error code in a transaction error, from its JSON or its message. */
156
+ function customErrorCode(error) {
157
+ const json = /"Custom":(\d+)/.exec(JSON.stringify(error) ?? "");
158
+ if (json)
159
+ return Number(json[1]);
160
+ const message = error instanceof Error ? error.message : "";
161
+ const hex = /custom program error: 0x([0-9a-f]+)/i.exec(message);
162
+ return hex ? parseInt(hex[1], 16) : undefined;
163
+ }
164
+ function createAtaIdempotent(payer, ata, owner, mint) {
165
+ return new TransactionInstruction({
166
+ programId: ATA_PROGRAM,
167
+ data: Buffer.from([1]), // CreateIdempotent
168
+ keys: [
169
+ { pubkey: payer, isSigner: true, isWritable: true },
170
+ { pubkey: ata, isSigner: false, isWritable: true },
171
+ { pubkey: owner, isSigner: false, isWritable: false },
172
+ { pubkey: mint, isSigner: false, isWritable: false },
173
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
174
+ { pubkey: TOKEN_PROGRAM, isSigner: false, isWritable: false },
175
+ ],
176
+ });
177
+ }
178
+ async function mintDecimals(connection, mint) {
179
+ const info = await connection.getAccountInfo(mint, COMMITMENT);
180
+ if (!info || !info.owner.equals(TOKEN_PROGRAM) || info.data.length <= MINT_DECIMALS_OFFSET)
181
+ throw new BondedError("INVALID_ACTION", `${mint.toBase58()} is not an SPL token mint`);
182
+ return info.data[MINT_DECIMALS_OFFSET];
183
+ }
184
+ /** Whole units (e.g. "12.5") to base units, refusing more decimals than the mint has. */
185
+ function toBaseUnits(amount, decimals) {
186
+ const match = /^(\d+)(?:\.(\d+))?$/.exec(amount.trim());
187
+ if (!match || (match[2]?.length ?? 0) > decimals)
188
+ throw new BondedError("INVALID_ACTION", `"${amount}" is not an amount with at most ${decimals} decimals`);
189
+ return BigInt(match[1] + (match[2] ?? "").padEnd(decimals, "0"));
190
+ }
191
+ /**
192
+ * A Solana Agent Kit plugin whose transfer and swap go through a Bonded vault. The kit's wallet
193
+ * must be the vault's agent key. Methods (`agent.methods.*`) take base units; actions take whole
194
+ * units, as an LLM writes them.
195
+ */
196
+ export function bondedVaultPlugin(options) {
197
+ const vaults = new WeakMap();
198
+ const vaultFor = (agent) => {
199
+ let vault = vaults.get(agent);
200
+ if (!vault) {
201
+ vault = new SolanaBondedVault(agent.connection, options.vault, agent.wallet);
202
+ vaults.set(agent, vault);
203
+ }
204
+ return vault;
205
+ };
206
+ const guarded = async (vault, inner, setup = [], lookupTables = []) => {
207
+ if (options.guard) {
208
+ const check = await vault.dryRun(inner, setup, lookupTables);
209
+ if (!check.ok)
210
+ throw new SolanaVaultError("PREDICTED_BREACH", `refused: the vault would record a breach (${check.breach?.type})`, check.breach);
211
+ }
212
+ return vault.execute(inner, setup, lookupTables);
213
+ };
214
+ const methods = {
215
+ /** The vault's account: mode, frozen, allowed programs, limit and breach log. */
216
+ bondedVaultStatus: (agent) => vaultFor(agent).status(),
217
+ bondedVaultTransfer: async (agent, params) => {
218
+ const vault = vaultFor(agent);
219
+ const plan = await vault.transferPlan(params);
220
+ return guarded(vault, plan.instruction, plan.setup);
221
+ },
222
+ bondedVaultSwap: async (agent, params) => {
223
+ if (!options.swap)
224
+ throw new BondedError("CONFIG", "set `swap` in bondedVaultPlugin to swap through the vault");
225
+ const vault = vaultFor(agent);
226
+ const built = await options.swap({
227
+ vault: vault.address,
228
+ inputMint: params.inputMint,
229
+ outputMint: params.outputMint,
230
+ amount: params.amount,
231
+ slippageBps: params.slippageBps ?? 50,
232
+ });
233
+ return guarded(vault, built.instruction, built.setup, built.lookupTables);
234
+ },
235
+ /** Any instruction with the vault as signer, through `execute`. */
236
+ bondedVaultExecute: (agent, instruction) => guarded(vaultFor(agent), instruction),
237
+ };
238
+ const report = (result) => ({
239
+ signature: result.signature,
240
+ breaches: result.breaches.map((b) => ({
241
+ ...b,
242
+ target: b.target.toBase58(),
243
+ loss: b.loss.toString(),
244
+ })),
245
+ message: result.breaches.length
246
+ ? `Done, but the vault recorded a breach (${result.breaches.map((b) => b.type).join(", ")}) and may have frozen the agent.`
247
+ : "Done within the mandate.",
248
+ });
249
+ const transferAction = {
250
+ name: "BONDED_VAULT_TRANSFER",
251
+ similes: ["send tokens from the vault", "transfer from the bonded vault"],
252
+ description: "Send SPL tokens from the Bonded vault that holds the agent's funds. The vault checks it against the mandate; on Solana only transfers to the vault's own accounts are allowed.",
253
+ examples: [
254
+ [
255
+ {
256
+ input: {
257
+ mint: "So11111111111111111111111111111111111111112",
258
+ to: "<vault>",
259
+ amount: "1.5",
260
+ },
261
+ output: { status: "success", signature: "5x...", breaches: [] },
262
+ explanation: "Move 1.5 tokens into the vault's own token account",
263
+ },
264
+ ],
265
+ ],
266
+ schema: schema({
267
+ mint: z.string().describe("The token's mint address"),
268
+ to: z.string().describe("The owner of the receiving token account"),
269
+ amount: z.string().describe("Amount in whole tokens, e.g. 12.5"),
270
+ }),
271
+ handler: async (agent, input) => {
272
+ const mint = new PublicKey(input.mint);
273
+ const amount = toBaseUnits(input.amount, await mintDecimals(agent.connection, mint));
274
+ return report(await methods.bondedVaultTransfer(agent, {
275
+ mint,
276
+ to: new PublicKey(input.to),
277
+ amount,
278
+ }));
279
+ },
280
+ };
281
+ const swapAction = {
282
+ name: "BONDED_VAULT_SWAP",
283
+ similes: ["swap tokens in the vault", "trade through the bonded vault"],
284
+ description: "Swap tokens held by the Bonded vault. The swap runs inside the vault's execute, which checks the program and the outflow limit.",
285
+ examples: [],
286
+ schema: schema({
287
+ inputMint: z.string(),
288
+ outputMint: z.string(),
289
+ amount: z.string().describe("Amount of the input token in whole tokens"),
290
+ slippageBps: z.number().int().min(0).max(10_000).optional(),
291
+ }),
292
+ handler: async (agent, input) => {
293
+ const inputMint = new PublicKey(input.inputMint);
294
+ const amount = toBaseUnits(input.amount, await mintDecimals(agent.connection, inputMint));
295
+ return report(await methods.bondedVaultSwap(agent, {
296
+ inputMint,
297
+ outputMint: new PublicKey(input.outputMint),
298
+ amount,
299
+ ...(input.slippageBps !== undefined ? { slippageBps: input.slippageBps } : {}),
300
+ }));
301
+ },
302
+ };
303
+ const statusAction = {
304
+ name: "BONDED_VAULT_STATUS",
305
+ similes: ["vault status", "is the vault frozen"],
306
+ description: "Report the Bonded vault's state: whether it is frozen, its mode, the programs it may call, its per-call limit and its breach log.",
307
+ examples: [],
308
+ schema: schema({}),
309
+ handler: async (agent) => {
310
+ const v = await methods.bondedVaultStatus(agent);
311
+ return {
312
+ vault: v.address.toBase58(),
313
+ frozen: v.frozen,
314
+ mode: v.enforce ? "enforce" : "monitor",
315
+ allowedPrograms: v.allowedPrograms.map((p) => p.toBase58()),
316
+ maxOutflowPerCall: v.maxOutflowPerCall.toString(),
317
+ breaches: v.breaches.map((b) => ({
318
+ id: b.id,
319
+ type: b.type,
320
+ blocked: b.blocked,
321
+ loss: b.loss.toString(),
322
+ })),
323
+ };
324
+ },
325
+ };
326
+ return {
327
+ name: "bonded-vault",
328
+ methods,
329
+ actions: [transferAction, statusAction, ...(options.swap ? [swapAction] : [])],
330
+ initialize: () => { },
331
+ };
332
+ }
333
+ //# sourceMappingURL=solana-agent-kit.js.map
@@ -0,0 +1,67 @@
1
+ import { type GuardContext, type SentinelGuardOptions, type TokenInfo } from "./guarded.js";
2
+ import type { EpisodeToolResult, ToolInputSchema } from "./mcp.js";
3
+ import { BondedVault } from "./vault.js";
4
+ export interface BondedVaultToolsOptions {
5
+ /** The vault (or module), with the agent key to send from and a `router` for swaps. */
6
+ vault: BondedVault;
7
+ /**
8
+ * Tokens the model may name by symbol. Default: the mandate's own assets, read from the vault
9
+ * once (`getAssets()`, with each token's symbol and decimals). Any token can also be named by
10
+ * address.
11
+ */
12
+ tokens?: readonly TokenInfo[];
13
+ /**
14
+ * Dry-run each write first and refuse a predicted breach, so the model gets an error instead
15
+ * of a recorded breach. On by default: an LLM-driven agent should not knowingly breach. The
16
+ * vault judges either way.
17
+ */
18
+ guard?: boolean;
19
+ /** Also ask Sentinel before each write that passes the dry run. Advisory; off unless set. */
20
+ sentinel?: SentinelGuardOptions;
21
+ /**
22
+ * What the agent read before this write (emails, posts, tool output), for Sentinel. Called
23
+ * before each write. The AI SDK tools fill it from the conversation's tool results by default.
24
+ */
25
+ context?: () => GuardContext | Promise<GuardContext>;
26
+ }
27
+ /** The tools' names: the same as the Arena episode's write tools, plus `vault_status`. */
28
+ export declare const BONDED_VAULT_TOOLS: readonly ["swap", "transfer", "approve", "vault_status"];
29
+ export type BondedVaultToolName = (typeof BONDED_VAULT_TOOLS)[number];
30
+ /** One vault tool, framework-neutral: wrap it for your framework, or use the helpers. */
31
+ export interface BondedVaultTool {
32
+ name: BondedVaultToolName;
33
+ description: string;
34
+ inputSchema: ToolInputSchema;
35
+ /**
36
+ * Runs the tool. A failure (a refused dry run, Sentinel's advice, a blocked or reverted call,
37
+ * bad arguments) is returned as `{ ok: false, error }`, not thrown, so the model can react.
38
+ */
39
+ call(args?: Record<string, unknown>, context?: GuardContext): Promise<EpisodeToolResult>;
40
+ }
41
+ /**
42
+ * The vault's trading tools, framework-neutral: `swap`, `transfer`, `approve` (the same names and
43
+ * arguments as the Arena episode's write tools, so the agent's prompts work the same while it is
44
+ * rated) and `vault_status`. Every write goes through `BondedVault`, so the vault checks it
45
+ * against the mandate; by default each is dry-run first and refused on a predicted breach.
46
+ * `bondedVaultLangChainTools` and `bondedVaultAiSdkTools` wrap these for their frameworks.
47
+ */
48
+ export declare function bondedVaultTools(options: BondedVaultToolsOptions): BondedVaultTool[];
49
+ /**
50
+ * The production vault from the environment, or undefined when `BONDED_VAULT_ADDRESS` isn't set
51
+ * (so the same code runs before the agent has a vault):
52
+ *
53
+ * | Variable | Meaning |
54
+ * |---|---|
55
+ * | `BONDED_VAULT_ADDRESS` | The Mandate Vault (or MandateModule) |
56
+ * | `BONDED_AGENT_KEY` | The vault's agent key (0x private key), required with the address |
57
+ * | `BONDED_VAULT_CHAIN_ID` | Default 46630 (Robinhood Chain testnet) |
58
+ * | `BONDED_VAULT_RPC_URL` | Default: the chain's public RPC |
59
+ * | `BONDED_ROUTER_ADDRESS` | The swap venue your mandate allows (Bonded's `DemoRouter` on testnet) |
60
+ *
61
+ * These are different from the `BONDED_*` variables the runner sets for an episode, and the
62
+ * runner removes them from a rated agent's environment. If they come back anyway (a `.env` file
63
+ * reloaded with `node --env-file` or dotenv), this still returns undefined while the process is
64
+ * rated, and warns once: a rating never trades the production vault.
65
+ */
66
+ export declare function bondedVaultFromEnv(env?: Record<string, string | undefined>): BondedVault | undefined;
67
+ //# sourceMappingURL=vault-tools.d.ts.map