import { createPublicClient, createWalletClient, http, type Account, type Address, type PublicClient, type WalletClient, type Hex, } from "viem"; import type { ChainName, TxResult, Deployed } from "./types.js"; import { resolveChain, resolveToken, type ChainConfig } from "./chains.js"; import { MissingSignerError, NoActiveVaultError } from "./errors.js"; import { VaultModule } from "./vault.js"; import { AccountantModule } from "./accountant.js"; import { TellerModule } from "./teller.js"; import { RolesModule } from "./roles.js"; import { IntegrationsModule } from "./integrations.js"; import { StrategyModule } from "./strategy.js"; import { QueueModule } from "./queue.js"; import { MonitorModule } from "./monitor.js"; import { NavModule } from "./nav.js"; import { FactoryV2Module } from "./factoryV2.js"; import { RegistryModule } from "./registryV2.js"; import { PolicyManagerModule } from "./policyManager.js"; import { IndexerClient } from "./indexerClient.js"; import { NavPipelineModule } from "./navPipeline.js"; import { ResearchToolsModule } from "./researchTools.js"; import { SimulatorClient } from "./simulatorClient.js"; export interface LucidlyClientOptions { /** Chain name ("ethereum" | "base" | "arbitrum") or numeric id. */ chain: ChainName | number; rpcUrl: string; /** A viem Account (e.g. from privateKeyToAccount). Omit for a read-only client. */ signer?: Account; /** Optional default active vault address. */ vault?: Address; /** Override viem's receipt/block polling interval (ms). Useful against fast local nodes. */ pollingInterval?: number; /** Optional lucidlypro-indexer GraphQL endpoint (e.g. "http://localhost:42069"). */ indexerUrl?: string; /** Optional lucidly-simulator endpoint (e.g. "https://simulator.rdv8.xyz"). When set, `client.simulator` * is wired up. The simulator runs Trail leaf calls against anvil forks and returns receipts + * balance diffs without broadcasting to mainnet. */ simulatorUrl?: string; } /** * viem-based entrypoint for Lucidly. Mirrors the CLI surface so an agent can call it programmatically. * Read methods work without a signer (dashboards); writes require `signer`. */ export class LucidlyClient { readonly chainConfig: ChainConfig; readonly publicClient: PublicClient; readonly walletClient?: WalletClient; readonly account?: Account; private _activeVault?: Address; private _active?: Deployed; readonly vault: VaultModule; readonly accountant: AccountantModule; readonly teller: TellerModule; readonly roles: RolesModule; readonly integrations: IntegrationsModule; readonly strategy: StrategyModule; readonly queue: QueueModule; readonly monitor: MonitorModule; readonly nav: NavModule; // v2.1 surfaces readonly factoryV2: FactoryV2Module; readonly registry: RegistryModule; readonly policyManager: PolicyManagerModule; readonly navPipeline: NavPipelineModule; readonly researchTools: ResearchToolsModule; /** Indexer client. `undefined` if no indexerUrl was provided — callers MUST guard or set it later. */ indexer?: IndexerClient; /** Simulator client. `undefined` if no simulatorUrl was provided. Use `setSimulatorUrl()` to attach * at runtime. Guards similarly to `indexer`. */ simulator?: SimulatorClient; constructor(opts: LucidlyClientOptions) { this.chainConfig = resolveChain(opts.chain); this.publicClient = createPublicClient({ chain: this.chainConfig.viemChain, transport: http(opts.rpcUrl), pollingInterval: opts.pollingInterval, }); if (opts.signer) { this.account = opts.signer; this.walletClient = createWalletClient({ account: opts.signer, chain: this.chainConfig.viemChain, transport: http(opts.rpcUrl), pollingInterval: opts.pollingInterval, }); } this._activeVault = opts.vault; this.vault = new VaultModule(this); this.accountant = new AccountantModule(this); this.teller = new TellerModule(this); this.roles = new RolesModule(this); this.integrations = new IntegrationsModule(this); this.strategy = new StrategyModule(this); this.queue = new QueueModule(this); this.monitor = new MonitorModule(this); this.nav = new NavModule(this); this.factoryV2 = new FactoryV2Module(this); this.registry = new RegistryModule(this); this.policyManager = new PolicyManagerModule(this); this.navPipeline = new NavPipelineModule(this); this.researchTools = new ResearchToolsModule(this); if (opts.indexerUrl) this.indexer = new IndexerClient(opts.indexerUrl); if (opts.simulatorUrl) this.simulator = new SimulatorClient({ url: opts.simulatorUrl }); } /** Attach (or replace) the indexer client at runtime. */ setIndexerUrl(url: string): void { this.indexer = new IndexerClient(url); } /** Attach (or replace) the simulator client at runtime. */ setSimulatorUrl(url: string): void { this.simulator = new SimulatorClient({ url }); } // ---- shared helpers used by modules ---- get activeVault(): Address | undefined { return this._activeVault; } get activeDeployment(): Deployed | undefined { return this._active; } setActiveVault(v: Address): void { this._activeVault = v; } /** Record a full deployment so accountant/teller/manager/rolesAuthority resolve automatically. */ setActiveDeployment(d: Deployed): void { this._active = d; this._activeVault = d.vault; } requireVault(op: string, override?: Address): Address { const v = override ?? this._activeVault; if (!v) throw new NoActiveVaultError(op); return v; } requireAccountant(op: string, override?: Address): Address { const a = override ?? this._active?.accountant; if (!a) throw new NoActiveVaultError(op + " (no accountant; pass an address or setActiveDeployment)"); return a; } requireTeller(op: string, override?: Address): Address { const t = override ?? this._active?.teller; if (!t) throw new NoActiveVaultError(op + " (no teller; pass an address or setActiveDeployment)"); return t; } requireManager(op: string, override?: Address): Address { const m = override ?? this._active?.manager; if (!m) throw new NoActiveVaultError(op + " (no manager; pass an address or setActiveDeployment)"); return m; } requireRolesAuthority(op: string, override?: Address): Address { const r = override ?? this._active?.rolesAuthority; if (!r) throw new NoActiveVaultError(op + " (no rolesAuthority; pass an address or setActiveDeployment)"); return r; } requireSigner(op: string): { walletClient: WalletClient; account: Account } { if (!this.walletClient || !this.account) throw new MissingSignerError(op); return { walletClient: this.walletClient, account: this.account }; } resolveToken(symbolOrAddress: Address | string): Address { return resolveToken(this.chainConfig, symbolOrAddress); } /** Read a contract (no signer required). */ async read(params: { address: Address; abi: readonly unknown[]; functionName: string; args?: readonly unknown[]; }): Promise { return (await this.publicClient.readContract({ address: params.address, abi: params.abi as any, functionName: params.functionName, args: (params.args ?? []) as any, })) as T; } /** Send a write tx and return its hash (does not wait). */ async send(params: { address: Address; abi: readonly unknown[]; functionName: string; args?: readonly unknown[]; value?: bigint; }): Promise { const { walletClient, account } = this.requireSigner(params.functionName); return walletClient.writeContract({ address: params.address, abi: params.abi as any, functionName: params.functionName, args: (params.args ?? []) as any, value: params.value, account, chain: this.chainConfig.viemChain, }); } /** Send a write tx, wait for the receipt, and return it. */ async sendAndWait(params: { address: Address; abi: readonly unknown[]; functionName: string; args?: readonly unknown[]; value?: bigint; }) { const hash = await this.send(params); return this.publicClient.waitForTransactionReceipt({ hash }); } /** Deploy a contract from an artifact, wait for the receipt, return the new address. */ async deployContract(params: { abi: readonly unknown[]; bytecode: Hex; args?: readonly unknown[]; }): Promise
{ const { walletClient, account } = this.requireSigner("deployContract"); const hash = await walletClient.deployContract({ abi: params.abi as any, bytecode: params.bytecode, args: (params.args ?? []) as any, account, chain: this.chainConfig.viemChain, }); const receipt = await this.publicClient.waitForTransactionReceipt({ hash }); if (!receipt.contractAddress) throw new Error("Deployment produced no contract address"); return receipt.contractAddress; } /** Wrap a hash in the documented { hash, wait() } shape. */ wrapTx(hash: Hex): TxResult { return { hash: hash as Address, wait: () => this.publicClient.waitForTransactionReceipt({ hash }), }; } }