@blockrun/franklin 3.13.1 → 3.14.0
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 +3 -3
- package/dist/tools/zerox-base.js +100 -55
- package/package.json +1 -1
package/dist/agent/context.js
CHANGED
|
@@ -213,11 +213,11 @@ You run on the BlockRun AI Gateway. When the user asks you to "test the BlockRun
|
|
|
213
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
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
|
-
**Base DEX swap (0x V2 Permit2)**
|
|
217
|
-
- Use the **\`Base0xQuote\` and \`Base0xSwap\` built-in tools** for swaps on Base (chain id 8453).
|
|
216
|
+
**Base DEX swap (0x V2 Permit2 via BlockRun gateway)**
|
|
217
|
+
- Use the **\`Base0xQuote\` and \`Base0xSwap\` built-in tools** for swaps on Base (chain id 8453). Tools route through BlockRun gateway \`/v1/zerox/{price,quote}\` (server-side 0x key, x402-paid). User pays $0.001 USDC per quote/swap call to the gateway; on-chain affiliate (20 bps in the sell-token) flows automatically to BlockRun treasury at swap settlement. **No 0x signup needed** — BlockRun manages the upstream key.
|
|
218
218
|
- Symbol shortcuts pre-mapped: ETH (native), WETH, USDC, USDT, CBBTC, CBETH, AERO, DAI. Raw \`0x...\` addresses pass through.
|
|
219
|
-
- **Each Franklin user supplies their OWN \`ZERO_EX_API_KEY\`** (free, no credit card, 10 req/s — sign up at https://dashboard.0x.org). The affiliate cut routes to BlockRun via the swap query params regardless of whose API key is making the call. If the swap tool errors with the env-var message, repeat the URL to the user — do not try to set the env yourself or invent a key.
|
|
220
219
|
- For native ETH → token: no Permit2 approval needed (native value path). For ERC-20 → token: first-time-per-token Permit2 approval auto-runs before the swap (one-time gas cost; future swaps of the same sell-token reuse it).
|
|
220
|
+
- The user signs Permit2 typed data locally with their Base keypair; the signed transaction is submitted to a Base RPC (default public mainnet-beta) — BlockRun never custodies keys.
|
|
221
221
|
|
|
222
222
|
**Sandbox (POST, x402-paid)**
|
|
223
223
|
- \`/v1/modal/{...path}\` — Modal GPU sandbox passthrough (create/exec/etc.).
|
package/dist/tools/zerox-base.js
CHANGED
|
@@ -24,18 +24,22 @@
|
|
|
24
24
|
import { createWalletClient, http, publicActions, concat, numberToHex, size, parseUnits, formatUnits, maxUint256, erc20Abi, getContract, } from 'viem';
|
|
25
25
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
26
26
|
import { base } from 'viem/chains';
|
|
27
|
-
import { getOrCreateWallet } from '@blockrun/llm';
|
|
27
|
+
import { getOrCreateWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, getOrCreateSolanaWallet, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
28
28
|
import { loadConfig } from '../commands/config.js';
|
|
29
|
+
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
29
30
|
// ─── BlockRun affiliate identity on Base ─────────────────────────────────
|
|
30
31
|
// Reuses the existing BlockRun ops wallet that already receives x402
|
|
31
32
|
// settlements on Base. Every swap routed through these tools deposits 20
|
|
32
33
|
// bps of the sell-token amount into this address at settlement.
|
|
33
34
|
const BLOCKRUN_BASE_AFFILIATE = '0xe9030014F5DAe217d0A152f02A043567b16c1aBf';
|
|
34
35
|
const BLOCKRUN_AFFILIATE_FEE_BPS = 20; // 0.2% — matches Jupiter Ultra path.
|
|
35
|
-
// ───
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
// ─── BlockRun gateway path for 0x ────────────────────────────────────────
|
|
37
|
+
// As of v3.14.0 we route through the BlockRun gateway (server-side 0x key),
|
|
38
|
+
// not directly to api.0x.org. User pays $0.001 via x402 per gateway call;
|
|
39
|
+
// affiliate 20 bps is force-set server-side and lands on-chain in the same
|
|
40
|
+
// BlockRun treasury that already collects x402 settlements.
|
|
41
|
+
const ZEROX_GATEWAY_PATH = '/v1/zerox';
|
|
42
|
+
const ZEROX_TIMEOUT_MS = 30_000;
|
|
39
43
|
// ─── Default Base RPC ────────────────────────────────────────────────────
|
|
40
44
|
// Public Base mainnet endpoint. Override via BASE_RPC_URL env or
|
|
41
45
|
// `franklin config set base-rpc-url <url>` (Alchemy, QuickNode public, etc.).
|
|
@@ -46,9 +50,6 @@ function resolveBaseRpcUrl() {
|
|
|
46
50
|
loadConfig()['base-rpc-url'] ||
|
|
47
51
|
DEFAULT_BASE_RPC);
|
|
48
52
|
}
|
|
49
|
-
function resolveZeroxApiKey() {
|
|
50
|
-
return process.env.ZERO_EX_API_KEY || loadConfig()['zerox-api-key'];
|
|
51
|
-
}
|
|
52
53
|
// ─── Session safety: cumulative live-swap counter ─────────────────────────
|
|
53
54
|
const DEFAULT_LIVE_SWAP_CAP = 10;
|
|
54
55
|
const liveSwapCap = (() => {
|
|
@@ -145,53 +146,104 @@ function makeClient(account) {
|
|
|
145
146
|
transport: http(resolveBaseRpcUrl()),
|
|
146
147
|
}).extend(publicActions);
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
"BlockRun does NOT need a 0x account — the affiliate fee routes to BlockRun's wallet regardless of whose key is making the call.");
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
'Content-Type': 'application/json',
|
|
160
|
-
'0x-api-key': apiKey,
|
|
161
|
-
'0x-version': ZEROX_VERSION,
|
|
149
|
+
// ─── 0x calls via BlockRun gateway (x402-paid) ───────────────────────────
|
|
150
|
+
async function gatewayGetWithPayment(path, params, ctx) {
|
|
151
|
+
const chain = loadChain();
|
|
152
|
+
const apiUrl = API_URLS[chain];
|
|
153
|
+
const endpoint = `${apiUrl}${ZEROX_GATEWAY_PATH}/${path}?${params.toString()}`;
|
|
154
|
+
const headers = {
|
|
155
|
+
Accept: 'application/json',
|
|
156
|
+
'User-Agent': `franklin/${VERSION}`,
|
|
162
157
|
};
|
|
163
|
-
}
|
|
164
|
-
// ─── 0x API calls ────────────────────────────────────────────────────────
|
|
165
|
-
async function zeroxFetch(path, params) {
|
|
166
|
-
const url = `${ZEROX_BASE}/${path}?${params.toString()}`;
|
|
167
158
|
const controller = new AbortController();
|
|
168
159
|
const timer = setTimeout(() => controller.abort(), ZEROX_TIMEOUT_MS);
|
|
160
|
+
const onAbort = () => controller.abort();
|
|
161
|
+
ctx.abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
169
162
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
163
|
+
let response = await fetch(endpoint, {
|
|
164
|
+
method: 'GET',
|
|
165
|
+
headers,
|
|
166
|
+
signal: controller.signal,
|
|
167
|
+
});
|
|
168
|
+
if (response.status === 402) {
|
|
169
|
+
const paymentHeaders = await signGatewayPayment(response, chain, endpoint);
|
|
170
|
+
if (!paymentHeaders) {
|
|
171
|
+
throw new Error('Payment signing failed — check wallet balance');
|
|
172
|
+
}
|
|
173
|
+
response = await fetch(endpoint, {
|
|
174
|
+
method: 'GET',
|
|
175
|
+
headers: { ...headers, ...paymentHeaders },
|
|
176
|
+
signal: controller.signal,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
const text = await response.text().catch(() => '');
|
|
181
|
+
throw new Error(`BlockRun gateway /v1/zerox/${path} returned ${response.status}: ${text.slice(0, 300)}`);
|
|
174
182
|
}
|
|
175
|
-
return (await
|
|
183
|
+
return (await response.json());
|
|
176
184
|
}
|
|
177
185
|
finally {
|
|
178
186
|
clearTimeout(timer);
|
|
187
|
+
ctx.abortSignal.removeEventListener('abort', onAbort);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function signGatewayPayment(response, chain, endpoint) {
|
|
191
|
+
try {
|
|
192
|
+
let header = response.headers.get('payment-required');
|
|
193
|
+
if (!header) {
|
|
194
|
+
try {
|
|
195
|
+
const body = (await response.json());
|
|
196
|
+
if (body.x402 || body.accepts)
|
|
197
|
+
header = btoa(JSON.stringify(body));
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
/* ignore */
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!header)
|
|
204
|
+
return null;
|
|
205
|
+
if (chain === 'solana') {
|
|
206
|
+
const wallet = await getOrCreateSolanaWallet();
|
|
207
|
+
const paymentRequired = parsePaymentRequired(header);
|
|
208
|
+
const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
|
|
209
|
+
const secretBytes = await solanaKeyToBytes(wallet.privateKey);
|
|
210
|
+
const feePayer = details.extra?.feePayer || details.recipient;
|
|
211
|
+
const payload = await createSolanaPaymentPayload(secretBytes, wallet.address, details.recipient, details.amount, feePayer, {
|
|
212
|
+
resourceUrl: details.resource?.url || endpoint,
|
|
213
|
+
resourceDescription: details.resource?.description || 'Franklin 0x swap call',
|
|
214
|
+
maxTimeoutSeconds: details.maxTimeoutSeconds || 60,
|
|
215
|
+
extra: details.extra,
|
|
216
|
+
});
|
|
217
|
+
return { 'PAYMENT-SIGNATURE': payload };
|
|
218
|
+
}
|
|
219
|
+
const wallet = await getOrCreateWallet();
|
|
220
|
+
const paymentRequired = parsePaymentRequired(header);
|
|
221
|
+
const details = extractPaymentDetails(paymentRequired);
|
|
222
|
+
const payload = await createPaymentPayload(wallet.privateKey, wallet.address, details.recipient, details.amount, details.network || 'eip155:8453', {
|
|
223
|
+
resourceUrl: details.resource?.url || endpoint,
|
|
224
|
+
resourceDescription: details.resource?.description || 'Franklin 0x swap call',
|
|
225
|
+
maxTimeoutSeconds: details.maxTimeoutSeconds || 60,
|
|
226
|
+
extra: details.extra,
|
|
227
|
+
});
|
|
228
|
+
return { 'PAYMENT-SIGNATURE': payload };
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
console.error(`[franklin] 0x gateway payment error: ${err.message}`);
|
|
232
|
+
return null;
|
|
179
233
|
}
|
|
180
234
|
}
|
|
181
235
|
function buildSwapParams(args) {
|
|
182
|
-
|
|
236
|
+
// Affiliate params (swapFeeRecipient/Bps/Token) are NOT set here —
|
|
237
|
+
// the BlockRun gateway forces them server-side, ensuring every
|
|
238
|
+
// gateway-routed swap pays affiliate to BlockRun treasury regardless
|
|
239
|
+
// of what the agent passes. See blockrun/src/lib/zerox.ts:proxyToZerox.
|
|
240
|
+
return new URLSearchParams({
|
|
183
241
|
chainId: String(base.id),
|
|
184
242
|
sellToken: args.sellTokenAddr,
|
|
185
243
|
buyToken: args.buyTokenAddr,
|
|
186
244
|
sellAmount: args.sellAmountAtomic,
|
|
187
245
|
taker: args.taker,
|
|
188
246
|
});
|
|
189
|
-
if (args.feeRecipient && args.feeBps && args.feeBps > 0 && args.feeToken) {
|
|
190
|
-
p.set('swapFeeRecipient', args.feeRecipient);
|
|
191
|
-
p.set('swapFeeBps', String(args.feeBps));
|
|
192
|
-
p.set('swapFeeToken', args.feeToken);
|
|
193
|
-
}
|
|
194
|
-
return p;
|
|
195
247
|
}
|
|
196
248
|
// ─── Formatting ──────────────────────────────────────────────────────────
|
|
197
249
|
function formatQuoteText(q) {
|
|
@@ -219,7 +271,7 @@ function formatQuoteText(q) {
|
|
|
219
271
|
return lines.join('\n');
|
|
220
272
|
}
|
|
221
273
|
// ─── Quote (read-only) ───────────────────────────────────────────────────
|
|
222
|
-
async function executeBase0xQuote(input) {
|
|
274
|
+
async function executeBase0xQuote(input, ctx) {
|
|
223
275
|
let walletAddress;
|
|
224
276
|
try {
|
|
225
277
|
const wallet = await loadEvmWallet();
|
|
@@ -240,14 +292,9 @@ async function executeBase0xQuote(input) {
|
|
|
240
292
|
buyTokenAddr,
|
|
241
293
|
sellAmountAtomic: sellAmount,
|
|
242
294
|
taker: walletAddress,
|
|
243
|
-
feeRecipient: BLOCKRUN_BASE_AFFILIATE,
|
|
244
|
-
feeBps: BLOCKRUN_AFFILIATE_FEE_BPS,
|
|
245
|
-
// Take the fee in the sell token so it's deterministic and doesn't
|
|
246
|
-
// depend on the output token having an existing recipient ATA / balance.
|
|
247
|
-
feeToken: sellTokenAddr,
|
|
248
295
|
});
|
|
249
296
|
try {
|
|
250
|
-
const price = await
|
|
297
|
+
const price = await gatewayGetWithPayment('price', params, ctx);
|
|
251
298
|
if (!price.liquidityAvailable && price.liquidityAvailable !== undefined) {
|
|
252
299
|
return {
|
|
253
300
|
output: `0x reports no liquidity for ${symbolFor(sellTokenAddr)} → ${symbolFor(buyTokenAddr)} on Base.`,
|
|
@@ -257,7 +304,7 @@ async function executeBase0xQuote(input) {
|
|
|
257
304
|
return { output: formatQuoteText(price) };
|
|
258
305
|
}
|
|
259
306
|
catch (err) {
|
|
260
|
-
return { output: `0x /price failed: ${err instanceof Error ? err.message : String(err)}`, isError: true };
|
|
307
|
+
return { output: `BlockRun gateway 0x /price failed: ${err instanceof Error ? err.message : String(err)}`, isError: true };
|
|
261
308
|
}
|
|
262
309
|
}
|
|
263
310
|
// ─── Swap (full execute) ─────────────────────────────────────────────────
|
|
@@ -290,18 +337,16 @@ async function executeBase0xSwap(input, ctx) {
|
|
|
290
337
|
buyTokenAddr,
|
|
291
338
|
sellAmountAtomic: sellAmount,
|
|
292
339
|
taker: wallet.address,
|
|
293
|
-
feeRecipient: BLOCKRUN_BASE_AFFILIATE,
|
|
294
|
-
feeBps: BLOCKRUN_AFFILIATE_FEE_BPS,
|
|
295
|
-
feeToken: sellTokenAddr,
|
|
296
340
|
});
|
|
297
|
-
// Step 1 — fetch the firm quote
|
|
341
|
+
// Step 1 — fetch the firm quote via BlockRun gateway (x402-paid).
|
|
342
|
+
// Gateway forces affiliate params server-side; user pays $0.001 USDC.
|
|
298
343
|
let quote;
|
|
299
344
|
try {
|
|
300
|
-
quote = await
|
|
345
|
+
quote = await gatewayGetWithPayment('quote', params, ctx);
|
|
301
346
|
}
|
|
302
347
|
catch (err) {
|
|
303
348
|
return {
|
|
304
|
-
output: `0x /quote failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
349
|
+
output: `BlockRun gateway 0x /quote failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
305
350
|
isError: true,
|
|
306
351
|
};
|
|
307
352
|
}
|
|
@@ -453,15 +498,15 @@ export const base0xQuoteCapability = {
|
|
|
453
498
|
properties: COMMON_INPUT_PROPERTIES,
|
|
454
499
|
},
|
|
455
500
|
},
|
|
456
|
-
execute: async (input) => {
|
|
457
|
-
return executeBase0xQuote(input);
|
|
501
|
+
execute: async (input, ctx) => {
|
|
502
|
+
return executeBase0xQuote(input, ctx);
|
|
458
503
|
},
|
|
459
504
|
concurrent: true,
|
|
460
505
|
};
|
|
461
506
|
export const base0xSwapCapability = {
|
|
462
507
|
spec: {
|
|
463
508
|
name: 'Base0xSwap',
|
|
464
|
-
description: "Execute a Base DEX swap via 0x V2 (Permit2). Quotes
|
|
509
|
+
description: "Execute a Base DEX swap via 0x V2 (Permit2). Quotes through BlockRun gateway (x402-paid, server-side 0x key — no user setup needed), asks the user to confirm, signs locally with the Franklin Base wallet, and submits via Base RPC. A 20 bps affiliate fee in the sell-token is collected on-chain by 0x as part of the swap (BlockRun affiliate program — official 0x integrator mechanism). Returns the BaseScan transaction link.",
|
|
465
510
|
input_schema: {
|
|
466
511
|
type: 'object',
|
|
467
512
|
required: ['sell_token', 'buy_token', 'sell_amount'],
|
package/package.json
CHANGED