balchemy 0.1.22 → 0.1.24
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/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/tui/AgentBridge.d.ts +12 -0
- package/dist/tui/AgentBridge.d.ts.map +1 -1
- package/dist/tui/AgentBridge.js +435 -45
- package/dist/tui/AgentBridge.js.map +1 -1
- package/dist/tui/ChatAgent.d.ts +9 -0
- package/dist/tui/ChatAgent.d.ts.map +1 -1
- package/dist/tui/ChatAgent.js +98 -8
- package/dist/tui/ChatAgent.js.map +1 -1
- package/dist/tui/setup-guidance.d.ts +1 -0
- package/dist/tui/setup-guidance.d.ts.map +1 -1
- package/dist/tui/setup-guidance.js +21 -5
- package/dist/tui/setup-guidance.js.map +1 -1
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +20 -3
- package/dist/wizard.js.map +1 -1
- package/package.json +7 -6
- package/templates/agent.config.yaml +71 -0
- package/LICENSE +0 -21
package/dist/tui/AgentBridge.js
CHANGED
|
@@ -4,7 +4,7 @@ import { AgentLoop, connectMcp } from "@balchemyai/agent-sdk";
|
|
|
4
4
|
import { ChatAgent } from "./ChatAgent.js";
|
|
5
5
|
import { buildSetupRequiredMessage, getInitialSetupStep, isSetupReady, parseNetworkSelection, parseSetupStatusSnapshot, } from "./setup-guidance.js";
|
|
6
6
|
import { buildStrategyUpdateArgs } from "./session-sync.js";
|
|
7
|
-
import { loadAgent } from "../agent-store.js";
|
|
7
|
+
import { loadAgent, saveAgent } from "../agent-store.js";
|
|
8
8
|
/** Truncate verbose API errors (429 JSON blobs, stack traces) to a readable one-liner. */
|
|
9
9
|
function truncateError(raw) {
|
|
10
10
|
// Extract HTTP status code if present
|
|
@@ -53,6 +53,23 @@ function parseJsonObject(text) {
|
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
function parseJsonObjectLoose(text) {
|
|
57
|
+
const direct = parseJsonObject(text);
|
|
58
|
+
if (direct)
|
|
59
|
+
return direct;
|
|
60
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
61
|
+
if (fenced?.[1]) {
|
|
62
|
+
const parsed = parseJsonObject(fenced[1].trim());
|
|
63
|
+
if (parsed)
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
const first = text.indexOf("{");
|
|
67
|
+
const last = text.lastIndexOf("}");
|
|
68
|
+
if (first >= 0 && last > first) {
|
|
69
|
+
return parseJsonObject(text.slice(first, last + 1));
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
56
73
|
function asRecord(value) {
|
|
57
74
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
58
75
|
? value
|
|
@@ -108,6 +125,25 @@ function mergeWallets(...groups) {
|
|
|
108
125
|
}
|
|
109
126
|
return wallets;
|
|
110
127
|
}
|
|
128
|
+
function mergeLatestWallets(...groups) {
|
|
129
|
+
const latest = new Map();
|
|
130
|
+
for (const group of groups) {
|
|
131
|
+
for (const wallet of group) {
|
|
132
|
+
latest.set(wallet.chain, wallet);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const ordered = [];
|
|
136
|
+
const solana = latest.get("solana");
|
|
137
|
+
const base = latest.get("base");
|
|
138
|
+
if (solana)
|
|
139
|
+
ordered.push(solana);
|
|
140
|
+
if (base)
|
|
141
|
+
ordered.push(base);
|
|
142
|
+
return ordered;
|
|
143
|
+
}
|
|
144
|
+
function walletAddressLabel(chain) {
|
|
145
|
+
return chain === "solana" ? "Solana trading wallet" : "Base trading wallet";
|
|
146
|
+
}
|
|
111
147
|
function readRecords(value) {
|
|
112
148
|
if (!Array.isArray(value))
|
|
113
149
|
return [];
|
|
@@ -278,6 +314,67 @@ function parseTradeLimits(value) {
|
|
|
278
314
|
...(maxTradeUsd !== undefined && Number.isFinite(maxTradeUsd) ? { maxTradeUsd } : {}),
|
|
279
315
|
};
|
|
280
316
|
}
|
|
317
|
+
function readStringArray(value) {
|
|
318
|
+
if (!Array.isArray(value))
|
|
319
|
+
return [];
|
|
320
|
+
return value.flatMap((item) => typeof item === "string" && item.trim() ? [item.trim()] : []);
|
|
321
|
+
}
|
|
322
|
+
function parseStrategyReviewResponse(text) {
|
|
323
|
+
const parsed = parseJsonObjectLoose(text);
|
|
324
|
+
if (!parsed)
|
|
325
|
+
return null;
|
|
326
|
+
const ready = parsed.ready === true;
|
|
327
|
+
const summary = firstString(parsed.summary, parsed.strategy, parsed.normalizedStrategy) ?? "";
|
|
328
|
+
const followUp = firstString(parsed.followUp, parsed.follow_up, parsed.question) ?? "";
|
|
329
|
+
const missing = readStringArray(parsed.missing);
|
|
330
|
+
const limits = parseTradeLimits(summary);
|
|
331
|
+
const maxTradeSol = firstNumber(parsed, ["maxTradeSol", "max_trade_sol"]) ?? limits.maxTradeSol;
|
|
332
|
+
const maxTradeUsd = firstNumber(parsed, ["maxTradeUsd", "max_trade_usd"]) ?? limits.maxTradeUsd;
|
|
333
|
+
return {
|
|
334
|
+
ready,
|
|
335
|
+
summary,
|
|
336
|
+
followUp,
|
|
337
|
+
missing,
|
|
338
|
+
...(maxTradeSol !== undefined ? { maxTradeSol } : {}),
|
|
339
|
+
...(maxTradeUsd !== undefined ? { maxTradeUsd } : {}),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function fallbackStrategyReview(chain, notes) {
|
|
343
|
+
const summary = notes.join("\n").trim();
|
|
344
|
+
const limits = parseTradeLimits(summary);
|
|
345
|
+
const lower = summary.toLowerCase();
|
|
346
|
+
const missing = [];
|
|
347
|
+
if (chain === "solana" && limits.maxTradeSol === undefined) {
|
|
348
|
+
missing.push("max SOL per trade");
|
|
349
|
+
}
|
|
350
|
+
if (chain === "base" && limits.maxTradeUsd === undefined) {
|
|
351
|
+
missing.push("max USD/USDC per trade");
|
|
352
|
+
}
|
|
353
|
+
if (!/(entry|filter|hacim|volume|mcap|market cap|holder|liquidity|giris|giriş)/i.test(summary)) {
|
|
354
|
+
missing.push("entry filters");
|
|
355
|
+
}
|
|
356
|
+
if (!/(stop|loss|zarar|sl\b)/i.test(lower)) {
|
|
357
|
+
missing.push("stop loss");
|
|
358
|
+
}
|
|
359
|
+
if (!/(take profit|profit|kar|sat|sell|tp\b|x\b)/i.test(lower)) {
|
|
360
|
+
missing.push("take profit");
|
|
361
|
+
}
|
|
362
|
+
if (!/(max.*position|position|pozisyon|concurrent|ayn[ıi] anda)/i.test(lower)) {
|
|
363
|
+
missing.push("max open positions");
|
|
364
|
+
}
|
|
365
|
+
if (!/(avoid|kaçın|kacin|yasak|alma|blacklist|tokenleri|categories|kategori)/i.test(lower)) {
|
|
366
|
+
missing.push("avoid rules");
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
ready: summary.length >= 20 && missing.length === 0,
|
|
370
|
+
summary,
|
|
371
|
+
missing,
|
|
372
|
+
followUp: missing.length > 0
|
|
373
|
+
? `I still need this before live setup: ${missing.join(", ")}. Send only the missing parts; I will merge them.`
|
|
374
|
+
: "",
|
|
375
|
+
...limits,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
281
378
|
function formatChains(chains) {
|
|
282
379
|
return chains.map((chain) => chain === "solana" ? "Solana" : "Base").join(" + ");
|
|
283
380
|
}
|
|
@@ -319,9 +416,11 @@ export class AgentBridge {
|
|
|
319
416
|
pendingLoopConfig = null;
|
|
320
417
|
setupPollTimer = null;
|
|
321
418
|
setupFlow = null;
|
|
419
|
+
knownWallets = [];
|
|
322
420
|
constructor(config, setters) {
|
|
323
421
|
this.config = config;
|
|
324
422
|
this.setters = setters;
|
|
423
|
+
this.knownWallets = this.loadStoredWallets();
|
|
325
424
|
// Replay-protected fetch for MCP calls
|
|
326
425
|
this.replayFetch = async (url, init) => {
|
|
327
426
|
const headers = new Headers(init?.headers);
|
|
@@ -435,6 +534,7 @@ export class AgentBridge {
|
|
|
435
534
|
provider: resolveProviderLabel(this.config.llmProvider, this.config.llmBaseUrl),
|
|
436
535
|
model: this.config.llmModel,
|
|
437
536
|
}));
|
|
537
|
+
this.syncKnownWalletsToStatus();
|
|
438
538
|
// Input is ready now. Setup must be deterministic; do not let an LLM drive it.
|
|
439
539
|
if (setupComplete) {
|
|
440
540
|
void this.greet(true);
|
|
@@ -452,7 +552,7 @@ export class AgentBridge {
|
|
|
452
552
|
}
|
|
453
553
|
try {
|
|
454
554
|
const prompt = "Check my portfolio and status, then greet me. Tell me my balance, wallets, and current strategy. Keep it brief and do not narrate tool calls.";
|
|
455
|
-
const reply = await this.chatAgent.chat(prompt, (name, result) => {
|
|
555
|
+
const reply = await this.chatAgent.chat(this.withRuntimeContext(prompt), (name, result) => {
|
|
456
556
|
this.applyToolResult(name, result);
|
|
457
557
|
if (name !== "setup_agent") {
|
|
458
558
|
this.addSystemMessage(`Tool: ${name}`);
|
|
@@ -498,7 +598,7 @@ export class AgentBridge {
|
|
|
498
598
|
if (!this.chatAgent)
|
|
499
599
|
return;
|
|
500
600
|
try {
|
|
501
|
-
const reply = await this.chatAgent.chat(text, (name, result) => {
|
|
601
|
+
const reply = await this.chatAgent.chat(this.withRuntimeContext(text), (name, result) => {
|
|
502
602
|
this.applyToolResult(name, result);
|
|
503
603
|
this.addSystemMessage(`Tool: ${name}`);
|
|
504
604
|
}, (preview) => this.setters.confirmTrade(preview));
|
|
@@ -542,6 +642,8 @@ export class AgentBridge {
|
|
|
542
642
|
const step = getInitialSetupStep(snapshot);
|
|
543
643
|
this.setupFlow = {
|
|
544
644
|
step,
|
|
645
|
+
rootWalletBound: snapshot.developerWalletBound === true,
|
|
646
|
+
...(snapshot.rootWalletKind ? { rootWalletKind: snapshot.rootWalletKind } : {}),
|
|
545
647
|
...(snapshot.selectedChains && snapshot.selectedChains.length > 0 ? { selectedChains: snapshot.selectedChains } : {}),
|
|
546
648
|
};
|
|
547
649
|
this.addAgentMessage(`${buildSetupRequiredMessage(snapshot)}\n\n${this.setupPromptFor(this.setupFlow.step)}`);
|
|
@@ -555,19 +657,85 @@ export class AgentBridge {
|
|
|
555
657
|
case "networks":
|
|
556
658
|
return "Which networks should this agent trade on: Solana, Base, or both?";
|
|
557
659
|
case "solana-recovery-wallet":
|
|
558
|
-
return "Paste your Solana recovery
|
|
660
|
+
return "Paste your Solana developer/recovery wallet address. For Solana-only setup this becomes the root master-key wallet; for Base+Solana it is the Solana withdrawal/recovery wallet. It is separate from the Solana trading wallet I create for execution.";
|
|
559
661
|
case "solana-recovery-wallet-confirm":
|
|
560
|
-
return "Paste the same Solana
|
|
662
|
+
return "Paste the same Solana wallet again to confirm it.";
|
|
561
663
|
case "slippage":
|
|
562
664
|
return "Set slippage in percent or bps. Examples: 1% = 100 bps, 3% = 300 bps, 5% = 500 bps.";
|
|
563
665
|
case "strategy":
|
|
564
|
-
return "
|
|
666
|
+
return "Describe the strategy in natural language. I will review it with AI, ask for missing risk details, and only then ask you to confirm.";
|
|
565
667
|
case "strategy-confirm":
|
|
566
668
|
return "Confirm with 'yes'/'evet', or say 'no'/'hayir' to rewrite the strategy.";
|
|
567
669
|
case "subscriptions":
|
|
568
670
|
return "Create monitoring subscriptions now? For Solana new launches I can enable launch monitoring. Answer yes/evet or no/hayir.";
|
|
569
671
|
}
|
|
570
672
|
}
|
|
673
|
+
setupPromptForStrategyChain(chain) {
|
|
674
|
+
const maxTrade = chain === "solana"
|
|
675
|
+
? "max SOL per trade"
|
|
676
|
+
: "max USD/USDC per trade";
|
|
677
|
+
return [
|
|
678
|
+
`${chainTitle(chain)} strategy:`,
|
|
679
|
+
"Describe what you want the agent to trade and how strict it must be.",
|
|
680
|
+
`Include ${maxTrade}, entry filters, stop loss, take profit, max open positions, and avoid rules.`,
|
|
681
|
+
"You can write it messy; I will ask follow-ups if anything critical is missing.",
|
|
682
|
+
].join("\n");
|
|
683
|
+
}
|
|
684
|
+
async createTradingWalletsForSetup(chains) {
|
|
685
|
+
const walletLines = [];
|
|
686
|
+
for (const chain of chains) {
|
|
687
|
+
const structured = await this.callSetupAgent({ action: "create_wallet", chain });
|
|
688
|
+
const wallet = readWallet({ ...structured, chain });
|
|
689
|
+
if (wallet)
|
|
690
|
+
this.upsertWallet(wallet);
|
|
691
|
+
const address = wallet?.address ?? firstString(structured.address, asRecord(structured.wallet)?.address);
|
|
692
|
+
walletLines.push(`${chainTitle(chain)} trading wallet: ${address ?? "(created)"}`);
|
|
693
|
+
}
|
|
694
|
+
return walletLines;
|
|
695
|
+
}
|
|
696
|
+
selectedChainsForSetup(flow) {
|
|
697
|
+
if (flow.selectedChains && flow.selectedChains.length > 0) {
|
|
698
|
+
return flow.selectedChains;
|
|
699
|
+
}
|
|
700
|
+
return [...new Set(this.knownWallets.map((wallet) => wallet.chain))];
|
|
701
|
+
}
|
|
702
|
+
async reviewStrategyWithAi(chain, selectedChains, notes) {
|
|
703
|
+
const fallback = fallbackStrategyReview(chain, notes);
|
|
704
|
+
if (!this.chatAgent)
|
|
705
|
+
return fallback;
|
|
706
|
+
const unit = chain === "solana" ? "SOL" : "USD/USDC";
|
|
707
|
+
const systemPrompt = [
|
|
708
|
+
"You are the strategy-coaching layer inside Balchemy CLI setup.",
|
|
709
|
+
"You do not call tools. You only decide whether the user's strategy is safe enough to configure.",
|
|
710
|
+
"Return strict JSON only. No markdown.",
|
|
711
|
+
"Required JSON shape:",
|
|
712
|
+
'{"ready":boolean,"summary":"string","missing":["string"],"followUp":"string","maxTradeSol":number|null,"maxTradeUsd":number|null}',
|
|
713
|
+
`Current chain: ${chainTitle(chain)}. Required max trade unit: ${unit}.`,
|
|
714
|
+
`Selected chains: ${formatChains(selectedChains)}.`,
|
|
715
|
+
"A ready strategy must include max trade size, entry filters, stop loss, take profit, max open positions, and avoid/blacklist rules.",
|
|
716
|
+
"If anything critical is missing, ready=false and followUp must ask only for the missing items in the same language as the user.",
|
|
717
|
+
"If ready=true, summary must be a faithful normalized version of all user notes with chain-specific limits preserved.",
|
|
718
|
+
].join("\n");
|
|
719
|
+
const userMessage = [
|
|
720
|
+
`Chain: ${chainTitle(chain)}`,
|
|
721
|
+
"User notes:",
|
|
722
|
+
notes.map((note, index) => `${index + 1}. ${note}`).join("\n"),
|
|
723
|
+
].join("\n\n");
|
|
724
|
+
try {
|
|
725
|
+
const response = await this.chatAgent.completeText(systemPrompt, userMessage);
|
|
726
|
+
const parsed = parseStrategyReviewResponse(response);
|
|
727
|
+
if (!parsed)
|
|
728
|
+
return fallback;
|
|
729
|
+
if (!parsed.ready && !parsed.followUp)
|
|
730
|
+
return fallback;
|
|
731
|
+
if (parsed.ready && parsed.summary.trim().length < 20)
|
|
732
|
+
return fallback;
|
|
733
|
+
return parsed;
|
|
734
|
+
}
|
|
735
|
+
catch (_error) {
|
|
736
|
+
return fallback;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
571
739
|
async handleSetupInput(text) {
|
|
572
740
|
const flow = this.setupFlow;
|
|
573
741
|
if (!flow)
|
|
@@ -586,7 +754,11 @@ export class AgentBridge {
|
|
|
586
754
|
if (flow.step === "developer-wallet-confirm") {
|
|
587
755
|
const wallet = text.trim();
|
|
588
756
|
if (!flow.developerWallet || wallet.toLowerCase() !== flow.developerWallet.toLowerCase()) {
|
|
589
|
-
this.setupFlow = {
|
|
757
|
+
this.setupFlow = {
|
|
758
|
+
...flow,
|
|
759
|
+
step: "developer-wallet",
|
|
760
|
+
developerWallet: undefined,
|
|
761
|
+
};
|
|
590
762
|
this.addAgentMessage("The confirmation did not match. Start again by pasting the Base/EVM 0x developer wallet.");
|
|
591
763
|
return;
|
|
592
764
|
}
|
|
@@ -596,10 +768,50 @@ export class AgentBridge {
|
|
|
596
768
|
walletAddressConfirm: wallet,
|
|
597
769
|
});
|
|
598
770
|
const masterKey = extractMasterKey(structured);
|
|
599
|
-
|
|
771
|
+
if (masterKey) {
|
|
772
|
+
this.persistMasterKey(masterKey);
|
|
773
|
+
}
|
|
774
|
+
if (flow.selectedChains && flow.selectedChains.length > 0) {
|
|
775
|
+
this.setupFlow = {
|
|
776
|
+
...flow,
|
|
777
|
+
step: "networks",
|
|
778
|
+
rootWalletBound: true,
|
|
779
|
+
rootWalletKind: "evm",
|
|
780
|
+
};
|
|
781
|
+
const walletLines = await this.createTradingWalletsForSetup(flow.selectedChains);
|
|
782
|
+
const needsSolanaWallet = flow.selectedChains.includes("solana") && !flow.solanaRecoveryWallet;
|
|
783
|
+
this.setupFlow = {
|
|
784
|
+
...flow,
|
|
785
|
+
step: needsSolanaWallet ? "solana-recovery-wallet" : "slippage",
|
|
786
|
+
rootWalletBound: true,
|
|
787
|
+
rootWalletKind: "evm",
|
|
788
|
+
};
|
|
789
|
+
this.addAgentMessage(`${masterKey ? `Developer wallet bound.
|
|
790
|
+
|
|
791
|
+
Master key saved encrypted locally and shown here so you can copy it externally:
|
|
792
|
+
${masterKey}
|
|
793
|
+
|
|
794
|
+
It is shown once in real environments.` : "Developer wallet bound."}
|
|
795
|
+
|
|
796
|
+
Copyable trading wallet addresses:
|
|
797
|
+
${walletLines.join("\n")}
|
|
798
|
+
|
|
799
|
+
${this.setupPromptFor(needsSolanaWallet ? "solana-recovery-wallet" : "slippage")}`);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
this.setupFlow = { step: "networks", rootWalletBound: true, rootWalletKind: "evm" };
|
|
600
803
|
this.addAgentMessage(masterKey
|
|
601
|
-
? `Developer wallet bound
|
|
602
|
-
|
|
804
|
+
? `Developer wallet bound.
|
|
805
|
+
|
|
806
|
+
Master key saved encrypted locally and shown here so you can copy it externally:
|
|
807
|
+
${masterKey}
|
|
808
|
+
|
|
809
|
+
It is shown once in real environments.
|
|
810
|
+
|
|
811
|
+
${this.setupPromptFor("networks")}`
|
|
812
|
+
: `Developer wallet bound.
|
|
813
|
+
|
|
814
|
+
${this.setupPromptFor("networks")}`);
|
|
603
815
|
return;
|
|
604
816
|
}
|
|
605
817
|
if (flow.step === "networks") {
|
|
@@ -608,17 +820,37 @@ export class AgentBridge {
|
|
|
608
820
|
this.addAgentMessage("Choose Solana, Base, or both.");
|
|
609
821
|
return;
|
|
610
822
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
823
|
+
if (!flow.rootWalletBound) {
|
|
824
|
+
if (chains.includes("base")) {
|
|
825
|
+
this.setupFlow = { ...flow, step: "developer-wallet", selectedChains: chains };
|
|
826
|
+
this.addAgentMessage(`Trading networks selected: ${formatChains(chains)}.
|
|
827
|
+
|
|
828
|
+
${this.setupPromptFor("developer-wallet")}`);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
this.setupFlow = { ...flow, step: "solana-recovery-wallet", selectedChains: chains };
|
|
832
|
+
this.addAgentMessage(`Trading networks selected: ${formatChains(chains)}.
|
|
833
|
+
|
|
834
|
+
${this.setupPromptFor("solana-recovery-wallet")}`);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (flow.rootWalletKind === "solana" && chains.includes("base")) {
|
|
838
|
+
this.addAgentMessage("This agent is bound with a Solana-only root wallet. Base trading requires a Base/EVM developer wallet, so choose Solana for this setup or start a new Base/both setup.");
|
|
839
|
+
return;
|
|
616
840
|
}
|
|
841
|
+
const walletLines = await this.createTradingWalletsForSetup(chains);
|
|
842
|
+
const needsSolanaWallet = chains.includes("solana") && !flow.solanaRecoveryWallet;
|
|
617
843
|
this.setupFlow = {
|
|
618
|
-
|
|
844
|
+
...flow,
|
|
845
|
+
step: needsSolanaWallet ? "solana-recovery-wallet" : "slippage",
|
|
619
846
|
selectedChains: chains,
|
|
620
847
|
};
|
|
621
|
-
this.addAgentMessage(`Trading networks configured: ${formatChains(chains)}
|
|
848
|
+
this.addAgentMessage(`Trading networks configured: ${formatChains(chains)}.
|
|
849
|
+
|
|
850
|
+
Copyable trading wallet addresses:
|
|
851
|
+
${walletLines.join("\n")}
|
|
852
|
+
|
|
853
|
+
${this.setupPromptFor(needsSolanaWallet ? "solana-recovery-wallet" : "slippage")}`);
|
|
622
854
|
return;
|
|
623
855
|
}
|
|
624
856
|
if (flow.step === "solana-recovery-wallet") {
|
|
@@ -637,11 +869,55 @@ export class AgentBridge {
|
|
|
637
869
|
this.setupFlow = {
|
|
638
870
|
step: "solana-recovery-wallet",
|
|
639
871
|
...(flow.developerWallet ? { developerWallet: flow.developerWallet } : {}),
|
|
872
|
+
...(flow.rootWalletBound !== undefined ? { rootWalletBound: flow.rootWalletBound } : {}),
|
|
873
|
+
...(flow.rootWalletKind ? { rootWalletKind: flow.rootWalletKind } : {}),
|
|
640
874
|
...(flow.selectedChains ? { selectedChains: flow.selectedChains } : {}),
|
|
641
875
|
};
|
|
642
876
|
this.addAgentMessage("The Solana wallet confirmation did not match. Paste the Solana recovery/withdrawal wallet again.");
|
|
643
877
|
return;
|
|
644
878
|
}
|
|
879
|
+
if (!flow.rootWalletBound) {
|
|
880
|
+
const structured = await this.callSetupAgent({
|
|
881
|
+
action: "bind_solana_root_wallet",
|
|
882
|
+
solanaWalletAddress: flow.solanaRecoveryWallet,
|
|
883
|
+
solanaWalletAddressConfirm: wallet,
|
|
884
|
+
});
|
|
885
|
+
const masterKey = extractMasterKey(structured);
|
|
886
|
+
if (masterKey) {
|
|
887
|
+
this.persistMasterKey(masterKey);
|
|
888
|
+
}
|
|
889
|
+
const selectedChains = flow.selectedChains && flow.selectedChains.length > 0 ? flow.selectedChains : ["solana"];
|
|
890
|
+
this.setupFlow = {
|
|
891
|
+
...flow,
|
|
892
|
+
step: "networks",
|
|
893
|
+
solanaRecoveryWallet: wallet,
|
|
894
|
+
rootWalletBound: true,
|
|
895
|
+
rootWalletKind: "solana",
|
|
896
|
+
selectedChains,
|
|
897
|
+
};
|
|
898
|
+
const walletLines = await this.createTradingWalletsForSetup(selectedChains);
|
|
899
|
+
this.setupFlow = {
|
|
900
|
+
...flow,
|
|
901
|
+
step: "slippage",
|
|
902
|
+
solanaRecoveryWallet: wallet,
|
|
903
|
+
rootWalletBound: true,
|
|
904
|
+
rootWalletKind: "solana",
|
|
905
|
+
selectedChains,
|
|
906
|
+
};
|
|
907
|
+
this.addAgentMessage(`Solana developer/recovery wallet confirmed:
|
|
908
|
+
${wallet}
|
|
909
|
+
|
|
910
|
+
${masterKey ? `Master key saved encrypted locally and shown here so you can copy it externally:
|
|
911
|
+
${masterKey}
|
|
912
|
+
|
|
913
|
+
It is shown once in real environments.
|
|
914
|
+
|
|
915
|
+
` : ""}Copyable trading wallet addresses:
|
|
916
|
+
${walletLines.join("\n")}
|
|
917
|
+
|
|
918
|
+
${this.setupPromptFor("slippage")}`);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
645
921
|
try {
|
|
646
922
|
await this.callSetupAgent({
|
|
647
923
|
action: "bind_solana_developer_wallet",
|
|
@@ -653,7 +929,10 @@ export class AgentBridge {
|
|
|
653
929
|
this.addSystemMessage(`Solana recovery wallet stored locally for this setup. MCP did not bind it directly: ${err instanceof Error ? err.message : String(err)}`);
|
|
654
930
|
}
|
|
655
931
|
this.setupFlow = { ...flow, step: "slippage", solanaRecoveryWallet: wallet };
|
|
656
|
-
this.addAgentMessage(`Solana recovery/withdrawal wallet confirmed
|
|
932
|
+
this.addAgentMessage(`Solana recovery/withdrawal wallet confirmed:
|
|
933
|
+
${wallet}
|
|
934
|
+
|
|
935
|
+
${this.setupPromptFor("slippage")}`);
|
|
657
936
|
return;
|
|
658
937
|
}
|
|
659
938
|
if (flow.step === "slippage") {
|
|
@@ -663,33 +942,83 @@ export class AgentBridge {
|
|
|
663
942
|
return;
|
|
664
943
|
}
|
|
665
944
|
await this.callSetupAgent({ action: "configure_slippage", slippageBps: bps });
|
|
666
|
-
const selectedChains = flow
|
|
945
|
+
const selectedChains = this.selectedChainsForSetup(flow);
|
|
946
|
+
if (selectedChains.length === 0) {
|
|
947
|
+
this.setupFlow = { ...flow, step: "networks" };
|
|
948
|
+
this.addAgentMessage(`I need the selected trading networks before strategy setup.\n\n${this.setupPromptFor("networks")}`);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
667
951
|
const currentStrategyChain = nextStrategyChain(selectedChains, {}) ?? selectedChains[0];
|
|
668
952
|
this.setupFlow = { ...flow, step: "strategy", slippageBps: bps, currentStrategyChain, chainStrategies: {} };
|
|
669
|
-
this.addAgentMessage(`Slippage set to ${bps} bps.\n\n${
|
|
953
|
+
this.addAgentMessage(`Slippage set to ${bps} bps.\n\n${this.setupPromptForStrategyChain(currentStrategyChain)}`);
|
|
670
954
|
return;
|
|
671
955
|
}
|
|
672
956
|
if (flow.step === "strategy") {
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
957
|
+
const strategyNote = text.trim();
|
|
958
|
+
const selectedChainsForFlow = this.selectedChainsForSetup(flow);
|
|
959
|
+
const chain = flow.currentStrategyChain ?? selectedChainsForFlow[0];
|
|
960
|
+
if (!chain) {
|
|
961
|
+
this.setupFlow = { ...flow, step: "networks" };
|
|
962
|
+
this.addAgentMessage(`I need the selected trading networks before strategy setup.\n\n${this.setupPromptFor("networks")}`);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (strategyNote.length < 8) {
|
|
966
|
+
this.addAgentMessage("That is too short for live setup. Add trade size, entry filters, exits, max positions, or avoid rules.");
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const previousNotes = flow.strategyNotes?.[chain] ?? [];
|
|
970
|
+
const notes = [...previousNotes, strategyNote];
|
|
971
|
+
const selectedChains = selectedChainsForFlow.length > 0 ? selectedChainsForFlow : [chain];
|
|
972
|
+
const strategyNotes = {
|
|
973
|
+
...(flow.strategyNotes ?? {}),
|
|
974
|
+
[chain]: notes,
|
|
975
|
+
};
|
|
976
|
+
this.setupFlow = { ...flow, strategyNotes };
|
|
977
|
+
const review = await this.reviewStrategyWithAi(chain, selectedChains, notes);
|
|
978
|
+
if (!review.ready) {
|
|
979
|
+
const missingLine = review.missing.length > 0 ? `\n\nMissing: ${review.missing.join(", ")}` : "";
|
|
980
|
+
this.addAgentMessage(`${review.followUp || "I need a little more detail before this can go live."}${missingLine}`);
|
|
676
981
|
return;
|
|
677
982
|
}
|
|
983
|
+
const strategyText = review.summary.trim() || notes.join("\n");
|
|
984
|
+
const parsedLimits = parseTradeLimits(strategyText);
|
|
985
|
+
const maxTradeSol = chain === "solana"
|
|
986
|
+
? review.maxTradeSol ?? parsedLimits.maxTradeSol ?? flow.maxTradeSol
|
|
987
|
+
: flow.maxTradeSol;
|
|
988
|
+
const maxTradeUsd = chain === "base"
|
|
989
|
+
? review.maxTradeUsd ?? parsedLimits.maxTradeUsd ?? flow.maxTradeUsd
|
|
990
|
+
: flow.maxTradeUsd;
|
|
678
991
|
const chainStrategies = {
|
|
679
992
|
...(flow.chainStrategies ?? {}),
|
|
680
|
-
|
|
993
|
+
[chain]: strategyText,
|
|
681
994
|
};
|
|
682
|
-
const selectedChains = flow.selectedChains ?? (flow.currentStrategyChain ? [flow.currentStrategyChain] : ["solana"]);
|
|
683
995
|
const nextChain = nextStrategyChain(selectedChains, chainStrategies);
|
|
684
996
|
if (nextChain) {
|
|
685
|
-
this.setupFlow = {
|
|
686
|
-
|
|
997
|
+
this.setupFlow = {
|
|
998
|
+
...flow,
|
|
999
|
+
chainStrategies,
|
|
1000
|
+
currentStrategyChain: nextChain,
|
|
1001
|
+
strategyNotes,
|
|
1002
|
+
...(maxTradeSol !== undefined ? { maxTradeSol } : {}),
|
|
1003
|
+
...(maxTradeUsd !== undefined ? { maxTradeUsd } : {}),
|
|
1004
|
+
};
|
|
1005
|
+
this.addAgentMessage(`${chainTitle(chain)} strategy drafted with AI review.\n\n${this.setupPromptForStrategyChain(nextChain)}`);
|
|
687
1006
|
return;
|
|
688
1007
|
}
|
|
689
1008
|
const combinedStrategy = buildCombinedStrategy({ ...flow, chainStrategies });
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
1009
|
+
const combinedLimits = parseTradeLimits(combinedStrategy);
|
|
1010
|
+
const finalMaxTradeSol = maxTradeSol ?? combinedLimits.maxTradeSol;
|
|
1011
|
+
const finalMaxTradeUsd = maxTradeUsd ?? combinedLimits.maxTradeUsd;
|
|
1012
|
+
this.setupFlow = {
|
|
1013
|
+
...flow,
|
|
1014
|
+
chainStrategies,
|
|
1015
|
+
strategyNotes,
|
|
1016
|
+
step: "strategy-confirm",
|
|
1017
|
+
strategyText: combinedStrategy,
|
|
1018
|
+
...(finalMaxTradeSol !== undefined ? { maxTradeSol: finalMaxTradeSol } : {}),
|
|
1019
|
+
...(finalMaxTradeUsd !== undefined ? { maxTradeUsd: finalMaxTradeUsd } : {}),
|
|
1020
|
+
};
|
|
1021
|
+
this.addAgentMessage(`AI-reviewed strategy draft:\n\n${combinedStrategy}\n\nMax SOL/trade: ${finalMaxTradeSol ?? "not specified"}\nMax USD/trade: ${finalMaxTradeUsd ?? "not specified"}\n\n${this.setupPromptFor("strategy-confirm")}`);
|
|
693
1022
|
return;
|
|
694
1023
|
}
|
|
695
1024
|
if (flow.step === "strategy-confirm") {
|
|
@@ -698,12 +1027,14 @@ export class AgentBridge {
|
|
|
698
1027
|
step: "strategy",
|
|
699
1028
|
...(flow.developerWallet ? { developerWallet: flow.developerWallet } : {}),
|
|
700
1029
|
...(flow.solanaRecoveryWallet ? { solanaRecoveryWallet: flow.solanaRecoveryWallet } : {}),
|
|
1030
|
+
...(flow.rootWalletBound !== undefined ? { rootWalletBound: flow.rootWalletBound } : {}),
|
|
1031
|
+
...(flow.rootWalletKind ? { rootWalletKind: flow.rootWalletKind } : {}),
|
|
701
1032
|
...(flow.selectedChains ? { selectedChains: flow.selectedChains } : {}),
|
|
702
1033
|
...(flow.slippageBps !== undefined ? { slippageBps: flow.slippageBps } : {}),
|
|
703
1034
|
chainStrategies: {},
|
|
704
|
-
currentStrategyChain: (flow
|
|
1035
|
+
currentStrategyChain: this.selectedChainsForSetup(flow)[0] ?? "solana",
|
|
705
1036
|
};
|
|
706
|
-
this.addAgentMessage(this.
|
|
1037
|
+
this.addAgentMessage(this.setupPromptForStrategyChain(this.selectedChainsForSetup(flow)[0] ?? "solana"));
|
|
707
1038
|
return;
|
|
708
1039
|
}
|
|
709
1040
|
if (!isAffirmative(text)) {
|
|
@@ -712,7 +1043,7 @@ export class AgentBridge {
|
|
|
712
1043
|
}
|
|
713
1044
|
if (!flow.strategyText) {
|
|
714
1045
|
this.setupFlow = { ...flow, step: "strategy" };
|
|
715
|
-
this.addAgentMessage(this.
|
|
1046
|
+
this.addAgentMessage(this.setupPromptForStrategyChain(flow.currentStrategyChain ?? this.selectedChainsForSetup(flow)[0] ?? "solana"));
|
|
716
1047
|
return;
|
|
717
1048
|
}
|
|
718
1049
|
await this.callSetupAgent({
|
|
@@ -729,7 +1060,7 @@ export class AgentBridge {
|
|
|
729
1060
|
}
|
|
730
1061
|
if (flow.step === "subscriptions") {
|
|
731
1062
|
if (isAffirmative(text)) {
|
|
732
|
-
const chains = flow
|
|
1063
|
+
const chains = this.selectedChainsForSetup(flow);
|
|
733
1064
|
if (chains.includes("solana")) {
|
|
734
1065
|
await this.mcp.callTool("create_subscription", {
|
|
735
1066
|
type: "new_token_launch",
|
|
@@ -777,18 +1108,16 @@ export class AgentBridge {
|
|
|
777
1108
|
return;
|
|
778
1109
|
if (name === "setup_agent") {
|
|
779
1110
|
const structured = asRecord(parsed.structured) ?? parsed;
|
|
780
|
-
const wallets = collectWallets(structured);
|
|
781
|
-
if (wallets.length > 0) {
|
|
782
|
-
this.setters.setStatus((prev) => ({
|
|
783
|
-
...prev,
|
|
784
|
-
wallets: mergeWallets(prev.wallets, wallets),
|
|
785
|
-
}));
|
|
786
|
-
}
|
|
787
1111
|
if (structured.action === "create_wallet") {
|
|
788
1112
|
const chain = normalizeChain(structured.chain);
|
|
789
|
-
const
|
|
790
|
-
if (
|
|
791
|
-
this.upsertWallet(
|
|
1113
|
+
const wallet = chain ? readWallet({ ...structured, chain }) : null;
|
|
1114
|
+
if (wallet) {
|
|
1115
|
+
this.upsertWallet(wallet);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
else if (structured.action === "get_status" || structured.action === "status") {
|
|
1119
|
+
for (const wallet of collectWallets(structured)) {
|
|
1120
|
+
this.upsertWallet(wallet);
|
|
792
1121
|
}
|
|
793
1122
|
}
|
|
794
1123
|
return;
|
|
@@ -810,20 +1139,81 @@ export class AgentBridge {
|
|
|
810
1139
|
?? (data ? firstNumber(data, ["totalValueUsd", "portfolioValueUsd", "portfolio_value_usd", "total_value_usd", "valueUsd"]) : undefined)
|
|
811
1140
|
?? summed.balanceUsd;
|
|
812
1141
|
const positions = collectPositions(data ?? snapshot);
|
|
1142
|
+
for (const wallet of wallets) {
|
|
1143
|
+
this.upsertWallet(wallet);
|
|
1144
|
+
}
|
|
813
1145
|
this.setters.setStatus((prev) => ({
|
|
814
1146
|
...prev,
|
|
815
1147
|
...(balanceSol !== undefined ? { balanceSol } : {}),
|
|
816
1148
|
...(balanceUsd !== undefined ? { balanceUsd } : {}),
|
|
817
|
-
...(wallets.length > 0 ? { wallets: mergeWallets(prev.wallets, wallets) } : {}),
|
|
818
1149
|
...(positions ? { activeTrades: positions } : {}),
|
|
819
1150
|
}));
|
|
820
1151
|
}
|
|
821
1152
|
upsertWallet(wallet) {
|
|
1153
|
+
this.knownWallets = mergeLatestWallets(this.knownWallets, [wallet]);
|
|
1154
|
+
this.persistWallet(wallet);
|
|
822
1155
|
this.setters.setStatus((prev) => {
|
|
823
1156
|
const wallets = prev.wallets.filter((existing) => existing.chain !== wallet.chain);
|
|
824
1157
|
return { ...prev, wallets: [...wallets, wallet] };
|
|
825
1158
|
});
|
|
826
1159
|
}
|
|
1160
|
+
loadStoredWallets() {
|
|
1161
|
+
const agent = loadAgent();
|
|
1162
|
+
if (!agent || agent.publicId !== this.config.publicId)
|
|
1163
|
+
return [];
|
|
1164
|
+
const wallets = [];
|
|
1165
|
+
if (agent.wallets?.solana)
|
|
1166
|
+
wallets.push({ chain: "solana", address: agent.wallets.solana });
|
|
1167
|
+
if (agent.wallets?.base)
|
|
1168
|
+
wallets.push({ chain: "base", address: agent.wallets.base });
|
|
1169
|
+
return wallets;
|
|
1170
|
+
}
|
|
1171
|
+
syncKnownWalletsToStatus() {
|
|
1172
|
+
if (this.knownWallets.length === 0)
|
|
1173
|
+
return;
|
|
1174
|
+
this.setters.setStatus((prev) => ({
|
|
1175
|
+
...prev,
|
|
1176
|
+
wallets: mergeLatestWallets(prev.wallets, this.knownWallets),
|
|
1177
|
+
}));
|
|
1178
|
+
}
|
|
1179
|
+
persistActiveAgent(update) {
|
|
1180
|
+
const agent = loadAgent();
|
|
1181
|
+
if (!agent || agent.publicId !== this.config.publicId)
|
|
1182
|
+
return;
|
|
1183
|
+
saveAgent(update(agent));
|
|
1184
|
+
}
|
|
1185
|
+
persistMasterKey(masterKey) {
|
|
1186
|
+
this.persistActiveAgent((agent) => ({
|
|
1187
|
+
...agent,
|
|
1188
|
+
masterKey,
|
|
1189
|
+
}));
|
|
1190
|
+
}
|
|
1191
|
+
persistWallet(wallet) {
|
|
1192
|
+
this.persistActiveAgent((agent) => {
|
|
1193
|
+
const wallets = { ...(agent.wallets ?? {}) };
|
|
1194
|
+
wallets[wallet.chain] = wallet.address;
|
|
1195
|
+
return {
|
|
1196
|
+
...agent,
|
|
1197
|
+
wallets,
|
|
1198
|
+
};
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
buildRuntimeContext() {
|
|
1202
|
+
if (this.knownWallets.length === 0)
|
|
1203
|
+
return "";
|
|
1204
|
+
return [
|
|
1205
|
+
"Known Balchemy runtime context from local encrypted CLI state:",
|
|
1206
|
+
...this.knownWallets.map((wallet) => `${walletAddressLabel(wallet.chain)}: ${wallet.address}`),
|
|
1207
|
+
"Funding rule: fund the Solana trading wallet with SOL. Fund the Base trading wallet with ETH on Base for gas and Base-chain capital as required.",
|
|
1208
|
+
"If the user asks where to fund, answer from these addresses.",
|
|
1209
|
+
].join("\n");
|
|
1210
|
+
}
|
|
1211
|
+
withRuntimeContext(userMessage) {
|
|
1212
|
+
const context = this.buildRuntimeContext();
|
|
1213
|
+
if (!context)
|
|
1214
|
+
return userMessage;
|
|
1215
|
+
return `${context}\n\nUser message:\n${userMessage}`;
|
|
1216
|
+
}
|
|
827
1217
|
async ensureDefaultSubscriptions() {
|
|
828
1218
|
if (!this.config.autoSeedSubscriptions) {
|
|
829
1219
|
return;
|