@blockrun/franklin 3.27.2 → 3.27.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.
@@ -30,6 +30,7 @@ import { jupiterQuoteCapability, jupiterSwapCapability } from './jupiter.js';
30
30
  import { base0xQuoteCapability, base0xSwapCapability } from './zerox-base.js';
31
31
  import { base0xGaslessSwapCapability } from './zerox-gasless.js';
32
32
  import { defiLlamaProtocolsCapability, defiLlamaProtocolCapability, defiLlamaChainsCapability, defiLlamaYieldsCapability, defiLlamaPriceCapability, } from './defillama.js';
33
+ import { multiChainRpcCapability } from './rpc.js';
33
34
  import { predictionMarketCapability } from './prediction.js';
34
35
  import { modalCapabilities } from './modal.js';
35
36
  import { blockrunCapability } from './blockrun.js';
@@ -167,6 +168,7 @@ export const allCapabilities = [
167
168
  defiLlamaChainsCapability,
168
169
  defiLlamaYieldsCapability,
169
170
  defiLlamaPriceCapability,
171
+ multiChainRpcCapability, // read-only JSON-RPC across 40+ chains ($0.002/call)
170
172
  predictionMarketCapability, // Polymarket / Kalshi / matching / smart money via Predexon
171
173
  blockrunCapability, // Generic x402-paid gateway primitive — future partners + long-tail Surf paths
172
174
  ...surfCapabilities, // SurfMarket / SurfChain / SurfSocial — endpoint-enum function tools (no path guessing, auto x402)
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Multi-chain RPC — read-only JSON-RPC across 40+ chains via the BlockRun
3
+ * `/v1/rpc/{network}` endpoint (Tatum gateway). x402-paid against the user's
4
+ * USDC wallet, flat $0.002 per call.
5
+ *
6
+ * For a trading agent this covers the on-chain reads the dedicated tools don't:
7
+ * native + ERC-20 balances on any chain, contract reads (eth_call), gas price,
8
+ * nonce, block height, and "did my tx land?" receipt checks — without needing
9
+ * a per-chain RPC key.
10
+ *
11
+ * READ-ONLY by design (per product decision): state-changing / signing methods
12
+ * are rejected. Sending transactions goes through the wallet / Jupiter / 0x
13
+ * tools, which handle signing + confirmation. This tool never signs a chain tx
14
+ * (the only signature it produces is the x402 micropayment for the API call).
15
+ *
16
+ * Direct gateway fetch (same pattern as the DefiLlama / Surf tools) — does not
17
+ * depend on the SDK's RpcClient, so it works on the pinned @blockrun/llm 2.x.
18
+ */
19
+ import type { CapabilityHandler } from '../agent/types.js';
20
+ declare const KNOWN_NETWORKS: string[];
21
+ export declare const multiChainRpcCapability: CapabilityHandler;
22
+ export { KNOWN_NETWORKS };
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Multi-chain RPC — read-only JSON-RPC across 40+ chains via the BlockRun
3
+ * `/v1/rpc/{network}` endpoint (Tatum gateway). x402-paid against the user's
4
+ * USDC wallet, flat $0.002 per call.
5
+ *
6
+ * For a trading agent this covers the on-chain reads the dedicated tools don't:
7
+ * native + ERC-20 balances on any chain, contract reads (eth_call), gas price,
8
+ * nonce, block height, and "did my tx land?" receipt checks — without needing
9
+ * a per-chain RPC key.
10
+ *
11
+ * READ-ONLY by design (per product decision): state-changing / signing methods
12
+ * are rejected. Sending transactions goes through the wallet / Jupiter / 0x
13
+ * tools, which handle signing + confirmation. This tool never signs a chain tx
14
+ * (the only signature it produces is the x402 micropayment for the API call).
15
+ *
16
+ * Direct gateway fetch (same pattern as the DefiLlama / Surf tools) — does not
17
+ * depend on the SDK's RpcClient, so it works on the pinned @blockrun/llm 2.x.
18
+ */
19
+ import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
20
+ import { loadChain, API_URLS, VERSION } from '../config.js';
21
+ import { logger } from '../logger.js';
22
+ const TIMEOUT_MS = 30_000;
23
+ // Curated chains the gateway exposes (for the agent + validation). Unknown but
24
+ // well-formed slugs still pass through — the gateway resolves them — so this is
25
+ // guidance, not a hard allowlist. Mirrors backend src/lib/tatum.ts.
26
+ const KNOWN_NETWORKS = [
27
+ 'ethereum', 'base', 'arbitrum', 'optimism', 'polygon', 'bsc', 'avalanche',
28
+ 'fantom', 'cronos', 'celo', 'gnosis', 'zksync', 'berachain', 'unichain',
29
+ 'monad', 'sonic', 'xdc', 'abstract', 'hyperevm', 'plume', 'ronin', 'rootstock',
30
+ 'solana', 'bitcoin', 'litecoin', 'dogecoin', 'bitcoin-cash', 'near', 'sui',
31
+ 'ripple', 'polkadot', 'zcash',
32
+ ];
33
+ // State-changing / signing methods — rejected (read-only tool). Matched
34
+ // case-insensitively against the method name.
35
+ const WRITE_METHODS = new Set([
36
+ // EVM
37
+ 'eth_sendrawtransaction', 'eth_sendtransaction', 'eth_sign',
38
+ 'eth_signtransaction', 'eth_signtypeddata', 'eth_signtypeddata_v3',
39
+ 'eth_signtypeddata_v4', 'personal_sign', 'personal_sendtransaction',
40
+ 'personal_unlockaccount', 'personal_importrawkey',
41
+ // Solana
42
+ 'sendtransaction', 'requestairdrop',
43
+ // Bitcoin-family
44
+ 'sendrawtransaction',
45
+ ].map((m) => m.toLowerCase()));
46
+ async function postRpcWithPayment(network, jsonRpcBody, ctx) {
47
+ const chain = loadChain();
48
+ const apiUrl = API_URLS[chain];
49
+ const endpoint = `${apiUrl}/v1/rpc/${network}`;
50
+ const bodyStr = JSON.stringify(jsonRpcBody);
51
+ const headers = {
52
+ 'Content-Type': 'application/json',
53
+ Accept: 'application/json',
54
+ 'User-Agent': `franklin/${VERSION}`,
55
+ };
56
+ const controller = new AbortController();
57
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
58
+ const onAbort = () => controller.abort();
59
+ ctx.abortSignal.addEventListener('abort', onAbort, { once: true });
60
+ try {
61
+ let response = await fetch(endpoint, {
62
+ method: 'POST',
63
+ signal: controller.signal,
64
+ headers,
65
+ body: bodyStr,
66
+ });
67
+ if (response.status === 402) {
68
+ const paymentHeaders = await signPayment(response, chain, endpoint);
69
+ if (!paymentHeaders) {
70
+ throw new Error('Payment signing failed — check wallet balance');
71
+ }
72
+ response = await fetch(endpoint, {
73
+ method: 'POST',
74
+ signal: controller.signal,
75
+ headers: { ...headers, ...paymentHeaders },
76
+ body: bodyStr,
77
+ });
78
+ }
79
+ if (!response.ok) {
80
+ const errText = await response.text().catch(() => '');
81
+ throw new Error(`RPC ${network} failed (${response.status}): ${errText.slice(0, 200)}`);
82
+ }
83
+ return {
84
+ body: await response.json(),
85
+ network: response.headers.get('x-network') || network,
86
+ cacheHit: (response.headers.get('x-cache') || '').toUpperCase() === 'HIT',
87
+ txHash: response.headers.get('x-payment-receipt'),
88
+ };
89
+ }
90
+ finally {
91
+ clearTimeout(timeout);
92
+ ctx.abortSignal.removeEventListener('abort', onAbort);
93
+ }
94
+ }
95
+ async function signPayment(response, chain, endpoint) {
96
+ try {
97
+ const paymentHeader = await extractPaymentReq(response);
98
+ if (!paymentHeader)
99
+ return null;
100
+ if (chain === 'solana') {
101
+ const wallet = await getOrCreateSolanaWallet();
102
+ const paymentRequired = parsePaymentRequired(paymentHeader);
103
+ const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
104
+ const secretBytes = await solanaKeyToBytes(wallet.privateKey);
105
+ const feePayer = details.extra?.feePayer || details.recipient;
106
+ const payload = await createSolanaPaymentPayload(secretBytes, wallet.address, details.recipient, details.amount, feePayer, {
107
+ resourceUrl: details.resource?.url || endpoint,
108
+ resourceDescription: details.resource?.description || 'Franklin multi-chain RPC',
109
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 60,
110
+ extra: details.extra,
111
+ });
112
+ return { 'PAYMENT-SIGNATURE': payload };
113
+ }
114
+ const wallet = await getOrCreateWallet();
115
+ const paymentRequired = parsePaymentRequired(paymentHeader);
116
+ const details = extractPaymentDetails(paymentRequired);
117
+ const payload = await createPaymentPayload(wallet.privateKey, wallet.address, details.recipient, details.amount, details.network || 'eip155:8453', {
118
+ resourceUrl: details.resource?.url || endpoint,
119
+ resourceDescription: details.resource?.description || 'Franklin multi-chain RPC',
120
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 60,
121
+ extra: details.extra,
122
+ });
123
+ return { 'PAYMENT-SIGNATURE': payload };
124
+ }
125
+ catch (err) {
126
+ logger.warn(`[franklin] RPC payment error: ${err.message}`);
127
+ return null;
128
+ }
129
+ }
130
+ async function extractPaymentReq(response) {
131
+ let header = response.headers.get('payment-required');
132
+ if (!header) {
133
+ try {
134
+ const body = (await response.json());
135
+ if (body.x402 || body.accepts)
136
+ header = btoa(JSON.stringify(body));
137
+ }
138
+ catch {
139
+ /* ignore */
140
+ }
141
+ }
142
+ return header;
143
+ }
144
+ export const multiChainRpcCapability = {
145
+ spec: {
146
+ name: 'MultiChainRPC',
147
+ description: 'Read-only JSON-RPC against 40+ chains through the BlockRun gateway (one endpoint, no per-chain key). ' +
148
+ '$0.002 per call (USDC). Use for on-chain reads the other tools do not cover: native/token balances on any ' +
149
+ 'chain, contract reads (eth_call), gas price, nonce, block height, and tx receipt checks ("did my swap land?"). ' +
150
+ 'EVM chains speak eth_* (eth_blockNumber, eth_getBalance, eth_call, eth_gasPrice, eth_getTransactionReceipt, ...); ' +
151
+ 'Solana speaks getSlot/getBalance/getAccountInfo/getTransaction; Bitcoin-family speaks getblockcount etc. ' +
152
+ 'Networks: ethereum, base, arbitrum, optimism, polygon, bsc, avalanche, solana, bitcoin, sui, ripple, and 30+ more ' +
153
+ '(common aliases like eth/arb/op/matic/sol/btc also work). ' +
154
+ 'READ-ONLY: signing / send-transaction methods are rejected — use the wallet, Jupiter, or 0x tools to move funds.',
155
+ input_schema: {
156
+ type: 'object',
157
+ properties: {
158
+ network: {
159
+ type: 'string',
160
+ description: 'Chain name or alias, e.g. "ethereum", "base", "solana", "arbitrum", "bitcoin".',
161
+ },
162
+ method: {
163
+ type: 'string',
164
+ description: 'JSON-RPC method, e.g. "eth_getBalance", "eth_call", "eth_blockNumber", "getSlot".',
165
+ },
166
+ params: {
167
+ type: 'array',
168
+ description: 'Method params array (optional). E.g. ["0xADDR","latest"] for eth_getBalance.',
169
+ items: {},
170
+ },
171
+ },
172
+ required: ['network', 'method'],
173
+ },
174
+ },
175
+ execute: async (input, ctx) => {
176
+ const params = input;
177
+ const network = (params.network ?? '').trim().toLowerCase();
178
+ const method = (params.method ?? '').trim();
179
+ if (!network)
180
+ return { output: 'Error: network is required', isError: true };
181
+ if (!method)
182
+ return { output: 'Error: method is required', isError: true };
183
+ if (!/^[a-z0-9][a-z0-9-]{0,40}$/.test(network)) {
184
+ return { output: `Error: malformed network "${params.network}"`, isError: true };
185
+ }
186
+ if (WRITE_METHODS.has(method.toLowerCase())) {
187
+ return {
188
+ output: `Error: "${method}" is a state-changing method and is blocked — MultiChainRPC is read-only. ` +
189
+ `To send funds or sign transactions, use the wallet / JupiterSwap / Base0x* tools.`,
190
+ isError: true,
191
+ };
192
+ }
193
+ const rpcParams = Array.isArray(params.params) ? params.params : [];
194
+ const jsonRpcBody = { jsonrpc: '2.0', id: 1, method, params: rpcParams };
195
+ try {
196
+ const res = await postRpcWithPayment(network, jsonRpcBody, ctx);
197
+ const envelope = res.body;
198
+ if (envelope && envelope.error) {
199
+ return {
200
+ output: `JSON-RPC error from ${res.network} ${method}: ` +
201
+ `${envelope.error.message ?? 'unknown'} (code ${envelope.error.code ?? 'n/a'})`,
202
+ isError: true,
203
+ };
204
+ }
205
+ const result = envelope?.result;
206
+ const pretty = typeof result === 'string'
207
+ ? result
208
+ : JSON.stringify(result, null, 2);
209
+ // Guard context size — chain reads (getLogs, large account data) can be big.
210
+ const trimmed = pretty.length > 6000 ? `${pretty.slice(0, 6000)}\n…(truncated)` : pretty;
211
+ const meta = res.cacheHit ? ' · cached' : '';
212
+ return { output: `## ${res.network} · ${method}${meta}\n\n${trimmed}` };
213
+ }
214
+ catch (err) {
215
+ return { output: `Error: ${err.message}`, isError: true };
216
+ }
217
+ },
218
+ concurrent: true,
219
+ };
220
+ export { KNOWN_NETWORKS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.27.2",
3
+ "version": "3.27.3",
4
4
  "description": "Franklin Agent — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {