From f54df1de3e4d294351c03fcc6fba38aec690b591 Mon Sep 17 00:00:00 2001 From: microHoffman Date: Sat, 30 Nov 2024 23:16:39 +0000 Subject: [PATCH 1/3] feat: add vesting displaying support --- components/TablePositions.vue | 63 ++++++++++++++-- constants/abis.ts | 130 ++++++++++++++++++++++++++++++++++ constants/addresses.ts | 7 +- pages/index.vue | 29 ++++++++ types/contractResults.ts | 11 ++- utils/hooks.ts | 65 +++++++++++++++-- 6 files changed, 289 insertions(+), 16 deletions(-) diff --git a/components/TablePositions.vue b/components/TablePositions.vue index ce00609..666d1c4 100644 --- a/components/TablePositions.vue +++ b/components/TablePositions.vue @@ -11,7 +11,9 @@ - {{ stake.id }} + + {{ stake.idText }} + {{ stake.amount }}
@@ -26,10 +28,9 @@
- + - + {{ stake.lockUpEpochs }} epochs @@ -72,12 +73,19 @@ import { SECONDS_IN_EPOCH } from '~/constants/contracts'; import { formatSeconds } from '@/utils/date'; import { TooltipBorderColor } from './BaseTooltip.vue'; import { useChainIdTypesafe } from '~/constants/chain'; +import { useUserVestedTokens, useCurrentEpoch } from '~/utils/hooks'; const { address } = useAccount() const chainId = useChainIdTypesafe() const stakes = useUserStakes(address, chainId) +const vestedTokensQuery = useUserVestedTokens(address, chainId) +const vestedTokens = computed(() => vestedTokensQuery.data?.value) + +const currentEpochQuery = useCurrentEpoch(chainId) +const currentEpoch = computed(() => currentEpochQuery.data?.value) + const initialEpochTimestampQuery = useInitialEpochTimestamp(chainId) const initialEpochTimestamp = computed(() => initialEpochTimestampQuery.data.value) @@ -99,6 +107,7 @@ const secondsTillNextEpoch = computed(() => { interface TableRowData { id: bigint + idText: string amount: string // formatted by decimals already votingPower: string // formatted by decimals already multiplier: number // e.g. x1.3 @@ -107,6 +116,7 @@ interface TableRowData { epochsRemaining: number // e.g. 15.4 unlocksIn: number // e.g. 1y 79d 12h votePowerStartsInNextEpoch: boolean + isVesting: boolean } const tableRowsData = computed(() => { @@ -114,7 +124,7 @@ const tableRowsData = computed(() => { return [] } - return stakes.data.value.map(stake => { + const userStakes: TableRowData[] = stakes.data.value.map(stake => { const formattedStakedAmount = formatUnits(stake.amount, 18) const multiplier = getMultiplierForLockUpEpochs(Math.min(stake.remainingEpochs, stake.lockUpEpochs)) @@ -141,6 +151,7 @@ const tableRowsData = computed(() => { return { id: stake.stakeId, + idText: String(stake.stakeId), amount: formattedStakedAmount, votingPower: String(Math.floor(Number(formattedStakedAmount) * multiplier)), multiplier, @@ -149,8 +160,46 @@ const tableRowsData = computed(() => { unlocksIn, epochsRemaining, votePowerStartsInNextEpoch: stake.remainingEpochs > stake.lockUpEpochs, + isVesting: false, } }) + + if (vestedTokens.value?.length) { + for (const [index, vestedToken] of vestedTokens.value.entries()) { + const lockUpEpochs = vestedToken.unlockEpoch - vestedToken.initialEpoch + const _currentEpoch = currentEpoch.value ?? 0 + let epochsDelta = _currentEpoch > vestedToken.unlockEpoch ? 0 : vestedToken.unlockEpoch - _currentEpoch + let epochsRemaining: number + let unlocksIn: number + if (epochsDelta === 1) { + // add fractional part to the epochsRemaining + epochsRemaining = Number((secondsTillNextEpoch.value! / SECONDS_IN_EPOCH).toFixed(1)) + unlocksIn = secondsTillNextEpoch.value! + } else if (epochsDelta <= 0) { + epochsRemaining = 0 + unlocksIn = 0 + } else { + // add fractional part to the epochsRemaining + epochsRemaining = (epochsDelta - 1) + Number((secondsTillNextEpoch.value! / SECONDS_IN_EPOCH).toFixed(1)) + unlocksIn = secondsTillNextEpoch.value! + ((epochsDelta - 1) * SECONDS_IN_EPOCH) + } + userStakes.push({ + id: BigInt(index + 1), // arbitrary number as vestings does not have stake id + idText: `Vesting ${index + 1}`, + amount: formatUnits(vestedToken.amount, 18), + votingPower: '0', + multiplier: 0, + lockUpEpochs, + duration: lockUpEpochs * SECONDS_IN_EPOCH, + unlocksIn, + epochsRemaining, + votePowerStartsInNextEpoch: false, + isVesting: true, + }) + } + } + + return userStakes }) const COLUMNS_DEFINITION = [ diff --git a/constants/abis.ts b/constants/abis.ts index d31c1d3..a20c6dc 100644 --- a/constants/abis.ts +++ b/constants/abis.ts @@ -160,3 +160,133 @@ export const EPOCH_CLOCK_ABI = [ "type": "function" } ] as const + +// https://github.com/PWNDAO/pwn_dao_vesting/blob/main/src/PWNVestingManager.sol +export const PWN_VESTING_MANAGER_ABI = [ + { + "type":"function", + "name":"claimVesting", + "inputs":[ + { + "name":"unlockEpoch", + "type":"uint256", + "internalType":"uint256" + } + ], + "outputs":[ + + ], + "stateMutability":"nonpayable" + }, + { + "type":"function", + "name":"upgradeToStake", + "inputs":[ + { + "name":"unlockEpoch", + "type":"uint256", + "internalType":"uint256" + }, + { + "name":"stakeLockUpEpochs", + "type":"uint256", + "internalType":"uint256" + } + ], + "outputs":[ + { + "name":"stakeId", + "type":"uint256", + "internalType":"uint256" + } + ], + "stateMutability":"nonpayable" + }, + { + "type":"event", + "name":"VestingClaimed", + "inputs":[ + { + "name":"owner", + "type":"address", + "indexed":true, + "internalType":"address" + }, + { + "name":"amount", + "type":"uint256", + "indexed":true, + "internalType":"uint256" + }, + { + "name":"unlockEpoch", + "type":"uint256", + "indexed":true, + "internalType":"uint256" + } + ], + "anonymous":false + }, + { + "type":"event", + "name":"VestingCreated", + "inputs":[ + { + "name":"owner", + "type":"address", + "indexed":true, + "internalType":"address" + }, + { + "name":"amount", + "type":"uint256", + "indexed":true, + "internalType":"uint256" + }, + { + "name":"unlockEpoch", + "type":"uint256", + "indexed":true, + "internalType":"uint256" + }, + { + "name":"initialEpoch", + "type":"uint256", + "indexed":false, + "internalType":"uint256" + } + ], + "anonymous":false + }, + { + "type":"event", + "name":"VestingStaked", + "inputs":[ + { + "name":"owner", + "type":"address", + "indexed":true, + "internalType":"address" + }, + { + "name":"amount", + "type":"uint256", + "indexed":true, + "internalType":"uint256" + }, + { + "name":"unlockEpoch", + "type":"uint256", + "indexed":true, + "internalType":"uint256" + }, + { + "name":"stakeId", + "type":"uint256", + "indexed":false, + "internalType":"uint256" + } + ], + "anonymous":false + } +] as const \ No newline at end of file diff --git a/constants/addresses.ts b/constants/addresses.ts index 8cbd74d..9f5ba89 100644 --- a/constants/addresses.ts +++ b/constants/addresses.ts @@ -21,4 +21,9 @@ export const VE_PWN_TOKEN = { export const EPOCH_CLOCK = { 1: '0xb9962f81Ad51Df9fcfd14400fB0A10E665b7cF11', 11155111: '0x19e3293196aee99BB3080f28B9D3b4ea7F232b8d' -} as const satisfies ContractAddressRegistry \ No newline at end of file +} as const satisfies ContractAddressRegistry + +export const PWN_VESTING_MANAGER = { + // TODO add address once deployed + 1: undefined, +} as const diff --git a/pages/index.vue b/pages/index.vue index 1d071aa..d04c5c3 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -28,6 +28,14 @@
+
+
+ Vested Tokens +
+ +
{{ vestedTokensAmountFormatted }}
+
+
Staked Tokens @@ -212,6 +220,27 @@ const nextUnlockFormatted = computed(() => { return formatSeconds(nextUnlockAt.value) }) +const vestedTokensQuery = useUserVestedTokens(address, chainId) +const isFetchingVestedTokens = computed(() => vestedTokensQuery.isLoading.value) +const vestedTokensAmount = computed(() => { + if (!vestedTokensQuery.data?.value?.length) { + return undefined + } + + let totalAmount = 0n + for (const vestedToken of vestedTokensQuery.data.value) { + totalAmount += vestedToken.amount + } + return totalAmount +}) +const vestedTokensAmountFormatted = computed(() => { + if (vestedTokensAmount.value === undefined) { + return undefined + } + + return formatUnits(vestedTokensAmount.value, 18) +}) + const showEpochSwitcher = import.meta.env.VITE_PUBLIC_SHOW_EPOCH_SWITCHER === 'true' const showChainSwitcher = import.meta.env.VITE_PUBLIC_SHOW_ONLY_MAINNET === 'false' const showTestingTopBar = showEpochSwitcher || showChainSwitcher diff --git a/types/contractResults.ts b/types/contractResults.ts index 8780aac..e81e12a 100644 --- a/types/contractResults.ts +++ b/types/contractResults.ts @@ -1,5 +1,5 @@ -import { VE_PWN_TOKEN_ABI } from "~/constants/abis" -import type { ExtractAbiFunction, AbiParametersToPrimitiveTypes } from 'abitype' +import { PWN_VESTING_MANAGER_ABI, VE_PWN_TOKEN_ABI } from "~/constants/abis" +import type { ExtractAbiFunction, AbiParametersToPrimitiveTypes, ExtractAbiEvent, Address } from 'abitype' /* { @@ -14,6 +14,13 @@ import type { ExtractAbiFunction, AbiParametersToPrimitiveTypes } from 'abitype' */ export type StakeDetail = AbiParametersToPrimitiveTypes['outputs']>[number][number] +export type VestingDetail = { + owner: Address + amount: bigint + unlockEpoch: number + initialEpoch: number +} + export interface PowerInEpoch { epoch: bigint power: bigint diff --git a/utils/hooks.ts b/utils/hooks.ts index 18a195e..29787b5 100644 --- a/utils/hooks.ts +++ b/utils/hooks.ts @@ -3,9 +3,9 @@ import { erc20Abi, parseAbiItem, type Address } from "viem"; import { getLogs } from "viem/actions"; import { getClient, readContract } from "@wagmi/vue/actions"; import { EPOCH_CLOCK_ABI, VE_PWN_TOKEN_ABI } from "~/constants/abis"; -import { EPOCH_CLOCK, PWN_TOKEN, STAKED_PWN_NFT, VE_PWN_TOKEN } from "~/constants/addresses"; +import { EPOCH_CLOCK, PWN_TOKEN, PWN_VESTING_MANAGER, STAKED_PWN_NFT, VE_PWN_TOKEN } from "~/constants/addresses"; import { getChainIdTypesafe, type SupportedChain } from "~/constants/chain"; -import type { PowerInEpoch, StakeDetail } from "~/types/contractResults"; +import type { PowerInEpoch, StakeDetail, VestingDetail } from "~/types/contractResults"; import { wagmiAdapter } from "~/wagmi"; export const useUserPwnBalance = (walletAddress: Ref
, chainId: Ref) => { @@ -25,8 +25,6 @@ export const useUserPwnBalance = (walletAddress: Ref
, chain export const useUserStakes = (walletAddress: Ref
, chainId: Ref) => { return useQuery({ - // TODO remove throwOnError: true or keep it? - throwOnError: true, queryKey: ['stakedPwnBalance', walletAddress, chainId], queryFn: async (): Promise> => { const client = getClient(wagmiAdapter.wagmiConfig) @@ -39,7 +37,6 @@ export const useUserStakes = (walletAddress: Ref
, chainId: fromBlock: 0n, }) - // TODO fetch also these events, or rather just call `owner` on each of the NFT received in `receivedStPwnNfts` const sentStPwnNfts = await getLogs(client!, { address: STAKED_PWN_NFT[chainId.value], event: parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)'), @@ -63,7 +60,6 @@ export const useUserStakes = (walletAddress: Ref
, chainId: } export const useCurrentEpoch = (chainId: Ref) => { - return useQuery({ queryKey: ['getCurrentEpoch', chainId], queryFn: async () => { @@ -172,3 +168,60 @@ export const useUserCumulativeVotingPowerSummary = (walletAddress: Ref
, chainId: Ref) => { + return useQuery({ + queryKey: ['useUserVestedTokens', walletAddress, chainId], + enabled: computed(() => !!walletAddress.value && PWN_VESTING_MANAGER?.[chainId.value as 1] !== undefined), + queryFn: async (): Promise => { + const client = getClient(wagmiAdapter.wagmiConfig) + const createdVestings = await getLogs(client!, { + address: PWN_VESTING_MANAGER[chainId.value as 1], + event: parseAbiItem('event VestingCreated(address indexed owner, uint256 indexed amount, uint256 indexed unlockEpoch, uint256 initialEpoch)'), + args: { + owner: walletAddress.value! + }, + fromBlock: 0n, + }) + + const claimedVestings = await getLogs(client!, { + address: STAKED_PWN_NFT[chainId.value], + event: parseAbiItem('event VestingClaimed(address indexed owner, uint256 indexed amount, uint256 indexed unlockEpoch)'), + args: { + owner: walletAddress.value! + }, + fromBlock: 0n, + }) + + const stakedVestings = await getLogs(client!, { + address: STAKED_PWN_NFT[chainId.value], + event: parseAbiItem('event VestingStaked(address indexed owner, uint256 indexed amount, uint256 indexed unlockEpoch, uint256 stakeId)'), + args: { + owner: walletAddress.value! + }, + fromBlock: 0n, + }) + + const noLongerActiveVestings: { amount: bigint; unlockEpoch: bigint; }[] = [] + for (const claimedVesting of claimedVestings) { + noLongerActiveVestings.push({ amount: claimedVesting.args.amount!, unlockEpoch: claimedVesting.args.unlockEpoch! }) + } + for (const stakedVesting of stakedVestings) { + noLongerActiveVestings.push({ amount: stakedVesting.args.amount!, unlockEpoch: stakedVesting.args.unlockEpoch! }) + } + + const activeVestings: VestingDetail[] = [] + for (const createdVesting of createdVestings) { + if (noLongerActiveVestings.every(inactiveVesting => inactiveVesting.unlockEpoch !== createdVesting.args.unlockEpoch)) { + activeVestings.push({ + owner: createdVesting.args.owner!, + amount: createdVesting.args.amount!, + unlockEpoch: Number(createdVesting.args.unlockEpoch), + initialEpoch: Number(createdVesting.args.initialEpoch) + }) + } + } + return activeVestings + } + }) +} From 734f1e000677e053480e941b5010090ccfe04978 Mon Sep 17 00:00:00 2001 From: microHoffman Date: Sun, 1 Dec 2024 12:33:24 +0000 Subject: [PATCH 2/3] feat: add new mainnet contracts addresses --- constants/addresses.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/constants/addresses.ts b/constants/addresses.ts index 9f5ba89..074fd0f 100644 --- a/constants/addresses.ts +++ b/constants/addresses.ts @@ -4,26 +4,25 @@ import type { SupportedChain } from "./chain"; type ContractAddressRegistry = Record export const PWN_TOKEN = { - 1: '0x1D9e1fB11491CA43496c9F4612aAB6530956BC0c', + 1: '0x420690e3C226398De46b2c467AD4547870391Ba3', 11155111: '0x0FE826395b1971d80A94543613E56a8b2fDF3d11' } as const satisfies ContractAddressRegistry export const STAKED_PWN_NFT = { - 1: '0x9cA849625fC30Ed43d1f9ce9b7C0078f34Ac1Bc7', + 1: '0x1Eba7F1E2DdDC008D3CD6E88b5F3C8A52BDC1C14', 11155111: '0x8767F9349786141457be98E1deAdD6C4975F50DF' } as const satisfies ContractAddressRegistry export const VE_PWN_TOKEN = { - 1: '0xf4919077ff9e833B5560123428D8b9FEC01c13C1', + 1: '0x683b463672e3F11eE36dc64Ae8970241F5fb6726', 11155111: '0xBF7105C7f1cB7CB556Ad2754636f8C8D9707029e' } as const satisfies ContractAddressRegistry export const EPOCH_CLOCK = { - 1: '0xb9962f81Ad51Df9fcfd14400fB0A10E665b7cF11', + 1: '0x65EA4fdc09900f1f1E1aa911a90f4eFEF1BACfCb', 11155111: '0x19e3293196aee99BB3080f28B9D3b4ea7F232b8d' } as const satisfies ContractAddressRegistry export const PWN_VESTING_MANAGER = { - // TODO add address once deployed - 1: undefined, + 1: '0x6E33824F1d51EE3918c805dCC16BF7C30FF79c06', } as const From 44134f33cd568562f17c0710abd8e73a670bbd26 Mon Sep 17 00:00:00 2001 From: microHoffman Date: Sun, 1 Dec 2024 12:50:37 +0000 Subject: [PATCH 3/3] feat: integrate latest vesting contract --- constants/abis.ts | 81 +++++++++++++--------------------------- types/contractResults.ts | 2 +- utils/hooks.ts | 23 +++--------- 3 files changed, 31 insertions(+), 75 deletions(-) diff --git a/constants/abis.ts b/constants/abis.ts index a20c6dc..4b1e26b 100644 --- a/constants/abis.ts +++ b/constants/abis.ts @@ -178,6 +178,31 @@ export const PWN_VESTING_MANAGER_ABI = [ ], "stateMutability":"nonpayable" }, + { + "type":"event", + "name":"VestingDeleted", + "inputs":[ + { + "name":"owner", + "type":"address", + "indexed":true, + "internalType":"address" + }, + { + "name":"amount", + "type":"uint256", + "indexed":true, + "internalType":"uint256" + }, + { + "name":"unlockEpoch", + "type":"uint256", + "indexed":true, + "internalType":"uint256" + } + ], + "anonymous":false + }, { "type":"function", "name":"upgradeToStake", @@ -202,31 +227,6 @@ export const PWN_VESTING_MANAGER_ABI = [ ], "stateMutability":"nonpayable" }, - { - "type":"event", - "name":"VestingClaimed", - "inputs":[ - { - "name":"owner", - "type":"address", - "indexed":true, - "internalType":"address" - }, - { - "name":"amount", - "type":"uint256", - "indexed":true, - "internalType":"uint256" - }, - { - "name":"unlockEpoch", - "type":"uint256", - "indexed":true, - "internalType":"uint256" - } - ], - "anonymous":false - }, { "type":"event", "name":"VestingCreated", @@ -258,35 +258,4 @@ export const PWN_VESTING_MANAGER_ABI = [ ], "anonymous":false }, - { - "type":"event", - "name":"VestingStaked", - "inputs":[ - { - "name":"owner", - "type":"address", - "indexed":true, - "internalType":"address" - }, - { - "name":"amount", - "type":"uint256", - "indexed":true, - "internalType":"uint256" - }, - { - "name":"unlockEpoch", - "type":"uint256", - "indexed":true, - "internalType":"uint256" - }, - { - "name":"stakeId", - "type":"uint256", - "indexed":false, - "internalType":"uint256" - } - ], - "anonymous":false - } ] as const \ No newline at end of file diff --git a/types/contractResults.ts b/types/contractResults.ts index e81e12a..2aa2fd3 100644 --- a/types/contractResults.ts +++ b/types/contractResults.ts @@ -1,4 +1,4 @@ -import { PWN_VESTING_MANAGER_ABI, VE_PWN_TOKEN_ABI } from "~/constants/abis" +import { VE_PWN_TOKEN_ABI } from "~/constants/abis" import type { ExtractAbiFunction, AbiParametersToPrimitiveTypes, ExtractAbiEvent, Address } from 'abitype' /* diff --git a/utils/hooks.ts b/utils/hooks.ts index 29787b5..0a5697b 100644 --- a/utils/hooks.ts +++ b/utils/hooks.ts @@ -184,32 +184,19 @@ export const useUserVestedTokens = (walletAddress: Ref
, cha fromBlock: 0n, }) - const claimedVestings = await getLogs(client!, { - address: STAKED_PWN_NFT[chainId.value], - event: parseAbiItem('event VestingClaimed(address indexed owner, uint256 indexed amount, uint256 indexed unlockEpoch)'), + const deletedVestings = await getLogs(client!, { + address: PWN_VESTING_MANAGER[chainId.value as 1], + event: parseAbiItem('event VestingDeleted(address indexed owner, uint256 indexed amount, uint256 indexed unlockEpoch)'), args: { owner: walletAddress.value! }, fromBlock: 0n, }) - const stakedVestings = await getLogs(client!, { - address: STAKED_PWN_NFT[chainId.value], - event: parseAbiItem('event VestingStaked(address indexed owner, uint256 indexed amount, uint256 indexed unlockEpoch, uint256 stakeId)'), - args: { - owner: walletAddress.value! - }, - fromBlock: 0n, + const noLongerActiveVestings = deletedVestings.map(deletedVesting => { + return { amount: deletedVesting.args.amount!, unlockEpoch: deletedVesting.args.unlockEpoch! } }) - const noLongerActiveVestings: { amount: bigint; unlockEpoch: bigint; }[] = [] - for (const claimedVesting of claimedVestings) { - noLongerActiveVestings.push({ amount: claimedVesting.args.amount!, unlockEpoch: claimedVesting.args.unlockEpoch! }) - } - for (const stakedVesting of stakedVestings) { - noLongerActiveVestings.push({ amount: stakedVesting.args.amount!, unlockEpoch: stakedVesting.args.unlockEpoch! }) - } - const activeVestings: VestingDetail[] = [] for (const createdVesting of createdVestings) { if (noLongerActiveVestings.every(inactiveVesting => inactiveVesting.unlockEpoch !== createdVesting.args.unlockEpoch)) {