@blockrun/franklin 3.12.0 → 3.12.2
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/dist/agent/context.js +6 -5
- package/dist/tools/defillama.d.ts +23 -0
- package/dist/tools/defillama.js +408 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/jupiter.d.ts +20 -0
- package/dist/tools/jupiter.js +326 -0
- package/package.json +1 -1
package/dist/agent/context.js
CHANGED
|
@@ -205,13 +205,14 @@ You run on the BlockRun AI Gateway. When the user asks you to "test the BlockRun
|
|
|
205
205
|
- \`GET /v1/models\` — full model catalog (id, owner, context window, pricing).
|
|
206
206
|
- \`GET /v1/health/overview\` · \`/v1/health/regions\` · \`/v1/health/chain\` · \`/v1/health/models\` — gateway status.
|
|
207
207
|
|
|
208
|
-
**Trading & DeFi (mixed methods, x402-paid
|
|
209
|
-
- \`
|
|
210
|
-
- \`POST /v1/jupiter/swap\` — body \`{ userPublicKey, quoteResponse }\`. Returns a base64-encoded **unsigned** Solana transaction. Caller signs locally; gateway never custodies keys. \$0.001/call.
|
|
211
|
-
- \`GET /v1/defillama/protocols\` · \`/v1/defillama/protocol/{slug}\` · \`/v1/defillama/chains\` · \`/v1/defillama/yields\` — TVL / yield-pool data, Apache-2.0 source. \$0.005/call.
|
|
212
|
-
- \`GET /v1/defillama/prices/{coins}\` — token price lookup (coingecko:bitcoin, ethereum:0x..., solana:mint, comma-separated). \$0.001/call.
|
|
208
|
+
**Trading & DeFi (mixed methods, x402-paid)**
|
|
209
|
+
- For DefiLlama data, **use the built-in tools** \`DeFiLlamaProtocols\`, \`DeFiLlamaProtocol\`, \`DeFiLlamaChains\`, \`DeFiLlamaYields\`, \`DeFiLlamaPrice\`. They handle x402 payment automatically and filter responses (DefiLlama raw payloads are 5–10 MB; the tools return ranked summaries). Do NOT call \`/v1/defillama/*\` via Bash + curl — the wallet won't sign payments through that path.
|
|
213
210
|
- \`POST /v1/solana/rpc\` — JSON-RPC passthrough to public mainnet-beta (getAccountInfo, getTokenSupply, sendTransaction, etc.). \$0.0005 per call (per element of a batch). Use this instead of running your own RPC infra.
|
|
214
211
|
|
|
212
|
+
**Solana DEX swap (Jupiter Ultra)**
|
|
213
|
+
- Use the **\`JupiterQuote\` and \`JupiterSwap\` built-in tools** — they call Jupiter's Ultra API directly from this process. The user is the first-party caller of Jupiter; we are not a gateway proxy here. A 20 bps platform fee is collected on-chain as part of the swap (Jupiter Referral Program — official integrator mechanism, not a hidden cost).
|
|
214
|
+
- Do NOT try to call \`/v1/jupiter/...\` on the BlockRun gateway — there is no such endpoint (Jupiter ToU forbids the gateway-proxy model).
|
|
215
|
+
|
|
215
216
|
**Sandbox (POST, x402-paid)**
|
|
216
217
|
- \`/v1/modal/{...path}\` — Modal GPU sandbox passthrough (create/exec/etc.).
|
|
217
218
|
- \`/v1/pm/{...path}\` — prediction-market data passthrough.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DefiLlama capabilities — TVL, yield pools, protocol metadata, and token
|
|
3
|
+
* prices via the BlockRun `/v1/defillama/*` endpoints. Each tool handles
|
|
4
|
+
* x402 payment automatically against the user's USDC wallet.
|
|
5
|
+
*
|
|
6
|
+
* Five tools, each filtered + formatted on the way back so we don't dump
|
|
7
|
+
* 5–10 MB of raw DefiLlama JSON into agent context:
|
|
8
|
+
* - DeFiLlamaProtocols $0.005/call — top-N protocols by TVL
|
|
9
|
+
* - DeFiLlamaProtocol $0.005/call — single protocol detail
|
|
10
|
+
* - DeFiLlamaChains $0.005/call — TVL ranked by chain
|
|
11
|
+
* - DeFiLlamaYields $0.005/call — yield pools, filtered + ranked
|
|
12
|
+
* - DeFiLlamaPrice $0.001/call — token price lookup
|
|
13
|
+
*
|
|
14
|
+
* DefiLlama is Apache 2.0 / "free for public and commercial use" — the
|
|
15
|
+
* BlockRun gateway adds metering + (future) caching/reliability layers,
|
|
16
|
+
* which is what the per-call charge funds.
|
|
17
|
+
*/
|
|
18
|
+
import type { CapabilityHandler } from '../agent/types.js';
|
|
19
|
+
export declare const defiLlamaProtocolsCapability: CapabilityHandler;
|
|
20
|
+
export declare const defiLlamaProtocolCapability: CapabilityHandler;
|
|
21
|
+
export declare const defiLlamaChainsCapability: CapabilityHandler;
|
|
22
|
+
export declare const defiLlamaYieldsCapability: CapabilityHandler;
|
|
23
|
+
export declare const defiLlamaPriceCapability: CapabilityHandler;
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DefiLlama capabilities — TVL, yield pools, protocol metadata, and token
|
|
3
|
+
* prices via the BlockRun `/v1/defillama/*` endpoints. Each tool handles
|
|
4
|
+
* x402 payment automatically against the user's USDC wallet.
|
|
5
|
+
*
|
|
6
|
+
* Five tools, each filtered + formatted on the way back so we don't dump
|
|
7
|
+
* 5–10 MB of raw DefiLlama JSON into agent context:
|
|
8
|
+
* - DeFiLlamaProtocols $0.005/call — top-N protocols by TVL
|
|
9
|
+
* - DeFiLlamaProtocol $0.005/call — single protocol detail
|
|
10
|
+
* - DeFiLlamaChains $0.005/call — TVL ranked by chain
|
|
11
|
+
* - DeFiLlamaYields $0.005/call — yield pools, filtered + ranked
|
|
12
|
+
* - DeFiLlamaPrice $0.001/call — token price lookup
|
|
13
|
+
*
|
|
14
|
+
* DefiLlama is Apache 2.0 / "free for public and commercial use" — the
|
|
15
|
+
* BlockRun gateway adds metering + (future) caching/reliability layers,
|
|
16
|
+
* which is what the per-call charge funds.
|
|
17
|
+
*/
|
|
18
|
+
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
19
|
+
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
20
|
+
const TIMEOUT_MS = 30_000;
|
|
21
|
+
// ─── Shared GET-with-x402 flow ────────────────────────────────────────────
|
|
22
|
+
async function getWithPayment(path, ctx) {
|
|
23
|
+
const chain = loadChain();
|
|
24
|
+
const apiUrl = API_URLS[chain];
|
|
25
|
+
const endpoint = `${apiUrl}${path}`;
|
|
26
|
+
const headers = {
|
|
27
|
+
Accept: 'application/json',
|
|
28
|
+
'User-Agent': `franklin/${VERSION}`,
|
|
29
|
+
};
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
32
|
+
const onAbort = () => controller.abort();
|
|
33
|
+
ctx.abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
34
|
+
try {
|
|
35
|
+
let response = await fetch(endpoint, {
|
|
36
|
+
method: 'GET',
|
|
37
|
+
signal: controller.signal,
|
|
38
|
+
headers,
|
|
39
|
+
});
|
|
40
|
+
if (response.status === 402) {
|
|
41
|
+
const paymentHeaders = await signPayment(response, chain, endpoint);
|
|
42
|
+
if (!paymentHeaders) {
|
|
43
|
+
throw new Error('Payment signing failed — check wallet balance');
|
|
44
|
+
}
|
|
45
|
+
response = await fetch(endpoint, {
|
|
46
|
+
method: 'GET',
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
headers: { ...headers, ...paymentHeaders },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const errText = await response.text().catch(() => '');
|
|
53
|
+
throw new Error(`DefiLlama ${path} failed (${response.status}): ${errText.slice(0, 200)}`);
|
|
54
|
+
}
|
|
55
|
+
return (await response.json());
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
clearTimeout(timeout);
|
|
59
|
+
ctx.abortSignal.removeEventListener('abort', onAbort);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function signPayment(response, chain, endpoint) {
|
|
63
|
+
try {
|
|
64
|
+
const paymentHeader = await extractPaymentReq(response);
|
|
65
|
+
if (!paymentHeader)
|
|
66
|
+
return null;
|
|
67
|
+
if (chain === 'solana') {
|
|
68
|
+
const wallet = await getOrCreateSolanaWallet();
|
|
69
|
+
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
70
|
+
const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
|
|
71
|
+
const secretBytes = await solanaKeyToBytes(wallet.privateKey);
|
|
72
|
+
const feePayer = details.extra?.feePayer || details.recipient;
|
|
73
|
+
const payload = await createSolanaPaymentPayload(secretBytes, wallet.address, details.recipient, details.amount, feePayer, {
|
|
74
|
+
resourceUrl: details.resource?.url || endpoint,
|
|
75
|
+
resourceDescription: details.resource?.description || 'Franklin DefiLlama call',
|
|
76
|
+
maxTimeoutSeconds: details.maxTimeoutSeconds || 60,
|
|
77
|
+
extra: details.extra,
|
|
78
|
+
});
|
|
79
|
+
return { 'PAYMENT-SIGNATURE': payload };
|
|
80
|
+
}
|
|
81
|
+
const wallet = await getOrCreateWallet();
|
|
82
|
+
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
83
|
+
const details = extractPaymentDetails(paymentRequired);
|
|
84
|
+
const payload = await createPaymentPayload(wallet.privateKey, wallet.address, details.recipient, details.amount, details.network || 'eip155:8453', {
|
|
85
|
+
resourceUrl: details.resource?.url || endpoint,
|
|
86
|
+
resourceDescription: details.resource?.description || 'Franklin DefiLlama call',
|
|
87
|
+
maxTimeoutSeconds: details.maxTimeoutSeconds || 60,
|
|
88
|
+
extra: details.extra,
|
|
89
|
+
});
|
|
90
|
+
return { 'PAYMENT-SIGNATURE': payload };
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
console.error(`[franklin] DefiLlama payment error: ${err.message}`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function extractPaymentReq(response) {
|
|
98
|
+
let header = response.headers.get('payment-required');
|
|
99
|
+
if (!header) {
|
|
100
|
+
try {
|
|
101
|
+
const body = (await response.json());
|
|
102
|
+
if (body.x402 || body.accepts)
|
|
103
|
+
header = btoa(JSON.stringify(body));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return header;
|
|
110
|
+
}
|
|
111
|
+
// ─── Formatting helpers ──────────────────────────────────────────────────
|
|
112
|
+
function formatUsd(value) {
|
|
113
|
+
if (value == null || !Number.isFinite(value))
|
|
114
|
+
return 'n/a';
|
|
115
|
+
if (value >= 1e9)
|
|
116
|
+
return `$${(value / 1e9).toFixed(2)}B`;
|
|
117
|
+
if (value >= 1e6)
|
|
118
|
+
return `$${(value / 1e6).toFixed(2)}M`;
|
|
119
|
+
if (value >= 1e3)
|
|
120
|
+
return `$${(value / 1e3).toFixed(2)}K`;
|
|
121
|
+
return `$${value.toFixed(2)}`;
|
|
122
|
+
}
|
|
123
|
+
function formatPct(value, digits = 2) {
|
|
124
|
+
if (value == null || !Number.isFinite(value))
|
|
125
|
+
return 'n/a';
|
|
126
|
+
const sign = value >= 0 ? '+' : '';
|
|
127
|
+
return `${sign}${value.toFixed(digits)}%`;
|
|
128
|
+
}
|
|
129
|
+
export const defiLlamaProtocolsCapability = {
|
|
130
|
+
spec: {
|
|
131
|
+
name: 'DeFiLlamaProtocols',
|
|
132
|
+
description: 'Rank DeFi protocols by total value locked (TVL) across all chains, optionally filtered by category, chain, or minimum TVL. ' +
|
|
133
|
+
'Returns the top-N protocols (default 20), each with TVL, 24h/7d change, chain breakdown, and slug. ' +
|
|
134
|
+
'Uses BlockRun gateway → DefiLlama. $0.005 per call. ' +
|
|
135
|
+
'Categories include: Lending, Liquid Staking, Bridge, Dexes, CDP, Yield, Yield Aggregator, Derivatives, Stablecoins, Insurance, etc.',
|
|
136
|
+
input_schema: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
top_n: { type: 'number', description: 'Max results (default 20, hard cap 100).' },
|
|
140
|
+
category: { type: 'string', description: 'Category filter, exact match (case-insensitive).' },
|
|
141
|
+
chain: { type: 'string', description: 'Chain name filter (e.g. "Ethereum", "Solana", "Base").' },
|
|
142
|
+
min_tvl_usd: { type: 'number', description: 'Drop protocols with TVL below this floor.' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
execute: async (input, ctx) => {
|
|
147
|
+
const params = input;
|
|
148
|
+
const topN = Math.min(Math.max(1, params.top_n ?? 20), 100);
|
|
149
|
+
try {
|
|
150
|
+
const res = await getWithPayment('/v1/defillama/protocols', ctx);
|
|
151
|
+
let list = res ?? [];
|
|
152
|
+
if (params.category) {
|
|
153
|
+
const want = params.category.toLowerCase();
|
|
154
|
+
list = list.filter((p) => (p.category ?? '').toLowerCase() === want);
|
|
155
|
+
}
|
|
156
|
+
if (params.chain) {
|
|
157
|
+
const want = params.chain.toLowerCase();
|
|
158
|
+
list = list.filter((p) => (p.chains ?? []).some((c) => c.toLowerCase() === want));
|
|
159
|
+
}
|
|
160
|
+
if (params.min_tvl_usd != null) {
|
|
161
|
+
list = list.filter((p) => Number(p.tvl) >= params.min_tvl_usd);
|
|
162
|
+
}
|
|
163
|
+
list.sort((a, b) => Number(b.tvl) - Number(a.tvl));
|
|
164
|
+
list = list.slice(0, topN);
|
|
165
|
+
if (list.length === 0)
|
|
166
|
+
return { output: 'No DefiLlama protocols matched the filters.' };
|
|
167
|
+
const lines = [
|
|
168
|
+
`## DefiLlama protocols — top ${list.length}` +
|
|
169
|
+
(params.category ? ` · category=${params.category}` : '') +
|
|
170
|
+
(params.chain ? ` · chain=${params.chain}` : ''),
|
|
171
|
+
];
|
|
172
|
+
list.forEach((p, i) => {
|
|
173
|
+
const chains = (p.chains ?? []).slice(0, 4).join(', ');
|
|
174
|
+
const more = (p.chains ?? []).length > 4 ? ` +${(p.chains ?? []).length - 4}` : '';
|
|
175
|
+
const cat = p.category ? ` · ${p.category}` : '';
|
|
176
|
+
const change = p.change_1d != null ? ` · 24h ${formatPct(p.change_1d)}` : '';
|
|
177
|
+
lines.push(`${i + 1}. **${p.name}** (${p.slug}) — ${formatUsd(Number(p.tvl))}${cat}${change}\n chains: ${chains}${more}`);
|
|
178
|
+
});
|
|
179
|
+
return { output: lines.join('\n') };
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
return { output: `Error: ${err.message}`, isError: true };
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
concurrent: true,
|
|
186
|
+
};
|
|
187
|
+
export const defiLlamaProtocolCapability = {
|
|
188
|
+
spec: {
|
|
189
|
+
name: 'DeFiLlamaProtocol',
|
|
190
|
+
description: 'Detailed TVL + chain breakdown for a single DeFi protocol identified by DefiLlama slug ' +
|
|
191
|
+
'(e.g. "aave", "uniswap", "lido", "jito", "marinade-finance"). ' +
|
|
192
|
+
'Returns TVL across each chain it operates on, recent change, audits, social. ' +
|
|
193
|
+
'$0.005 per call. To find a slug, run DeFiLlamaProtocols first.',
|
|
194
|
+
input_schema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
slug: { type: 'string', description: 'DefiLlama protocol slug, lowercase, dash-separated.' },
|
|
198
|
+
},
|
|
199
|
+
required: ['slug'],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
execute: async (input, ctx) => {
|
|
203
|
+
const params = input;
|
|
204
|
+
if (!params.slug)
|
|
205
|
+
return { output: 'Error: slug is required', isError: true };
|
|
206
|
+
try {
|
|
207
|
+
const safeSlug = encodeURIComponent(params.slug.trim().toLowerCase());
|
|
208
|
+
const p = await getWithPayment(`/v1/defillama/protocol/${safeSlug}`, ctx);
|
|
209
|
+
const chainBreakdown = p.currentChainTvls
|
|
210
|
+
? Object.entries(p.currentChainTvls)
|
|
211
|
+
.filter(([k]) => !k.endsWith('-staking') && !k.endsWith('-borrowed') && !k.endsWith('-pool2'))
|
|
212
|
+
.sort((a, b) => b[1] - a[1])
|
|
213
|
+
.slice(0, 8)
|
|
214
|
+
.map(([chain, tvl]) => `${chain} ${formatUsd(tvl)}`)
|
|
215
|
+
.join(', ')
|
|
216
|
+
: 'n/a';
|
|
217
|
+
const totalTvl = p.currentChainTvls
|
|
218
|
+
? Object.values(p.currentChainTvls).reduce((a, b) => a + b, 0)
|
|
219
|
+
: Array.isArray(p.tvl)
|
|
220
|
+
? p.tvl[p.tvl.length - 1]?.totalLiquidityUSD ?? 0
|
|
221
|
+
: p.tvl ?? 0;
|
|
222
|
+
const lines = [
|
|
223
|
+
`## ${p.name}${p.category ? ` (${p.category})` : ''}`,
|
|
224
|
+
'',
|
|
225
|
+
`TVL: ${formatUsd(totalTvl)}`,
|
|
226
|
+
];
|
|
227
|
+
if (p.change_1d != null)
|
|
228
|
+
lines.push(`24h change: ${formatPct(p.change_1d)}`);
|
|
229
|
+
if (p.change_7d != null)
|
|
230
|
+
lines.push(`7d change: ${formatPct(p.change_7d)}`);
|
|
231
|
+
lines.push(`Chains: ${chainBreakdown}`);
|
|
232
|
+
if (p.url)
|
|
233
|
+
lines.push(`URL: ${p.url}`);
|
|
234
|
+
if (p.twitter)
|
|
235
|
+
lines.push(`Twitter: @${p.twitter}`);
|
|
236
|
+
if (p.audits)
|
|
237
|
+
lines.push(`Audits: ${p.audits}`);
|
|
238
|
+
if (p.description)
|
|
239
|
+
lines.push('', p.description);
|
|
240
|
+
return { output: lines.join('\n') };
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
return { output: `Error: ${err.message}`, isError: true };
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
concurrent: true,
|
|
247
|
+
};
|
|
248
|
+
export const defiLlamaChainsCapability = {
|
|
249
|
+
spec: {
|
|
250
|
+
name: 'DeFiLlamaChains',
|
|
251
|
+
description: 'TVL ranking across every chain DefiLlama tracks. Default returns top 20 by TVL. $0.005 per call.',
|
|
252
|
+
input_schema: {
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
top_n: { type: 'number', description: 'Max results (default 20, hard cap 200).' },
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
execute: async (input, ctx) => {
|
|
260
|
+
const params = input;
|
|
261
|
+
const topN = Math.min(Math.max(1, params.top_n ?? 20), 200);
|
|
262
|
+
try {
|
|
263
|
+
const list = await getWithPayment('/v1/defillama/chains', ctx);
|
|
264
|
+
const sorted = (list ?? []).slice().sort((a, b) => Number(b.tvl) - Number(a.tvl)).slice(0, topN);
|
|
265
|
+
if (sorted.length === 0)
|
|
266
|
+
return { output: 'DefiLlama returned no chains.' };
|
|
267
|
+
const lines = [`## TVL by chain — top ${sorted.length}`];
|
|
268
|
+
sorted.forEach((c, i) => {
|
|
269
|
+
const sym = c.tokenSymbol ? ` (${c.tokenSymbol})` : '';
|
|
270
|
+
lines.push(`${i + 1}. **${c.name}**${sym} — ${formatUsd(Number(c.tvl))}`);
|
|
271
|
+
});
|
|
272
|
+
return { output: lines.join('\n') };
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
return { output: `Error: ${err.message}`, isError: true };
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
concurrent: true,
|
|
279
|
+
};
|
|
280
|
+
export const defiLlamaYieldsCapability = {
|
|
281
|
+
spec: {
|
|
282
|
+
name: 'DeFiLlamaYields',
|
|
283
|
+
description: 'Search DeFi yield pools (lending, LPs, vaults, staking) by symbol/chain/project, ranked by APY. ' +
|
|
284
|
+
'Returns top-N pools (default 10). $0.005 per call. ' +
|
|
285
|
+
'Default filters: TVL > $1M (avoid microcaps), APY > 0. Override via params. ' +
|
|
286
|
+
'Use stablecoin_only=true for "where can my USDC earn?" queries.',
|
|
287
|
+
input_schema: {
|
|
288
|
+
type: 'object',
|
|
289
|
+
properties: {
|
|
290
|
+
symbol: { type: 'string', description: 'Token symbol filter (e.g. "USDC", "ETH"). Matches tokens in the pool.' },
|
|
291
|
+
chain: { type: 'string', description: 'Chain filter (e.g. "Ethereum", "Solana", "Base").' },
|
|
292
|
+
project: { type: 'string', description: 'DefiLlama project slug filter (e.g. "aave-v3", "lido", "kamino").' },
|
|
293
|
+
min_tvl_usd: { type: 'number', description: 'Minimum pool TVL in USD (default 1_000_000).' },
|
|
294
|
+
min_apy_pct: { type: 'number', description: 'Minimum APY in percent (default 0).' },
|
|
295
|
+
stablecoin_only: { type: 'boolean', description: 'If true, only stablecoin pools.' },
|
|
296
|
+
top_n: { type: 'number', description: 'Max results (default 10, hard cap 50).' },
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
execute: async (input, ctx) => {
|
|
301
|
+
const params = input;
|
|
302
|
+
const topN = Math.min(Math.max(1, params.top_n ?? 10), 50);
|
|
303
|
+
const minTvl = params.min_tvl_usd ?? 1_000_000;
|
|
304
|
+
const minApy = params.min_apy_pct ?? 0;
|
|
305
|
+
try {
|
|
306
|
+
const res = await getWithPayment('/v1/defillama/yields', ctx);
|
|
307
|
+
const pools = res.data ?? [];
|
|
308
|
+
let filtered = pools.filter((p) => (p.apy ?? -1) >= minApy && (p.tvlUsd ?? 0) >= minTvl);
|
|
309
|
+
if (params.symbol) {
|
|
310
|
+
const want = params.symbol.toUpperCase();
|
|
311
|
+
filtered = filtered.filter((p) => p.symbol?.toUpperCase().includes(want));
|
|
312
|
+
}
|
|
313
|
+
if (params.chain) {
|
|
314
|
+
const want = params.chain.toLowerCase();
|
|
315
|
+
filtered = filtered.filter((p) => p.chain?.toLowerCase() === want);
|
|
316
|
+
}
|
|
317
|
+
if (params.project) {
|
|
318
|
+
const want = params.project.toLowerCase();
|
|
319
|
+
filtered = filtered.filter((p) => p.project?.toLowerCase() === want);
|
|
320
|
+
}
|
|
321
|
+
if (params.stablecoin_only) {
|
|
322
|
+
filtered = filtered.filter((p) => p.stablecoin === true);
|
|
323
|
+
}
|
|
324
|
+
filtered.sort((a, b) => (b.apy ?? 0) - (a.apy ?? 0));
|
|
325
|
+
const top = filtered.slice(0, topN);
|
|
326
|
+
if (top.length === 0)
|
|
327
|
+
return { output: 'No yield pools matched the filters.' };
|
|
328
|
+
const filterDesc = [
|
|
329
|
+
params.symbol && `symbol=${params.symbol}`,
|
|
330
|
+
params.chain && `chain=${params.chain}`,
|
|
331
|
+
params.project && `project=${params.project}`,
|
|
332
|
+
params.stablecoin_only && 'stablecoin_only',
|
|
333
|
+
`min_tvl=${formatUsd(minTvl)}`,
|
|
334
|
+
`min_apy=${minApy}%`,
|
|
335
|
+
]
|
|
336
|
+
.filter(Boolean)
|
|
337
|
+
.join(' · ');
|
|
338
|
+
const lines = [`## Yield pools — top ${top.length} by APY · ${filterDesc}`];
|
|
339
|
+
top.forEach((p, i) => {
|
|
340
|
+
const breakdown = p.apyBase != null && p.apyReward != null
|
|
341
|
+
? ` (base ${p.apyBase.toFixed(2)}% + reward ${p.apyReward.toFixed(2)}%)`
|
|
342
|
+
: '';
|
|
343
|
+
const il = p.ilRisk ? ` · IL: ${p.ilRisk}` : '';
|
|
344
|
+
lines.push(`${i + 1}. **${p.project}** / ${p.chain} / ${p.symbol} — ${(p.apy ?? 0).toFixed(2)}% APY${breakdown}\n TVL: ${formatUsd(p.tvlUsd)}${il} · pool: ${p.pool}`);
|
|
345
|
+
});
|
|
346
|
+
return { output: lines.join('\n') };
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
return { output: `Error: ${err.message}`, isError: true };
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
concurrent: true,
|
|
353
|
+
};
|
|
354
|
+
export const defiLlamaPriceCapability = {
|
|
355
|
+
spec: {
|
|
356
|
+
name: 'DeFiLlamaPrice',
|
|
357
|
+
description: 'Token price lookup via DefiLlama (covers thousands of tokens — anything DEX-listed). $0.001 per call. ' +
|
|
358
|
+
'Coin identifier syntax: "{platform}:{address}" or "coingecko:{slug}". ' +
|
|
359
|
+
'Examples: "coingecko:bitcoin" (BTC USD), "ethereum:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" (WETH), ' +
|
|
360
|
+
'"solana:So11111111111111111111111111111111111111112" (SOL/wSOL), ' +
|
|
361
|
+
'"solana:DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263" (BONK). ' +
|
|
362
|
+
'Pass multiple identifiers for batch lookup. ' +
|
|
363
|
+
'Use the existing /v1/crypto/price/{symbol} endpoint via TradingMarket for the major-asset shorthand instead — ' +
|
|
364
|
+
'this tool is for arbitrary on-chain mints / addresses.',
|
|
365
|
+
input_schema: {
|
|
366
|
+
type: 'object',
|
|
367
|
+
properties: {
|
|
368
|
+
coins: {
|
|
369
|
+
type: 'array',
|
|
370
|
+
items: { type: 'string' },
|
|
371
|
+
description: 'Coin identifiers in DefiLlama syntax. Up to 50 per call.',
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
required: ['coins'],
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
execute: async (input, ctx) => {
|
|
378
|
+
const params = input;
|
|
379
|
+
if (!params.coins || params.coins.length === 0) {
|
|
380
|
+
return { output: 'Error: coins array is required', isError: true };
|
|
381
|
+
}
|
|
382
|
+
if (params.coins.length > 50) {
|
|
383
|
+
return { output: `Error: max 50 coins per call (got ${params.coins.length})`, isError: true };
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const safeList = params.coins.map((c) => encodeURIComponent(c.trim())).join(',');
|
|
387
|
+
const res = await getWithPayment(`/v1/defillama/prices/${safeList}`, ctx);
|
|
388
|
+
const coins = res.coins ?? {};
|
|
389
|
+
const lines = [`## Token prices — ${Object.keys(coins).length} match(es)`];
|
|
390
|
+
for (const id of params.coins) {
|
|
391
|
+
const entry = coins[id];
|
|
392
|
+
if (!entry || entry.price == null) {
|
|
393
|
+
lines.push(`- ${id}: no price returned`);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const sym = entry.symbol ? ` (${entry.symbol})` : '';
|
|
397
|
+
const conf = entry.confidence != null ? ` · conf ${(entry.confidence * 100).toFixed(0)}%` : '';
|
|
398
|
+
const ts = entry.timestamp ? ` · ${new Date(entry.timestamp * 1000).toISOString().slice(0, 19)}Z` : '';
|
|
399
|
+
lines.push(`- **${id}**${sym}: $${entry.price.toFixed(entry.price < 0.01 ? 8 : 4)}${conf}${ts}`);
|
|
400
|
+
}
|
|
401
|
+
return { output: lines.join('\n') };
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
return { output: `Error: ${err.message}`, isError: true };
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
concurrent: true,
|
|
408
|
+
};
|
package/dist/tools/index.js
CHANGED
|
@@ -25,6 +25,8 @@ import { postToXCapability } from './posttox.js';
|
|
|
25
25
|
import { moaCapability } from './moa.js';
|
|
26
26
|
import { webhookPostCapability } from './webhook.js';
|
|
27
27
|
import { walletCapability } from './wallet.js';
|
|
28
|
+
import { jupiterQuoteCapability, jupiterSwapCapability } from './jupiter.js';
|
|
29
|
+
import { defiLlamaProtocolsCapability, defiLlamaProtocolCapability, defiLlamaChainsCapability, defiLlamaYieldsCapability, defiLlamaPriceCapability, } from './defillama.js';
|
|
28
30
|
import { createTradingCapabilities } from './trading-execute.js';
|
|
29
31
|
import { Portfolio } from '../trading/portfolio.js';
|
|
30
32
|
import { RiskEngine } from '../trading/risk.js';
|
|
@@ -144,6 +146,13 @@ export const allCapabilities = [
|
|
|
144
146
|
moaCapability,
|
|
145
147
|
webhookPostCapability,
|
|
146
148
|
walletCapability,
|
|
149
|
+
jupiterQuoteCapability,
|
|
150
|
+
jupiterSwapCapability,
|
|
151
|
+
defiLlamaProtocolsCapability,
|
|
152
|
+
defiLlamaProtocolCapability,
|
|
153
|
+
defiLlamaChainsCapability,
|
|
154
|
+
defiLlamaYieldsCapability,
|
|
155
|
+
defiLlamaPriceCapability,
|
|
147
156
|
];
|
|
148
157
|
export { readCapability, writeCapability, editCapability, bashCapability, globCapability, grepCapability, webFetchCapability, webSearchCapability, taskCapability, detachCapability, };
|
|
149
158
|
export { createSubAgentCapability } from './subagent.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jupiter Ultra Swap — Solana DEX aggregator (built-in Franklin tool).
|
|
3
|
+
*
|
|
4
|
+
* We do NOT proxy through the BlockRun gateway — Jupiter's ToU forbids that.
|
|
5
|
+
* Instead the agent calls Jupiter's Ultra API directly from this process,
|
|
6
|
+
* embedding BlockRun's Referral Program identity in every order. The 20 bps
|
|
7
|
+
* platform fee flows on-chain to BlockRun's referral wallet at swap settlement
|
|
8
|
+
* (Jupiter's officially-supported integrator monetization mechanism — same one
|
|
9
|
+
* Phantom and other wallets use).
|
|
10
|
+
*
|
|
11
|
+
* Two tools exposed:
|
|
12
|
+
* - JupiterQuote — read-only price check (no AskUser, no signing)
|
|
13
|
+
* - JupiterSwap — full flow: order → AskUser confirm → sign → execute
|
|
14
|
+
*
|
|
15
|
+
* Reference implementation:
|
|
16
|
+
* https://github.com/Jupiter-DevRel/typescript-examples/tree/main/ultra/order-execute-with-referral-accounts
|
|
17
|
+
*/
|
|
18
|
+
import type { CapabilityHandler } from '../agent/types.js';
|
|
19
|
+
export declare const jupiterQuoteCapability: CapabilityHandler;
|
|
20
|
+
export declare const jupiterSwapCapability: CapabilityHandler;
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jupiter Ultra Swap — Solana DEX aggregator (built-in Franklin tool).
|
|
3
|
+
*
|
|
4
|
+
* We do NOT proxy through the BlockRun gateway — Jupiter's ToU forbids that.
|
|
5
|
+
* Instead the agent calls Jupiter's Ultra API directly from this process,
|
|
6
|
+
* embedding BlockRun's Referral Program identity in every order. The 20 bps
|
|
7
|
+
* platform fee flows on-chain to BlockRun's referral wallet at swap settlement
|
|
8
|
+
* (Jupiter's officially-supported integrator monetization mechanism — same one
|
|
9
|
+
* Phantom and other wallets use).
|
|
10
|
+
*
|
|
11
|
+
* Two tools exposed:
|
|
12
|
+
* - JupiterQuote — read-only price check (no AskUser, no signing)
|
|
13
|
+
* - JupiterSwap — full flow: order → AskUser confirm → sign → execute
|
|
14
|
+
*
|
|
15
|
+
* Reference implementation:
|
|
16
|
+
* https://github.com/Jupiter-DevRel/typescript-examples/tree/main/ultra/order-execute-with-referral-accounts
|
|
17
|
+
*/
|
|
18
|
+
import { Keypair, VersionedTransaction } from '@solana/web3.js';
|
|
19
|
+
import { getOrCreateSolanaWallet, solanaKeyToBytes, } from '@blockrun/llm';
|
|
20
|
+
// ─── BlockRun Referral identity ───────────────────────────────────────────
|
|
21
|
+
// Set up via referral.jup.ag. Owns ATAs for USDC, wSOL, JUP, USDT, etc. Every
|
|
22
|
+
// swap routed through these tools deposits 20 bps of output to this wallet's
|
|
23
|
+
// matching ATA.
|
|
24
|
+
const BLOCKRUN_REFERRAL_ACCOUNT = 'DUGyfGMTAvyHtrvCa2qPE2KJd3qtGBe4ra7u6URne4xQ';
|
|
25
|
+
const BLOCKRUN_REFERRAL_FEE_BPS = 20; // 0.2% — Jupiter docs default; well below Phantom's 85 bps.
|
|
26
|
+
// ─── Ultra API endpoints ──────────────────────────────────────────────────
|
|
27
|
+
const ULTRA_BASE = 'https://lite-api.jup.ag/ultra/v1';
|
|
28
|
+
const ORDER_TIMEOUT_MS = 15_000;
|
|
29
|
+
const EXECUTE_TIMEOUT_MS = 30_000;
|
|
30
|
+
// ─── Symbol → mint shortcuts ──────────────────────────────────────────────
|
|
31
|
+
// Agents prefer "USDC" / "SOL" over 44-char base58 mint addresses. Anything
|
|
32
|
+
// not in this map is passed through verbatim — power users can drop in any
|
|
33
|
+
// mint they want.
|
|
34
|
+
const SYMBOL_TO_MINT = {
|
|
35
|
+
SOL: 'So11111111111111111111111111111111111111112', // wSOL
|
|
36
|
+
WSOL: 'So11111111111111111111111111111111111111112',
|
|
37
|
+
USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
|
38
|
+
USDT: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
|
|
39
|
+
JUP: 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN',
|
|
40
|
+
BONK: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
|
|
41
|
+
WIF: 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm',
|
|
42
|
+
TRUMP: '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN',
|
|
43
|
+
PUMP: 'pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn',
|
|
44
|
+
};
|
|
45
|
+
const TOKEN_DECIMALS = {
|
|
46
|
+
// wSOL — 9
|
|
47
|
+
So11111111111111111111111111111111111111112: 9,
|
|
48
|
+
// USDC — 6
|
|
49
|
+
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: 6,
|
|
50
|
+
// USDT — 6
|
|
51
|
+
Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: 6,
|
|
52
|
+
// JUP — 6
|
|
53
|
+
JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN: 6,
|
|
54
|
+
// BONK — 5
|
|
55
|
+
DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263: 5,
|
|
56
|
+
// WIF — 6
|
|
57
|
+
EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm: 6,
|
|
58
|
+
};
|
|
59
|
+
function resolveMint(input) {
|
|
60
|
+
const upper = input.trim().toUpperCase();
|
|
61
|
+
if (SYMBOL_TO_MINT[upper])
|
|
62
|
+
return SYMBOL_TO_MINT[upper];
|
|
63
|
+
return input.trim();
|
|
64
|
+
}
|
|
65
|
+
function decimalsFor(mint, fallback = 9) {
|
|
66
|
+
return TOKEN_DECIMALS[mint] ?? fallback;
|
|
67
|
+
}
|
|
68
|
+
function symbolFor(mint) {
|
|
69
|
+
for (const [sym, m] of Object.entries(SYMBOL_TO_MINT)) {
|
|
70
|
+
if (m === mint)
|
|
71
|
+
return sym;
|
|
72
|
+
}
|
|
73
|
+
return mint.slice(0, 4) + '…';
|
|
74
|
+
}
|
|
75
|
+
function toAtomicUnits(amount, decimals) {
|
|
76
|
+
// BigInt math — JavaScript number can lose precision for large lamport counts.
|
|
77
|
+
const scale = BigInt(10) ** BigInt(decimals);
|
|
78
|
+
const whole = BigInt(Math.floor(amount));
|
|
79
|
+
const fractional = BigInt(Math.round((amount - Math.floor(amount)) * Number(scale)));
|
|
80
|
+
return (whole * scale + fractional).toString();
|
|
81
|
+
}
|
|
82
|
+
function fromAtomicUnits(atomic, decimals) {
|
|
83
|
+
const value = typeof atomic === 'string' ? Number(atomic) : atomic;
|
|
84
|
+
return value / Math.pow(10, decimals);
|
|
85
|
+
}
|
|
86
|
+
async function ultraOrder(params) {
|
|
87
|
+
const url = new URL(`${ULTRA_BASE}/order`);
|
|
88
|
+
url.searchParams.set('inputMint', params.inputMint);
|
|
89
|
+
url.searchParams.set('outputMint', params.outputMint);
|
|
90
|
+
url.searchParams.set('amount', params.amount);
|
|
91
|
+
if (params.taker)
|
|
92
|
+
url.searchParams.set('taker', params.taker);
|
|
93
|
+
url.searchParams.set('referralAccount', BLOCKRUN_REFERRAL_ACCOUNT);
|
|
94
|
+
url.searchParams.set('referralFee', String(BLOCKRUN_REFERRAL_FEE_BPS));
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
const timer = setTimeout(() => controller.abort(), ORDER_TIMEOUT_MS);
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetch(url.toString(), {
|
|
99
|
+
method: 'GET',
|
|
100
|
+
headers: { Accept: 'application/json' },
|
|
101
|
+
signal: controller.signal,
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
const text = await res.text();
|
|
105
|
+
throw new Error(`Jupiter Ultra /order returned ${res.status}: ${text}`);
|
|
106
|
+
}
|
|
107
|
+
return (await res.json());
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function ultraExecute(args) {
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const timer = setTimeout(() => controller.abort(), EXECUTE_TIMEOUT_MS);
|
|
116
|
+
try {
|
|
117
|
+
const res = await fetch(`${ULTRA_BASE}/execute`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
120
|
+
body: JSON.stringify(args),
|
|
121
|
+
signal: controller.signal,
|
|
122
|
+
});
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const text = await res.text();
|
|
125
|
+
throw new Error(`Jupiter Ultra /execute returned ${res.status}: ${text}`);
|
|
126
|
+
}
|
|
127
|
+
return (await res.json());
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function loadSolanaKeypair() {
|
|
134
|
+
const wallet = await getOrCreateSolanaWallet();
|
|
135
|
+
const bytes = await solanaKeyToBytes(wallet.privateKey);
|
|
136
|
+
return Keypair.fromSecretKey(bytes);
|
|
137
|
+
}
|
|
138
|
+
function formatQuote(order) {
|
|
139
|
+
const inMint = order.inputMint;
|
|
140
|
+
const outMint = order.outputMint;
|
|
141
|
+
const inDec = decimalsFor(inMint);
|
|
142
|
+
const outDec = decimalsFor(outMint);
|
|
143
|
+
const inAmount = fromAtomicUnits(order.inAmount, inDec);
|
|
144
|
+
const outAmount = fromAtomicUnits(order.outAmount, outDec);
|
|
145
|
+
const inSym = symbolFor(inMint);
|
|
146
|
+
const outSym = symbolFor(outMint);
|
|
147
|
+
const impact = order.priceImpactPct
|
|
148
|
+
? `${(Number(order.priceImpactPct) * 100).toFixed(3)}%`
|
|
149
|
+
: 'n/a';
|
|
150
|
+
const route = order.routePlan
|
|
151
|
+
?.map((step) => step.swapInfo?.label)
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.join(' → ') || 'Jupiter Ultra';
|
|
154
|
+
const rate = inAmount > 0 ? outAmount / inAmount : 0;
|
|
155
|
+
return [
|
|
156
|
+
`${inAmount.toFixed(Math.min(6, inDec))} ${inSym} → ${outAmount.toFixed(Math.min(6, outDec))} ${outSym}`,
|
|
157
|
+
`Rate: 1 ${inSym} ≈ ${rate.toPrecision(6)} ${outSym}`,
|
|
158
|
+
`Price impact: ${impact}`,
|
|
159
|
+
`Route: ${route}`,
|
|
160
|
+
`Platform fee: ${BLOCKRUN_REFERRAL_FEE_BPS} bps (BlockRun referral)`,
|
|
161
|
+
].join('\n');
|
|
162
|
+
}
|
|
163
|
+
async function executeJupiterQuote(input) {
|
|
164
|
+
const inputMint = resolveMint(input.input_mint);
|
|
165
|
+
const outputMint = resolveMint(input.output_mint);
|
|
166
|
+
const inDec = decimalsFor(inputMint);
|
|
167
|
+
const amountAtomic = toAtomicUnits(input.amount, inDec);
|
|
168
|
+
try {
|
|
169
|
+
const order = await ultraOrder({ inputMint, outputMint, amount: amountAtomic });
|
|
170
|
+
if (order.errorMessage) {
|
|
171
|
+
return { output: `Jupiter Ultra rejected the quote: ${order.errorMessage}`, isError: true };
|
|
172
|
+
}
|
|
173
|
+
return { output: formatQuote(order) };
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
return {
|
|
177
|
+
output: `Jupiter Ultra /order failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
178
|
+
isError: true,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function executeJupiterSwap(input, ctx) {
|
|
183
|
+
const inputMint = resolveMint(input.input_mint);
|
|
184
|
+
const outputMint = resolveMint(input.output_mint);
|
|
185
|
+
const inDec = decimalsFor(inputMint);
|
|
186
|
+
const amountAtomic = toAtomicUnits(input.amount, inDec);
|
|
187
|
+
// Load wallet first — fail fast if Solana isn't set up.
|
|
188
|
+
let keypair;
|
|
189
|
+
try {
|
|
190
|
+
keypair = await loadSolanaKeypair();
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
return {
|
|
194
|
+
output: `No Solana wallet found. Run \`franklin setup solana\` to generate one, or check ~/.blockrun/solana-wallet.json.\n` +
|
|
195
|
+
`(${err instanceof Error ? err.message : 'unknown error'})`,
|
|
196
|
+
isError: true,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// Step 1 — fetch order with our referral identity attached.
|
|
200
|
+
let order;
|
|
201
|
+
try {
|
|
202
|
+
order = await ultraOrder({
|
|
203
|
+
inputMint,
|
|
204
|
+
outputMint,
|
|
205
|
+
amount: amountAtomic,
|
|
206
|
+
taker: keypair.publicKey.toBase58(),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
return {
|
|
211
|
+
output: `Jupiter Ultra /order failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
212
|
+
isError: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (order.errorMessage || !order.transaction) {
|
|
216
|
+
return {
|
|
217
|
+
output: `Jupiter Ultra rejected the order: ${order.errorMessage ?? 'no transaction returned'}`,
|
|
218
|
+
isError: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// Step 2 — confirm with user (unless explicit auto_approve override).
|
|
222
|
+
if (!input.auto_approve && ctx.onAskUser) {
|
|
223
|
+
const quoteText = formatQuote(order);
|
|
224
|
+
const question = `Execute this Jupiter swap?\n\n${quoteText}\n\nWallet: ${keypair.publicKey.toBase58()}`;
|
|
225
|
+
const answer = await ctx.onAskUser(question, ['Confirm', 'Cancel']);
|
|
226
|
+
if (answer.toLowerCase() !== 'confirm') {
|
|
227
|
+
return { output: 'Swap cancelled by user.' };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Step 3 — sign locally with the user's Solana keypair.
|
|
231
|
+
let signedBase64;
|
|
232
|
+
try {
|
|
233
|
+
const txBytes = Buffer.from(order.transaction, 'base64');
|
|
234
|
+
const tx = VersionedTransaction.deserialize(txBytes);
|
|
235
|
+
tx.sign([keypair]);
|
|
236
|
+
signedBase64 = Buffer.from(tx.serialize()).toString('base64');
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
return {
|
|
240
|
+
output: `Failed to sign Jupiter transaction: ${err instanceof Error ? err.message : String(err)}`,
|
|
241
|
+
isError: true,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// Step 4 — submit through Jupiter Ultra (handles broadcast + confirmation).
|
|
245
|
+
try {
|
|
246
|
+
const exec = await ultraExecute({
|
|
247
|
+
signedTransaction: signedBase64,
|
|
248
|
+
requestId: order.requestId,
|
|
249
|
+
});
|
|
250
|
+
if (exec.status !== 'Success') {
|
|
251
|
+
return {
|
|
252
|
+
output: `Jupiter Ultra /execute reported ${exec.status}` +
|
|
253
|
+
(exec.error ? `: ${exec.error}` : '') +
|
|
254
|
+
(exec.code ? ` (code ${exec.code})` : ''),
|
|
255
|
+
isError: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const sig = exec.signature ?? '<unknown>';
|
|
259
|
+
const explorer = `https://solscan.io/tx/${sig}`;
|
|
260
|
+
return {
|
|
261
|
+
output: [
|
|
262
|
+
'✓ Swap executed.',
|
|
263
|
+
formatQuote(order),
|
|
264
|
+
`Signature: ${sig}`,
|
|
265
|
+
explorer,
|
|
266
|
+
].join('\n'),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
return {
|
|
271
|
+
output: `Jupiter Ultra /execute failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
272
|
+
isError: true,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// ─── Capability handlers ──────────────────────────────────────────────────
|
|
277
|
+
const COMMON_INPUT_PROPERTIES = {
|
|
278
|
+
input_mint: {
|
|
279
|
+
type: 'string',
|
|
280
|
+
description: 'Input SPL mint address, OR a symbol shortcut: SOL, USDC, USDT, JUP, BONK, WIF, TRUMP, PUMP.',
|
|
281
|
+
},
|
|
282
|
+
output_mint: {
|
|
283
|
+
type: 'string',
|
|
284
|
+
description: 'Output SPL mint address, OR a symbol shortcut (same list as input_mint).',
|
|
285
|
+
},
|
|
286
|
+
amount: {
|
|
287
|
+
type: 'number',
|
|
288
|
+
description: 'Amount of input_mint to swap, in human units (e.g. 1.5 USDC, 0.05 SOL). Decimals are looked up automatically for known mints; defaults to 9 for unknown mints.',
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
export const jupiterQuoteCapability = {
|
|
292
|
+
spec: {
|
|
293
|
+
name: 'JupiterQuote',
|
|
294
|
+
description: "Read-only price quote for a Solana DEX swap via Jupiter Ultra. Returns input/output amounts, rate, price impact, and routing path. Free — no on-chain transaction, no signing. Use this before JupiterSwap to inspect what a trade would do.",
|
|
295
|
+
input_schema: {
|
|
296
|
+
type: 'object',
|
|
297
|
+
required: ['input_mint', 'output_mint', 'amount'],
|
|
298
|
+
properties: COMMON_INPUT_PROPERTIES,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
execute: async (input) => {
|
|
302
|
+
return executeJupiterQuote(input);
|
|
303
|
+
},
|
|
304
|
+
concurrent: true,
|
|
305
|
+
};
|
|
306
|
+
export const jupiterSwapCapability = {
|
|
307
|
+
spec: {
|
|
308
|
+
name: 'JupiterSwap',
|
|
309
|
+
description: "Execute a Solana DEX swap via Jupiter Ultra. Quotes the order, asks the user to confirm via AskUser, signs locally with the Franklin Solana wallet, and submits. A 20 bps platform fee is collected on-chain by Jupiter as part of the swap (BlockRun referral — official integrator program). Returns the Solscan transaction link.",
|
|
310
|
+
input_schema: {
|
|
311
|
+
type: 'object',
|
|
312
|
+
required: ['input_mint', 'output_mint', 'amount'],
|
|
313
|
+
properties: {
|
|
314
|
+
...COMMON_INPUT_PROPERTIES,
|
|
315
|
+
auto_approve: {
|
|
316
|
+
type: 'boolean',
|
|
317
|
+
description: 'If true, skip the AskUser confirm step. Default false — agent should leave this false unless the user explicitly authorized batch execution for this turn.',
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
execute: async (input, ctx) => {
|
|
323
|
+
return executeJupiterSwap(input, ctx);
|
|
324
|
+
},
|
|
325
|
+
concurrent: false,
|
|
326
|
+
};
|
package/package.json
CHANGED