diff --git a/web/components/Strategy.tsx b/web/components/Strategy.tsx index 684046c..119eaa8 100644 --- a/web/components/Strategy.tsx +++ b/web/components/Strategy.tsx @@ -29,6 +29,8 @@ import Image from "next/image"; import { pluralize } from "@/utils/pluralize"; import { findMostRepeatedString } from "@/utils/findMostRepeatedString"; import { useTweetShare } from "@/hooks/useTweetShare"; +import { BigNumber } from "ethers"; +import { parseUnits } from "ethers/lib/utils"; import clsx from "clsx"; export default function Strategy(props: { @@ -67,6 +69,11 @@ export default function Strategy(props: { const [selectedNetwork, setSelectedNetwork] = useState(networkName); + const { + tokens, + updateToken: updateToken, + selectedToken, + } = useToken(selectedNetwork); const { execute: executeDonation, getBalance, @@ -77,6 +84,7 @@ export default function Strategy(props: { props.fetchedStrategies, amount, selectedNetwork, + selectedToken, overwrites ); const [ @@ -86,14 +94,8 @@ export default function Strategy(props: { const [{ wallet }, connectWallet] = useConnectWallet(); const { data: session } = useSession(); const router = useRouter(); - const { - tokens, - updateToken: updateToken, - selectedToken, - } = useToken(selectedNetwork); - - const { strategies, handleAmountUpdate, handleNetworkUpdate } = - strategiesHandler; + + const { strategies, handleAmountUpdate, handleNetworkUpdate, handleTokenUpdate } = strategiesHandler; const selectedStrategiesLength = strategies.filter((x) => x.selected).length; const tweetUrl = useTweetShare( props.runId, @@ -104,6 +106,7 @@ export default function Strategy(props: { const [isFundingPending, setIsFundingPending] = useState(false); useEffect(() => { + handleTokenUpdate() setBalance((currentBalance) => { if (currentBalance) return null; }); @@ -117,7 +120,7 @@ export default function Strategy(props: { setIsFundingPending(true); try { - if (selectedStrategiesLength === 0 || amount === "0") return; + if (selectedStrategiesLength === 0 || !amount || amount === "0") return; const currentNetworkId = SUPPORTED_NETWORKS[selectedNetwork]; if (connectedChain && currentNetworkId !== +connectedChain.id) { @@ -131,10 +134,11 @@ export default function Strategy(props: { return; } - const balance = await getBalance(wallet, selectedToken); - - if (+amount >= +balance) { - setBalance(balance); + const fetchedBalance = await getBalance(wallet, selectedToken); + const amountInDecimals = parseUnits(amount, selectedToken.decimals) + const selectedStrategies = strategies.filter((x) => x.selected); + if (fetchedBalance.lt(amountInDecimals)) { + setBalance(fetchedBalance.toString()); setIsFundingPending(false); return; } @@ -145,33 +149,27 @@ export default function Strategy(props: { selectedNetwork ); - const donations = strategies - .filter((x) => x.selected) - .map((strategy) => { - const networkIndex = strategy.networks.indexOf(selectedNetwork); - return { - amount: strategy.amount as string, - description: strategy.project.description as string, - title: strategy.project.title as string, - recipient: strategy.recipients[networkIndex], - }; - }); - - const amounts = donations - .map((x) => Number(x.amount)) - .filter((x) => x > 0); + const amounts = selectedStrategies + .filter((x) => !!x.amount) + .map((x) => x.amount as string) - const totalAmount = amounts.reduce((a, b) => a + b, 0); + const recipientAddresses = selectedStrategies + .map((strategy) => strategy.recipients[strategy.networks.indexOf(selectedNetwork)]); - if (+allowance < totalAmount) { - await approve(wallet, selectedToken, totalAmount, selectedNetwork); + const totalAmount = amounts.reduce((a, b) => a.add(b), BigNumber.from(0)) + if (allowance.lt(totalAmount)) { + await approve( + wallet, + selectedToken, + totalAmount, + selectedNetwork + ); } - await executeDonation( selectedNetwork, selectedToken, - donations.map((x) => x.recipient), - amounts + recipientAddresses, + amounts.map(a => Number(a)) ); setShowSuccessModal(true); @@ -250,7 +248,7 @@ export default function Strategy(props: { - +
x.name !== selectedToken.name) .map((x) => ({ value: x.name }))} field={{ value: selectedToken.name }} - onChange={async (newToken) => - await updateToken(newToken) - } + onChange={updateToken} /> } value={amount !== "0" ? amount : undefined} diff --git a/web/components/StrategyTable.tsx b/web/components/StrategyTable.tsx index 440c95e..77edd5d 100644 --- a/web/components/StrategyTable.tsx +++ b/web/components/StrategyTable.tsx @@ -11,10 +11,11 @@ import { StrategyInformation, StrategiesHandler, } from "@/hooks/useStrategiesHandler"; -import { NetworkName } from "@/utils/ethereum"; +import { NetworkName, TokenInformation } from "@/utils/ethereum"; import { SparkleIcon } from "./Icons"; import { CaretRight } from "@phosphor-icons/react"; import { Tooltip } from "./Tooltip"; +import { formatUnits, parseUnits } from "ethers/lib/utils"; interface WeightInputProps { selected: boolean; @@ -88,7 +89,7 @@ function WeightInput({ ); } -export function StrategyTable(props: StrategiesHandler & { network: NetworkName }) { +export function StrategyTable(props: StrategiesHandler & { network: NetworkName, token: TokenInformation }) { const [{ wallet }] = useConnectWallet(); const { strategies, @@ -261,7 +262,7 @@ export function StrategyTable(props: StrategiesHandler & { network: NetworkName
{!!wallet && (
{`$${ - entry.amount || "0.00" + entry.amount ? Number(entry.amount) / 10 ** props.token.decimals : "0" }`}
)}
{ @@ -27,22 +26,14 @@ export function useDonation() { const signer = ethersProvider.getSigner(); - const token = getTokensForNetwork(selectedNetwork).find( - (t) => t.name == selectedToken.name - ); - - if (!token) { - throw new Error(`Token with name: ${selectedToken} is not valid`); - } - console.log(recipientAddresses, amounts, signer, token, selectedNetwork); + console.log(recipientAddresses, amounts.map(x => x.toString()), signer, token, network); await splitTransferFunds( recipientAddresses, amounts, signer, - selectedNetwork, - token.address, - token.decimals + network, + token.address ); }; @@ -55,11 +46,10 @@ export function useDonation() { const signer = ethersProvider.getSigner(); const tokenContract = new ethers.Contract(token.address, ERC20_ABI, signer); const currentAddress = await signer.getAddress(); - const balance = await tokenContract.balanceOf(currentAddress); - return ethers.utils.formatUnits(balance.toString(), token.decimals); + return await tokenContract.balanceOf(currentAddress); }; - const getAllowance = async (wallet: WalletState, token: TokenInformation, network: NetworkName) => { + const getAllowance = async (wallet: WalletState, token: TokenInformation, network: NetworkName): Promise => { const ethersProvider = new ethers.providers.Web3Provider( wallet.provider, "any" @@ -70,14 +60,13 @@ export function useDonation() { const currentAddress = await signer.getAddress(); const contractAddress = DISPERSE_CONTRACT_ADDRESSES[network]; - const balance = await tokenContract.allowance(currentAddress, contractAddress); - return ethers.utils.formatUnits(balance.toString(), token.decimals); + return await tokenContract.allowance(currentAddress, contractAddress); }; const approve = async ( wallet: WalletState, token: TokenInformation, - amount: number, + amount: BigNumber, network: NetworkName ) => { const ethersProvider = new ethers.providers.Web3Provider( @@ -88,8 +77,7 @@ export function useDonation() { const tokenContract = new ethers.Contract(token.address, ERC20_ABI, signer); const contractAddress = DISPERSE_CONTRACT_ADDRESSES[network]; - const amountInDecimals = ethers.utils.parseUnits(amount.toString(), token.decimals); - const approveTx = await tokenContract.approve(contractAddress, amountInDecimals); + const approveTx = await tokenContract.approve(contractAddress, amount); await approveTx.wait(1); }; diff --git a/web/hooks/useStrategiesHandler.ts b/web/hooks/useStrategiesHandler.ts index 60264fc..f9f432b 100644 --- a/web/hooks/useStrategiesHandler.ts +++ b/web/hooks/useStrategiesHandler.ts @@ -4,7 +4,7 @@ import { distributeWeights, redistributeWeights, } from "@/utils/distributeWeights"; -import { NetworkName } from "@/utils/ethereum"; +import { NetworkName, TokenInformation } from "@/utils/ethereum"; import { Dispatch, SetStateAction, useState } from "react"; export interface StrategiesHandler { @@ -16,6 +16,7 @@ export interface StrategiesHandler { handleSelectProject: (isChecked: boolean, index: number) => void; handleAmountUpdate: (amount: string) => void; handleNetworkUpdate: (network: NetworkName) => void; + handleTokenUpdate: () => void; } export type StrategyEntry = Tables<"strategy_entries">; @@ -31,7 +32,7 @@ export type StrategyInformation = StrategyEntry & { networks: NetworkName[]; disabled?: boolean; recipients: string[]; - weight?: number + weight?: number; }; export type StrategiesWithProjects = StrategyInformation[]; @@ -39,6 +40,7 @@ export function useStrategiesHandler( initStrategies: StrategiesWithProjects, totalAmount: string, networkName: NetworkName, + currentToken: TokenInformation, overwrites: { weights?: string[] | null; projects?: string[] | null; @@ -153,11 +155,16 @@ export function useStrategiesHandler( index, }); + const amounts = distributeWeights( + newPercentages.map((w) => +w), + +totalAmount, + currentToken.decimals + ).map((w) => (w / 100).toFixed(0)); const newStrategies = strategies.map((s, i) => { const weight = newPercentages[i] / 100; return { ...s, - amount: (+totalAmount * weight).toFixed(2), + amount: amounts[i], weight, selected: !(weight === 0), }; @@ -177,8 +184,8 @@ export function useStrategiesHandler( const amounts = distributeWeights( newWeights.map((w) => +w), +totalAmount, - 2 - ).map((w) => (w / 100).toFixed(2)); + currentToken.decimals + ).map((w) => (w / 100).toFixed(0)); const newStrategies = strategies.map((s, i) => { return { ...s, @@ -201,13 +208,16 @@ export function useStrategiesHandler( selectedWeights ); - const amounts = distributeWeights(newWeights, +totalAmount, 2); - + const amounts = distributeWeights( + newWeights, + +totalAmount, + currentToken.decimals + ); const newStrategy = strategies.map((s, i) => { return { ...s, weight: newWeights[i], - amount: amounts[i].toFixed(2), + amount: amounts[i].toFixed(0), selected: newWeights[i] > 0, }; }); @@ -220,11 +230,11 @@ export function useStrategiesHandler( const handleAmountUpdate = (amount: string) => { const selectedStrategies = strategies.filter((x) => x.selected); const weights = selectedStrategies.map((s) => s.weight) as number[]; - const amounts = distributeWeights(weights, +amount, 2); + const amounts = distributeWeights(weights, +amount, currentToken.decimals); let amountIndex = 0; const newStrategies = strategies.map((s) => ({ ...s, - amount: s.selected ? amounts[amountIndex++].toFixed(2) : undefined, + amount: s.selected ? amounts[amountIndex++].toFixed(0) : undefined, })); modifyStrategies(newStrategies); }; @@ -234,12 +244,16 @@ export function useStrategiesHandler( strategies.map((s) => s.defaultWeight), strategies.map((s) => s.networks.includes(network)) ); - const amounts = distributeWeights(weights, +totalAmount, 2); + const amounts = distributeWeights( + weights, + +totalAmount, + currentToken.decimals + ); const newStrategies = strategies .map((s, i) => { return { ...s, - amount: amounts[i].toFixed(2), + amount: amounts[i].toFixed(0), weight: weights[i], selected: weights[i] !== 0, disabled: !s.networks.includes(network), @@ -260,6 +274,19 @@ export function useStrategiesHandler( modifyStrategies(newStrategies); }; + const handleTokenUpdate = () => { + const amounts = distributeWeights( + strategies.map((s) => s.weight), + +totalAmount, + currentToken.decimals + ); + const newStrategies = strategies.map((s, i) => ({ + ...s, + amount: amounts[i].toFixed(0), + })); + modifyStrategies(newStrategies); + }; + return { strategies, formatted: { @@ -272,5 +299,6 @@ export function useStrategiesHandler( handleSelectProject, handleAmountUpdate, handleNetworkUpdate, + handleTokenUpdate, }; } diff --git a/web/utils/distributeWeights.test.ts b/web/utils/distributeWeights.test.ts index 8f4dbe6..7d70f07 100644 --- a/web/utils/distributeWeights.test.ts +++ b/web/utils/distributeWeights.test.ts @@ -42,7 +42,7 @@ describe("Distribute weights", () => { const newWeights = applyUserWeight(weights, [33.1, 0, 0, 0, 0, 0, 0, 0, 0], { percentage: 33.1, index: 0 }); const total = newWeights.reduce((acc, x) => acc + x); expect(newWeights[0]).toBe(33.1); - expect(newWeights[1]).toBe(11.982089552238806); + expect(newWeights[1]).toBe(11.9821); expect(newWeights[2]).toBe(0); expect(total.toFixed(0)).toBe("100"); }) diff --git a/web/utils/distributeWeights.ts b/web/utils/distributeWeights.ts index dc43d76..8ce90c3 100644 --- a/web/utils/distributeWeights.ts +++ b/web/utils/distributeWeights.ts @@ -1,38 +1,12 @@ -export function distributeWeights(weights: number[], total: number, decimals: number): number[] { - // Calculate initial amounts - let amounts = weights.map(weight => weight * total); - - // Round amounts to two decimals and calculate the sum of these amounts - let roundedAmounts = amounts.map(amount => parseFloat(amount.toFixed(decimals))); - let sumOfRoundedAmounts = roundedAmounts.reduce((a, b) => a + b, 0); - - // Calculate the remainder - let remainder = total - sumOfRoundedAmounts; - - // Distribute the remainder - // The idea here is to distribute the remainder starting from the largest fraction part - while (Math.abs(remainder) >= 0.01) { - let index = amounts.findIndex((amount, idx) => - roundedAmounts[idx] < amount && - (remainder > 0 || roundedAmounts[idx] > 0) - ); - - if (index === -1) { - break; // Break if no suitable item is found - } +export const WEIGHT_DECIMALS = 10000 - if (remainder > 0) { - roundedAmounts[index] += 0.01; - remainder -= 0.01; - } else { - roundedAmounts[index] -= 0.01; - remainder += 0.01; - } - - roundedAmounts[index] = parseFloat(roundedAmounts[index].toFixed(decimals)); - } - - return roundedAmounts; +export function distributeWeights(weights: number[], total: number, decimals: number): number[] { + // Weight always come as decimals, convert it to integer with four decimals + const intWeights = weights.map(w => Number((w * WEIGHT_DECIMALS).toPrecision(4))) + // Calculate the amount based on total + const amounts = intWeights.map(weight => weight * total); + // Convert back to number with two decimals + return amounts.map(amount => (amount / WEIGHT_DECIMALS) * 10 ** decimals); } export function redistributeWeights( @@ -95,11 +69,9 @@ export function applyUserWeight( if (weights[index] === 0) return 0; if (overwrites[index] !== 0) return overwrites[index]; - // If there is any weight set to zero it means that we can not take - // as base the default weights, so we work with the total and percentages only const amountToUpdate = total * percentage; return amountToUpdate / (total / 100); }); - return newOverwrites; + return newOverwrites.map((x) => Number(x.toPrecision(6))) } \ No newline at end of file diff --git a/web/utils/ethereum/splitTransferFunds.ts b/web/utils/ethereum/splitTransferFunds.ts index 6fa1b45..e78f6df 100644 --- a/web/utils/ethereum/splitTransferFunds.ts +++ b/web/utils/ethereum/splitTransferFunds.ts @@ -31,7 +31,6 @@ export async function splitTransferFunds( signer: ethers.Signer, selectedNetwork: NetworkName, tokenAddress?: string, - tokenDecimals?: number, ) { const disperseContract = new ethers.Contract( DISPERSE_CONTRACT_ADDRESSES[selectedNetwork], @@ -42,24 +41,21 @@ export async function splitTransferFunds( const validAddresses = addresses.filter((address) => ethers.utils.getAddress(address) ); - const values = amounts.map((amount) => - ethers.utils.parseUnits(amount.toString(), tokenDecimals) - ); - const totalValue = values.reduce( - (acc, value) => acc.add(value), + const totalValue = amounts.reduce( + (acc, value) => ethers.BigNumber.from(acc).add(value), ethers.constants.Zero ); if (!tokenAddress || tokenAddress === ethers.constants.AddressZero) { // Ether transfer - await disperseContract.disperseEther(validAddresses, values, { + await disperseContract.disperseEther(validAddresses, amounts.map(a => a.toString()), { value: totalValue, }); } else { const transferTx = await disperseContract.disperseTokenSimple( tokenAddress, validAddresses, - values + amounts.map(a => a.toString()) ); await transferTx.wait(1); }