hybrid 1.2.2 → 1.2.3

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.
@@ -0,0 +1,607 @@
1
+ /**
2
+ * @fileoverview Blockchain Tools for Crypto Agents
3
+ *
4
+ * This module provides comprehensive blockchain interaction tools for crypto-enabled agents.
5
+ * Supports Ethereum and other EVM-compatible chains with features like balance checking,
6
+ * transaction sending, contract interaction, and more.
7
+ *
8
+ * @module BlockchainTools
9
+ */
10
+
11
+ import {
12
+ createPublicClient,
13
+ createWalletClient,
14
+ formatEther,
15
+ http,
16
+ parseEther,
17
+ type Address,
18
+ type Hash
19
+ } from "viem"
20
+ import { privateKeyToAccount } from "viem/accounts"
21
+ import {
22
+ arbitrum,
23
+ base,
24
+ mainnet,
25
+ optimism,
26
+ polygon,
27
+ sepolia
28
+ } from "viem/chains"
29
+ import { z } from "zod"
30
+ import { createTool } from "../core/tool"
31
+
32
+ // Supported chains configuration
33
+ const SUPPORTED_CHAINS = {
34
+ mainnet,
35
+ sepolia,
36
+ polygon,
37
+ arbitrum,
38
+ optimism,
39
+ base
40
+ } as const
41
+
42
+ type SupportedChain = keyof typeof SUPPORTED_CHAINS
43
+
44
+ // Runtime extension interface for blockchain tools
45
+ export interface BlockchainRuntimeExtension {
46
+ rpcUrl?: string
47
+ privateKey?: string
48
+ defaultChain?: SupportedChain
49
+ }
50
+
51
+ /**
52
+ * Get Balance Tool
53
+ *
54
+ * Retrieves the native token balance for a given address on a specified chain.
55
+ *
56
+ * @tool getBalance
57
+ * @category Blockchain
58
+ *
59
+ * @param {string} address - The wallet address to check balance for
60
+ * @param {string} [chain] - The blockchain network (defaults to mainnet)
61
+ *
62
+ * @returns {Promise<{success: boolean, balance: string, balanceWei: string, address: string, chain: string, error?: string}>}
63
+ */
64
+ export const getBalanceTool = createTool({
65
+ id: "getBalance",
66
+ description:
67
+ "Get the native token balance for a wallet address on a blockchain",
68
+ inputSchema: z.object({
69
+ address: z.string().describe("The wallet address to check balance for"),
70
+ chain: z
71
+ .enum(["mainnet", "sepolia", "polygon", "arbitrum", "optimism", "base"])
72
+ .default("mainnet")
73
+ .describe("The blockchain network to check on")
74
+ }),
75
+ outputSchema: z.object({
76
+ success: z.boolean(),
77
+ balance: z
78
+ .string()
79
+ .describe("Balance in human readable format (ETH, MATIC, etc.)"),
80
+ balanceWei: z.string().describe("Balance in wei (smallest unit)"),
81
+ address: z.string(),
82
+ chain: z.string(),
83
+ error: z.string().optional()
84
+ }),
85
+ execute: async ({ input, runtime }) => {
86
+ try {
87
+ const { address, chain } = input
88
+ const chainConfig = SUPPORTED_CHAINS[chain]
89
+
90
+ // Use runtime RPC URL if provided, otherwise use default
91
+ const rpcUrl =
92
+ (runtime as any).rpcUrl || chainConfig.rpcUrls.default.http[0]
93
+
94
+ const client = createPublicClient({
95
+ chain: chainConfig,
96
+ transport: http(rpcUrl)
97
+ })
98
+
99
+ console.log(`🔍 [getBalance] Checking balance for ${address} on ${chain}`)
100
+
101
+ const balanceWei = await client.getBalance({
102
+ address: address as Address
103
+ })
104
+
105
+ const balance = formatEther(balanceWei)
106
+
107
+ console.log(
108
+ `✅ [getBalance] Balance: ${balance} ${chainConfig.nativeCurrency.symbol}`
109
+ )
110
+
111
+ return {
112
+ success: true,
113
+ balance: `${balance} ${chainConfig.nativeCurrency.symbol}`,
114
+ balanceWei: balanceWei.toString(),
115
+ address,
116
+ chain
117
+ }
118
+ } catch (error) {
119
+ const errorMessage =
120
+ error instanceof Error ? error.message : String(error)
121
+ console.error("❌ [getBalance] Error:", errorMessage)
122
+ return {
123
+ success: false,
124
+ balance: "0",
125
+ balanceWei: "0",
126
+ address: input.address,
127
+ chain: input.chain,
128
+ error: errorMessage
129
+ }
130
+ }
131
+ }
132
+ })
133
+
134
+ /**
135
+ * Get Transaction Tool
136
+ *
137
+ * Retrieves transaction details by transaction hash.
138
+ *
139
+ * @tool getTransaction
140
+ * @category Blockchain
141
+ *
142
+ * @param {string} hash - The transaction hash to look up
143
+ * @param {string} [chain] - The blockchain network (defaults to mainnet)
144
+ *
145
+ * @returns {Promise<{success: boolean, transaction?: object, error?: string}>}
146
+ */
147
+ export const getTransactionTool = createTool({
148
+ id: "getTransaction",
149
+ description: "Get transaction details by transaction hash",
150
+ inputSchema: z.object({
151
+ hash: z.string().describe("The transaction hash to look up"),
152
+ chain: z
153
+ .enum(["mainnet", "sepolia", "polygon", "arbitrum", "optimism", "base"])
154
+ .default("mainnet")
155
+ .describe("The blockchain network to check on")
156
+ }),
157
+ outputSchema: z.object({
158
+ success: z.boolean(),
159
+ transaction: z
160
+ .object({
161
+ hash: z.string(),
162
+ from: z.string(),
163
+ to: z.string().nullable(),
164
+ value: z.string(),
165
+ gasUsed: z.string().optional(),
166
+ gasPrice: z.string().optional(),
167
+ blockNumber: z.string().optional(),
168
+ status: z.string().optional()
169
+ })
170
+ .optional(),
171
+ error: z.string().optional()
172
+ }),
173
+ execute: async ({ input, runtime }) => {
174
+ try {
175
+ const { hash, chain } = input
176
+ const chainConfig = SUPPORTED_CHAINS[chain]
177
+
178
+ const rpcUrl =
179
+ (runtime as any).rpcUrl || chainConfig.rpcUrls.default.http[0]
180
+
181
+ const client = createPublicClient({
182
+ chain: chainConfig,
183
+ transport: http(rpcUrl)
184
+ })
185
+
186
+ console.log(
187
+ `🔍 [getTransaction] Looking up transaction ${hash} on ${chain}`
188
+ )
189
+
190
+ const transaction = await client.getTransaction({
191
+ hash: hash as Hash
192
+ })
193
+
194
+ const receipt = await client
195
+ .getTransactionReceipt({
196
+ hash: hash as Hash
197
+ })
198
+ .catch(() => null) // Transaction might be pending
199
+
200
+ console.log(
201
+ `✅ [getTransaction] Found transaction from ${transaction.from} to ${transaction.to}`
202
+ )
203
+
204
+ return {
205
+ success: true,
206
+ transaction: {
207
+ hash: transaction.hash,
208
+ from: transaction.from,
209
+ to: transaction.to,
210
+ value: formatEther(transaction.value),
211
+ gasUsed: receipt?.gasUsed.toString(),
212
+ gasPrice: transaction.gasPrice?.toString(),
213
+ blockNumber: transaction.blockNumber?.toString(),
214
+ status:
215
+ receipt?.status === "success"
216
+ ? "success"
217
+ : receipt?.status === "reverted"
218
+ ? "failed"
219
+ : "pending"
220
+ }
221
+ }
222
+ } catch (error) {
223
+ const errorMessage =
224
+ error instanceof Error ? error.message : String(error)
225
+ console.error("❌ [getTransaction] Error:", errorMessage)
226
+ return {
227
+ success: false,
228
+ error: errorMessage
229
+ }
230
+ }
231
+ }
232
+ })
233
+
234
+ /**
235
+ * Send Transaction Tool
236
+ *
237
+ * Sends a native token transaction to another address.
238
+ * Requires a private key to be configured in the runtime.
239
+ *
240
+ * @tool sendTransaction
241
+ * @category Blockchain
242
+ *
243
+ * @param {string} to - The recipient address
244
+ * @param {string} amount - The amount to send (in ETH, MATIC, etc.)
245
+ * @param {string} [chain] - The blockchain network (defaults to mainnet)
246
+ *
247
+ * @returns {Promise<{success: boolean, hash?: string, error?: string}>}
248
+ */
249
+ export const sendTransactionTool = createTool({
250
+ id: "sendTransaction",
251
+ description: "Send native tokens to another address",
252
+ inputSchema: z.object({
253
+ to: z.string().describe("The recipient address"),
254
+ amount: z.string().describe("The amount to send (in ETH, MATIC, etc.)"),
255
+ chain: z
256
+ .enum(["mainnet", "sepolia", "polygon", "arbitrum", "optimism", "base"])
257
+ .default("mainnet")
258
+ .describe("The blockchain network to send on")
259
+ }),
260
+ outputSchema: z.object({
261
+ success: z.boolean(),
262
+ hash: z.string().optional(),
263
+ from: z.string().optional(),
264
+ to: z.string(),
265
+ amount: z.string(),
266
+ chain: z.string(),
267
+ error: z.string().optional()
268
+ }),
269
+ execute: async ({ input, runtime }) => {
270
+ try {
271
+ const { to, amount, chain } = input
272
+ const chainConfig = SUPPORTED_CHAINS[chain]
273
+
274
+ const privateKey = (runtime as any).privateKey
275
+ if (!privateKey) {
276
+ return {
277
+ success: false,
278
+ to,
279
+ amount,
280
+ chain,
281
+ error: "Private key not configured in runtime"
282
+ }
283
+ }
284
+
285
+ const rpcUrl =
286
+ (runtime as any).rpcUrl || chainConfig.rpcUrls.default.http[0]
287
+ const account = privateKeyToAccount(privateKey as `0x${string}`)
288
+
289
+ const client = createWalletClient({
290
+ account,
291
+ chain: chainConfig,
292
+ transport: http(rpcUrl)
293
+ })
294
+
295
+ console.log(
296
+ `💸 [sendTransaction] Sending ${amount} ${chainConfig.nativeCurrency.symbol} to ${to} on ${chain}`
297
+ )
298
+
299
+ const hash = await client.sendTransaction({
300
+ to: to as Address,
301
+ value: parseEther(amount)
302
+ })
303
+
304
+ console.log(`✅ [sendTransaction] Transaction sent: ${hash}`)
305
+
306
+ return {
307
+ success: true,
308
+ hash,
309
+ from: account.address,
310
+ to,
311
+ amount,
312
+ chain
313
+ }
314
+ } catch (error) {
315
+ const errorMessage =
316
+ error instanceof Error ? error.message : String(error)
317
+ console.error("❌ [sendTransaction] Error:", errorMessage)
318
+ return {
319
+ success: false,
320
+ to: input.to,
321
+ amount: input.amount,
322
+ chain: input.chain,
323
+ error: errorMessage
324
+ }
325
+ }
326
+ }
327
+ })
328
+
329
+ /**
330
+ * Get Block Tool
331
+ *
332
+ * Retrieves information about a specific block.
333
+ *
334
+ * @tool getBlock
335
+ * @category Blockchain
336
+ *
337
+ * @param {string} [blockNumber] - Block number (defaults to latest)
338
+ * @param {string} [chain] - The blockchain network (defaults to mainnet)
339
+ *
340
+ * @returns {Promise<{success: boolean, block?: object, error?: string}>}
341
+ */
342
+ export const getBlockTool = createTool({
343
+ id: "getBlock",
344
+ description: "Get information about a blockchain block",
345
+ inputSchema: z.object({
346
+ blockNumber: z
347
+ .string()
348
+ .optional()
349
+ .describe("Block number (defaults to latest)"),
350
+ chain: z
351
+ .enum(["mainnet", "sepolia", "polygon", "arbitrum", "optimism", "base"])
352
+ .default("mainnet")
353
+ .describe("The blockchain network to check on")
354
+ }),
355
+ outputSchema: z.object({
356
+ success: z.boolean(),
357
+ block: z
358
+ .object({
359
+ number: z.string(),
360
+ hash: z.string(),
361
+ timestamp: z.string(),
362
+ transactionCount: z.number(),
363
+ gasUsed: z.string(),
364
+ gasLimit: z.string()
365
+ })
366
+ .optional(),
367
+ error: z.string().optional()
368
+ }),
369
+ execute: async ({ input, runtime }) => {
370
+ try {
371
+ const { blockNumber, chain } = input
372
+ const chainConfig = SUPPORTED_CHAINS[chain]
373
+
374
+ const rpcUrl =
375
+ (runtime as any).rpcUrl || chainConfig.rpcUrls.default.http[0]
376
+
377
+ const client = createPublicClient({
378
+ chain: chainConfig,
379
+ transport: http(rpcUrl)
380
+ })
381
+
382
+ console.log(
383
+ `🔍 [getBlock] Getting block ${blockNumber || "latest"} on ${chain}`
384
+ )
385
+
386
+ const block = await client.getBlock({
387
+ blockNumber: blockNumber ? BigInt(blockNumber) : undefined
388
+ })
389
+
390
+ console.log(
391
+ `✅ [getBlock] Found block ${block.number} with ${block.transactions.length} transactions`
392
+ )
393
+
394
+ return {
395
+ success: true,
396
+ block: {
397
+ number: block.number.toString(),
398
+ hash: block.hash,
399
+ timestamp: block.timestamp.toString(),
400
+ transactionCount: block.transactions.length,
401
+ gasUsed: block.gasUsed.toString(),
402
+ gasLimit: block.gasLimit.toString()
403
+ }
404
+ }
405
+ } catch (error) {
406
+ const errorMessage =
407
+ error instanceof Error ? error.message : String(error)
408
+ console.error("❌ [getBlock] Error:", errorMessage)
409
+ return {
410
+ success: false,
411
+ error: errorMessage
412
+ }
413
+ }
414
+ }
415
+ })
416
+
417
+ /**
418
+ * Get Gas Price Tool
419
+ *
420
+ * Retrieves current gas price information for a blockchain.
421
+ *
422
+ * @tool getGasPrice
423
+ * @category Blockchain
424
+ *
425
+ * @param {string} [chain] - The blockchain network (defaults to mainnet)
426
+ *
427
+ * @returns {Promise<{success: boolean, gasPrice?: string, error?: string}>}
428
+ */
429
+ export const getGasPriceTool = createTool({
430
+ id: "getGasPrice",
431
+ description: "Get current gas price for a blockchain",
432
+ inputSchema: z.object({
433
+ chain: z
434
+ .enum(["mainnet", "sepolia", "polygon", "arbitrum", "optimism", "base"])
435
+ .default("mainnet")
436
+ .describe("The blockchain network to check on")
437
+ }),
438
+ outputSchema: z.object({
439
+ success: z.boolean(),
440
+ gasPrice: z.string().optional().describe("Gas price in gwei"),
441
+ gasPriceWei: z.string().optional().describe("Gas price in wei"),
442
+ chain: z.string(),
443
+ error: z.string().optional()
444
+ }),
445
+ execute: async ({ input, runtime }) => {
446
+ try {
447
+ const { chain } = input
448
+ const chainConfig = SUPPORTED_CHAINS[chain]
449
+
450
+ const rpcUrl =
451
+ (runtime as any).rpcUrl || chainConfig.rpcUrls.default.http[0]
452
+
453
+ const client = createPublicClient({
454
+ chain: chainConfig,
455
+ transport: http(rpcUrl)
456
+ })
457
+
458
+ console.log(`⛽ [getGasPrice] Getting gas price for ${chain}`)
459
+
460
+ const gasPrice = await client.getGasPrice()
461
+ const gasPriceGwei = formatEther(gasPrice * BigInt(1000000000)) // Convert to gwei
462
+
463
+ console.log(`✅ [getGasPrice] Current gas price: ${gasPriceGwei} gwei`)
464
+
465
+ return {
466
+ success: true,
467
+ gasPrice: `${gasPriceGwei} gwei`,
468
+ gasPriceWei: gasPrice.toString(),
469
+ chain
470
+ }
471
+ } catch (error) {
472
+ const errorMessage =
473
+ error instanceof Error ? error.message : String(error)
474
+ console.error("❌ [getGasPrice] Error:", errorMessage)
475
+ return {
476
+ success: false,
477
+ chain: input.chain,
478
+ error: errorMessage
479
+ }
480
+ }
481
+ }
482
+ })
483
+
484
+ /**
485
+ * Estimate Gas Tool
486
+ *
487
+ * Estimates gas required for a transaction.
488
+ *
489
+ * @tool estimateGas
490
+ * @category Blockchain
491
+ *
492
+ * @param {string} to - The recipient address
493
+ * @param {string} [amount] - The amount to send (defaults to 0)
494
+ * @param {string} [data] - Transaction data (for contract calls)
495
+ * @param {string} [chain] - The blockchain network (defaults to mainnet)
496
+ *
497
+ * @returns {Promise<{success: boolean, gasEstimate?: string, error?: string}>}
498
+ */
499
+ export const estimateGasTool = createTool({
500
+ id: "estimateGas",
501
+ description: "Estimate gas required for a transaction",
502
+ inputSchema: z.object({
503
+ to: z.string().describe("The recipient address"),
504
+ amount: z
505
+ .string()
506
+ .default("0")
507
+ .describe("The amount to send (defaults to 0)"),
508
+ data: z
509
+ .string()
510
+ .optional()
511
+ .describe("Transaction data (for contract calls)"),
512
+ chain: z
513
+ .enum(["mainnet", "sepolia", "polygon", "arbitrum", "optimism", "base"])
514
+ .default("mainnet")
515
+ .describe("The blockchain network to estimate on")
516
+ }),
517
+ outputSchema: z.object({
518
+ success: z.boolean(),
519
+ gasEstimate: z.string().optional(),
520
+ to: z.string(),
521
+ amount: z.string(),
522
+ chain: z.string(),
523
+ error: z.string().optional()
524
+ }),
525
+ execute: async ({ input, runtime }) => {
526
+ try {
527
+ const { to, amount, data, chain } = input
528
+ const chainConfig = SUPPORTED_CHAINS[chain]
529
+
530
+ const privateKey = (runtime as any).privateKey
531
+ if (!privateKey) {
532
+ return {
533
+ success: false,
534
+ to,
535
+ amount,
536
+ chain,
537
+ error: "Private key not configured in runtime"
538
+ }
539
+ }
540
+
541
+ const rpcUrl =
542
+ (runtime as any).rpcUrl || chainConfig.rpcUrls.default.http[0]
543
+ const account = privateKeyToAccount(privateKey as `0x${string}`)
544
+
545
+ const client = createPublicClient({
546
+ chain: chainConfig,
547
+ transport: http(rpcUrl)
548
+ })
549
+
550
+ console.log(
551
+ `⛽ [estimateGas] Estimating gas for transaction to ${to} on ${chain}`
552
+ )
553
+
554
+ const gasEstimate = await client.estimateGas({
555
+ account: account.address,
556
+ to: to as Address,
557
+ value: parseEther(amount),
558
+ data: data as `0x${string}` | undefined
559
+ })
560
+
561
+ console.log(`✅ [estimateGas] Estimated gas: ${gasEstimate.toString()}`)
562
+
563
+ return {
564
+ success: true,
565
+ gasEstimate: gasEstimate.toString(),
566
+ to,
567
+ amount,
568
+ chain
569
+ }
570
+ } catch (error) {
571
+ const errorMessage =
572
+ error instanceof Error ? error.message : String(error)
573
+ console.error("❌ [estimateGas] Error:", errorMessage)
574
+ return {
575
+ success: false,
576
+ to: input.to,
577
+ amount: input.amount,
578
+ chain: input.chain,
579
+ error: errorMessage
580
+ }
581
+ }
582
+ }
583
+ })
584
+
585
+ /**
586
+ * Collection of blockchain tools for crypto agents
587
+ *
588
+ * These tools provide comprehensive blockchain interaction capabilities including
589
+ * balance checking, transaction sending, gas estimation, and more.
590
+ *
591
+ * @namespace blockchainTools
592
+ *
593
+ * @property {Tool} getBalance - Get native token balance for an address
594
+ * @property {Tool} getTransaction - Get transaction details by hash
595
+ * @property {Tool} sendTransaction - Send native tokens to another address
596
+ * @property {Tool} getBlock - Get information about a blockchain block
597
+ * @property {Tool} getGasPrice - Get current gas price for a blockchain
598
+ * @property {Tool} estimateGas - Estimate gas required for a transaction
599
+ */
600
+ export const blockchainTools = {
601
+ getBalance: getBalanceTool,
602
+ getTransaction: getTransactionTool,
603
+ sendTransaction: sendTransactionTool,
604
+ getBlock: getBlockTool,
605
+ getGasPrice: getGasPriceTool,
606
+ estimateGas: estimateGasTool
607
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @fileoverview Hybrid Agent Tools Standard Library
3
+ *
4
+ * This module provides a comprehensive set of tools for building crypto-enabled agents.
5
+ * Tools are organized by category and can be imported individually or as complete sets.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { blockchainTools, xmtpTools } from "hybrid/tools"
10
+ * import { Agent } from "hybrid"
11
+ *
12
+ * const agent = new Agent({
13
+ * name: "crypto-agent",
14
+ * model: myModel,
15
+ * tools: {
16
+ * ...blockchainTools,
17
+ * ...xmtpTools
18
+ * },
19
+ * instructions: "You are a crypto agent with blockchain and messaging capabilities.",
20
+ * createRuntime: (runtime) => ({
21
+ * rpcUrl: process.env.RPC_URL,
22
+ * privateKey: process.env.PRIVATE_KEY,
23
+ * defaultChain: "mainnet" as const
24
+ * })
25
+ * })
26
+ * ```
27
+ *
28
+ * @module HybridTools
29
+ */
30
+
31
+ // Export blockchain tools
32
+ export {
33
+ blockchainTools,
34
+ estimateGasTool,
35
+ getBalanceTool,
36
+ getBlockTool,
37
+ getGasPriceTool,
38
+ getTransactionTool,
39
+ sendTransactionTool,
40
+ type BlockchainRuntimeExtension
41
+ } from "./blockchain"
42
+
43
+ // Export XMTP tools
44
+ export {
45
+ getMessageTool,
46
+ sendMessageTool,
47
+ sendReactionTool,
48
+ sendReplyTool,
49
+ xmtpTools
50
+ } from "./xmtp"