diff --git a/app/license/[licenseType]/[licenseId]/page.tsx b/app/license/[licenseType]/[licenseId]/page.tsx index 8f2c285..dff04d7 100644 --- a/app/license/[licenseType]/[licenseId]/page.tsx +++ b/app/license/[licenseType]/[licenseId]/page.tsx @@ -10,49 +10,49 @@ import * as types from '@/typedefs/blockchain'; import { Skeleton } from '@heroui/skeleton'; import { cache, Suspense } from 'react'; -export async function generateMetadata({ params }) { - const { licenseType, licenseId } = await params; - const errorMetadata = { - title: 'Error', - openGraph: { - title: 'Error', - }, - }; - - if (!licenseType || !['ND', 'MND', 'GND'].includes(licenseType)) { - return errorMetadata; - } +export async function generateMetadata({ params }) { + const { licenseType, licenseId } = await params; + const errorMetadata = { + title: 'Error', + openGraph: { + title: 'Error', + }, + }; + + if (!licenseType || !['ND', 'MND', 'GND'].includes(licenseType)) { + return errorMetadata; + } const licenseIdNum = parseInt(licenseId); - if (isNaN(licenseIdNum) || licenseIdNum < 0 || licenseIdNum > 10000) { - return errorMetadata; - } - - const canonical = `/license/${encodeURIComponent(licenseType)}/${encodeURIComponent(licenseId)}`; - const errorMetadataWithCanonical = { - ...errorMetadata, - alternates: { - canonical, - }, - }; - - try { - await fetchLicense(licenseType, licenseId, config.environment); - } catch (error) { - return errorMetadataWithCanonical; - } - - return { - title: `License #${licenseId}`, - openGraph: { - title: `License #${licenseId}`, - }, - alternates: { - canonical, - }, - }; -} + if (isNaN(licenseIdNum) || licenseIdNum < 0 || licenseIdNum > 10000) { + return errorMetadata; + } + + const canonical = `/license/${encodeURIComponent(licenseType)}/${encodeURIComponent(licenseId)}`; + const errorMetadataWithCanonical = { + ...errorMetadata, + alternates: { + canonical, + }, + }; + + try { + await fetchLicense(licenseType, licenseId, config.environment); + } catch (error) { + return errorMetadataWithCanonical; + } + + return { + title: `License #${licenseId}`, + openGraph: { + title: `License #${licenseId}`, + }, + alternates: { + canonical, + }, + }; +} const fetchLicense = async ( licenseType: 'ND' | 'MND' | 'GND', diff --git a/app/server-components/LicensePage/LicensePageNodeCardWrapper.tsx b/app/server-components/LicensePage/LicensePageNodeCardWrapper.tsx index dc592a4..ec221d5 100644 --- a/app/server-components/LicensePage/LicensePageNodeCardWrapper.tsx +++ b/app/server-components/LicensePage/LicensePageNodeCardWrapper.tsx @@ -13,7 +13,7 @@ export default async function LicensePageNodeCardWrapper({ await cachedGetNodeAvailability(); if (!nodeResponse) { - return ; + return null; } return ; diff --git a/app/server-components/Licenses/License.tsx b/app/server-components/Licenses/License.tsx index 7b29048..2054b9a 100644 --- a/app/server-components/Licenses/License.tsx +++ b/app/server-components/Licenses/License.tsx @@ -87,7 +87,7 @@ export default async function License({ licenseType, licenseId }: Props) {
Not assigned
diff --git a/app/server-components/Licenses/LicenseRewardsPoA.tsx b/app/server-components/Licenses/LicenseRewardsPoA.tsx index 30ff666..34f4abe 100644 --- a/app/server-components/Licenses/LicenseRewardsPoA.tsx +++ b/app/server-components/Licenses/LicenseRewardsPoA.tsx @@ -8,10 +8,12 @@ import { SmallTag } from '../shared/SmallTag'; export default async function LicenseRewardsPoA({ license, licenseType, + licenseId, getNodeAvailability, }: { license: types.License; licenseType: 'ND' | 'MND' | 'GND'; + licenseId: string; getNodeAvailability: () => Promise<(types.OraclesAvailabilityResult & types.OraclesDefaultResult) | undefined>; }) { try { @@ -21,9 +23,7 @@ export default async function LicenseRewardsPoA({ await getNodeAvailability(); if (!nodeResponse) { - return ( - Error loading rewards} isSmall /> - ); + return null; } const firstCheckEpoch: number = getLicenseFirstCheckEpoch(license.assignTimestamp); @@ -32,6 +32,7 @@ export default async function LicenseRewardsPoA({ rewards = await getLicenseRewards( license, licenseType, + BigInt(licenseId), nodeResponse.epochs.slice(lastClaimEpoch - firstCheckEpoch), nodeResponse.epochs_vals.slice(lastClaimEpoch - firstCheckEpoch), ); diff --git a/app/server-components/main-cards/LicenseCard.tsx b/app/server-components/main-cards/LicenseCard.tsx index 9f03dbd..f828975 100644 --- a/app/server-components/main-cards/LicenseCard.tsx +++ b/app/server-components/main-cards/LicenseCard.tsx @@ -115,11 +115,12 @@ export default async function LicenseCard({ license, licenseType, licenseId, own }> - + {licenseType === 'ND' && ( diff --git a/blockchain/MNDContract.ts b/blockchain/MNDContract.ts index 75651df..9570c06 100644 --- a/blockchain/MNDContract.ts +++ b/blockchain/MNDContract.ts @@ -1,4 +1,19 @@ export const MNDContractAbi = [ + { + inputs: [], + name: 'AssignedAmountExceedsLimit', + type: 'error', + }, + { + inputs: [], + name: 'CannotReassignWithin24Hours', + type: 'error', + }, + { + inputs: [], + name: 'CannotUnlinkBeforeClaimingRewards', + type: 'error', + }, { inputs: [], name: 'ERC721EnumerableForbiddenBatchMint', @@ -133,16 +148,71 @@ export const MNDContractAbi = [ name: 'ExpectedPause', type: 'error', }, + { + inputs: [], + name: 'IncorrectNumberOfParams', + type: 'error', + }, + { + inputs: [], + name: 'InvalidEpochs', + type: 'error', + }, { inputs: [], name: 'InvalidInitialization', type: 'error', }, + { + inputs: [], + name: 'InvalidLicensePower', + type: 'error', + }, + { + inputs: [], + name: 'InvalidNodeAddress', + type: 'error', + }, + { + inputs: [], + name: 'InvalidNodeAddressForRewards', + type: 'error', + }, + { + inputs: [], + name: 'MaxTokenSupplyReached', + type: 'error', + }, + { + inputs: [], + name: 'MaxTotalAssignedTokensReached', + type: 'error', + }, + { + inputs: [], + name: 'MismatchedInputArraysLength', + type: 'error', + }, + { + inputs: [], + name: 'NodeAddressAlreadyRegistered', + type: 'error', + }, + { + inputs: [], + name: 'NonexistentTokenURI', + type: 'error', + }, { inputs: [], name: 'NotInitializing', type: 'error', }, + { + inputs: [], + name: 'NotLicenseOwner', + type: 'error', + }, { inputs: [ { @@ -271,6 +341,16 @@ export const MNDContractAbi = [ name: 'ReentrancyGuardReentrantCall', type: 'error', }, + { + inputs: [], + name: 'SoulboundNonTransferableToken', + type: 'error', + }, + { + inputs: [], + name: 'TimestampBeforeStartEpoch', + type: 'error', + }, { anonymous: false, inputs: [ @@ -469,6 +549,18 @@ export const MNDContractAbi = [ name: 'rewardsAmount', type: 'uint256', }, + { + indexed: false, + internalType: 'uint256', + name: 'carryoverAmount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'withheldAmount', + type: 'uint256', + }, { indexed: false, internalType: 'uint256', @@ -476,7 +568,7 @@ export const MNDContractAbi = [ type: 'uint256', }, ], - name: 'RewardsClaimed', + name: 'RewardsClaimedV2', type: 'event', }, { @@ -599,6 +691,19 @@ export const MNDContractAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [], + name: 'adoptionOracle', + outputs: [ + { + internalType: 'contract IAdoptionOracle', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [ { @@ -617,6 +722,25 @@ export const MNDContractAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + name: 'awbBalances', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [ { @@ -693,6 +817,16 @@ export const MNDContractAbi = [ name: 'rewardsAmount', type: 'uint256', }, + { + internalType: 'uint256', + name: 'carryoverAmount', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'withheldAmount', + type: 'uint256', + }, ], internalType: 'struct ComputeRewardsResult[]', name: '', @@ -1067,6 +1201,29 @@ export const MNDContractAbi = [ stateMutability: 'view', type: 'function', }, + { + inputs: [ + { + internalType: 'uint256[]', + name: 'licenseIds', + type: 'uint256[]', + }, + { + internalType: 'address[]', + name: 'newNodeAddresses', + type: 'address[]', + }, + { + internalType: 'bytes', + name: 'signature', + type: 'bytes', + }, + ], + name: 'linkMultiNode', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [ { @@ -1090,6 +1247,19 @@ export const MNDContractAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [], + name: 'maxCarryoverReleaseFactor', + outputs: [ + { + internalType: 'uint8', + name: '', + type: 'uint8', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [], name: 'name', @@ -1251,6 +1421,19 @@ export const MNDContractAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [ + { + internalType: 'address', + name: 'adoptionOracle_', + type: 'address', + }, + ], + name: 'setAdoptionOracle', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [ { @@ -1338,6 +1521,19 @@ export const MNDContractAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [ + { + internalType: 'uint8', + name: 'newFactor', + type: 'uint8', + }, + ], + name: 'setMaxCarryoverReleaseFactor', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [ { diff --git a/lib/api/blockchain.ts b/lib/api/blockchain.ts index a50c12d..5fda8fb 100644 --- a/lib/api/blockchain.ts +++ b/lib/api/blockchain.ts @@ -10,6 +10,7 @@ import console from 'console'; import { differenceInSeconds } from 'date-fns'; import Moralis from 'moralis'; import { EvmAddress, EvmChain } from 'moralis/common-evm-utils'; +import type { ReadContractReturnType } from 'viem'; import { isZeroAddress } from '../utils'; import { getPublicClient } from './client'; @@ -70,7 +71,7 @@ export async function getNodeLicenseDetails(nodeAddress: types.EthAddress): Prom .then(async (result) => { const licenseType = [undefined, 'ND', 'MND', 'GND'][result.licenseType] as 'ND' | 'MND' | 'GND' | undefined; let firstMiningEpoch: bigint | undefined; - if (licenseType === 'MND') { + if (licenseType === 'MND' || licenseType === 'GND') { firstMiningEpoch = ( await publicClient.readContract({ address: config.mndContractAddress, @@ -245,16 +246,17 @@ export const getBlockByTimestamp = async (targetTimestamp: number) => { export const getLicenseRewards = async ( license: types.License, licenseType: 'ND' | 'MND' | 'GND', + licenseId: bigint, epochs: number[], epochs_vals: number[], -): Promise => { +): Promise => { switch (licenseType) { case 'ND': return getNdLicenseRewards(license, epochs, epochs_vals); + case 'MND': - return getMndLicenseRewards(license, epochs, epochs_vals); case 'GND': - return getGndLicenseRewards(license, epochs, epochs_vals); + return getMndOrGndLicenseRewards(license, licenseId, epochs, epochs_vals); } }; @@ -353,72 +355,44 @@ export async function fetchR1MintedLastEpoch() { return value; } -const getNdLicenseRewards = async (license: types.License, epochs: number[], epochs_vals: number[]): Promise => { - return calculateLicenseRewards(license, epochs, epochs_vals, config.ndVestingEpochs); -}; - -const getMndLicenseRewards = async (license: types.License, epochs: number[], epochs_vals: number[]): Promise => { - return calculateMndLicenseRewards(license, epochs, epochs_vals); -}; - -const getGndLicenseRewards = async (license: types.License, epochs: number[], epochs_vals: number[]): Promise => { - return calculateLicenseRewards(license, epochs, epochs_vals, config.gndVestingEpochs); -}; - -const calculateLicenseRewards = async ( +const getNdLicenseRewards = async ( license: types.License, epochs: number[], epochs_vals: number[], - vestingEpochs: number, - cliffEpochs: number = 0, -): Promise => { +): Promise => { const currentEpoch = getCurrentEpoch(); + const epochsToClaim = currentEpoch - Number(license.lastClaimEpoch); - const firstEpochToClaim = - cliffEpochs > 0 - ? license.lastClaimEpoch >= cliffEpochs - ? Number(license.lastClaimEpoch) - : cliffEpochs - : Number(license.lastClaimEpoch); - - const epochsToClaim = currentEpoch - firstEpochToClaim; - - if ((cliffEpochs > 0 && currentEpoch < cliffEpochs) || epochsToClaim <= 0) { + if (!epochsToClaim) { return 0n; } - // Disregard epochs before the cliff epoch for MNDs - if (cliffEpochs > 0 && epochs[0] < cliffEpochs) { - const start = cliffEpochs - epochs[0]; - epochs = epochs.slice(start); - epochs_vals = epochs_vals.slice(start); - } - if (epochsToClaim !== epochs.length || epochsToClaim !== epochs_vals.length) { - console.error( - `Invalid epochs array length. Received ${epochs.length} epochs, but there are ${epochsToClaim} epochs to claim.`, - ); - - return 0n; + return undefined; } - const maxRewardsPerEpoch = license.totalAssignedAmount / BigInt(vestingEpochs); let rewards_amount = 0n; + const maxRewardsPerEpoch = license.totalAssignedAmount / BigInt(config.ndVestingEpochs); for (let i = 0; i < epochsToClaim; i++) { rewards_amount += (maxRewardsPerEpoch * BigInt(epochs_vals[i])) / 255n; } const maxRemainingClaimAmount = license.totalAssignedAmount - license.totalClaimedAmount; - return rewards_amount < maxRemainingClaimAmount ? rewards_amount : maxRemainingClaimAmount; + return rewards_amount > maxRemainingClaimAmount ? maxRemainingClaimAmount : rewards_amount; }; -const calculateMndLicenseRewards = async (license: types.License, epochs: number[], epochs_vals: number[]): Promise => { +const getMndOrGndLicenseRewards = async ( + license: types.License, + licenseId: bigint, + epochs: number[], + epochs_vals: number[], +): Promise => { const currentEpoch = getCurrentEpoch(); const firstMiningEpoch = license.firstMiningEpoch; if (firstMiningEpoch === undefined) { - throw new Error('First mining epoch is undefined for MND license'); + throw new Error('First mining epoch is undefined for MND/GND license'); } const firstEpochToClaim = @@ -436,38 +410,36 @@ const calculateMndLicenseRewards = async (license: types.License, epochs: number } if (epochsToClaim !== epochs.length || epochsToClaim !== epochs_vals.length) { - console.error( - `Invalid epochs array length. Received ${epochs.length} epochs, but there are ${epochsToClaim} epochs to claim.`, - ); - return 0n; + return undefined; } - let rewards_amount = 0n; - const logisticPlateau = 300_505239501691000000n; // 300.50 - const licensePlateau = (license.totalAssignedAmount * BigInt(1e18)) / logisticPlateau; + const publicClient = await getPublicClient(); - for (let i = 0; i < epochsToClaim; i++) { - const maxRewardsPerEpoch = calculateMndMaxEpochRelease(epochs[i], firstMiningEpoch, licensePlateau); - rewards_amount += (maxRewardsPerEpoch * BigInt(epochs_vals[i])) / 255n; + let result: ReadContractReturnType | undefined; + + try { + result = await publicClient.readContract({ + address: config.mndContractAddress, + abi: MNDContractAbi, + functionName: 'calculateRewards', + args: [ + [ + { + licenseId, + nodeAddress: license.nodeAddress, + epochs: epochs.map((epoch) => BigInt(epoch)), + availabilies: epochs_vals, + }, + ], + ], + }); + } catch { + return undefined; } - const maxRemainingClaimAmount = license.totalAssignedAmount - license.totalClaimedAmount; - return rewards_amount > maxRemainingClaimAmount ? maxRemainingClaimAmount : rewards_amount; -}; - -const calculateMndMaxEpochRelease = (epoch: number, firstMiningEpoch: bigint, licensePlateau: bigint): bigint => { - let x = epoch - Number(firstMiningEpoch); - if (x > config.mndVestingEpochs) { - x = config.mndVestingEpochs; + if (!result || result.length !== 1) { + throw new Error('Invalid rewards calculation result'); } - const frac = logisticFraction(x); - return (licensePlateau * BigInt(frac * 1e18)) / BigInt(1e18); -}; -const logisticFraction = (x: number): number => { - const length = config.mndVestingEpochs; - const k = 5.0; - const midPrc = 0.7; - const midpoint = length * midPrc; - return 1.0 / (1.0 + Math.exp((-k * (x - midpoint)) / length)); + return result[0].rewardsAmount + result[0].carryoverAmount; }; diff --git a/package-lock.json b/package-lock.json index f45b256..a2e9360 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,13 +25,14 @@ "date-fns": "^4.1.0", "ethereum-blockies": "^0.1.1", "framer-motion": "^12.0.11", + "lodash": "4.17.23", "mapbox-gl": "^3.15.0", "maplibre-gl": "^5.7.1", "moralis": "^2.27.2", - "next": "^15.5.9", + "next": "15.5.10", "qs": "^6.14.1", - "react": "^19.2.1", - "react-dom": "^19.2.1", + "react": "19.2.4", + "react-dom": "19.2.4", "react-icons": "^5.4.0", "react-map-gl": "^8.0.4", "recharts": "^2.15.4", @@ -3183,9 +3184,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.9", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", - "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==", + "version": "15.5.10", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.10.tgz", + "integrity": "sha512-plg+9A/KoZcTS26fe15LHg+QxReTazrIOoKKUC3Uz4leGGeNPgLHdevVraAAOX0snnUs3WkRx3eUQpj9mreG6A==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -8863,9 +8864,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.merge": { @@ -9196,12 +9197,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.9", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", - "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", + "version": "15.5.10", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.10.tgz", + "integrity": "sha512-r0X65PNwyDDyOrWNKpQoZvOatw7BcsTPRKdwEqtc9cj3wv7mbBIk9tKed4klRaFXJdX0rugpuMTHslDrAU1bBg==", "license": "MIT", "dependencies": { - "@next/env": "15.5.9", + "@next/env": "15.5.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -10051,24 +10052,24 @@ } }, "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.3" + "react": "^19.2.4" } }, "node_modules/react-icons": { diff --git a/package.json b/package.json index 311c0a4..dd560b8 100644 --- a/package.json +++ b/package.json @@ -33,13 +33,14 @@ "date-fns": "^4.1.0", "ethereum-blockies": "^0.1.1", "framer-motion": "^12.0.11", + "lodash": "4.17.23", "mapbox-gl": "^3.15.0", "maplibre-gl": "^5.7.1", "moralis": "^2.27.2", - "next": "^15.5.9", + "next": "15.5.10", "qs": "^6.14.1", - "react": "^19.2.1", - "react-dom": "^19.2.1", + "react": "19.2.4", + "react-dom": "19.2.4", "react-icons": "^5.4.0", "react-map-gl": "^8.0.4", "recharts": "^2.15.4",