diff --git a/.changeset/selfish-eggs-hide.md b/.changeset/selfish-eggs-hide.md new file mode 100644 index 0000000..8065cb4 --- /dev/null +++ b/.changeset/selfish-eggs-hide.md @@ -0,0 +1,5 @@ +--- +"@folks-finance/xchain-sdk": patch +--- + +deposit boost support diff --git a/.gitignore b/.gitignore index 6c5b896..6f48981 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ yarn-error.log* # local env files .env .env*.local + +# intellij +.idea diff --git a/src/chains/evm/common/constants/contract.ts b/src/chains/evm/common/constants/contract.ts index 4870a0b..a3636ec 100644 --- a/src/chains/evm/common/constants/contract.ts +++ b/src/chains/evm/common/constants/contract.ts @@ -10,3 +10,6 @@ export const GAS_LIMIT_ESTIMATE_INCREASE = 10_000n; export const SEND_TOKEN_ACTION_RETURN_GAS_LIMIT = 500_000n; export const RECEIVER_VALUE_SLIPPAGE = 0.01; export const RETRY_REVERSE_GAS_LIMIT_SLIPPAGE = 0.05; +export const UPDATE_USER_POINTS_IN_LOANS_GAS_LIMIT_SLIPPAGE = 0.1; +export const UPDATE_ACCOUNT_POINTS_FOR_REWARDS_GAS_LIMIT_SLIPPAGE = 0.01; +export const CLAIM_REWARDS_GAS_LIMIT_SLIPPAGE = 0; diff --git a/src/chains/evm/common/types/module.ts b/src/chains/evm/common/types/module.ts index d09651a..7287ba5 100644 --- a/src/chains/evm/common/types/module.ts +++ b/src/chains/evm/common/types/module.ts @@ -1,8 +1,9 @@ import type { MessageReceived } from "./gmp.js"; -import type { GenericAddress } from "../../../../common/types/address.js"; +import type { EvmAddress, GenericAddress } from "../../../../common/types/address.js"; import type { AccountId } from "../../../../common/types/lending.js"; import type { MessageParams } from "../../../../common/types/message.js"; import type { SpokeTokenData } from "../../../../common/types/token.js"; +import type { PoolEpoch } from "../../hub/types/rewards.js"; import type { Hex } from "viem"; export type PrepareCall = { @@ -103,3 +104,18 @@ export type PrepareResendWormholeMessageCall = { deliveryProviderAddress: GenericAddress; wormholeRelayerAddress: GenericAddress; } & Omit; + +export type PrepareUpdateUserPointsInLoans = { + loanManagerAddress: GenericAddress; +} & Omit; + +export type PrepareUpdateAccountsPointsForRewardsCall = { + poolEpochs: Array; + rewardsV1Address: GenericAddress; +} & Omit; + +export type PrepareClaimRewardsCall = { + poolEpochs: Array; + receiver: EvmAddress; + rewardsV1Address: GenericAddress; +} & Omit; diff --git a/src/chains/evm/hub/constants/abi/rewards-v1-abi.ts b/src/chains/evm/hub/constants/abi/rewards-v1-abi.ts new file mode 100644 index 0000000..ce0d5f3 --- /dev/null +++ b/src/chains/evm/hub/constants/abi/rewards-v1-abi.ts @@ -0,0 +1,638 @@ +export const RewardsV1Abi = [ + { + inputs: [ + { internalType: "address", name: "admin", type: "address" }, + { + internalType: "contract IAccountManager", + name: "accountManager_", + type: "address", + }, + { + internalType: "contract LoanManager", + name: "loanManager_", + type: "address", + }, + { internalType: "uint16", name: "hubChainId_", type: "uint16" }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { inputs: [], name: "AccessControlBadConfirmation", type: "error" }, + { + inputs: [{ internalType: "uint48", name: "schedule", type: "uint48" }], + name: "AccessControlEnforcedDefaultAdminDelay", + type: "error", + }, + { inputs: [], name: "AccessControlEnforcedDefaultAdminRules", type: "error" }, + { + inputs: [{ internalType: "address", name: "defaultAdmin", type: "address" }], + name: "AccessControlInvalidDefaultAdmin", + type: "error", + }, + { + inputs: [ + { internalType: "address", name: "account", type: "address" }, + { internalType: "bytes32", name: "neededRole", type: "bytes32" }, + ], + name: "AccessControlUnauthorizedAccount", + type: "error", + }, + { + inputs: [ + { internalType: "uint8", name: "poolId", type: "uint8" }, + { internalType: "uint16", name: "epoch", type: "uint16" }, + { internalType: "uint256", name: "expired", type: "uint256" }, + ], + name: "CannotUpdateExpiredEpoch", + type: "error", + }, + { + inputs: [ + { internalType: "uint8", name: "poolId", type: "uint8" }, + { internalType: "uint16", name: "epochIndex", type: "uint16" }, + ], + name: "EpochNotActive", + type: "error", + }, + { + inputs: [ + { internalType: "uint8", name: "poolId", type: "uint8" }, + { internalType: "uint16", name: "epoch", type: "uint16" }, + { internalType: "uint256", name: "end", type: "uint256" }, + ], + name: "EpochNotEnded", + type: "error", + }, + { + inputs: [ + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "FailedToClaimRewards", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "length", type: "uint256" }, + { internalType: "uint256", name: "minimum", type: "uint256" }, + ], + name: "InvalidEpochLength", + type: "error", + }, + { + inputs: [ + { internalType: "uint8", name: "poolId", type: "uint8" }, + { internalType: "uint256", name: "previousEpochEnd", type: "uint256" }, + { internalType: "uint256", name: "newEpochStart", type: "uint256" }, + ], + name: "InvalidEpochStart", + type: "error", + }, + { inputs: [], name: "MathOverflowedMulDiv", type: "error" }, + { + inputs: [ + { internalType: "bytes32", name: "accountId", type: "bytes32" }, + { internalType: "address", name: "addr", type: "address" }, + ], + name: "NoPermissionOnHub", + type: "error", + }, + { + inputs: [ + { internalType: "uint8", name: "bits", type: "uint8" }, + { internalType: "uint256", name: "value", type: "uint256" }, + ], + name: "SafeCastOverflowedUintDowncast", + type: "error", + }, + { + anonymous: false, + inputs: [], + name: "DefaultAdminDelayChangeCanceled", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint48", + name: "newDelay", + type: "uint48", + }, + { + indexed: false, + internalType: "uint48", + name: "effectSchedule", + type: "uint48", + }, + ], + name: "DefaultAdminDelayChangeScheduled", + type: "event", + }, + { + anonymous: false, + inputs: [], + name: "DefaultAdminTransferCanceled", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "newAdmin", + type: "address", + }, + { + indexed: false, + internalType: "uint48", + name: "acceptSchedule", + type: "uint48", + }, + ], + name: "DefaultAdminTransferScheduled", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "uint8", name: "poolId", type: "uint8" }, + { + indexed: false, + internalType: "uint256", + name: "start", + type: "uint256", + }, + { indexed: false, internalType: "uint256", name: "end", type: "uint256" }, + { + indexed: false, + internalType: "uint256", + name: "totalRewards", + type: "uint256", + }, + { + indexed: false, + internalType: "uint16", + name: "epochIndex", + type: "uint16", + }, + ], + name: "EpochAdded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "uint8", name: "poolId", type: "uint8" }, + { + indexed: false, + internalType: "uint16", + name: "epochIndex", + type: "uint16", + }, + { + indexed: false, + internalType: "uint256", + name: "totalRewards", + type: "uint256", + }, + ], + name: "EpochUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "Funded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "bytes32", + name: "accountId", + type: "bytes32", + }, + { + indexed: false, + internalType: "address", + name: "receiver", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "RewardsClaimed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "bytes32", + name: "previousAdminRole", + type: "bytes32", + }, + { + indexed: true, + internalType: "bytes32", + name: "newAdminRole", + type: "bytes32", + }, + ], + name: "RoleAdminChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RoleGranted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RoleRevoked", + type: "event", + }, + { + inputs: [], + name: "DEFAULT_ADMIN_ROLE", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "LISTING_ROLE", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "acceptDefaultAdminTransfer", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "accountId", type: "bytes32" }, + { internalType: "uint8", name: "poolId", type: "uint8" }, + { internalType: "uint16", name: "epochIndex", type: "uint16" }, + ], + name: "accountEpochPoints", + outputs: [{ internalType: "uint256", name: "points", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "accountId", type: "bytes32" }, + { internalType: "uint8", name: "poolId", type: "uint8" }, + ], + name: "accountLastUpdatedPoints", + outputs: [{ internalType: "uint256", name: "points", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "accountManager", + outputs: [{ internalType: "contract IAccountManager", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint8", name: "poolId", type: "uint8" }, + { internalType: "uint256", name: "start", type: "uint256" }, + { internalType: "uint256", name: "end", type: "uint256" }, + { internalType: "uint256", name: "totalRewards", type: "uint256" }, + ], + name: "addEpoch", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newAdmin", type: "address" }], + name: "beginDefaultAdminTransfer", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "cancelDefaultAdminTransfer", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint48", name: "newDelay", type: "uint48" }], + name: "changeDefaultAdminDelay", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "accountId", type: "bytes32" }, + { + components: [ + { internalType: "uint8", name: "poolId", type: "uint8" }, + { internalType: "uint16", name: "epochIndex", type: "uint16" }, + ], + internalType: "struct RewardsV1.PoolEpoch[]", + name: "poolEpochsToClaim", + type: "tuple[]", + }, + { internalType: "address", name: "receiver", type: "address" }, + ], + name: "claimRewards", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "defaultAdmin", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "defaultAdminDelay", + outputs: [{ internalType: "uint48", name: "", type: "uint48" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "defaultAdminDelayIncreaseWait", + outputs: [{ internalType: "uint48", name: "", type: "uint48" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "fund", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [{ internalType: "uint8", name: "poolId", type: "uint8" }], + name: "getActiveEpoch", + outputs: [ + { internalType: "uint16", name: "epochIndex", type: "uint16" }, + { + components: [ + { internalType: "uint256", name: "start", type: "uint256" }, + { internalType: "uint256", name: "end", type: "uint256" }, + { internalType: "uint256", name: "totalRewards", type: "uint256" }, + ], + internalType: "struct RewardsV1.Epoch", + name: "epoch", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "role", type: "bytes32" }], + name: "getRoleAdmin", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "accountId", type: "bytes32" }, + { + components: [ + { internalType: "uint8", name: "poolId", type: "uint8" }, + { internalType: "uint16", name: "epochIndex", type: "uint16" }, + ], + internalType: "struct RewardsV1.PoolEpoch[]", + name: "poolEpochsToClaim", + type: "tuple[]", + }, + ], + name: "getUnclaimedRewards", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "grantRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "hasRole", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "hubChainId", + outputs: [{ internalType: "uint16", name: "", type: "uint16" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "loanManager", + outputs: [{ internalType: "contract LoanManager", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pendingDefaultAdmin", + outputs: [ + { internalType: "address", name: "newAdmin", type: "address" }, + { internalType: "uint48", name: "schedule", type: "uint48" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pendingDefaultAdminDelay", + outputs: [ + { internalType: "uint48", name: "newDelay", type: "uint48" }, + { internalType: "uint48", name: "schedule", type: "uint48" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint8", name: "poolId", type: "uint8" }], + name: "poolEpochIndex", + outputs: [{ internalType: "uint16", name: "epochIndex", type: "uint16" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint8", name: "poolId", type: "uint8" }, + { internalType: "uint16", name: "epochIndex", type: "uint16" }, + ], + name: "poolEpochs", + outputs: [ + { internalType: "uint256", name: "start", type: "uint256" }, + { internalType: "uint256", name: "end", type: "uint256" }, + { internalType: "uint256", name: "totalRewards", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint8", name: "poolId", type: "uint8" }, + { internalType: "uint16", name: "epochIndex", type: "uint16" }, + ], + name: "poolTotalEpochPoints", + outputs: [{ internalType: "uint256", name: "points", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "renounceRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "revokeRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "rollbackDefaultAdminDelay", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes4", name: "interfaceId", type: "bytes4" }], + name: "supportsInterface", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32[]", name: "accountIds", type: "bytes32[]" }, + { + components: [ + { internalType: "uint8", name: "poolId", type: "uint8" }, + { internalType: "uint16", name: "epochIndex", type: "uint16" }, + ], + internalType: "struct RewardsV1.PoolEpoch[]", + name: "poolEpochsToUpdate", + type: "tuple[]", + }, + ], + name: "updateAccountPoints", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "uint8", name: "poolId", type: "uint8" }, + { internalType: "uint16", name: "epochIndex", type: "uint16" }, + ], + internalType: "struct RewardsV1.PoolEpoch", + name: "poolEpoch", + type: "tuple", + }, + { internalType: "uint256", name: "totalRewards", type: "uint256" }, + ], + name: "updateEpochTotalRewards", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/src/chains/evm/hub/constants/chain.ts b/src/chains/evm/hub/constants/chain.ts index 31b6802..037bdec 100644 --- a/src/chains/evm/hub/constants/chain.ts +++ b/src/chains/evm/hub/constants/chain.ts @@ -213,6 +213,10 @@ export const HUB_CHAIN: Record = { supportedLoanTypes: new Set([LoanTypeId.DEPOSIT, LoanTypeId.GENERAL]), }, } satisfies Record, + rewardsV1Address: convertToGenericAddress( + "0x7c532A6209350cF27EfC3D06E82E35ACFd362C7C" as EvmAddress, + ChainType.EVM, + ), }, [NetworkType.TESTNET]: { folksChainId: FOLKS_CHAIN_ID.AVALANCHE_FUJI, @@ -345,5 +349,9 @@ export const HUB_CHAIN: Record = { supportedLoanTypes: new Set([LoanTypeId.DEPOSIT, LoanTypeId.GENERAL]), }, } satisfies Record, + rewardsV1Address: convertToGenericAddress( + "0xB8Aa9782d5922B00fC63e7def85F276059B4aCd0" as EvmAddress, + ChainType.EVM, + ), }, }; diff --git a/src/chains/evm/hub/modules/folks-hub-rewards.ts b/src/chains/evm/hub/modules/folks-hub-rewards.ts index 4d19507..3283f6d 100644 --- a/src/chains/evm/hub/modules/folks-hub-rewards.ts +++ b/src/chains/evm/hub/modules/folks-hub-rewards.ts @@ -1,26 +1,306 @@ import { multicall } from "viem/actions"; import { calcAccruedRewards } from "../../../../common/utils/formulae.js"; +import { increaseByPercent, unixTime } from "../../../../common/utils/math-lib.js"; +import { + CLAIM_REWARDS_GAS_LIMIT_SLIPPAGE, + UPDATE_ACCOUNT_POINTS_FOR_REWARDS_GAS_LIMIT_SLIPPAGE, + UPDATE_USER_POINTS_IN_LOANS_GAS_LIMIT_SLIPPAGE, +} from "../../common/constants/contract.js"; +import { getEvmSignerAccount } from "../../common/utils/chain.js"; import { getHubChain } from "../utils/chain.js"; -import { getLoanManagerContract } from "../utils/contract.js"; +import { getLoanManagerContract, getRewardsV1Contract } from "../utils/contract.js"; import { getUserLoans } from "./folks-hub-loan.js"; +import type { EvmAddress } from "../../../../common/types/address.js"; import type { NetworkType } from "../../../../common/types/chain.js"; import type { AccountId, LoanId } from "../../../../common/types/lending.js"; import type { LoanTypeId } from "../../../../common/types/module.js"; import type { FolksTokenId } from "../../../../common/types/token.js"; +import type { + PrepareClaimRewardsCall, + PrepareUpdateAccountsPointsForRewardsCall, + PrepareUpdateUserPointsInLoans, +} from "../../common/types/module.js"; +import type { RewardsV1Abi } from "../constants/abi/rewards-v1-abi.js"; +import type { HubChain } from "../types/chain.js"; import type { LoanTypeInfo } from "../types/loan.js"; -import type { AccountPoolRewards, UserRewards } from "../types/rewards.js"; -import type { Client, ContractFunctionParameters } from "viem"; +import type { + PoolsPoints, + ActiveEpochs, + PoolEpoch, + UserPoints, + LastUpdatedPointsForRewards, + Epochs, + Epoch, +} from "../types/rewards.js"; +import type { HubTokenData } from "../types/token.js"; +import type { + Client, + ContractFunctionParameters, + EstimateGasParameters, + ReadContractReturnType, + WalletClient, +} from "viem"; -export async function getUserRewards( +function getActivePoolEpochs(activeEpochs: ActiveEpochs): Array { + return Object.values(activeEpochs).map(({ poolId, epochIndex }) => ({ poolId, epochIndex })); +} + +function getHistoricalPoolEpochs(historicalEpochs: Epochs, ignoreZeroTotalRewards = true): Array { + const poolEpochs: Array = []; + for (const historicalPoolEpochs of Object.values(historicalEpochs)) { + for (const { poolId, epochIndex, totalRewards } of historicalPoolEpochs) { + if (!ignoreZeroTotalRewards || totalRewards > 0n) poolEpochs.push({ poolId, epochIndex }); + } + } + return poolEpochs; +} + +export const prepare = { + async updateUserPointsInLoans( + provider: Client, + sender: EvmAddress, + loanIds: Array, + hubChain: HubChain, + transactionOptions: EstimateGasParameters = { + account: sender, + }, + ): Promise { + const loanManager = getLoanManagerContract(provider, hubChain.loanManagerAddress); + + const gasLimit = await loanManager.estimateGas.updateUserLoansPoolsRewards([loanIds], { + ...transactionOptions, + value: undefined, + }); + + return { + gasLimit: increaseByPercent(gasLimit, UPDATE_USER_POINTS_IN_LOANS_GAS_LIMIT_SLIPPAGE), + loanManagerAddress: hubChain.loanManagerAddress, + }; + }, + + async updateAccountsPointsForRewards( + provider: Client, + sender: EvmAddress, + hubChain: HubChain, + accountIds: Array, + activeEpochs: ActiveEpochs, + transactionOptions: EstimateGasParameters = { + account: sender, + }, + ): Promise { + const poolEpochs = getActivePoolEpochs(activeEpochs); + const rewardsV1 = getRewardsV1Contract(provider, hubChain.rewardsV1Address); + + const gasLimit = await rewardsV1.estimateGas.updateAccountPoints([accountIds, poolEpochs], { + ...transactionOptions, + value: undefined, + }); + + return { + gasLimit: increaseByPercent(gasLimit, UPDATE_ACCOUNT_POINTS_FOR_REWARDS_GAS_LIMIT_SLIPPAGE), + poolEpochs, + rewardsV1Address: hubChain.rewardsV1Address, + }; + }, + + async claimRewards( + provider: Client, + sender: EvmAddress, + hubChain: HubChain, + accountId: AccountId, + historicalEpochs: Epochs, + transactionOptions: EstimateGasParameters = { + account: sender, + }, + ): Promise { + const poolEpochs = getHistoricalPoolEpochs(historicalEpochs); + const rewardsV1 = getRewardsV1Contract(provider, hubChain.rewardsV1Address); + + const gasLimit = await rewardsV1.estimateGas.claimRewards([accountId, poolEpochs, sender], { + ...transactionOptions, + value: undefined, + }); + + return { + gasLimit: increaseByPercent(gasLimit, CLAIM_REWARDS_GAS_LIMIT_SLIPPAGE), + poolEpochs, + receiver: sender, + rewardsV1Address: hubChain.rewardsV1Address, + }; + }, +}; + +export const write = { + async updateUserPointsInLoans( + provider: Client, + signer: WalletClient, + loanIds: Array, + prepareCall: PrepareUpdateUserPointsInLoans, + ) { + const { gasLimit, loanManagerAddress } = prepareCall; + + const loanManager = getLoanManagerContract(provider, loanManagerAddress, signer); + + return await loanManager.write.updateUserLoansPoolsRewards([loanIds], { + account: getEvmSignerAccount(signer), + chain: signer.chain, + gas: gasLimit, + }); + }, + + async updateAccountsPointsForRewards( + provider: Client, + signer: WalletClient, + accountIds: Array, + prepareCall: PrepareUpdateAccountsPointsForRewardsCall, + ) { + const { gasLimit, poolEpochs, rewardsV1Address } = prepareCall; + + const rewardsV1 = getRewardsV1Contract(provider, rewardsV1Address, signer); + + return await rewardsV1.write.updateAccountPoints([accountIds, poolEpochs], { + account: getEvmSignerAccount(signer), + chain: signer.chain, + gas: gasLimit, + }); + }, + + async claimRewards( + provider: Client, + signer: WalletClient, + accountId: AccountId, + prepareCall: PrepareClaimRewardsCall, + ) { + const { gasLimit, poolEpochs, receiver, rewardsV1Address } = prepareCall; + + const rewardsV1 = getRewardsV1Contract(provider, rewardsV1Address, signer); + + return await rewardsV1.write.claimRewards([accountId, poolEpochs, receiver], { + account: getEvmSignerAccount(signer), + chain: signer.chain, + gas: gasLimit, + }); + }, +}; + +export async function getHistoricalEpochs( + provider: Client, + network: NetworkType, + tokens: Array, +): Promise { + const hubChain = getHubChain(network); + const rewardsV1 = getRewardsV1Contract(provider, hubChain.rewardsV1Address); + + // get latest pool epoch indexes + const poolEpochIndexes = (await multicall(provider, { + contracts: tokens.map(({ poolId }) => ({ + address: rewardsV1.address, + abi: rewardsV1.abi, + functionName: "poolEpochIndex", + args: [poolId], + })), + allowFailure: false, + })) as Array>; + + const latestPoolEpochIndexes: Partial> = {}; + for (const [i, epochIndex] of poolEpochIndexes.entries()) { + const { folksTokenId } = tokens[i]; + latestPoolEpochIndexes[folksTokenId] = epochIndex; + } + + // get all pool epochs + const getPoolEpochs: Array = []; + for (const { folksTokenId, poolId } of tokens) { + const latestPoolEpochIndex = latestPoolEpochIndexes[folksTokenId] ?? 0; + for (let epochIndex = 1; epochIndex <= latestPoolEpochIndex; epochIndex++) { + getPoolEpochs.push({ + address: rewardsV1.address, + abi: rewardsV1.abi, + functionName: "poolEpochs", + args: [poolId, epochIndex], + }); + } + } + + const poolEpochs = (await multicall(provider, { + contracts: getPoolEpochs, + allowFailure: false, + })) as Array>; + + // create historical epochs + const currTimestamp = BigInt(unixTime()); + let indexIntoPoolEpoch = 0; + const historicalEpochs: Epochs = {}; + for (const { folksTokenId, poolId } of tokens) { + const historicalPoolEpochs: Array = []; + const latestPoolEpochIndex = latestPoolEpochIndexes[folksTokenId] ?? 0; + for (let epochIndex = 1; epochIndex <= latestPoolEpochIndex; epochIndex++) { + const [startTimestamp, endTimestamp, totalRewards] = poolEpochs[indexIntoPoolEpoch]; + indexIntoPoolEpoch++; + if (endTimestamp < currTimestamp) + historicalPoolEpochs.push({ poolId, epochIndex, startTimestamp, endTimestamp, totalRewards }); + } + historicalEpochs[folksTokenId] = historicalPoolEpochs; + } + + return historicalEpochs; +} + +export async function getActiveEpochs( + provider: Client, + network: NetworkType, + tokens: Array, +): Promise { + const hubChain = getHubChain(network); + const rewardsV1 = getRewardsV1Contract(provider, hubChain.rewardsV1Address); + + const getActiveEpochs: Array = tokens.map(({ poolId }) => ({ + address: rewardsV1.address, + abi: rewardsV1.abi, + functionName: "getActiveEpoch", + args: [poolId], + })); + + const maybeActiveEpochs = await multicall(provider, { + contracts: getActiveEpochs, + allowFailure: true, + }); + + const activeEpochs: ActiveEpochs = {}; + for (const [i, result] of maybeActiveEpochs.entries()) { + const { folksTokenId, poolId } = tokens[i]; + if (result.status === "success") { + const [epochIndex, { start: startTimestamp, end: endTimestamp, totalRewards }] = + result.result as ReadContractReturnType; + activeEpochs[folksTokenId] = { poolId, epochIndex, startTimestamp, endTimestamp, totalRewards }; + } + } + return activeEpochs; +} + +export async function getUnclaimedRewards( + provider: Client, + network: NetworkType, + accountId: AccountId, + historicalEpochs: Epochs, +): Promise { + const hubChain = getHubChain(network); + const rewardsV1 = getRewardsV1Contract(provider, hubChain.rewardsV1Address); + + const poolEpochs = getHistoricalPoolEpochs(historicalEpochs); + return await rewardsV1.read.getUnclaimedRewards([accountId, poolEpochs]); +} + +export async function getUserPoints( provider: Client, network: NetworkType, accountId: AccountId, loanIds: Array, loanTypesInfo: Partial>, -): Promise { +): Promise { const hubChain = getHubChain(network); const loanManager = getLoanManagerContract(provider, hubChain.loanManagerAddress); @@ -49,18 +329,18 @@ export async function getUserRewards( }); } - const accountPoolRewards: Array = (await multicall(provider, { + const accountPoolRewards: Array = (await multicall(provider, { contracts: getUsersPoolRewards, allowFailure: false, - })) as Array; + })) as Array; // initialise with all the rewards which are updated - const rewards: Partial> = {}; + const rewards: Partial> = {}; for (const [i, accountPoolReward] of accountPoolRewards.entries()) { const folksTokenId = folksTokenIds[i]; rewards[folksTokenId] = accountPoolReward; } - const userRewards: UserRewards = { accountId, rewards }; + const userRewards: UserPoints = { accountId, poolsPoints: rewards }; // add all the rewards which are not updated const userLoans = await getUserLoans(provider, network, loanIds); @@ -92,7 +372,7 @@ export async function getUserRewards( const accrued = calcAccruedRewards(balance, collateralRewardIndex, [rewardIndex, 18]); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - userRewards.rewards[folksTokenId]!.collateral += accrued; + userRewards.poolsPoints[folksTokenId]!.collateral += accrued; } for (const [i, poolId] of borPools.entries()) { @@ -106,9 +386,32 @@ export async function getUserRewards( const accrued = calcAccruedRewards(amount, borrowRewardIndex, [rewardIndex, 18]); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - userRewards.rewards[folksTokenId]!.collateral += accrued; + userRewards.poolsPoints[folksTokenId]!.collateral += accrued; } } return userRewards; } + +export async function lastUpdatedPointsForRewards( + provider: Client, + network: NetworkType, + accountId: AccountId, + activeEpochs: ActiveEpochs, +): Promise { + const hubChain = getHubChain(network); + const rewardsV1 = getRewardsV1Contract(provider, hubChain.rewardsV1Address); + + const entries = await Promise.all( + Object.entries(activeEpochs).map(async ([folksTokenId, { poolId, epochIndex }]) => { + const [lastWrittenPoints, writtenEpochPoints] = await Promise.all([ + rewardsV1.read.accountLastUpdatedPoints([accountId, poolId]), + rewardsV1.read.accountEpochPoints([accountId, poolId, epochIndex]), + ]); + + return [folksTokenId as FolksTokenId, { lastWrittenPoints, writtenEpochPoints }] as const; + }), + ); + + return Object.fromEntries(entries); +} diff --git a/src/chains/evm/hub/types/chain.ts b/src/chains/evm/hub/types/chain.ts index a37e26f..43149d2 100644 --- a/src/chains/evm/hub/types/chain.ts +++ b/src/chains/evm/hub/types/chain.ts @@ -13,4 +13,5 @@ export type HubChain = { accountManagerAddress: GenericAddress; loanManagerAddress: GenericAddress; tokens: Partial>; + rewardsV1Address: GenericAddress; } & IFolksChain; diff --git a/src/chains/evm/hub/types/rewards.ts b/src/chains/evm/hub/types/rewards.ts index 644ebbe..b02a3ce 100644 --- a/src/chains/evm/hub/types/rewards.ts +++ b/src/chains/evm/hub/types/rewards.ts @@ -1,15 +1,50 @@ import type { AccountId } from "../../../../common/types/lending.js"; import type { FolksTokenId } from "../../../../common/types/token.js"; +import type { Dnum } from "dnum"; -export type AccountPoolRewards = { +export type Epoch = { + poolId: number; + epochIndex: number; + startTimestamp: bigint; + endTimestamp: bigint; + totalRewards: bigint; +}; + +export type PoolEpoch = { + poolId: number; + epochIndex: number; +}; + +export type Epochs = Partial>>; + +export type ActiveEpochs = Partial>; + +export type ActiveEpochInfo = { + remainingRewards: bigint; + rewardsApr: Dnum; +} & Epoch; + +export type ActiveEpochsInfo = Partial>; + +export type PendingRewards = Partial>; + +export type PoolsPoints = { collateral: bigint; borrow: bigint; interestPaid: bigint; }; -export type AccountRewards = Partial>; - -export type UserRewards = { +export type UserPoints = { accountId: AccountId; - rewards: AccountRewards; + poolsPoints: Partial>; }; + +export type LastUpdatedPointsForRewards = Partial< + Record< + FolksTokenId, + { + lastWrittenPoints: bigint; + writtenEpochPoints: bigint; + } + > +>; diff --git a/src/chains/evm/hub/utils/contract.ts b/src/chains/evm/hub/utils/contract.ts index 7392db9..86f3a57 100644 --- a/src/chains/evm/hub/utils/contract.ts +++ b/src/chains/evm/hub/utils/contract.ts @@ -8,6 +8,7 @@ import { HubAbi } from "../constants/abi/hub-abi.js"; import { HubPoolAbi } from "../constants/abi/hub-pool-abi.js"; import { LoanManagerAbi } from "../constants/abi/loan-manager-abi.js"; import { OracleManagerAbi } from "../constants/abi/oracle-manager-abi.js"; +import { RewardsV1Abi } from "../constants/abi/rewards-v1-abi.js"; import type { GenericAddress } from "../../../../common/types/address.js"; import type { GetReadContractReturnType } from "../../common/types/contract.js"; @@ -60,11 +61,21 @@ export function getHubPoolContract( export function getLoanManagerContract( provider: Client, address: GenericAddress, -): GetReadContractReturnType { +): GetReadContractReturnType; +export function getLoanManagerContract( + provider: Client, + address: GenericAddress, + signer: WalletClient, +): GetContractReturnType; +export function getLoanManagerContract( + provider: Client, + address: GenericAddress, + signer?: WalletClient, +): GetReadContractReturnType | GetContractReturnType { return getContract({ abi: LoanManagerAbi, address: convertFromGenericAddress(address, ChainType.EVM), - client: { public: provider }, + client: { wallet: signer, public: provider }, }); } @@ -79,6 +90,27 @@ export function getOracleManagerContract( }); } +export function getRewardsV1Contract( + provider: Client, + address: GenericAddress, +): GetReadContractReturnType; +export function getRewardsV1Contract( + provider: Client, + address: GenericAddress, + signer: WalletClient, +): GetContractReturnType; +export function getRewardsV1Contract( + provider: Client, + address: GenericAddress, + signer?: WalletClient, +): GetReadContractReturnType | GetContractReturnType { + return getContract({ + abi: RewardsV1Abi, + address: convertFromGenericAddress(address, ChainType.EVM), + client: { wallet: signer, public: provider }, + }); +} + export function getHubContract(provider: Client, address: GenericAddress): GetReadContractReturnType; export function getHubContract( provider: Client, diff --git a/src/common/types/module.ts b/src/common/types/module.ts index c1f2e37..bca4794 100644 --- a/src/common/types/module.ts +++ b/src/common/types/module.ts @@ -17,6 +17,9 @@ import type { PrepareRetryMessageCall as PrepareRetryMessageEVMCall, PrepareReverseMessageCall as PrepareReverseMessageEVMCall, PrepareResendWormholeMessageCall as PrepareResendWormholeMessageEVMCall, + PrepareUpdateUserPointsInLoans as PrepareUpdateUserLoanPoolEVMPoints, + PrepareUpdateAccountsPointsForRewardsCall as PrepareUpdateAccountsPointsForRewardsEVMCall, + PrepareClaimRewardsCall as PrepareClaimRewardsEVMCall, } from "../../chains/evm/common/types/module.js"; export enum LoanTypeId { @@ -44,3 +47,6 @@ export type PrepareLiquidateCall = PrepareLiquidateEVMCall; export type PrepareRetryMessageCall = PrepareRetryMessageEVMCall; export type PrepareReverseMessageCall = PrepareReverseMessageEVMCall; export type PrepareResendWormholeMessageCall = PrepareResendWormholeMessageEVMCall; +export type PrepareUpdateUserLoanPoolPoints = PrepareUpdateUserLoanPoolEVMPoints; +export type PrepareUpdateAccountsPointsForRewardsCall = PrepareUpdateAccountsPointsForRewardsEVMCall; +export type PrepareClaimRewardsCall = PrepareClaimRewardsEVMCall; diff --git a/src/xchain/modules/folks-loan.ts b/src/xchain/modules/folks-loan.ts index 1125884..cd294a9 100644 --- a/src/xchain/modules/folks-loan.ts +++ b/src/xchain/modules/folks-loan.ts @@ -1129,7 +1129,6 @@ export const write = { async liquidate(accountId: AccountId, prepareCall: PrepareLiquidateCall) { const folksChain = FolksCore.getSelectedFolksChain(); - assertHubChainSelected(folksChain.folksChainId, folksChain.network); return await FolksHubLoan.write.liquidate( diff --git a/src/xchain/modules/folks-rewards.ts b/src/xchain/modules/folks-rewards.ts index f866196..7d1c878 100644 --- a/src/xchain/modules/folks-rewards.ts +++ b/src/xchain/modules/folks-rewards.ts @@ -1,18 +1,162 @@ +import * as dn from "dnum"; + +import { + getActiveEpochs, + getHistoricalEpochs, + getUnclaimedRewards, +} from "../../chains/evm/hub/modules/folks-hub-rewards.js"; import { FolksHubRewards } from "../../chains/evm/hub/modules/index.js"; +import { getHubChain, getHubTokensData } from "../../chains/evm/hub/utils/chain.js"; +import { convertFromGenericAddress } from "../../common/utils/address.js"; +import { assertHubChainSelected, getSignerGenericAddress } from "../../common/utils/chain.js"; +import { SECONDS_IN_YEAR, unixTime } from "../../common/utils/math-lib.js"; import { FolksCore } from "../core/folks-core.js"; +import type { PrepareUpdateUserPointsInLoans } from "../../chains/evm/common/types/index.js"; import type { LoanTypeInfo } from "../../chains/evm/hub/types/loan.js"; -import type { UserRewards } from "../../chains/evm/hub/types/rewards.js"; +import type { PoolInfo } from "../../chains/evm/hub/types/pool.js"; +import type { + ActiveEpochs, + ActiveEpochsInfo, + Epochs, + LastUpdatedPointsForRewards, + PendingRewards, + UserPoints, +} from "../../chains/evm/hub/types/rewards.js"; +import type { ChainType } from "../../common/types/chain.js"; import type { AccountId, LoanId } from "../../common/types/lending.js"; -import type { LoanTypeId } from "../../common/types/module.js"; +import type { + LoanTypeId, + PrepareClaimRewardsCall, + PrepareUpdateAccountsPointsForRewardsCall, +} from "../../common/types/module.js"; +import type { FolksTokenId } from "../../common/types/token.js"; + +export const prepare = { + async updateUserPointsInLoans(loanIds: Array): Promise { + const folksChain = FolksCore.getSelectedFolksChain(); + assertHubChainSelected(folksChain.folksChainId, folksChain.network); + const hubChain = getHubChain(folksChain.network); + + const userAddress = getSignerGenericAddress({ + signer: FolksCore.getFolksSigner().signer, + chainType: folksChain.chainType, + }); + + return await FolksHubRewards.prepare.updateUserPointsInLoans( + FolksCore.getProvider(folksChain.folksChainId), + convertFromGenericAddress(userAddress, folksChain.chainType), + loanIds, + hubChain, + ); + }, + + async updateAccountsPointsForRewards( + accountIds: Array, + activeEpochs: ActiveEpochs, + ): Promise { + const folksChain = FolksCore.getSelectedFolksChain(); + assertHubChainSelected(folksChain.folksChainId, folksChain.network); + const hubChain = getHubChain(folksChain.network); + + const userAddress = getSignerGenericAddress({ + signer: FolksCore.getFolksSigner().signer, + chainType: folksChain.chainType, + }); + + return await FolksHubRewards.prepare.updateAccountsPointsForRewards( + FolksCore.getProvider(folksChain.folksChainId), + convertFromGenericAddress(userAddress, folksChain.chainType), + hubChain, + accountIds, + activeEpochs, + ); + }, + + async claimRewards(accountId: AccountId, historicalEpochs: Epochs): Promise { + const folksChain = FolksCore.getSelectedFolksChain(); + assertHubChainSelected(folksChain.folksChainId, folksChain.network); + const hubChain = getHubChain(folksChain.network); + + const userAddress = getSignerGenericAddress({ + signer: FolksCore.getFolksSigner().signer, + chainType: folksChain.chainType, + }); + + return await FolksHubRewards.prepare.claimRewards( + FolksCore.getProvider(folksChain.folksChainId), + convertFromGenericAddress(userAddress, folksChain.chainType), + hubChain, + accountId, + historicalEpochs, + ); + }, +}; + +export const write = { + async updateUserPointsInLoans(loanIds: Array, prepareCall: PrepareUpdateUserPointsInLoans) { + const folksChain = FolksCore.getSelectedFolksChain(); + assertHubChainSelected(folksChain.folksChainId, folksChain.network); + + return await FolksHubRewards.write.updateUserPointsInLoans( + FolksCore.getProvider(folksChain.folksChainId), + FolksCore.getSigner(), + loanIds, + prepareCall, + ); + }, + + async updateAccountsPointsForRewards( + accountIds: Array, + prepareCall: PrepareUpdateAccountsPointsForRewardsCall, + ) { + const folksChain = FolksCore.getSelectedFolksChain(); + assertHubChainSelected(folksChain.folksChainId, folksChain.network); + + return await FolksHubRewards.write.updateAccountsPointsForRewards( + FolksCore.getProvider(folksChain.folksChainId), + FolksCore.getSigner(), + accountIds, + prepareCall, + ); + }, + + async claimRewards(accountId: AccountId, prepareCall: PrepareClaimRewardsCall) { + const folksChain = FolksCore.getSelectedFolksChain(); + assertHubChainSelected(folksChain.folksChainId, folksChain.network); + + return await FolksHubRewards.write.claimRewards( + FolksCore.getProvider(folksChain.folksChainId), + FolksCore.getSigner(), + accountId, + prepareCall, + ); + }, +}; export const read = { - async rewards( + historicalEpochs(): Promise { + const network = FolksCore.getSelectedNetwork(); + const tokensData = Object.values(getHubTokensData(network)); + return getHistoricalEpochs(FolksCore.getHubProvider(), FolksCore.getSelectedNetwork(), tokensData); + }, + + activeEpochs(): Promise { + const network = FolksCore.getSelectedNetwork(); + const tokensData = Object.values(getHubTokensData(network)); + return getActiveEpochs(FolksCore.getHubProvider(), FolksCore.getSelectedNetwork(), tokensData); + }, + + unclaimedRewards(accountId: AccountId, historicalEpochs: Epochs): Promise { + return getUnclaimedRewards(FolksCore.getHubProvider(), FolksCore.getSelectedNetwork(), accountId, historicalEpochs); + }, + + async userPoints( accountId: AccountId, loanIds: Array, loanTypesInfo: Partial>, - ): Promise { - return FolksHubRewards.getUserRewards( + ): Promise { + return FolksHubRewards.getUserPoints( FolksCore.getHubProvider(), FolksCore.getSelectedNetwork(), accountId, @@ -20,4 +164,83 @@ export const read = { loanTypesInfo, ); }, + + async lastUpdatedPointsForRewards( + accountId: AccountId, + activeEpochs: ActiveEpochs, + ): Promise { + return FolksHubRewards.lastUpdatedPointsForRewards( + FolksCore.getHubProvider(), + FolksCore.getSelectedNetwork(), + accountId, + activeEpochs, + ); + }, +}; + +export const util = { + activeEpochsInfo(poolsInfo: Partial>, activeEpochs: ActiveEpochs): ActiveEpochsInfo { + const activeEpochsInfo: ActiveEpochsInfo = {}; + const currTimestamp = BigInt(unixTime()); + + for (const [folksTokenId, activeEpoch] of Object.entries(activeEpochs)) { + // calculations assumes reward rate is constant and consistent + const remainingTime = activeEpoch.endTimestamp - BigInt(currTimestamp); + const fullEpochTime = activeEpoch.endTimestamp - activeEpoch.startTimestamp; + + // remaining rewards is proportional to remaining time in epoch + const remainingRewards = (remainingTime * activeEpoch.totalRewards) / fullEpochTime; + + // apr is total rewards over the total deposit, scaling by epoch length + const poolInfo = poolsInfo[folksTokenId as FolksTokenId]; + if (!poolInfo) throw new Error(`Unknown folks token id ${folksTokenId}`); + const rewardsApr = dn.mul( + dn.div(activeEpoch.totalRewards, poolInfo.depositData.totalAmount, { decimals: 18 }), + dn.div(SECONDS_IN_YEAR, remainingTime, { decimals: 18 }), + ); + + activeEpochsInfo[folksTokenId as FolksTokenId] = { + ...activeEpoch, + remainingRewards, + rewardsApr, + }; + } + + return activeEpochsInfo; + }, + + pendingRewards( + loanTypesInfo: Partial>, + activeEpochs: ActiveEpochs, + userPoints: UserPoints, + lastUpdatedPointsForRewards: LastUpdatedPointsForRewards, + ): PendingRewards { + const pendingRewards: PendingRewards = {}; + + for (const [folksTokenId, activeEpoch] of Object.entries(activeEpochs)) { + // calculations assumes reward rate is constant and consistent + const fullEpochTime = activeEpoch.endTimestamp - activeEpoch.startTimestamp; + + // consider all loan types to calculate total points in given out in epoch for token + let totalRewardSpeed = dn.from(0, 18); + for (const loanTypeInfo of Object.values(loanTypesInfo)) { + const loanPool = loanTypeInfo.pools[folksTokenId as FolksTokenId]; + if (!loanPool) continue; + totalRewardSpeed = dn.add(totalRewardSpeed, loanPool.reward.collateralSpeed); + } + const [totalPointsInEpoch] = dn.mul(totalRewardSpeed, fullEpochTime, { decimals: 0 }); + + // consider points earned in active epoch + const userLatestPoints = userPoints.poolsPoints[folksTokenId as FolksTokenId]?.collateral ?? 0n; + const userLastWrittenPoints = lastUpdatedPointsForRewards[folksTokenId as FolksTokenId]?.lastWrittenPoints ?? 0n; + const userWrittenEpochPoints = + lastUpdatedPointsForRewards[folksTokenId as FolksTokenId]?.writtenEpochPoints ?? 0n; + const userEpochPoints = userLatestPoints - userLastWrittenPoints + userWrittenEpochPoints; + + // proportional to the percentage of points you already have of the total points (incl for rest of epoch) + pendingRewards[folksTokenId as FolksTokenId] = (userEpochPoints * activeEpoch.totalRewards) / totalPointsInEpoch; + } + + return pendingRewards; + }, };