@grimoirelabs/cli 0.7.0 → 0.9.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.
Files changed (38) hide show
  1. package/dist/commands/cast.d.ts +8 -1
  2. package/dist/commands/cast.d.ts.map +1 -1
  3. package/dist/commands/cast.js +564 -2
  4. package/dist/commands/cast.js.map +1 -1
  5. package/dist/commands/cross-chain-helpers.d.ts +36 -0
  6. package/dist/commands/cross-chain-helpers.d.ts.map +1 -0
  7. package/dist/commands/cross-chain-helpers.js +200 -0
  8. package/dist/commands/cross-chain-helpers.js.map +1 -0
  9. package/dist/commands/history.d.ts.map +1 -1
  10. package/dist/commands/history.js +12 -0
  11. package/dist/commands/history.js.map +1 -1
  12. package/dist/commands/log.d.ts.map +1 -1
  13. package/dist/commands/log.js +20 -0
  14. package/dist/commands/log.js.map +1 -1
  15. package/dist/commands/resume.d.ts +9 -0
  16. package/dist/commands/resume.d.ts.map +1 -0
  17. package/dist/commands/resume.js +280 -0
  18. package/dist/commands/resume.js.map +1 -0
  19. package/dist/commands/simulate.d.ts +8 -0
  20. package/dist/commands/simulate.d.ts.map +1 -1
  21. package/dist/commands/simulate.js +377 -1
  22. package/dist/commands/simulate.js.map +1 -1
  23. package/dist/commands/validate.d.ts.map +1 -1
  24. package/dist/commands/validate.js +92 -3
  25. package/dist/commands/validate.js.map +1 -1
  26. package/dist/commands/venue-doctor.d.ts +48 -0
  27. package/dist/commands/venue-doctor.d.ts.map +1 -0
  28. package/dist/commands/venue-doctor.js +267 -0
  29. package/dist/commands/venue-doctor.js.map +1 -0
  30. package/dist/commands/venue.d.ts.map +1 -1
  31. package/dist/commands/venue.js +15 -1
  32. package/dist/commands/venue.js.map +1 -1
  33. package/dist/commands/venues.d.ts.map +1 -1
  34. package/dist/commands/venues.js +31 -1
  35. package/dist/commands/venues.js.map +1 -1
  36. package/dist/index.js +27 -1
  37. package/dist/index.js.map +1 -1
  38. package/package.json +3 -3
