@txfence/react
React hooks for building frontends on top of txfence agents. Chain-agnostic — no dependency on @txfence/evm or viem; inject your own adapters.
Installation
npm install @txfence/react @txfence/core @txfence/evmuseAgent
Creates a stable agent instance memoized for the component lifetime.
The agent is memoized with an empty dependency array — config changes after mount do not propagate. If your config, policy, adapters, or rpcUrls need to change at runtime, pass a key prop to the component and let React remount it.
type UseAgentOptions = {
config: AgentConfig
adapters: AdapterMap
rpcUrls: Partial<Record<ChainId, string>>
executor?: (
action: Action,
chainId: ChainId,
rpcUrl: string,
evaluation: PolicyEvaluation,
simulation: SimulationResult,
) => Promise<SuccessReceipt>
capLockProvider?: CapLockProvider
metadataVerifier?: MetadataVerifier
}
function useAgent(options: UseAgentOptions): Agentimport { useAgent } from '@txfence/react'
import { simulateEvmAction, executeEvmAction, privateKeySigner } from '@txfence/evm'
const signer = privateKeySigner(import.meta.env.VITE_PRIVATE_KEY)
function MyComponent() {
const agent = useAgent({
config: {
chains: ['ethereum'],
policies: policy,
signer,
},
adapters: { ethereum: { simulate: simulateEvmAction } },
rpcUrls: { ethereum: 'https://ethereum.publicnode.com' },
executor: (action, chainId, rpcUrl, evaluation, simulation) =>
executeEvmAction(action, chainId, rpcUrl, signer, evaluation, simulation),
})
// use agent.submit(), agent.dryRun(), agent.executeIntent()
}useSubmit
Wraps the full pipeline with loading, result, and error state.
function useSubmit(agent: Agent): {
submit: (action: Action, policy: Policy) => Promise<void>
reset: () => void
result: ExecutionResult | null
loading: boolean
error: string | null
}import { useAgent, useSubmit } from '@txfence/react'
function TransferButton() {
const agent = useAgent({ config, adapters, rpcUrls, executor })
const { result, loading, submit } = useSubmit(agent)
return (
<button onClick={() => submit(action, policy)} disabled={loading}>
{loading ? 'submitting...' : 'send'}
</button>
)
}result.status is one of: success, policy_rejected, simulation_failed, approval_timeout, execution_failed, simulation_stale.
useSimulate
Wraps adapter simulation with loading, result, and error state. Takes adapters and rpcUrls directly — does not take an Agent, since simulation does not need the full pipeline.
function useSimulate(
adapters: AdapterMap,
rpcUrls: Partial<Record<ChainId, string>>,
): {
simulate: (action: Action, rpcUrl?: string) => Promise<void> // rpcUrl optional — overrides rpcUrls[action.chain]
reset: () => void
result: SimulationResult | null
loading: boolean
error: string | null // error messages are strings, not Error instances
}function SimulationPreview({ action }: { action: Action }) {
const { simulate, result, loading } = useSimulate(
{ ethereum: { simulate: simulateEvmAction } },
{ ethereum: 'https://ethereum.publicnode.com' },
)
useEffect(() => { simulate(action) }, [action])
if (loading) return <span>simulating...</span>
if (result?.wouldRevert) return <span>would revert: {result.revertReason}</span>
return <span>gas estimate: {result?.gasEstimate?.toLocaleString()}</span>
}useReceipt
Fetches a transaction receipt using a caller-supplied chain-specific fetcher. The hook runs once when all four arguments are non-null and supports cancellation if the inputs change while a fetch is in flight.
function useReceipt(
txHash: string | null,
chain: ChainId | null,
rpcUrl: string | null,
fetcher: ((txHash: string, chain: ChainId, rpcUrl: string) => Promise<SuccessReceipt>) | null,
): {
receipt: SuccessReceipt | null
loading: boolean
error: string | null
}The fetcher receives (txHash, chain, rpcUrl) — keep it chain-agnostic so the same hook works for EVM, Solana, and Cosmos receipts.
function ReceiptStatus({ txHash, chain, rpcUrl }: {
txHash: string
chain: ChainId
rpcUrl: string
}) {
const { receipt, loading } = useReceipt(txHash, chain, rpcUrl, fetchEvmReceipt)
if (loading) return <span>waiting for confirmation...</span>
if (receipt) return <span>confirmed in block {receipt.confirmedAtBlock}</span>
return null
}useDryRun
Wraps agent.dryRun() with loading and result state.
function useDryRun(agent: Agent): {
dryRun: (action: Action, policy: Policy) => Promise<void>
reset: () => void
result: DryRunResult | null
loading: boolean
error: string | null
}function PreflightCheck({ action, policy }: { action: Action; policy: Policy }) {
const agent = useAgent({ config, adapters, rpcUrls, executor })
const { dryRun, result, loading } = useDryRun(agent)
useEffect(() => { dryRun(action, policy) }, [action])
if (loading) return <span>checking...</span>
if (!result) return null
if (result.wouldProceed) return <span>✓ ready to submit</span>
return <span>blocked: {result.blockers.map(b => b.kind).join(', ')}</span>
}useIntentSubmit
Wraps agent.executeIntent() with loading, result, and error state.
function useIntentSubmit(agent: Agent): {
executeIntent: (intent: Intent) => Promise<void>
reset: () => void
result: IntentExecutionResult | null
loading: boolean
error: string | null
}function IntentButton({ intent }: { intent: Intent }) {
const agent = useAgent({ config, adapters, rpcUrls, executor })
const { executeIntent, result, loading } = useIntentSubmit(agent)
return (
<button onClick={() => executeIntent(intent)} disabled={loading}>
{loading ? 'executing...' : 'execute intent'}
</button>
)
}useAgentHealth
Polls agent.health() at a configurable interval.
function useAgentHealth(
agent: Agent,
options?: { pollIntervalMs?: number }
): AgentHealthfunction HealthIndicator() {
const agent = useAgent({ config, adapters, rpcUrls, executor })
const health = useAgentHealth(agent, { pollIntervalMs: 5000 })
return (
<span>
{health.status} · {health.inFlight} in-flight · uptime {Math.floor(health.uptime / 1000)}s
</span>
)
}AgentHealth fields: status ('healthy' | 'shutting_down'), inFlight, uptime.
useAgentHealth is useful for Kubernetes readiness UI — show a degraded state in your frontend when the agent is draining.