UNPKG

brahma-trade-widget

Version:

A React component for trade automation within the Brahma ecosystem.

271 lines (233 loc) 8.41 kB
import { Address, ConsoleKit } from "brahma-console-kit" import { createPublicClient, fromHex, Hex, http } from "viem" import { base } from "viem/chains" import { PROD_URL } from "@/components/morphoStrategy/constants" import { URL_API_KEY_PROD } from "@/constants" import { TAsset } from "@/types" import { convertToFullString, formatUnits, isHexMatch } from "@/utils" import { Trade } from "./types" import { ERC20_TRANSFER_TOPIC, GNOSIS_SAFE_ABI } from "./constants" export const basePublicClient = createPublicClient({ chain: base, transport: http( "https://base-mainnet.g.alchemy.com/v2/q2VDjsGh2h0P6WZSXUq2eTw7y0_Ffkdq", ), }) 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) /** * Index positions for topics in Ethereum event logs. * * Ethereum logs store event data in an indexed `topics` array, where: * - `topicIndex` (0) holds the event signature (e.g., Transfer event hash) * - `fromIndex` (1) holds the sender's address (for Transfer events, it's the sender of the tokens) * - `toIndex` (2) holds the recipient's address (for Transfer events, it's the receiver of the tokens) */ const topicIndex = 0 const fromIndex = 1 const toIndex = 2 /** * Fetches and processes trades based on agent logs. * * This function retrieves agent logs, extracts relevant transaction details, * fetches transaction receipts and block data, and determines token transfer details. * * @param {string} agentId - The ID of the agentId. * @param {TAsset} targetToken - The target token being swapped to. * @param {TAsset} allocatedToken - The token being swapped from. * @param {Address} eoa - The externally owned account (EOA) executing the trades. * @param {Address} subAccountAddress - The sub-account address handling the transactions. * @returns {Promise<Trade[]>} A promise resolving to an array of trade objects. */ export const fetchTrades = async ( agentId: string, targetToken: TAsset, allocatedToken: TAsset, eoa: Address, subAccountAddress: Address, ): Promise<Trade[]> => { console.log("Fetching agent logs for agentId:", agentId) const txns = (await kit.automationContext.fetchAutomationLogs(agentId)) || [] if (!txns.length) { console.warn("No transactions found for agentId:", agentId) return [] } console.log("Retrieved", txns.length, "transactions") // Extract transaction hashes const txnHashes = txns.map((log) => log.outputTxHash as Hex) console.log( "Fetching transaction receipts for", txnHashes.length, "transactions", ) // Fetch all transaction receipts in parallel const receipts = await Promise.all( txnHashes.map((hash) => basePublicClient.getTransactionReceipt({ hash })), ) // Extract block numbers from receipts const blockNumbers = receipts.map((receipt) => receipt.blockNumber) console.log("Fetching block data for", blockNumbers.length, "blocks") // Fetch all block timestamps in parallel const blocks = await Promise.all( blockNumbers.map((blockNumber) => basePublicClient.getBlock({ blockNumber }), ), ) return txns .map((txn, index) => { const receipt = receipts[index] const block = blocks[index] if (!receipt || !block) return null const logs = receipt.logs const timestamp = block.timestamp ? new Date(Number(block.timestamp) * 1000).toLocaleString() : "" if (logs.length < 3) return null console.log("Processing logs for transaction:", txn.outputTxHash) const tokenInlog = logs.find((log, index) => { try { return ( index > 0 && // not first element isHexMatch(log.topics[topicIndex], ERC20_TRANSFER_TOPIC) && isHexMatch(log.address, allocatedToken.address) && isHexMatch(log.topics[fromIndex], subAccountAddress) ) } catch (error) { console.error("Error processing tokenInlog:", error) return false } }) const tokenOutlog = logs.find((log) => { try { return ( isHexMatch(log.topics[topicIndex], ERC20_TRANSFER_TOPIC) && isHexMatch(log.address, targetToken.address) && isHexMatch(log.topics[toIndex], subAccountAddress) ) } catch (error) { console.error("Error processing tokenOutlog:", error) return false } }) const sentToWalletLog = logs.find((log) => { try { return ( isHexMatch(log.topics[topicIndex], ERC20_TRANSFER_TOPIC) && isHexMatch(log.address, targetToken.address) && isHexMatch(log.topics[fromIndex], subAccountAddress) && isHexMatch(log.topics[toIndex], eoa) ) } catch (error) { console.error("Error processing sentToWalletLog:", error) return false } }) if (!tokenInlog || !tokenOutlog || !sentToWalletLog) { console.warn("Missing logs for transaction:", txn.outputTxHash) return null } console.log("Logs matched for transaction:", txn.outputTxHash) return { txnHash: txn.outputTxHash as Hex, date: timestamp, tokenInData: { asset: allocatedToken, amount: formatUnits( convertToFullString(fromHex(tokenInlog.data ?? "0x00", "number")), allocatedToken.decimals, ), }, tokenOutData: { asset: targetToken, amount: formatUnits( convertToFullString(fromHex(tokenOutlog.data ?? "0x00", "number")), targetToken.decimals, ), }, sentToWalletAmount: formatUnits( convertToFullString( fromHex( !sentToWalletLog.data || sentToWalletLog?.data === "0x" ? "0x00" : sentToWalletLog?.data, "number", ), ), targetToken.decimals, ), } }) .filter((trade) => trade !== null) } export const isOneToOneSafe = async ( address: Address, ): Promise<boolean | undefined> => { try { if (!basePublicClient) return undefined const [owners, threshold] = await Promise.all([ basePublicClient.readContract({ address: address, abi: GNOSIS_SAFE_ABI, functionName: "getOwners", }), basePublicClient.readContract({ address: address, abi: GNOSIS_SAFE_ABI, functionName: "getThreshold", }), ]) return owners.length === 1 && threshold === BigInt(1) } catch (error) { console.error(`Error checking ${address}:`, error) return false // Or handle the error differently, e.g., re-throw } } export const lowercaseKeys = ( obj: Record<string, string>, ): Record<string, string> => Object.fromEntries( Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]), ) export const calculateAutomationEvery = ( amount: string, price: number | null, duration: number, //hours maxTradeSize: number, ) => { const parsedAmount = parseFloat(amount) const twoPercentOfAmount = parsedAmount * 0.02 const twoDollarEquivalentAmount = price ? 2 / price : twoPercentOfAmount const minTradeSize = `${Math.max(twoDollarEquivalentAmount, twoPercentOfAmount)}` const durationInSeconds = duration * 3_600 const medianTradeSize = (maxTradeSize + Number(minTradeSize)) / 2 const formattedDuration = Math.floor( durationInSeconds / (Number(amount) / medianTradeSize), ) return formattedDuration } export const formatDurationToHumanReadable = (seconds: number): string => { const units = [ { value: 86400, singular: "day", plural: "days" }, { value: 3600, singular: "hour", plural: "hours" }, { value: 60, singular: "minute", plural: "minutes" }, { value: 1, singular: "second", plural: "seconds" }, ] // Handle zero case if (seconds === 0) return "0 seconds" // Find the two largest units let result = "" let count = 0 for (const unit of units) { if (seconds >= unit.value && count < 2) { const value = Math.floor(seconds / unit.value) seconds %= unit.value if (result) result += " " result += `${value} ${value === 1 ? unit.singular : unit.plural}` count++ } } return result }