@blockrun/franklin 3.8.29 → 3.8.30

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.
@@ -274,6 +274,29 @@ a:hover { text-decoration:underline; }
274
274
  .empty { color:var(--text-dim); text-align:center; padding:56px 24px; font-size:13px; }
275
275
 
276
276
  /* ── Wallet page ── */
277
+ .chain-switcher {
278
+ display:inline-flex; padding:3px; gap:2px;
279
+ background:oklch(0 0 0 / 35%); border:1px solid var(--border);
280
+ border-radius:10px; margin-bottom:14px;
281
+ }
282
+ .chain-switcher button {
283
+ font-family:var(--mono); font-size:12px; font-weight:600;
284
+ letter-spacing:0.6px; text-transform:uppercase;
285
+ padding:7px 18px; border-radius:7px;
286
+ background:transparent; border:none; color:var(--text-muted);
287
+ cursor:pointer; transition:all .15s ease;
288
+ }
289
+ .chain-switcher button:hover:not(.active):not(:disabled) {
290
+ color:var(--text); background:oklch(1 0 0 / 5%);
291
+ }
292
+ .chain-switcher button.active {
293
+ background:var(--brand); color:#fff;
294
+ }
295
+ .chain-switcher button:disabled { opacity:0.5; cursor:wait; }
296
+ .chain-switcher-note {
297
+ margin-left:10px; font-size:12px; color:var(--text-dim);
298
+ font-style:italic;
299
+ }
277
300
  .wallet-grid { display:grid; grid-template-columns:1.1fr 1fr; gap:14px; }
278
301
  .wallet-grid .card { display:flex; flex-direction:column; gap:10px; }
279
302
  .wallet-receive { grid-row:span 2; align-items:flex-start; }
@@ -472,8 +495,14 @@ a:hover { text-decoration:underline; }
472
495
  <div class="tab" id="tab-wallet">
473
496
  <div class="content-header">
474
497
  <h2>Wallet</h2>
475
- <p>Receive USDC, back up your key, or import an existing wallet</p>
498
+ <p>Receive USDC, back up your key, or switch chains</p>
499
+ </div>
500
+
501
+ <div class="chain-switcher" role="tablist" aria-label="Payment chain">
502
+ <button type="button" data-chain="base" id="chain-btn-base" role="tab">Base</button>
503
+ <button type="button" data-chain="solana" id="chain-btn-solana" role="tab">Solana</button>
476
504
  </div>
505
+ <span class="chain-switcher-note" id="chain-switcher-note"></span>
477
506
 
478
507
  <div class="wallet-grid">
479
508
  <div class="card wallet-receive">
