@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
package/dist/vault.js ADDED
@@ -0,0 +1,326 @@
1
+ import { breachTypeFromCode, mandateModuleAbi, mandateVaultAbi, parseEpisodeEnv, } from "@bondedhq/shared";
2
+ import { createPublicClient, createWalletClient, custom, erc20Abi, getAddress, http, isAddressEqual, } from "viem";
3
+ import { simulateCalls } from "viem/actions";
4
+ import { demoRouter, erc20Call, normaliseAction, } from "./actions.js";
5
+ import { breachReason, readExecuteOutcome, toDryRunResult, } from "./breaches.js";
6
+ import { BlockedByMandateError, BondedError, VaultFrozenError } from "./errors.js";
7
+ import { vaultRevertError } from "./revert.js";
8
+ /**
9
+ * A Mandate Vault, from the agent's side. Every write is `MandateVault.execute(target, data)`
10
+ * sent from the agent key, so the vault checks it against the mandate and records any breach.
11
+ *
12
+ * ```ts
13
+ * const vault = new BondedVault({ vault, chain, account: privateKeyToAccount(agentKey) });
14
+ * const check = await vault.dryRun({ type: "transfer", token, to, amount });
15
+ * if (check.ok) await vault.transfer({ token, to, amount });
16
+ * ```
17
+ */
18
+ export class BondedVault {
19
+ address;
20
+ publicClient;
21
+ walletClient;
22
+ router;
23
+ identity;
24
+ constructor(config) {
25
+ this.address = getAddress(config.vault);
26
+ // Without an rpcUrl, a wallet built here sends through the given public client's RPC.
27
+ const transport = config.rpcUrl || !config.publicClient ? http(config.rpcUrl) : custom(config.publicClient);
28
+ if (config.publicClient)
29
+ this.publicClient = config.publicClient;
30
+ else if (config.chain)
31
+ this.publicClient = createPublicClient({ chain: config.chain, transport });
32
+ else
33
+ throw new BondedError("CONFIG", "BondedVault needs a publicClient or a chain");
34
+ if (config.walletClient) {
35
+ if (!config.walletClient.account)
36
+ throw new BondedError("CONFIG", "the walletClient needs an account (the agent key)");
37
+ this.walletClient = config.walletClient;
38
+ }
39
+ else if (config.account) {
40
+ const chain = config.chain ?? this.publicClient.chain;
41
+ if (!chain)
42
+ throw new BondedError("CONFIG", "BondedVault needs a chain to send transactions");
43
+ this.walletClient = createWalletClient({ account: config.account, chain, transport });
44
+ }
45
+ this.router = typeof config.router === "string" ? demoRouter(config.router) : config.router;
46
+ }
47
+ /** The address that sends this vault's actions, if it can send. */
48
+ get agentAddress() {
49
+ return this.walletClient?.account.address;
50
+ }
51
+ async status() {
52
+ const read = (functionName) => this.publicClient.readContract({ address: this.address, abi: mandateVaultAbi, functionName });
53
+ const [frozen, mode, breachCount, agent, guardian, mandateHash, agentId, freezeOnBreach] = await Promise.all([
54
+ read("frozen"),
55
+ read("mode"),
56
+ read("breachCount"),
57
+ read("agent"),
58
+ read("guardian"),
59
+ read("mandateHash"),
60
+ read("agentId"),
61
+ read("freezeOnBreach"),
62
+ ]);
63
+ const { kind, holder } = await this.identify();
64
+ return {
65
+ kind,
66
+ holder,
67
+ frozen,
68
+ mode: mode === 1 ? "enforce" : "monitor",
69
+ breachCount,
70
+ agent,
71
+ guardian,
72
+ mandateHash,
73
+ agentId,
74
+ freezeOnBreach,
75
+ };
76
+ }
77
+ /**
78
+ * Whether this is a Mandate Vault or a MandateModule, and where its funds are (the vault
79
+ * itself, or the module's account). Read once from the chain: a module has `account()`.
80
+ */
81
+ identify() {
82
+ this.identity ??= this.publicClient
83
+ .readContract({ address: this.address, abi: mandateModuleAbi, functionName: "account" })
84
+ .then((account) => ({ kind: "module", holder: account }), (error) => {
85
+ // A vault has no `account()`: the call reverts. Anything else (network) is an error.
86
+ if (!isMissingFunction(error)) {
87
+ this.identity = undefined;
88
+ throw error;
89
+ }
90
+ return { kind: "vault", holder: this.address };
91
+ });
92
+ return this.identity;
93
+ }
94
+ /** The vault's breach log, oldest first (from `fromId`, default 0). */
95
+ async breaches(fromId = 0n) {
96
+ const count = await this.publicClient.readContract({
97
+ address: this.address,
98
+ abi: mandateVaultAbi,
99
+ functionName: "breachCount",
100
+ });
101
+ const ids = [];
102
+ for (let id = fromId; id < count; id++)
103
+ ids.push(id);
104
+ return Promise.all(ids.map(async (id) => {
105
+ const b = await this.publicClient.readContract({
106
+ address: this.address,
107
+ abi: mandateVaultAbi,
108
+ functionName: "getBreach",
109
+ args: [id],
110
+ });
111
+ const type = breachTypeFromCode(b.breachType);
112
+ return {
113
+ id,
114
+ type,
115
+ reason: breachReason(type),
116
+ target: b.target,
117
+ selector: b.selector,
118
+ lossUsd6: b.lossUsd6,
119
+ blocked: b.blocked,
120
+ };
121
+ }));
122
+ }
123
+ swap(params) {
124
+ return this.execute({ type: "swap", ...params });
125
+ }
126
+ transfer(params) {
127
+ return this.execute({ type: "transfer", ...params });
128
+ }
129
+ approve(params) {
130
+ return this.execute({ type: "approve", ...params });
131
+ }
132
+ call(params) {
133
+ return this.execute({ type: "call", ...params });
134
+ }
135
+ /**
136
+ * Sends an action through the vault and waits for it. Resolves when the vault ran it (in
137
+ * Monitor mode possibly with breaches recorded: see `breaches`). Throws `BlockedByMandateError`
138
+ * when Enforce mode stopped it, `VaultFrozenError` when the vault is or became frozen,
139
+ * `NotAgentError` for the wrong key and `CallRevertedError` when the call itself failed.
140
+ */
141
+ async execute(action) {
142
+ refuseProductionVaultWhileRated(this, "execute");
143
+ const wallet = this.requireWallet();
144
+ const calls = await this.plan(action);
145
+ const result = { hashes: [], breaches: [] };
146
+ for (const [i, call] of calls.entries()) {
147
+ let hash;
148
+ try {
149
+ hash = await wallet.writeContract({
150
+ address: this.address,
151
+ abi: mandateVaultAbi,
152
+ functionName: "execute",
153
+ args: [call.target, call.data],
154
+ account: wallet.account,
155
+ chain: wallet.chain,
156
+ });
157
+ }
158
+ catch (error) {
159
+ throw vaultRevertError(error, {
160
+ vault: this.address,
161
+ target: call.target,
162
+ signer: this.agentAddress,
163
+ });
164
+ }
165
+ const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
166
+ if (receipt.status !== "success")
167
+ throw new BondedError("REVERTED", `execute reverted on chain (transaction ${hash})`);
168
+ const outcome = readExecuteOutcome(this.address, receipt.logs);
169
+ result.hashes.push(hash);
170
+ result.breaches.push(...outcome.breaches);
171
+ if (!outcome.executed) {
172
+ const blocked = outcome.breaches.find((b) => b.blocked);
173
+ if (blocked)
174
+ throw new BlockedByMandateError(blocked, hash);
175
+ throw new VaultFrozenError(this.address, outcome.breaches[0], hash);
176
+ }
177
+ // A breach froze the vault partway through: the rest of the action can't be sent.
178
+ if (outcome.froze && i < calls.length - 1)
179
+ throw new VaultFrozenError(this.address, outcome.breaches[0], hash);
180
+ }
181
+ return result;
182
+ }
183
+ /**
184
+ * What the vault would do with `action` right now: simulates `execute` from the agent key
185
+ * against the chain's current state (`eth_simulateV1`) and reads the vault's own events, so
186
+ * the verdict is the contract's, never a copy of its rules. Nothing is sent.
187
+ *
188
+ * Throws what sending would throw before the vault judges anything: `VaultFrozenError`,
189
+ * `NotAgentError`, `CallRevertedError`.
190
+ */
191
+ async dryRun(action) {
192
+ const calls = await this.plan(action);
193
+ const from = this.agentAddress ??
194
+ (await this.publicClient.readContract({
195
+ address: this.address,
196
+ abi: mandateVaultAbi,
197
+ functionName: "agent",
198
+ }));
199
+ let simulation;
200
+ try {
201
+ // The action from this package's viem, so a caller's older client works too.
202
+ simulation = await simulateCalls(this.publicClient, {
203
+ account: from,
204
+ calls: calls.map((call) => ({
205
+ to: this.address,
206
+ abi: mandateVaultAbi,
207
+ functionName: "execute",
208
+ args: [call.target, call.data],
209
+ })),
210
+ });
211
+ }
212
+ catch (error) {
213
+ if (isUnsupported(error))
214
+ throw new BondedError("DRY_RUN_UNSUPPORTED", "this RPC doesn't support eth_simulateV1, which dry runs need; use an RPC that does", { cause: error });
215
+ throw error;
216
+ }
217
+ const merged = { executed: true, breaches: [], froze: false };
218
+ for (const [i, result] of simulation.results.entries()) {
219
+ if (result.status === "failure") {
220
+ // An earlier step's breach froze the vault: the prediction is that breach.
221
+ if (merged.froze)
222
+ return toDryRunResult({ ...merged, executed: false });
223
+ throw vaultRevertError(result.error, {
224
+ vault: this.address,
225
+ target: calls[i].target,
226
+ signer: from,
227
+ });
228
+ }
229
+ const outcome = readExecuteOutcome(this.address, result.logs ?? []);
230
+ merged.breaches.push(...outcome.breaches);
231
+ merged.froze ||= outcome.froze;
232
+ if (!outcome.executed)
233
+ return toDryRunResult({ ...merged, executed: false });
234
+ }
235
+ return toDryRunResult(merged);
236
+ }
237
+ /** The `execute` calls an action takes, in order. A swap approves the router first if needed. */
238
+ async plan(action) {
239
+ const a = normaliseAction(action);
240
+ switch (a.type) {
241
+ case "transfer":
242
+ return [erc20Call("transfer", a.token, a.to, a.amount)];
243
+ case "approve":
244
+ return [erc20Call("approve", a.token, a.spender, a.amount)];
245
+ case "call":
246
+ if (isAddressEqual(a.target, this.address))
247
+ throw new BondedError("INVALID_ACTION", "the vault can't call itself");
248
+ return [{ target: a.target, data: a.data }];
249
+ case "swap": {
250
+ const router = this.router;
251
+ if (!router)
252
+ throw new BondedError("CONFIG", "set `router` to swap (an allowed target of the mandate), or use call()");
253
+ if (a.minOut !== undefined && router.quote) {
254
+ const quoted = await router.quote(this.publicClient, a);
255
+ if (quoted < a.minOut)
256
+ throw new BondedError("BELOW_MIN_OUT", `the router quotes ${quoted} for this swap, below minOut ${a.minOut}; nothing was sent`);
257
+ }
258
+ // A module's tokens are its account's: the account approves and receives.
259
+ const { holder } = await this.identify();
260
+ const allowance = await this.publicClient.readContract({
261
+ address: a.tokenIn,
262
+ abi: erc20Abi,
263
+ functionName: "allowance",
264
+ args: [holder, router.address],
265
+ });
266
+ const swap = {
267
+ target: router.address,
268
+ data: router.encodeSwap({ ...a, minOut: a.minOut ?? 0n, recipient: holder }),
269
+ };
270
+ return allowance >= a.amountIn
271
+ ? [swap]
272
+ : [erc20Call("approve", a.tokenIn, router.address, a.amountIn), swap];
273
+ }
274
+ }
275
+ }
276
+ requireWallet() {
277
+ if (!this.walletClient)
278
+ throw new BondedError("READ_ONLY", "this BondedVault has no agent key: pass `account` or `walletClient` to send actions");
279
+ return this.walletClient;
280
+ }
281
+ }
282
+ /**
283
+ * Whether this process is being rated: the runner set a Bonded episode's variables. Fails closed:
284
+ * a `BONDED_EPISODE_ID` alone counts, even when the rest of the episode's variables are malformed.
285
+ */
286
+ export function isRatedProcess(env = typeof process === "undefined" ? {} : process.env) {
287
+ return Boolean(env.BONDED_EPISODE_ID?.trim()) || parseEpisodeEnv(env).ok;
288
+ }
289
+ /**
290
+ * Vaults built from the production variables (`bondedVaultFromEnv`). A process that is being rated
291
+ * never sends from one, whatever put those variables back in its environment (a `.env` file
292
+ * reloaded with `node --env-file` or dotenv, say).
293
+ */
294
+ const productionVaults = new WeakSet();
295
+ /** Marks `vault` as the production vault (internal: `bondedVaultFromEnv` calls it). */
296
+ export function markProductionVault(vault) {
297
+ productionVaults.add(vault);
298
+ return vault;
299
+ }
300
+ /** Throws `BondedError` (`RATED_PRODUCTION_VAULT`) when `vault` is the production vault and this process is rated. */
301
+ export function refuseProductionVaultWhileRated(vault, what) {
302
+ if (productionVaults.has(vault) && isRatedProcess())
303
+ throw ratedProductionVaultError(what);
304
+ }
305
+ export function ratedProductionVaultError(what) {
306
+ return new BondedError("RATED_PRODUCTION_VAULT", `${what}: this process is running a Bonded episode, so the production vault (BONDED_VAULT_ADDRESS, BONDED_AGENT_KEY) is never used. Trade through the episode's tools while rated. If those variables came from a .env file, that is why they are set; nothing was sent.`);
307
+ }
308
+ /** A call to a function the contract doesn't have: it reverts, or returns nothing. */
309
+ function isMissingFunction(error) {
310
+ for (let e = error, depth = 0; e instanceof Error && depth < 20; e = e.cause, depth++)
311
+ if (e.name === "ContractFunctionRevertedError" ||
312
+ e.name === "ContractFunctionZeroDataError" ||
313
+ e.name === "ExecutionRevertedError")
314
+ return true;
315
+ return false;
316
+ }
317
+ function isUnsupported(error) {
318
+ for (let e = error, depth = 0; e instanceof Error && depth < 20; e = e.cause, depth++) {
319
+ if (e.code === -32601)
320
+ return true;
321
+ if (/method not found|does not exist|is not available|not supported/i.test(e.message))
322
+ return true;
323
+ }
324
+ return false;
325
+ }
326
+ //# sourceMappingURL=vault.js.map
package/package.json CHANGED
@@ -1,6 +1,137 @@
1
1
  {
2
2
  "name": "@bondedhq/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Trade through a Bonded Mandate Vault and get rated by the Bonded Arena from any TypeScript agent: a typed vault client with dry runs, Sentinel checks and a mandate builder; the Arena episode's tools for LangChain, the Vercel AI SDK, ElizaOS and Coinbase AgentKit; and an Agent URL handler.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "bonded",
8
+ "ai-agents",
9
+ "mandate-vault",
10
+ "mcp",
11
+ "langchain",
12
+ "langgraph",
13
+ "vercel-ai-sdk",
14
+ "agentkit",
15
+ "solana-agent-kit",
16
+ "elizaos",
17
+ "viem"
18
+ ],
19
+ "homepage": "https://github.com/ddboy19912/bonded/tree/main/packages/sdk#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/ddboy19912/bonded/issues"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/ddboy19912/bonded.git",
26
+ "directory": "packages/sdk"
27
+ },
28
+ "type": "module",
29
+ "sideEffects": false,
30
+ "bin": {
31
+ "bonded-mcp-config": "./dist/mcp-config-cli.js"
32
+ },
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ },
40
+ "./mcp": {
41
+ "types": "./dist/mcp.d.ts",
42
+ "import": "./dist/mcp.js"
43
+ },
44
+ "./agent-url": {
45
+ "types": "./dist/agent-url.d.ts",
46
+ "import": "./dist/agent-url.js"
47
+ },
48
+ "./langchain": {
49
+ "types": "./dist/langchain.d.ts",
50
+ "import": "./dist/langchain.js"
51
+ },
52
+ "./ai-sdk": {
53
+ "types": "./dist/ai-sdk.d.ts",
54
+ "import": "./dist/ai-sdk.js"
55
+ },
56
+ "./agentkit": {
57
+ "types": "./dist/agentkit.d.ts",
58
+ "import": "./dist/agentkit.js"
59
+ },
60
+ "./solana-agent-kit": {
61
+ "types": "./dist/solana-agent-kit.d.ts",
62
+ "import": "./dist/solana-agent-kit.js"
63
+ },
64
+ "./eliza": {
65
+ "types": "./dist/eliza.d.ts",
66
+ "import": "./dist/eliza.js"
67
+ },
68
+ "./package.json": "./package.json"
69
+ },
70
+ "files": [
71
+ "dist",
72
+ "!dist/**/*.map",
73
+ "README.md",
74
+ "LICENSE"
75
+ ],
76
+ "engines": {
77
+ "node": ">=22"
78
+ },
79
+ "publishConfig": {
80
+ "access": "public"
81
+ },
82
+ "dependencies": {
83
+ "@modelcontextprotocol/sdk": "1.30.1",
84
+ "viem": "^2.56.9",
85
+ "zod": "^4.6.5",
86
+ "@bondedhq/shared": "^0.1.1"
87
+ },
88
+ "devDependencies": {
89
+ "@coinbase/agentkit": "0.10.4",
90
+ "@elizaos/core": "1.7.2",
91
+ "@langchain/core": "1.2.12",
92
+ "@solana/spl-token": "0.4.15",
93
+ "@solana/web3.js": "^1.98.4",
94
+ "ai": "7.0.114",
95
+ "solana-agent-kit": "2.0.10",
96
+ "@bondedhq/solana": "0.1.1",
97
+ "@bonded/sandbox": "0.1.0"
98
+ },
99
+ "peerDependencies": {
100
+ "@coinbase/agentkit": "^0.10.4",
101
+ "@elizaos/core": "^1.7.2",
102
+ "@langchain/core": "^1.0.0",
103
+ "@solana/web3.js": "^1.98.4",
104
+ "ai": "^5.0.0 || ^6.0.0 || ^7.0.0",
105
+ "solana-agent-kit": "^2.0.10",
106
+ "@bondedhq/solana": "^0.1.1"
107
+ },
108
+ "peerDependenciesMeta": {
109
+ "@bondedhq/solana": {
110
+ "optional": true
111
+ },
112
+ "@coinbase/agentkit": {
113
+ "optional": true
114
+ },
115
+ "@elizaos/core": {
116
+ "optional": true
117
+ },
118
+ "@langchain/core": {
119
+ "optional": true
120
+ },
121
+ "@solana/web3.js": {
122
+ "optional": true
123
+ },
124
+ "ai": {
125
+ "optional": true
126
+ },
127
+ "solana-agent-kit": {
128
+ "optional": true
129
+ }
130
+ },
131
+ "scripts": {
132
+ "build": "tsc -p tsconfig.build.json",
133
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.eliza.json",
134
+ "lint": "eslint src test",
135
+ "test": "vitest run"
136
+ }
6
137
  }