brahma-trade-widget
Version:
A React component for trade automation within the Brahma ecosystem.
208 lines (182 loc) • 5.6 kB
text/typescript
import { readContracts } from "@wagmi/core"
import { Address, erc20Abi, zeroAddress } from "viem"
import multicallAbi from "../multicallAbi.json"
import {
MULTICALL_CONTRACT_ADDRESS,
SCAM_TOKEN_ADDRESSES,
SCAM_TOKEN_WORDS,
SUPPORTED_CHAINS_IDS,
} from "../constants"
import { SupportedChainIds, TAsset } from "../types"
import { formatUnits, sortAssets } from "./actions"
import { baseWagmiConfig as wagmiConfig } from "../wagmi"
type FetchContractBalance = {
accountAddress: Address
assets: TAsset[]
}
type TokenCall = {
abi: unknown
functionName: string
address?: Address
args?: readonly unknown[]
}
type CallType = {
result: bigint
status: "success" | "failure"
}
type TokenMultiCall = TokenCall[]
const MULTI_CALL_FUNCTIONS = ["balanceOf"]
const fetchAssetsBalanceMultiCall = async ({
accountAddress,
assets,
}: FetchContractBalance): Promise<TAsset[]> => {
try {
const filteredByDeployedChains = assets.filter((asset) => {
return SUPPORTED_CHAINS_IDS.includes(asset.chainId)
})
const getBalanceOfCall = (
tokenAddress: Address,
chainId: SupportedChainIds,
): TokenMultiCall => {
const erc20Config = {
abi: erc20Abi,
address: tokenAddress,
chainId,
} as const
return [
{
...erc20Config,
functionName: MULTI_CALL_FUNCTIONS[0],
args: [accountAddress],
},
]
}
const getETHBalanceOfCall = (
chainId: SupportedChainIds,
): TokenMultiCall => {
const callConfig = {
abi: multicallAbi,
address: MULTICALL_CONTRACT_ADDRESS,
chainId,
} as const
return [
{
...callConfig,
functionName: "getEthBalance",
args: [accountAddress],
},
]
}
const tokenCalls: TokenMultiCall = []
filteredByDeployedChains
.filter((asset) => asset.address.includes("0x"))
.forEach((token) => {
const tokenCall =
token.address.toLowerCase() === zeroAddress.toLowerCase()
? getETHBalanceOfCall(token.chainId)
: getBalanceOfCall(token.address, token.chainId)
tokenCall.forEach((call) => {
tokenCalls.push(call)
})
})
const multiCallResult = await readContracts(wagmiConfig, {
allowFailure: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
contracts: tokenCalls as any,
multicallAddress: MULTICALL_CONTRACT_ADDRESS,
batchSize: 0, // 0 is unlimited
})
const assetsWithBalance = []
for (
let i = 0;
i < multiCallResult.length;
i += MULTI_CALL_FUNCTIONS.length
) {
const assetWithBalance = formatMultiCallResult(
multiCallResult[i] as CallType,
filteredByDeployedChains[i],
)
assetsWithBalance.push(assetWithBalance)
}
return assetsWithBalance
} catch (err) {
console.error("[ERROR] on balances multiCall", err)
return []
}
}
const formatMultiCallResult = (
call: CallType,
selectedAsset: TAsset,
): TAsset => {
try {
const assetBalanceOf = call.result ? call.result : BigInt(0)
const asset: TAsset = {
...selectedAsset,
balanceOf: {
decimals: selectedAsset.decimals,
formatted: formatUnits(assetBalanceOf, selectedAsset.decimals),
symbol: selectedAsset.name,
value: assetBalanceOf,
},
}
return asset
} catch (err) {
console.error("[ERROR] on formatMultiCallResult", err)
return {
...selectedAsset,
prices: selectedAsset.prices || { default: 0 },
value: "0", // formatUnits(totalAssetValue, selectedAsset.decimals),
balanceOf: {
decimals: selectedAsset.decimals,
formatted: "0.0", //formatUnits(assetBalanceOf, selectedAsset.decimals),
symbol: selectedAsset.name,
value: BigInt(0), // assetBalanceOf
},
}
}
}
/**
* Fetches and filters asset balances for a given account address.
*
* @param {Address} accountAddress The address to fetch balances for.
* @param {TAsset[]} assets An array of asset objects to fetch balances for.
* @returns {Promise<TAsset[]>} A promise that resolves to an array of filtered asset objects.
* @throws {Error} If there is an error fetching the asset balances.
*/
export const fetchAndFilterAssetBalances = async (
accountAddress: Address,
assets: TAsset[],
): Promise<TAsset[]> => {
try {
const balances = await fetchAssetsBalanceMultiCall({
accountAddress: accountAddress as Address,
assets: assets,
})
const filteredUserAssets = balances.filter((asset) => {
const isNonZeroBalance =
!asset.balanceOf || asset.balanceOf.value !== BigInt(0)
const containsScamWords = SCAM_TOKEN_WORDS.some((scamWord) =>
asset.name.toLowerCase().includes(scamWord),
)
const containsScamAddresses = SCAM_TOKEN_ADDRESSES.includes(
asset.address.toLowerCase(),
)
return isNonZeroBalance && !containsScamWords && !containsScamAddresses
})
// Filter out duplicate assets based on address
const uniqueAssets = filteredUserAssets.reduce<TAsset[]>((acc, asset) => {
if (
!acc.find(
(a) => a.address.toLowerCase() === asset.address.toLowerCase(),
)
) {
acc.push(asset)
}
return acc
}, [])
return sortAssets(uniqueAssets)
} catch (err) {
console.error("Error fetching and filtering asset balances:", err)
throw err // Re-throw the error to be handled by the caller
}
}