brahma-trade-widget
Version:
A React component for trade automation within the Brahma ecosystem.
325 lines (275 loc) • 8.97 kB
text/typescript
import {
parseUnits as parseUnitsViem,
formatUnits as formatUnitsViem,
isAddress,
Hex,
Address,
isHex,
} from "viem"
import { v4, V4Options } from "uuid"
import { TAsset } from "@/types"
import { USER_REJECTED_REQUEST_CODE } from "@/constants"
export const formatUnits = (value: string | bigint, unitName = 18): string => {
try {
if (!value) return "0"
if (typeof value === "bigint") return formatUnitsViem(value, unitName)
return formatUnitsViem(BigInt(value), unitName)
} catch (err) {
console.error("[ERROR ON FORMAT UNITS]:", err)
return "0"
}
}
export const parseUnits = (value: string, unitName = 18): bigint => {
return parseUnitsViem(value, unitName)
}
export const truncateString = (str: string, start = 6, end = 4) => {
if (!str) return ""
if (str.length <= start + end) {
return str
}
const truncated = `${str.substring(0, start)}...${str.substring(
str.length - end,
)}`
return truncated
}
export const formatNumberWithCommas = (
_input: string | number,
): string | number => {
if (!_input) return _input
const result = _input
.toString()
.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",")
return result
}
export const sliceDecimalString = (
input: string,
decimals = 6,
addThousandsFormatting = false,
): string => {
if (!input) return "0"
const parts = input.split(".")
if (parts.length === 1) {
// Input string does not contain a decimal point
return input
}
const integerPart = parts[0]
let decimalPart = parts[1]
decimalPart = decimalPart.slice(0, decimals)
while (decimalPart.endsWith("0")) {
// Remove trailing zeros
decimalPart = decimalPart.slice(0, -1)
}
if (decimalPart.length === 0) {
return addThousandsFormatting
? (formatNumberWithCommas(integerPart) as string)
: integerPart
}
const result = `${integerPart}.${decimalPart}`
if (addThousandsFormatting) {
return formatNumberWithCommas(result) as string
}
return result
}
// This was defined by product team, order: verified, balanceOf, alphabetical
export function sortAssets(assets: TAsset[]): TAsset[] {
return assets.sort((a, b) => {
if (a.verified && !b.verified) {
return -1
} else if (!a.verified && b.verified) {
return 1
} else if (a.balanceOf && b.balanceOf) {
const balanceA = parseFloat(a.balanceOf.formatted)
const balanceB = parseFloat(b.balanceOf.formatted)
return balanceB - balanceA
} else if (a.balanceOf && !b.balanceOf) {
return -1
} else if (!a.balanceOf && b.balanceOf) {
return 1
} else {
return a.name.localeCompare(b.name)
}
})
}
export const minifyCurrencyValue = (value: string | number) => {
// remove $ and commas if the value is a string
const numericValue =
typeof value === "string" ? parseFloat(value.replace(/\$|,/g, "")) : value
if (isNaN(numericValue)) {
return "Invalid value"
}
// just format if 1e3 and minify if above 1k
if (numericValue < 1e3) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(numericValue)
} else if (numericValue < 1e6) {
return "$" + (numericValue / 1e3).toFixed(1) + "k"
} else if (numericValue < 1e9) {
return "$" + (numericValue / 1e6).toFixed(1) + "M"
} else if (numericValue < 1e12) {
return "$" + (numericValue / 1e9).toFixed(1) + "B"
} else {
return "$" + (numericValue / 1e12).toFixed(1) + "T"
}
}
export const openInNewTab = (href: string) => {
window.open(href, "_blank", "noreferrer noopener")
}
export const openInSameTab = (href: string) => {
window.location.href = href
}
export const copyToClipboard = async (text?: string) => {
try {
const toCopy = text || ""
await navigator.clipboard.writeText(toCopy)
} catch (err) {
console.error("Failed to copy: ", err)
}
}
export const generateUUID = (options?: V4Options) => {
return v4(options)
}
export const formatRejectMetamaskErrorMessage = (err: any) => {
return err?.code === USER_REJECTED_REQUEST_CODE ||
err?.code === "ACTION_REJECTED"
? "Transaction not signed. If this was an error, please attempt to sign again."
: err?.mesage
}
export const isNotZero = (value: string) => {
const trimmedValue = value.trim()
// Check if the string is empty or contains only zeros or decimal points
const regex = /^0*\.?0*$/
return !regex.test(trimmedValue)
}
export const isZero = (value: string) => !isNotZero(value)
function detectJsonObjects(input: string): object[] {
const jsonObjects: object[] = []
const jsonRegex = /\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\}/g
// Remove comments from JSON (single-line and multi-line)
const cleanedInput = input.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, "")
let match: RegExpExecArray | null
while ((match = jsonRegex.exec(cleanedInput)) !== null) {
try {
const jsonObject = JSON.parse(match[0])
jsonObjects.push(jsonObject)
} catch (error) {
// Ignore invalid JSON matches
}
}
return jsonObjects
}
export const extractObjectFromAiResponse = (
aiMessage: string,
keys: string[],
): Record<string, any> | null => {
try {
const jsonMatchesData = detectJsonObjects(aiMessage).map((obj) => ({
json: obj,
keys: Object.keys(obj),
}))
// Find the object whose keys exactly match the provided keys (order-independent)
for (const jsonMatch of jsonMatchesData) {
const objJson = jsonMatch?.json || ""
const objKeys = jsonMatch?.keys || []
if (
objKeys.length === keys.length &&
objKeys.every((key) => keys.includes(key))
) {
return objJson
}
}
return null // Return null if no matching object is found
} catch (e) {
console.error("Error parsing AI message:", e)
return null
}
}
export const removeMatchingJSON = (message: string, jsonKey: string[]) => {
// Split the message into parts based on JSON objects
const parts = message.split(/(\{[^}]+\})/g)
// Filter out the parts that are JSON objects and match the keys to remove
const filteredParts = parts.filter((part) => {
try {
const obj = JSON.parse(part)
return !jsonKey.every((key) => key in obj)
} catch (e) {
return true // Keep non-JSON parts
}
})
// Join the filtered parts back into a single message
return filteredParts.join("")
}
export const formatAmountWithOptions = (
amount: number | string,
options?: Intl.NumberFormatOptions,
) => {
const stringifiedAmount = amount.toString()
const amountInNumber = parseFloat(stringifiedAmount)
if (isNaN(amountInNumber)) return stringifiedAmount
return amountInNumber.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
// style: 'currency',
// currency: 'USD',
notation: "compact",
compactDisplay: "short",
signDisplay: "never",
...(options ? options : {}),
})
}
export const isValidEthereumAddress = (address: string): boolean => {
return isAddress(address)
}
export const removeSectionAndJson = (
message: string,
stringToRemove: string,
) => {
const index = message.indexOf(stringToRemove)
if (index !== -1) {
return message.substring(0, index)
}
return message
}
export const formatHours = (hours: number) => {
const days = Math.floor(hours / 24)
const remainingHours = hours % 24
return {
day: days > 0 ? `${days}` : "",
hour: `${remainingHours}`,
seconds: `${hours * 60 * 60}`,
}
}
export function convertToFullString(value: string | number): string {
const numberValue = typeof value === "string" ? Number(value) : value
return numberValue.toLocaleString("fullwide", {
useGrouping: false,
})
}
// Utility function to check if a string is a valid hex
const isValidHex = (value: string) => isHex(value, { strict: true })
export const isHexMatch = (
hexOrAddress1: Hex | Address | undefined,
hexOrAddress2: Hex | Address | undefined,
): boolean => {
if (!hexOrAddress1 || !hexOrAddress2) return false
// Trim whitespace and ensure lowercase for case-insensitive comparison
const normalize = (value: string) => value.trim().toLowerCase()
// Ensure both inputs are valid hex strings
if (!isValidHex(hexOrAddress1) || !isValidHex(hexOrAddress2)) return false
// Normalize inputs
const normalized1 = normalize(hexOrAddress1)
const normalized2 = normalize(hexOrAddress2)
// If both are already 42-character Ethereum addresses, compare directly
if (normalized1.length === 42 && normalized2.length === 42)
return normalized1 === normalized2
// Extract last 40 characters (20-byte address from hex) and normalize
const extractAddress = (hex: string) =>
hex.length >= 42 ? `0x${hex.slice(-40)}` : hex
return (
normalize(extractAddress(normalized1)) ===
normalize(extractAddress(normalized2))
)
}