@@ -21,7 +21,14 @@ interface CastOptions {
21
21
  mnemonic?: string;
22
22
  keystore?: string;
23
23
  passwordEnv?: string;
24
- rpcUrl?: string;
24
+ rpcUrl?: string | string[];
25
+ destinationSpell?: string;
26
+ destinationChain?: string;
27
+ handoffTimeoutSec?: string;
28
+ pollIntervalSec?: string;
29
+ watch?: boolean;
30
+ morphoMarketId?: string | string[];
31
+ morphoMarketMap?: string;
25
32
  gasMultiplier?: string;
26
33
  skipConfirm?: boolean;
27
34
  verbose?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"cast.d.ts","sourceRoot":"","sources":["../../src/commands/cast.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4CH,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrD,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC3C,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA+HxF"}
1
+ {"version":3,"file":"cast.d.ts","sourceRoot":"","sources":["../../src/commands/cast.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA+DH,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrD,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC3C,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA+IxF"}
@@ -7,7 +7,7 @@ import { homedir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import * as readline from "node:readline";
9
9
  import { Writable } from "node:stream";
10
- import { compileFile, createProvider, createWalletFromConfig, execute, formatWei, getChainName, getNativeCurrencySymbol, isTestnet, loadPrivateKey, } from "@grimoirelabs/core";
10
+ import { SqliteStateStore, compileFile, createProvider, createRunRecord, createWalletFromConfig, execute, formatWei, getChainName, getNativeCurrencySymbol, isTestnet, loadPrivateKey, orchestrateCrossChain, toCrossChainReceipt, } from "@grimoirelabs/core";
11
11
  import { adapters, createHyperliquidAdapter } from "@grimoirelabs/venues";
12
12
  import chalk from "chalk";
13
13
  import ora from "ora";
@@ -15,6 +15,7 @@ import { hydrateParamsFromEnsProfile, resolveEnsProfile } from "../lib/ens-profi
15
15
  import { resolveAdvisorSkillsDirs } from "./advisor-skill-helpers.js";
16
16
  import { resolveAdvisoryHandler } from "./advisory-handlers.js";
17
17
  import { createAdvisoryLiveTraceLogger } from "./advisory-live-trace.js";
18
+ import { createLogicalRunId, parseMorphoMarketMappings, parseRpcUrlMappings, requireExplicitRpcMappings, resolveRpcUrlForChain, validateMorphoMappingsForSpells, } from "./cross-chain-helpers.js";
18
19
  import { buildRuntimeProvenanceManifest, enforceFreshnessPolicy, resolveDataPolicy, resolveReplayParams, } from "./data-provenance.js";
19
20
  import { buildRunReportEnvelope, formatRunReportText } from "./run-report.js";
20
21
  import { withStatePersistence } from "./state-helpers.js";
@@ -90,6 +91,21 @@ export async function castCommand(spellPath, options) {
90
91
  const chainId = Number.parseInt(options.chain ?? "1", 10);
91
92
  const chainName = getChainName(chainId);
92
93
  const isTest = isTestnet(chainId);
94
+ if (options.destinationSpell) {
95
+ await executeCrossChainCast({
96
+ sourceSpellPath: spellPath,
97
+ sourceSpell: spell,
98
+ sourceChainId: chainId,
99
+ params,
100
+ options,
101
+ noState,
102
+ mode,
103
+ hasKey,
104
+ dataPolicy,
105
+ replayResolution,
106
+ });
107
+ return;
108
+ }
93
109
  console.log();
94
110
  console.log(chalk.cyan("🔗 Network:"));
95
111
  console.log(` ${chalk.dim("Chain:")} ${chainName} (${chainId})`);
@@ -153,7 +169,7 @@ async function executeWithWallet(spell, params, options, noState, chainId, isTes
153
169
  catch {
154
170
  // Keep default adapters if key extraction fails.
155
171
  }
156
- const rpcUrl = options.rpcUrl ?? process.env.RPC_URL;
172
+ const rpcUrl = resolveRpcUrlFromOption(chainId, options.rpcUrl);
157
173
  if (!rpcUrl) {
158
174
  spinner.warn(chalk.yellow("No RPC URL provided, using default public RPC"));
159
175
  }
@@ -365,6 +381,394 @@ async function executeSimulation(spell, params, options, noState, chainId, runti
365
381
  process.exit(1);
366
382
  }
367
383
  }
384
+ async function executeCrossChainCast(input) {
385
+ const destinationSpellPath = input.options.destinationSpell;
386
+ if (!destinationSpellPath) {
387
+ throw new Error("Cross-chain mode requires --destination-spell");
388
+ }
389
+ const destinationChainId = parseRequiredNumber(input.options.destinationChain, "--destination-chain");
390
+ const handoffTimeoutSec = parseRequiredNumber(input.options.handoffTimeoutSec, "--handoff-timeout-sec");
391
+ const pollIntervalSec = input.options.pollIntervalSec
392
+ ? parseRequiredNumber(input.options.pollIntervalSec, "--poll-interval-sec")
393
+ : 30;
394
+ const destinationCompile = await compileFile(destinationSpellPath);
395
+ if (!destinationCompile.success || !destinationCompile.ir) {
396
+ throw new Error(`Destination spell compilation failed: ${destinationCompile.errors.map((e) => `[${e.code}] ${e.message}`).join("; ")}`);
397
+ }
398
+ const destinationSpell = destinationCompile.ir;
399
+ const rpcMappings = parseRpcUrlMappings(input.options.rpcUrl);
400
+ requireExplicitRpcMappings(rpcMappings, input.sourceChainId, destinationChainId);
401
+ const sourceRpcUrl = resolveRpcUrlForChain(input.sourceChainId, rpcMappings);
402
+ const destinationRpcUrl = resolveRpcUrlForChain(destinationChainId, rpcMappings);
403
+ if (!sourceRpcUrl || !destinationRpcUrl) {
404
+ throw new Error("Could not resolve RPC URLs for both source and destination chains.");
405
+ }
406
+ const morphoMarketIds = parseMorphoMarketMappings({
407
+ morphoMarketId: input.options.morphoMarketId,
408
+ morphoMarketMap: input.options.morphoMarketMap,
409
+ });
410
+ validateMorphoMappingsForSpells(input.sourceSpell, destinationSpell, morphoMarketIds);
411
+ const sourceProvider = createProvider(input.sourceChainId, sourceRpcUrl);
412
+ const destinationProvider = createProvider(destinationChainId, destinationRpcUrl);
413
+ const spinner = ora("Preparing cross-chain orchestration...").start();
414
+ let keyConfig;
415
+ let configuredAdapters = adapters;
416
+ let sourceWallet;
417
+ let destinationWallet;
418
+ if (input.mode === "execute") {
419
+ keyConfig = await resolveKeyConfig(input.options, spinner);
420
+ try {
421
+ const rawKey = loadPrivateKey(keyConfig);
422
+ configuredAdapters = adapters.map((adapter) => {
423
+ if (adapter.meta.name === "hyperliquid") {
424
+ return createHyperliquidAdapter({
425
+ privateKey: rawKey,
426
+ assetMap: { ETH: 4 },
427
+ });
428
+ }
429
+ return adapter;
430
+ });
431
+ }
432
+ catch {
433
+ configuredAdapters = adapters;
434
+ }
435
+ sourceWallet = createWalletFromConfig(keyConfig, input.sourceChainId, sourceProvider.rpcUrl);
436
+ destinationWallet = createWalletFromConfig(keyConfig, destinationChainId, destinationProvider.rpcUrl);
437
+ }
438
+ else if (input.hasKey) {
439
+ keyConfig = await resolveKeyConfig(input.options, spinner);
440
+ try {
441
+ const rawKey = loadPrivateKey(keyConfig);
442
+ configuredAdapters = adapters.map((adapter) => {
443
+ if (adapter.meta.name === "hyperliquid") {
444
+ return createHyperliquidAdapter({
445
+ privateKey: rawKey,
446
+ assetMap: { ETH: 4 },
447
+ });
448
+ }
449
+ return adapter;
450
+ });
451
+ }
452
+ catch {
453
+ configuredAdapters = adapters;
454
+ }
455
+ }
456
+ const vault = resolveVaultAddress(input.options.vault, sourceWallet?.address);
457
+ const runId = createLogicalRunId();
458
+ const runtimeFlow = input.mode === "execute"
459
+ ? "cast_execute"
460
+ : input.mode === "dry-run"
461
+ ? "cast_dry_run"
462
+ : "simulate";
463
+ const advisorSkillsDirs = resolveAdvisorSkillsDirs(input.options.advisorSkillsDir) ?? [];
464
+ const sourceOnAdvisory = await resolveAdvisoryHandler(input.sourceSpell.id, {
465
+ advisoryPi: input.options.advisoryPi,
466
+ advisoryReplay: input.options.advisoryReplay,
467
+ advisoryProvider: input.options.advisoryProvider,
468
+ advisoryModel: input.options.advisoryModel,
469
+ advisoryThinking: input.options.advisoryThinking,
470
+ advisoryTools: input.options.advisoryTools,
471
+ advisoryTraceVerbose: input.options.advisoryTraceVerbose,
472
+ advisoryTraceLogger: input.options.json ? undefined : console.log,
473
+ advisorSkillsDirs,
474
+ stateDir: input.options.stateDir,
475
+ noState: input.noState,
476
+ agentDir: input.options.piAgentDir,
477
+ cwd: process.cwd(),
478
+ });
479
+ const destinationOnAdvisory = await resolveAdvisoryHandler(destinationSpell.id, {
480
+ advisoryPi: input.options.advisoryPi,
481
+ advisoryReplay: input.options.advisoryReplay,
482
+ advisoryProvider: input.options.advisoryProvider,
483
+ advisoryModel: input.options.advisoryModel,
484
+ advisoryThinking: input.options.advisoryThinking,
485
+ advisoryTools: input.options.advisoryTools,
486
+ advisoryTraceVerbose: input.options.advisoryTraceVerbose,
487
+ advisoryTraceLogger: input.options.json ? undefined : console.log,
488
+ advisorSkillsDirs,
489
+ stateDir: input.options.stateDir,
490
+ noState: input.noState,
491
+ agentDir: input.options.piAgentDir,
492
+ cwd: process.cwd(),
493
+ });
494
+ const advisoryEventCallback = input.options.json
495
+ ? undefined
496
+ : createAdvisoryLiveTraceLogger(console.log, {
497
+ verbose: input.options.advisoryTraceVerbose,
498
+ });
499
+ const dbPath = input.options.stateDir ? join(input.options.stateDir, "grimoire.db") : undefined;
500
+ let store;
501
+ if (!input.noState) {
502
+ try {
503
+ store = new SqliteStateStore({ dbPath });
504
+ }
505
+ catch (error) {
506
+ if (isMissingNodeSqliteBackend(error)) {
507
+ console.log(chalk.yellow("State persistence unavailable in Node (missing better-sqlite3). Continuing without persisted state."));
508
+ }
509
+ else {
510
+ throw error;
511
+ }
512
+ }
513
+ }
514
+ let sourceState = store ? ((await store.load(input.sourceSpell.id)) ?? {}) : {};
515
+ let destinationState = store ? ((await store.load(destinationSpell.id)) ?? {}) : {};
516
+ const lifecycleEvents = [];
517
+ const manifest = {
518
+ schema_version: "grimoire.cross_chain.phase1.v1",
519
+ run_id: runId,
520
+ source_spell_path: input.sourceSpellPath,
521
+ destination_spell_path: destinationSpellPath,
522
+ source_spell_id: input.sourceSpell.id,
523
+ destination_spell_id: destinationSpell.id,
524
+ source_chain_id: input.sourceChainId,
525
+ destination_chain_id: destinationChainId,
526
+ mode: input.mode,
527
+ watch: input.options.watch === true,
528
+ handoff_timeout_sec: handoffTimeoutSec,
529
+ poll_interval_sec: pollIntervalSec,
530
+ rpc_by_chain: {
531
+ [input.sourceChainId]: sourceRpcUrl,
532
+ [destinationChainId]: destinationRpcUrl,
533
+ },
534
+ params: input.params,
535
+ vault,
536
+ morpho_market_ids: morphoMarketIds,
537
+ };
538
+ const gasMultiplier = input.options.gasMultiplier
539
+ ? Number.parseFloat(input.options.gasMultiplier)
540
+ : 1.1;
541
+ const confirmCallback = input.options.skipConfirm || isTestnet(input.sourceChainId)
542
+ ? async () => true
543
+ : async (message) => {
544
+ console.log(message);
545
+ return await confirmPrompt(chalk.yellow("Proceed? (yes/no): "));
546
+ };
547
+ spinner.text = "Running source/destination orchestration...";
548
+ const orchestration = await orchestrateCrossChain({
549
+ runId,
550
+ sourceSpellId: input.sourceSpell.id,
551
+ destinationSpellId: destinationSpell.id,
552
+ sourceChainId: input.sourceChainId,
553
+ destinationChainId,
554
+ vault: vault,
555
+ sourceParams: input.params,
556
+ destinationParams: input.params,
557
+ mode: input.mode,
558
+ watch: input.options.watch === true,
559
+ handoffTimeoutSec,
560
+ pollIntervalSec,
561
+ executeSource: async () => {
562
+ const result = await execute({
563
+ spell: input.sourceSpell,
564
+ runId,
565
+ vault: vault,
566
+ chain: input.sourceChainId,
567
+ params: input.params,
568
+ persistentState: sourceState,
569
+ simulate: input.mode === "simulate",
570
+ executionMode: input.mode === "simulate" ? undefined : input.mode,
571
+ wallet: input.mode === "execute" ? sourceWallet : undefined,
572
+ provider: sourceProvider,
573
+ gasMultiplier,
574
+ confirmCallback,
575
+ skipTestnetConfirmation: input.options.skipConfirm ?? false,
576
+ adapters: configuredAdapters,
577
+ advisorSkillsDirs: advisorSkillsDirs.length > 0 ? advisorSkillsDirs : undefined,
578
+ onAdvisory: sourceOnAdvisory,
579
+ eventCallback: advisoryEventCallback,
580
+ warningCallback: (message) => console.log(chalk.yellow(`Warning: ${message}`)),
581
+ crossChain: {
582
+ enabled: true,
583
+ runId,
584
+ trackId: "source",
585
+ role: "source",
586
+ morphoMarketIds,
587
+ },
588
+ });
589
+ sourceState = result.finalState;
590
+ return result;
591
+ },
592
+ executeDestination: async (params) => {
593
+ const result = await execute({
594
+ spell: destinationSpell,
595
+ runId,
596
+ vault: vault,
597
+ chain: destinationChainId,
598
+ params,
599
+ persistentState: destinationState,
600
+ simulate: input.mode === "simulate",
601
+ executionMode: input.mode === "simulate" ? undefined : input.mode,
602
+ wallet: input.mode === "execute" ? destinationWallet : undefined,
603
+ provider: destinationProvider,
604
+ gasMultiplier,
605
+ confirmCallback,
606
+ skipTestnetConfirmation: input.options.skipConfirm ?? false,
607
+ adapters: configuredAdapters,
608
+ advisorSkillsDirs: advisorSkillsDirs.length > 0 ? advisorSkillsDirs : undefined,
609
+ onAdvisory: destinationOnAdvisory,
610
+ eventCallback: advisoryEventCallback,
611
+ warningCallback: (message) => console.log(chalk.yellow(`Warning: ${message}`)),
612
+ crossChain: {
613
+ enabled: true,
614
+ runId,
615
+ trackId: "destination",
616
+ role: "destination",
617
+ morphoMarketIds,
618
+ },
619
+ });
620
+ destinationState = result.finalState;
621
+ return result;
622
+ },
623
+ resolveHandoffStatus: async (handoff) => {
624
+ const across = configuredAdapters.find((adapter) => adapter.meta.name === "across");
625
+ if (!across) {
626
+ return { status: "pending" };
627
+ }
628
+ const resolver = across.bridgeLifecycle?.resolveHandoffStatus ?? across.resolveHandoffStatus;
629
+ if (!resolver) {
630
+ return { status: "pending" };
631
+ }
632
+ return resolver({
633
+ handoffId: handoff.handoffId,
634
+ originChainId: handoff.originChainId,
635
+ destinationChainId: handoff.destinationChainId,
636
+ originTxHash: handoff.originTxHash,
637
+ reference: handoff.reference,
638
+ asset: handoff.asset,
639
+ submittedAmount: handoff.submittedAmount,
640
+ walletAddress: vault,
641
+ });
642
+ },
643
+ onLifecycleEvent: (event) => {
644
+ lifecycleEvents.push(event);
645
+ },
646
+ });
647
+ const crossChainReceipt = toCrossChainReceipt({
648
+ runId,
649
+ sourceSpellId: input.sourceSpell.id,
650
+ destinationSpellId: destinationSpell.id,
651
+ sourceChainId: input.sourceChainId,
652
+ destinationChainId,
653
+ tracks: orchestration.tracks,
654
+ handoffs: orchestration.handoffs,
655
+ });
656
+ const sourceResult = orchestration.sourceResult;
657
+ const destinationResult = orchestration.destinationResult;
658
+ if (sourceResult) {
659
+ sourceResult.crossChain = crossChainReceipt;
660
+ }
661
+ if (destinationResult) {
662
+ destinationResult.crossChain = crossChainReceipt;
663
+ }
664
+ if (orchestration.pending && !store) {
665
+ throw new Error("Cannot leave a run in waiting state without persistence. Disable --no-state.");
666
+ }
667
+ if (store) {
668
+ if (sourceResult) {
669
+ await store.save(input.sourceSpell.id, sourceResult.finalState);
670
+ const sourceProvenance = {
671
+ ...buildRuntimeProvenanceManifest({
672
+ runtimeMode: runtimeFlow,
673
+ chainId: input.sourceChainId,
674
+ policy: input.dataPolicy,
675
+ replay: input.replayResolution,
676
+ params: input.params,
677
+ blockNumber: await safeGetBlockNumber(sourceProvider),
678
+ rpcUrl: sourceProvider.rpcUrl,
679
+ }),
680
+ cross_chain: manifest,
681
+ };
682
+ await store.addRun(input.sourceSpell.id, createRunRecord(sourceResult, sourceProvenance));
683
+ await store.saveLedger(input.sourceSpell.id, runId, appendLifecycleLedgerEntries(sourceResult.ledgerEvents, lifecycleEvents, runId, input.sourceSpell.id));
684
+ }
685
+ if (destinationResult) {
686
+ await store.save(destinationSpell.id, destinationResult.finalState);
687
+ const destinationProvenance = {
688
+ ...buildRuntimeProvenanceManifest({
689
+ runtimeMode: runtimeFlow,
690
+ chainId: destinationChainId,
691
+ policy: input.dataPolicy,
692
+ replay: input.replayResolution,
693
+ params: input.params,
694
+ blockNumber: await safeGetBlockNumber(destinationProvider),
695
+ rpcUrl: destinationProvider.rpcUrl,
696
+ }),
697
+ cross_chain: manifest,
698
+ };
699
+ await store.addRun(destinationSpell.id, createRunRecord(destinationResult, destinationProvenance));
700
+ await store.saveLedger(destinationSpell.id, runId, destinationResult.ledgerEvents);
701
+ }
702
+ await persistCrossChainState(store, {
703
+ runId,
704
+ tracks: orchestration.tracks,
705
+ handoffs: orchestration.handoffs,
706
+ sourceSpellId: input.sourceSpell.id,
707
+ destinationSpellId: destinationSpell.id,
708
+ sourceResult,
709
+ destinationResult,
710
+ });
711
+ }
712
+ spinner.stop();
713
+ const mergedResult = destinationResult ?? sourceResult;
714
+ if (!mergedResult) {
715
+ throw new Error("Cross-chain orchestration returned no execution results.");
716
+ }
717
+ if (input.options.json) {
718
+ console.log(stringifyJson({
719
+ success: orchestration.success,
720
+ pending: orchestration.pending,
721
+ runId,
722
+ crossChain: crossChainReceipt,
723
+ source: sourceResult
724
+ ? {
725
+ success: sourceResult.success,
726
+ error: sourceResult.structuredError,
727
+ receipt: sourceResult.receipt,
728
+ }
729
+ : undefined,
730
+ destination: destinationResult
731
+ ? {
732
+ success: destinationResult.success,
733
+ error: destinationResult.structuredError,
734
+ receipt: destinationResult.receipt,
735
+ }
736
+ : undefined,
737
+ }));
738
+ }
739
+ else {
740
+ console.log();
741
+ console.log(chalk.cyan("🔀 Cross-Chain Run:"));
742
+ console.log(` ${chalk.dim("Run ID:")} ${runId}`);
743
+ console.log(` ${chalk.dim("Source:")} ${input.sourceSpell.meta.name} (${input.sourceChainId}) -> ${chalk.dim("Destination:")} ${destinationSpell.meta.name} (${destinationChainId})`);
744
+ console.log(` ${chalk.dim("Watch:")} ${input.options.watch === true ? "Yes" : "No"}`);
745
+ console.log(` ${chalk.dim("Status:")} ${orchestration.pending ? chalk.yellow("waiting") : orchestration.success ? chalk.green("completed") : chalk.red("failed")}`);
746
+ for (const track of orchestration.tracks) {
747
+ const trackStatus = track.status === "completed"
748
+ ? chalk.green(track.status)
749
+ : track.status === "failed"
750
+ ? chalk.red(track.status)
751
+ : track.status === "waiting"
752
+ ? chalk.yellow(track.status)
753
+ : chalk.dim(track.status);
754
+ console.log(` ${chalk.dim(`track:${track.trackId}`)} ${trackStatus} chain=${track.chainId} spell=${track.spellId}`);
755
+ if (track.error) {
756
+ console.log(` ${chalk.red(track.error)}`);
757
+ }
758
+ }
759
+ for (const handoff of orchestration.handoffs) {
760
+ console.log(` ${chalk.dim(`handoff:${handoff.handoffId}`)} ${handoff.status} submitted=${handoff.submittedAmount.toString()} settled=${handoff.settledAmount?.toString() ?? "n/a"}`);
761
+ }
762
+ if (orchestration.pending) {
763
+ console.log();
764
+ console.log(chalk.yellow("Run is waiting for handoff settlement. Resume with:"));
765
+ console.log(chalk.yellow(` grimoire resume ${runId} --watch`));
766
+ }
767
+ }
768
+ if (!orchestration.success && !orchestration.pending) {
769
+ process.exit(1);
770
+ }
771
+ }
368
772
  async function safeGetBlockNumber(provider) {
369
773
  try {
370
774
  return await provider.getBlockNumber();
@@ -373,6 +777,164 @@ async function safeGetBlockNumber(provider) {
373
777
  return undefined;
374
778
  }
375
779
  }
780
+ async function resolveKeyConfig(options, spinner) {
781
+ const hasExplicitKey = !!(options.privateKey || options.mnemonic || options.keystore);
782
+ const hasEnvKey = !!(options.keyEnv && process.env[options.keyEnv]);
783
+ const hasDefaultKeystore = !hasExplicitKey && !hasEnvKey && existsSync(DEFAULT_KEYSTORE_PATH);
784
+ if (options.privateKey) {
785
+ return { type: "raw", source: options.privateKey };
786
+ }
787
+ if (options.keyEnv && process.env[options.keyEnv]) {
788
+ return { type: "env", source: options.keyEnv };
789
+ }
790
+ if (options.mnemonic) {
791
+ return { type: "mnemonic", source: options.mnemonic };
792
+ }
793
+ if (hasDefaultKeystore || options.keystore) {
794
+ const keystorePath = options.keystore ?? DEFAULT_KEYSTORE_PATH;
795
+ if (!existsSync(keystorePath)) {
796
+ spinner.fail(chalk.red(`No key provided and no keystore found at ${keystorePath}`));
797
+ console.log(chalk.dim(" Run 'grimoire wallet generate' to create one."));
798
+ process.exit(1);
799
+ throw new Error("unreachable");
800
+ }
801
+ const password = await resolveKeystorePassword(options, spinner);
802
+ if (!password) {
803
+ process.exit(1);
804
+ throw new Error("unreachable");
805
+ }
806
+ const keystoreJson = readFileSync(keystorePath, "utf-8");
807
+ return { type: "keystore", source: keystoreJson, password };
808
+ }
809
+ throw new Error("Execution mode requires wallet credentials, but no key source was provided");
810
+ }
811
+ function resolveVaultAddress(explicitVault, fallbackWalletAddress) {
812
+ if (explicitVault && explicitVault.length > 0) {
813
+ return explicitVault;
814
+ }
815
+ if (fallbackWalletAddress && fallbackWalletAddress.length > 0) {
816
+ return fallbackWalletAddress;
817
+ }
818
+ return "0x0000000000000000000000000000000000000000";
819
+ }
820
+ function parseRequiredNumber(value, flag) {
821
+ if (!value) {
822
+ throw new Error(`${flag} is required in cross-chain mode`);
823
+ }
824
+ const parsed = Number.parseInt(value, 10);
825
+ if (!Number.isFinite(parsed) || parsed <= 0) {
826
+ throw new Error(`${flag} must be a positive integer`);
827
+ }
828
+ return parsed;
829
+ }
830
+ function resolveRpcUrlFromOption(chainId, value) {
831
+ const parsed = parseRpcUrlMappings(value);
832
+ return resolveRpcUrlForChain(chainId, parsed);
833
+ }
834
+ function isMissingNodeSqliteBackend(error) {
835
+ if (!(error instanceof Error))
836
+ return false;
837
+ return error.message.includes("SqliteStateStore requires bun:sqlite (Bun) or better-sqlite3 (Node)");
838
+ }
839
+ function appendLifecycleLedgerEntries(entries, lifecycleEvents, runId, spellId) {
840
+ if (lifecycleEvents.length === 0) {
841
+ return entries;
842
+ }
843
+ const start = entries.length;
844
+ const extras = lifecycleEvents.map((event, index) => ({
845
+ id: `evt_cc_${String(start + index).padStart(3, "0")}`,
846
+ timestamp: Date.now(),
847
+ runId,
848
+ spellId,
849
+ event,
850
+ }));
851
+ return [...entries, ...extras];
852
+ }
853
+ async function persistCrossChainState(store, input) {
854
+ const nowIso = new Date().toISOString();
855
+ for (const track of input.tracks) {
856
+ const row = {
857
+ runId: input.runId,
858
+ trackId: track.trackId,
859
+ role: track.role,
860
+ spellId: track.spellId,
861
+ chainId: track.chainId,
862
+ status: track.status,
863
+ lastStepId: track.lastStepId,
864
+ error: track.error,
865
+ updatedAt: nowIso,
866
+ };
867
+ await store.upsertRunTrack(row);
868
+ }
869
+ for (const handoff of input.handoffs) {
870
+ const row = {
871
+ runId: input.runId,
872
+ handoffId: handoff.handoffId,
873
+ sourceTrackId: handoff.sourceTrackId,
874
+ destinationTrackId: handoff.destinationTrackId,
875
+ sourceStepId: handoff.sourceStepId,
876
+ originChainId: handoff.originChainId,
877
+ destinationChainId: handoff.destinationChainId,
878
+ asset: handoff.asset,
879
+ submittedAmount: handoff.submittedAmount.toString(),
880
+ settledAmount: handoff.settledAmount?.toString(),
881
+ status: handoff.status,
882
+ reference: handoff.reference,
883
+ originTxHash: handoff.originTxHash,
884
+ reason: handoff.reason,
885
+ createdAt: nowIso,
886
+ updatedAt: nowIso,
887
+ expiresAt: handoff.status === "expired" ? nowIso : undefined,
888
+ };
889
+ await store.upsertRunHandoff(row);
890
+ }
891
+ const sourceSteps = collectStepStatuses(input.sourceResult, "source", input.runId);
892
+ for (const step of sourceSteps) {
893
+ await store.upsertRunStepResult(step);
894
+ }
895
+ const destinationSteps = collectStepStatuses(input.destinationResult, "destination", input.runId);
896
+ for (const step of destinationSteps) {
897
+ await store.upsertRunStepResult(step);
898
+ }
899
+ }
900
+ function collectStepStatuses(result, trackId, runId) {
901
+ if (!result?.receipt) {
902
+ return [];
903
+ }
904
+ const nowIso = new Date().toISOString();
905
+ const byStep = new Map();
906
+ for (const planned of result.receipt.plannedActions) {
907
+ byStep.set(planned.stepId, {
908
+ runId,
909
+ trackId,
910
+ stepId: planned.stepId,
911
+ status: "pending",
912
+ idempotencyKey: `${runId}:${trackId}:${planned.stepId}`,
913
+ updatedAt: nowIso,
914
+ });
915
+ }
916
+ for (const tx of result.commit?.transactions ?? []) {
917
+ const existing = byStep.get(tx.stepId);
918
+ if (!existing)
919
+ continue;
920
+ existing.status = tx.success ? "confirmed" : "failed";
921
+ existing.reference = tx.hash;
922
+ existing.error = tx.error;
923
+ }
924
+ if (result.success && !result.commit) {
925
+ for (const step of byStep.values()) {
926
+ step.status = "confirmed";
927
+ }
928
+ }
929
+ if (!result.success) {
930
+ for (const step of byStep.values()) {
931
+ if (step.status !== "confirmed") {
932
+ step.status = "failed";
933
+ }
934
+ }
935
+ }
936
+ return [...byStep.values()];
937
+ }
376
938
  async function resolveKeystorePassword(options, spinner) {
377
939
  const envName = options.passwordEnv ?? "KEYSTORE_PASSWORD";
378
940
  const envValue = process.env[envName];