brahma-trade-widget
Version:
A React component for trade automation within the Brahma ecosystem.
1,018 lines (911 loc) • 28.2 kB
text/typescript
// store.ts
import axios from "axios"
import { ConsoleKit } from "brahma-console-kit"
import { toast } from "react-toastify"
import { encodePacked, fromHex, WalletClient } from "viem"
import { base } from "viem/chains"
import { create } from "zustand"
import Safe from "@safe-global/protocol-kit"
import { signTypedData, waitForTransactionReceipt } from "@wagmi/core"
import withdrawAbi from "@/components/shared/abi/withdrawContractAbi.json"
import { BASE_CHAIN_ID, URL_API_KEY_PROD } from "@/constants"
import { SupportedChainIds, TAsset } from "@/types"
import {
fetchAndFilterAssetBalances,
formatRejectMetamaskErrorMessage,
truncateString,
} from "@/utils"
import { baseWagmiConfig as wagmiConfig } from "@/wagmi"
import { PROD_URL } from "../morphoStrategy/constants"
import { dispatchToast } from "../shared/components"
import localStorageService from "../shared/services/localStoage"
import useConfigStore from "../shared/store"
import { Address } from "../shared/types"
import { INITIAL_AUTOMATION_DATA } from "./constants"
import {
AutomationAgent,
AutomationAgentData,
AutomationAgentMetadata,
AutomationAgentSubscription,
NewSwapAgentStep,
SwapAgentTab,
} from "./types"
import { isOneToOneSafe } from "./utils"
// const HARDCODED_REGISTRY_ID_DEV = "f2bc4f31-e837-4c94-8ef8-73b4535191dd"
const HARDCODED_REGISTRY_ID_PROD = "ae40c25f-6292-4028-9753-04cc9459e298"
const HARDCODED_REGISTRY_ID = HARDCODED_REGISTRY_ID_PROD
const API_URL = PROD_URL
const API_KEY = URL_API_KEY_PROD
// const API_URL = DEV_URL
// const API_KEY = URL_API_KEY_DEV
const kit = new ConsoleKit(API_KEY, API_URL)
const stepOrder: NewSwapAgentStep[] = [
"SELECT_AGENT",
"LOADING_SELECT_AGENT_ANIMATION",
"ONE",
"TWO",
"THREE",
"FOUR",
"RUNNING",
]
type SwapAgentStore = {
automations: {
activeAutomations: AutomationAgentSubscription[]
historyAutomations: AutomationAgentSubscription[]
loading: boolean
}
existingSafeAddress: Address | null
currentStep: NewSwapAgentStep
currentTab: SwapAgentTab
selectedAgent: AutomationAgent
automationAgentData: Record<AutomationAgent, AutomationAgentData>
eoaBalances: {
data: TAsset[]
loading: boolean
}
balances: {
data: TAsset[]
loading: boolean
}
preComputedConsoleAddress: Address | null
feeEstimateSignature: string | null
feeEstimate: string | null
permissionModalData: {
isShowingModal: boolean
state: "PENDING" | "SUCCESS"
}
isWithdrawLoading: boolean
fetchEoaAssets: (eoa: Address, assets: TAsset[]) => Promise<void>
setCurrentTab: (tab: SwapAgentTab) => void
setCurrentStep: (step: NewSwapAgentStep) => void
setSelectedAgent: (agent: AutomationAgent) => void
goToNextStep: () => void
goToPreviousStep: () => void
updateAgentData: (
agent: AutomationAgent,
state: Partial<AutomationAgentData>,
) => void
resetAgentsData: () => void
fetchPreComputedConsoleAddress: (
owner: Address,
chainId: SupportedChainIds,
feeToken: Address,
) => Promise<void>
withdrawAmount: (
eoa: Address,
signer: WalletClient,
subAccountAddress: Address,
consoleAddress: Address,
targetToken: TAsset,
allocatedToken: TAsset,
closeExitModal?: () => void,
) => Promise<void>
subscribeToAutomationWithExistingConsole: (
eoa: Address,
signer: WalletClient,
chainId: SupportedChainIds,
tokenInputs: Record<Address, string>,
tokenLimits: Record<Address, string>,
metadata: AutomationAgentMetadata,
existingConsoleAddress: Address,
setConsoleDeployedLoading: React.Dispatch<React.SetStateAction<boolean>>,
) => Promise<void>
fetchExistingSwapAgentConsole: (eoa: Address) => Promise<void>
generateAndDeploySubAccount: (
eoa: Address,
chainId: SupportedChainIds,
feeToken: Address,
feeEstimate: string,
tokens: Address[],
amounts: string[],
tokenInputs: Record<Address, string>,
tokenLimits: Record<Address, string>,
metadata: AutomationAgentMetadata,
) => Promise<{
taskId: string
} | null>
fetchConsoleBalances: (
assets: TAsset[],
consoleAddress: Address,
addressToDetect?: Address,
) => Promise<boolean>
fetchAutomations: (eoa: Address, showLoading?: boolean) => Promise<void>
pollTaskStatus(taskId: string): Promise<string | undefined>
createTransactionEntry(
txnHash: string,
chainId: SupportedChainIds,
): Promise<void>
updatePermissionModalData: (
state: Partial<SwapAgentStore["permissionModalData"]>,
) => void
setExistingSafeAddress: (safeAddress: Address) => void
}
const useAutomationAgentStore = create<SwapAgentStore>((set, get) => ({
existingSafeAddress: null,
automations: {
activeAutomations: [],
historyAutomations: [],
loading: false,
},
currentStep: "SELECT_AGENT",
currentTab: SwapAgentTab.NEW_AGENT,
selectedAgent: "SURGE",
automationAgentData: INITIAL_AUTOMATION_DATA,
permissionModalData: {
isShowingModal: false,
state: "PENDING",
},
isWithdrawLoading: false,
eoaBalances: {
data: [],
loading: false,
},
balances: {
data: [],
loading: false,
},
preComputedConsoleAddress: null,
feeEstimate: null,
feeEstimateSignature: null,
setExistingSafeAddress: (safeAddress) => {
set({ existingSafeAddress: safeAddress })
},
updatePermissionModalData: (
updatedState: Partial<SwapAgentStore["permissionModalData"]>,
) => {
set((state) => ({
permissionModalData: { ...state.permissionModalData, ...updatedState },
}))
},
setCurrentTab: (tab: SwapAgentTab) => {
set({ currentTab: tab })
},
setCurrentStep: (step: NewSwapAgentStep) => {
set({ currentStep: step })
},
setSelectedAgent: (agent: AutomationAgent) => {
set({ selectedAgent: agent })
},
goToNextStep: () => {
const currentStep = get().currentStep
const currentIndex = stepOrder.indexOf(currentStep)
if (currentIndex < stepOrder.length - 1) {
set({ currentStep: stepOrder[currentIndex + 1] })
}
},
goToPreviousStep: () => {
const currentStep = get().currentStep
const currentIndex = stepOrder.indexOf(currentStep)
if (currentStep === "ONE") {
set({ currentStep: "SELECT_AGENT" })
return
}
if (currentIndex > 0) {
set({ currentStep: stepOrder[currentIndex - 1] })
}
},
updateAgentData: (
agent: AutomationAgent,
state: Partial<AutomationAgentData>,
) => {
const { automationAgentData } = get()
const updatedAgentState = { ...automationAgentData[agent], ...state }
set((prev) => ({
automationAgentData: {
...prev.automationAgentData,
[agent]: updatedAgentState,
},
}))
},
fetchEoaAssets: async (eoa, assets) => {
try {
const filteredUserAssets = await fetchAndFilterAssetBalances(eoa, assets)
set({
eoaBalances: {
data: filteredUserAssets,
loading: false,
},
})
} catch (err) {
console.error("error on fetching eoa assets", err)
}
},
resetAgentsData: () => {
set({
automationAgentData: INITIAL_AUTOMATION_DATA,
currentStep: "SELECT_AGENT",
existingSafeAddress: null,
currentTab: SwapAgentTab.NEW_AGENT,
automations: {
activeAutomations: [],
historyAutomations: [],
loading: false,
},
eoaBalances: {
data: [],
loading: false,
},
balances: {
data: [],
loading: false,
},
preComputedConsoleAddress: null,
})
},
fetchPreComputedConsoleAddress: async (owner, chainId, feeToken) => {
set((state) => ({ ...state, loading: true }))
try {
const data = await kit.publicDeployer.fetchPreComputeData(
owner,
chainId,
feeToken,
)
if (
!data ||
!data.feeEstimate ||
!data.feeEstimateSignature ||
!data.precomputedAddress
) {
throw new Error("Invalid data received from fetchPreComputeAddress")
}
set((state) => ({
...state,
loading: false,
feeEstimate: data.feeEstimate,
feeEstimateSignature: data.feeEstimateSignature,
preComputedConsoleAddress: data.precomputedAddress,
}))
} catch (err: any) {
console.log(`Error fetching precompute address: ${err}`)
dispatchToast({
id: "fetch-precompute-address-error",
title: "Error fetching precompute address",
description: {
value:
err?.message ||
"An error occurred while fetching precompute address",
},
type: "error",
})
set((state) => ({ ...state, loading: false })) // Ensure loading is set to false on error
}
},
withdrawAmount: async (
eoa,
signer,
subAccountAddress,
consoleAddress,
targetToken,
allocatedToken,
closeExitModal,
) => {
const { createTransactionEntry, fetchAutomations } = get()
set({ isWithdrawLoading: true })
dispatchToast({
id: "withdraw-start",
type: "loading",
title: "Transaction in progress",
description: {
value:
"Cancelling agent transaction and withdrawing funds. Please wait...",
},
toastOptions: {
autoClose: false,
},
})
try {
console.log(
"[Withdraw] Step 1: Initiating cancellation with console kit...",
)
const response = await kit.automationContext.cancelAutomation({
chainId: BASE_CHAIN_ID,
subAccountAddress,
data: {
ownerConsole: consoleAddress,
sweepTokens: [targetToken.address, allocatedToken.address],
execViaSubAcc: [],
sweepTokenReceiver: eoa,
},
})
console.log(
"[Withdraw] Step 1 Complete: Agent transaction cancelled successfully",
response,
)
console.log("[Withdraw] Step 2: Initializing Safe transaction...")
const safe = await Safe.init({
provider:
"https://base-mainnet.g.alchemy.com/v2/q2VDjsGh2h0P6WZSXUq2eTw7y0_Ffkdq",
safeAddress: consoleAddress,
})
const transaction = await safe.createTransaction({
transactions: response.data.transactions,
onlyCalls: false,
})
const {
baseGas,
data,
gasPrice,
gasToken,
operation,
refundReceiver,
safeTxGas,
to,
value,
} = transaction.data
console.log("[Withdraw] Step 3: Generating transaction signature...")
const signature = encodePacked(
["bytes12", "address", "bytes32", "bytes1"],
[
"0x000000000000000000000000",
eoa,
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x01",
],
)
console.log("[Withdraw] Step 4: Executing transaction on Safe...")
const txnHash = await signer.writeContract({
chain: base,
address: consoleAddress,
abi: withdrawAbi,
functionName: "execTransaction",
account: eoa,
gas: BigInt(2000000),
args: [
to,
value,
data,
operation,
safeTxGas,
baseGas,
gasPrice,
gasToken,
refundReceiver,
signature,
],
})
if (!txnHash) throw new Error("execTransaction failed")
console.log(`[Withdraw] Step 5: Transaction sent with hash: ${txnHash}`)
console.log("[Withdraw] Step 6: Waiting for transaction receipt...")
await waitForTransactionReceipt(wagmiConfig, { hash: txnHash })
console.log("[Withdraw] Step 7: Storing transaction entry...")
await createTransactionEntry(txnHash, BASE_CHAIN_ID)
console.log("[Withdraw] Step 8: Refreshing agent data...")
fetchAutomations(eoa)
dispatchToast({
id: "withdraw-success",
type: "success",
title: "Withdrawal Successful",
description: {
value: `Transaction completed successfully! Hash: ${truncateString(txnHash, 10)}`,
},
})
closeExitModal && setTimeout(closeExitModal, 1000)
} catch (err: any) {
console.error("[Withdraw] Error occurred:", err)
dispatchToast({
id: "withdraw-failed",
type: "error",
title: "Transaction Failed",
description: {
value:
formatRejectMetamaskErrorMessage(err) ||
err?.shortMessage ||
err?.message ||
"Something went wrong. Please try again.",
},
})
} finally {
toast.dismiss("withdraw-start")
set({ isWithdrawLoading: false })
}
},
subscribeToAutomationWithExistingConsole: async (
eoa,
signer,
chainId,
tokenInputs,
tokenLimits,
metadata,
existingConsoleAddress,
setConsoleDeployedLoading,
) => {
const {
createTransactionEntry,
fetchAutomations,
updatePermissionModalData,
goToNextStep,
} = get()
if (!signer?.account) {
dispatchToast({
id: "error-missing-wallet",
type: "error",
title: "Wallet not connected",
description: { value: "Please connect your wallet and try again." },
})
return
}
setConsoleDeployedLoading(true)
updatePermissionModalData({ isShowingModal: true, state: "PENDING" })
dispatchToast({
id: "subscribe-start",
type: "loading",
title: "Transaction in progress",
description: { value: "Subscribing to agent. Please wait..." },
toastOptions: { autoClose: false },
})
try {
console.log(
"[Subscribe] Step 1: Initiating subscription with console kit...",
)
const response = await kit.automationContext.subscribeToAutomation({
data: {
metadata,
tokenInputs,
tokenLimits,
registryID: HARDCODED_REGISTRY_ID,
chainId,
ownerAddress: existingConsoleAddress,
duration: 0,
whitelistedAddresses: [eoa],
},
chainId,
})
console.log(
"[Subscribe] Step 1 Complete: Subscription initialized",
response,
)
console.log("[Subscribe] Step 2: Initializing Safe transaction...")
const safe = await Safe.init({
provider:
"https://base-mainnet.g.alchemy.com/v2/q2VDjsGh2h0P6WZSXUq2eTw7y0_Ffkdq",
safeAddress: existingConsoleAddress,
})
const transaction = await safe.createTransaction({
transactions: response.data.transactions,
onlyCalls: false,
})
const {
baseGas,
data,
gasPrice,
gasToken,
operation,
refundReceiver,
safeTxGas,
to,
value,
} = transaction.data
console.log("[Subscribe] Step 3: Generating transaction signature...")
const signature = encodePacked(
["bytes12", "address", "bytes32", "bytes1"],
[
"0x000000000000000000000000", // Placeholder
eoa,
"0x0000000000000000000000000000000000000000000000000000000000000000", // Placeholder
"0x01", // Placeholder
],
)
console.log("[Subscribe] Step 4: Executing transaction on Safe...")
const txnHash = await signer.writeContract({
chain: base,
address: existingConsoleAddress,
abi: withdrawAbi,
functionName: "execTransaction",
account: eoa,
gas: BigInt(2000000),
args: [
to,
value,
data,
operation,
safeTxGas,
baseGas,
gasPrice,
gasToken,
refundReceiver,
signature,
],
})
if (!txnHash) throw new Error("execTransaction failed")
console.log(`[Subscribe] Step 5: Transaction sent with hash: ${txnHash}`)
console.log("[Subscribe] Step 6: Waiting for transaction receipt...")
await waitForTransactionReceipt(wagmiConfig, { hash: txnHash as Address })
console.log("[Subscribe] Step 7: Storing transaction entry...")
await createTransactionEntry(txnHash, BASE_CHAIN_ID)
console.log("[Subscribe] Step 8: Updating permission modal data...")
updatePermissionModalData({ isShowingModal: true, state: "SUCCESS" })
console.log("[Subscribe] Step 9: Moving to the next step...")
goToNextStep()
await new Promise((resolve) => setTimeout(resolve, 3000))
console.log("[Subscribe] Step 10: Refreshing agent data...")
fetchAutomations(eoa)
dispatchToast({
id: "subscribe-success",
type: "success",
title: "Subscription Successful",
description: {
value: `Subscription completed successfully! Hash: ${truncateString(txnHash, 10)}`,
},
})
} catch (err: any) {
console.error("[Subscribe] Error occurred:", err)
dispatchToast({
id: "subscribe-failed",
type: "error",
title: "Setup Failed",
description: {
value:
formatRejectMetamaskErrorMessage(err) ||
err?.shortMessage ||
err?.message ||
"Failed to deploy agent account",
},
})
updatePermissionModalData({ isShowingModal: false })
} finally {
toast.dismiss("subscribe-start")
setConsoleDeployedLoading(false)
}
},
generateAndDeploySubAccount: async (
eoa,
chainId,
feeToken,
feeEstimate,
tokens,
amounts,
tokenInputs,
tokenLimits,
metadata,
) => {
const { preComputedConsoleAddress, feeEstimateSignature } = get()
if (!preComputedConsoleAddress) {
dispatchToast({
id: "fetch-precomputed-account-address-error",
title: "Error fetching precomputed account address",
description: {
value: "Precomputed account address not found",
},
type: "error",
})
return null
}
set((state) => ({ ...state, loading: true }))
try {
const generateData =
await kit.publicDeployer.generateAutomationSubAccount(
eoa,
preComputedConsoleAddress,
chainId,
HARDCODED_REGISTRY_ID,
feeToken,
feeEstimate,
tokens,
amounts,
{
duration: 0,
tokenInputs: tokenInputs,
tokenLimits: tokenLimits,
whitelistedAddresses: [eoa],
},
metadata,
)
if (!generateData || !feeEstimateSignature) {
dispatchToast({
id: "fetch-signature-error",
title: "Error fetching signature",
description: {
value: "An error occurred while fetching signature",
},
type: "error",
})
return null
}
const {
signaturePayload: { domain, message, types, primaryType },
subAccountPolicyCommit,
subscriptionDraftID,
} = generateData
const signature = await signTypedData(wagmiConfig, {
domain: {
verifyingContract: domain.verifyingContract,
chainId: fromHex(domain.chainId as Address, "number"),
},
types,
primaryType,
message,
})
if (!signature) {
dispatchToast({
id: "upgrade-console-error",
type: "error",
title: "Error",
description: {
value: "User rejected the transaction",
},
})
return null
}
// Deploy Brahma Account
const deployData = await kit.publicDeployer.deployBrahmaAccount(
eoa,
chainId,
HARDCODED_REGISTRY_ID,
subscriptionDraftID,
subAccountPolicyCommit,
feeToken,
tokens,
amounts,
signature, // Use the signature obtained from the previous step
feeEstimateSignature,
feeEstimate,
{},
)
if (!deployData) {
dispatchToast({
id: "deploy-account-error",
title: "Error deploying account and sub-account",
description: {
value: "An error occurred during deployment",
},
type: "error",
})
return null
}
// Update state with taskId and set loading to false
set((state) => ({
...state,
signature, // Update the state with the signature
loading: false,
}))
return deployData
} catch (err: any) {
console.error("Error in generate and deploy sub-account:", err)
dispatchToast({
id: "generate-deploy-error",
title: "Error in generate and deploy sub-account",
description: {
value: err?.message || "An error occurred during the process",
},
type: "error",
})
return null
}
},
fetchConsoleBalances: async (assets, consoleAddress, addressToDetect) => {
if (!consoleAddress) {
dispatchToast({
id: "fetch-precomputed-account-balances-error",
title: "Error fetching balances",
description: {
value: "Console address not found",
},
type: "error",
})
return false
}
set((state) => ({ balances: { ...state.balances, loading: true } }))
try {
const filteredUserAssets = await fetchAndFilterAssetBalances(
consoleAddress,
assets,
)
set({
balances: {
data: filteredUserAssets,
loading: false,
},
})
if (!addressToDetect) {
return false
}
const targetToken = filteredUserAssets.find(
(asset) =>
asset.address.toLowerCase() === addressToDetect.toLowerCase(),
)
if (!targetToken) {
return false
}
const targetTokenHasAmount = targetToken?.balanceOf?.value !== BigInt(0)
return targetTokenHasAmount
} catch (err) {
console.error("Error fetching precomputed console balances:", err)
set((state) => ({ balances: { ...state.balances, loading: false } }))
return false
}
},
fetchExistingSwapAgentConsole: async (eoa: Address) => {
try {
// Check if we already have a 1:1 safe in local storage
const existingSafeAddress =
localStorageService.getDeployedSwapAgentConsole()
if (existingSafeAddress) {
console.log("Using 1:1 Safe from local storage:", existingSafeAddress)
set({
existingSafeAddress: existingSafeAddress as Address,
})
return
}
const eoaAccounts = await kit.coreActions.fetchExistingAccounts(eoa)
if (!eoaAccounts.length) return
const filteredEoaAccountsForBaseChainId = eoaAccounts.filter(
(account) => account.chainId === BASE_CHAIN_ID,
)
for (const account of filteredEoaAccountsForBaseChainId) {
try {
if (!account) continue
const isSafe = await isOneToOneSafe(account.consoleAddress)
if (isSafe) {
console.log("Found 1:1 Safe:", account.consoleAddress)
localStorageService.setDeployedSwapAgentConsole(
account.consoleAddress,
) // Store in local storage
set({ existingSafeAddress: account.consoleAddress }) // Update state with the found address
return // Exit the loop after finding the first 1:1 Safe
}
} catch (error) {
console.error(
`Error checking ${account.consoleAddress} or setting local storage:`,
error,
)
}
}
// If no 1:1 Safe is found, clear the local storage (optional)
localStorageService.clearDeployedSwapAgentConsole()
set({ existingSafeAddress: null }) // No 1:1 Safe found, update state accordingly
} catch (error) {
console.error("Error fetching 1:1 Safes:", error)
set({ existingSafeAddress: null })
}
},
fetchAutomations: async (eoa, showLoading = true) => {
const { fetchAndAddToConfig, assets } = useConfigStore.getState()
if (showLoading) {
set((state) => ({
...state,
automations: { ...state.automations, loading: true },
}))
}
try {
const { data } = await axios.get(
`${API_URL}/automations/subscriptions/owner/${eoa}/${HARDCODED_REGISTRY_ID}`,
{
headers: {
"x-api-key": API_KEY,
},
},
)
const currentAccountAutomations: AutomationAgentSubscription[] =
data?.data
const allTokens = currentAccountAutomations.flatMap(({ metadata }) => [
metadata.sellToken,
metadata.buyToken,
]) as Address[]
const tokensNotInMetadata = allTokens.filter(
(token) =>
!assets.some(
(asset) => asset.address.toLowerCase() === token.toLowerCase(),
),
)
await fetchAndAddToConfig(tokensNotInMetadata)
const activeAutomations: AutomationAgentSubscription[] =
currentAccountAutomations.filter(
(automation) =>
automation.registryId.toLowerCase() ===
(HARDCODED_REGISTRY_ID || "").toLowerCase() &&
automation.status === 2,
)
const historyAutomations: AutomationAgentSubscription[] =
currentAccountAutomations.filter(
(automation) =>
automation.registryId.toLowerCase() ===
(HARDCODED_REGISTRY_ID || "").toLowerCase() &&
automation.status === 4,
)
set((prev) => ({
automations: {
...prev.automations,
activeAutomations,
historyAutomations,
},
}))
} catch (error) {
console.error("Error in fetchAutomations:", error)
} finally {
set((state) => ({
...state,
automations: { ...state.automations, loading: false },
}))
}
},
async pollTaskStatus(taskId) {
const POLLING_INTERVAL = 5000
const MAX_ATTEMPTS = 20
let attempts = 0
return new Promise((resolve, reject) => {
const poll = async () => {
try {
try {
const response =
await kit.publicDeployer.fetchDeploymentStatus(taskId)
// Check if response or data is empty
if (!response || !response.outputTransactionHash) {
attempts++
if (attempts >= MAX_ATTEMPTS) {
reject(new Error("Polling timeout exceeded"))
return ""
}
setTimeout(poll, POLLING_INTERVAL)
return ""
}
if (response.status === "successful") {
resolve(response.outputTransactionHash)
return
}
attempts++
if (attempts >= MAX_ATTEMPTS) {
reject(new Error("Polling timeout exceeded"))
return ""
}
setTimeout(poll, POLLING_INTERVAL)
} catch (error) {
console.error("Error fetching task status:", error)
attempts++
if (attempts >= MAX_ATTEMPTS) {
reject(new Error("Polling timeout exceeded"))
return ""
}
setTimeout(poll, POLLING_INTERVAL)
}
} catch (error) {
console.error("Unexpected error in poll:", error)
attempts++
if (attempts >= MAX_ATTEMPTS) {
reject(new Error("Polling timeout exceeded"))
return ""
}
setTimeout(poll, POLLING_INTERVAL)
}
}
poll()
})
},
async createTransactionEntry(txnHash, chainId) {
try {
await kit.coreActions.indexTransaction(txnHash, chainId)
} catch (error) {
dispatchToast({
id: "indexer-error",
title: "Error",
description: {
value:
error instanceof Error
? error.message
: "Failed to process transaction",
},
type: "error",
})
}
},
}))
export default useAutomationAgentStore