brahma-trade-widget
Version:
A React component for trade automation within the Brahma ecosystem.
271 lines (233 loc) • 8.41 kB
text/typescript
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
}