@@ -841,6 +870,14 @@ async function loadWallet() {
841
870
  document.getElementById('wallet-balance-big').textContent = usdBig(w.balance) + ' USDC';
842
871
  document.getElementById('wallet-chain-pill').textContent = w.chain || '—';
843
872
 
873
+ // Chain switcher — highlight active button
874
+ const baseBtn = document.getElementById('chain-btn-base');
875
+ const solanaBtn = document.getElementById('chain-btn-solana');
876
+ if (baseBtn && solanaBtn) {
877
+ baseBtn.classList.toggle('active', w.chain === 'base');
878
+ solanaBtn.classList.toggle('active', w.chain === 'solana');
879
+ }
880
+
844
881
  // QR via server — never leak address to third parties
845
882
  const qrBox = document.getElementById('wallet-qr');
846
883
  const hint = document.getElementById('wallet-qr-hint');
@@ -848,7 +885,7 @@ async function loadWallet() {
848
885
  const svg = await fetch('/api/wallet/qr?data=' + encodeURIComponent(addr)).then(r => r.ok ? r.text() : null);
849
886
  qrBox.innerHTML = svg || '';
850
887
  hint.textContent = w.chain === 'solana'
851
- ? 'Scan to send USDC (Solana) to this address.'
888
+ ? 'Scan to send USDC (Solana SPL) to this address.'
852
889
  : 'Scan to send USDC on Base to this address.';
853
890
  } else {
854
891
  qrBox.innerHTML = '';
@@ -856,6 +893,48 @@ async function loadWallet() {
856
893
  }
857
894
  }
858
895
 
896
+ // Chain switcher — click "Base" or "Solana" to flip payment chain.
897
+ // Creates a wallet on the target chain if one does not exist yet.
898
+ // Note: a currently-running franklin agent reads its chain at startup,
899
+ // so a mid-session switch only affects the next agent invocation.
900
+ ['chain-btn-base', 'chain-btn-solana'].forEach((id) => {
901
+ const btn = document.getElementById(id);
902
+ if (!btn) return;
903
+ btn.addEventListener('click', async () => {
904
+ const target = btn.getAttribute('data-chain');
905
+ const note = document.getElementById('chain-switcher-note');
906
+ const baseBtn = document.getElementById('chain-btn-base');
907
+ const solanaBtn = document.getElementById('chain-btn-solana');
908
+ // Skip if already active
909
+ if (btn.classList.contains('active')) return;
910
+ baseBtn.disabled = true;
911
+ solanaBtn.disabled = true;
912
+ note.textContent = 'Switching to ' + target + '…';
913
+ try {
914
+ const r = await fetch('/api/chain', {
915
+ method: 'POST',
916
+ headers: { 'Content-Type': 'application/json' },
917
+ body: JSON.stringify({ chain: target }),
918
+ });
919
+ const data = await r.json().catch(() => ({}));
920
+ if (!r.ok || !data.ok) {
921
+ note.textContent = 'Error: ' + (data.error || r.statusText);
922
+ return;
923
+ }
924
+ note.textContent = 'Switched to ' + target + ' · restart Franklin to use this chain';
925
+ await loadWallet();
926
+ // Sidebar balance + address also refresh
927
+ document.getElementById('sidebar-balance').textContent = usdBig(data.balance) + ' USDC';
928
+ document.getElementById('sidebar-addr').textContent = (data.address || '').slice(0, 6) + '…' + (data.address || '').slice(-4);
929
+ } catch (err) {
930
+ note.textContent = 'Error: ' + (err && err.message ? err.message : 'network error');
931
+ } finally {
932
+ baseBtn.disabled = false;
933
+ solanaBtn.disabled = false;
934
+ }
935
+ });
936
+ });
937
+
859
938
  // Copy button
860
939
  document.getElementById('wallet-copy-btn').addEventListener('click', async () => {
861
940
  const addr = document.getElementById('wallet-address-full').textContent;
@@ -6,7 +6,7 @@
6
6
  import http from 'node:http';
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
- import { BLOCKRUN_DIR, loadChain } from '../config.js';
9
+ import { BLOCKRUN_DIR, loadChain, saveChain } from '../config.js';
10
10
  import { getStatsSummary } from '../stats/tracker.js';
11
11
  import { generateInsights } from '../stats/insights.js';
12
12
  import { listSessions, loadSessionHistory } from '../session/storage.js';
@@ -313,6 +313,53 @@ export function createPanelServer(port) {
313
313
  }
314
314
  return;
315
315
  }
316
+ // ─── Chain switch (loopback only) ───────────────────────────────
317
+ // Switches the active payment chain (base ↔ solana) for subsequent
318
+ // Franklin runs. Writes ~/.blockrun/payment-chain, then ensures a
319
+ // wallet exists on the target chain (creates if missing). Returns
320
+ // the new wallet address + balance so the UI can re-render without
321
+ // a follow-up round trip.
322
+ //
323
+ // NOTE: a currently-running `franklin` agent reads the chain once
324
+ // at startup. The Panel switch takes effect immediately for Panel
325
+ // reads and for the *next* agent invocation, but won't flip chain
326
+ // mid-session for an already-running agent. UI copy makes this clear.
327
+ if (p === '/api/chain' && req.method === 'POST') {
328
+ if (!isLoopback(req)) {
329
+ json(res, { error: 'forbidden' }, 403);
330
+ return;
331
+ }
332
+ try {
333
+ const raw = await readBody(req);
334
+ const body = JSON.parse(raw);
335
+ const target = body.chain;
336
+ if (target !== 'base' && target !== 'solana') {
337
+ json(res, { error: 'chain must be "base" or "solana"' }, 400);
338
+ return;
339
+ }
340
+ saveChain(target);
341
+ // Creates-or-loads the wallet on the target chain.
342
+ let address = '';
343
+ let balance = 0;
344
+ if (target === 'solana') {
345
+ const { setupAgentSolanaWallet } = await import('@blockrun/llm');
346
+ const client = await setupAgentSolanaWallet({ silent: true });
347
+ address = await client.getWalletAddress();
348
+ balance = await client.getBalance();
349
+ }
350
+ else {
351
+ const { setupAgentWallet } = await import('@blockrun/llm');
352
+ const client = setupAgentWallet({ silent: true });
353
+ address = client.getWalletAddress();
354
+ balance = await client.getBalance();
355
+ }
356
+ json(res, { ok: true, chain: target, address, balance });
357
+ }
358
+ catch (err) {
359
+ json(res, { error: err.message }, 500);
360
+ }
361
+ return;
362
+ }
316
363
  if (p === '/api/markets') {
317
364
  // Snapshot of every active data provider for the Markets panel:
318
365
  // pipeline wiring (which endpoint serves which asset class), live
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.8.29",
3
+ "version": "3.8.30",
4
4
  "description": "Franklin — 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": {