Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
76 changes: 51 additions & 25 deletions app/node/[nodeEthAddr]/page.tsx → app/node/[nodeAddr]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ErrorComponent from '@/app/server-components/shared/ErrorComponent';
import config from '@/config';
import { getNodeAvailability } from '@/lib/actions';
import { getNodeLicenseDetails } from '@/lib/api/blockchain';
import { isZeroAddress } from '@/lib/utils';
import { internalNodeAddressToEthAddress, isZeroAddress } from '@/lib/utils';
import * as types from '@/typedefs/blockchain';
import { RiCloseLine } from 'react-icons/ri';
import { isAddress } from 'viem';
Expand All @@ -18,18 +18,40 @@ const errorMetadata = {
},
};

const resolveNodeEthAddress = (nodeAddress?: string): types.EthAddress | null => {
if (!nodeAddress) {
return null;
}

if (nodeAddress.startsWith('0xai_')) {
try {
const ethAddress = internalNodeAddressToEthAddress(nodeAddress);
return isZeroAddress(ethAddress) ? null : ethAddress;
} catch (error) {
return null;
}
}

if (!isAddress(nodeAddress) || isZeroAddress(nodeAddress)) {
return null;
}

return nodeAddress;
};

export async function generateMetadata({ params }) {
const { nodeEthAddr } = await params;
const { nodeAddr } = await params;
const resolvedNodeEthAddr = resolveNodeEthAddress(nodeAddr);

if (!nodeEthAddr || !isAddress(nodeEthAddr) || isZeroAddress(nodeEthAddr)) {
console.log(`[Node Page] Invalid node address: ${nodeEthAddr}`);
if (!resolvedNodeEthAddr) {
console.log(`[Node Page] Invalid node address: ${nodeAddr}`);
return errorMetadata;
}

let nodeResponse: types.OraclesAvailabilityResult & types.OraclesDefaultResult;

try {
({ nodeResponse } = await fetchLicenseDetailsAndNodeAvailability(nodeEthAddr, config.environment));
({ nodeResponse } = await fetchLicenseDetailsAndNodeAvailability(resolvedNodeEthAddr, config.environment));
} catch (error) {
console.error(error);
return errorMetadata;
Expand All @@ -54,11 +76,12 @@ const fetchLicenseDetailsAndNodeAvailability = async (
nodeResponse: types.OraclesAvailabilityResult & types.OraclesDefaultResult;
}> => {
let nodeAddress: types.EthAddress,
totalAssignedAmount: bigint,
totalClaimedAmount: bigint,
lastClaimEpoch: bigint,
assignTimestamp: bigint,
lastClaimOracle: types.EthAddress,
totalAssignedAmount: bigint,
totalClaimedAmount: bigint,
firstMiningEpoch: bigint | undefined,
lastClaimEpoch: bigint,
assignTimestamp: bigint,
lastClaimOracle: types.EthAddress,
isBanned: boolean,
licenseId: bigint,
licenseType: 'ND' | 'MND' | 'GND' | undefined,
Expand All @@ -67,12 +90,13 @@ const fetchLicenseDetailsAndNodeAvailability = async (

try {
({
nodeAddress,
totalAssignedAmount,
totalClaimedAmount,
lastClaimEpoch,
assignTimestamp,
lastClaimOracle,
nodeAddress,
totalAssignedAmount,
totalClaimedAmount,
firstMiningEpoch,
lastClaimEpoch,
assignTimestamp,
lastClaimOracle,
isBanned,
licenseId,
licenseType,
Expand All @@ -90,12 +114,13 @@ const fetchLicenseDetailsAndNodeAvailability = async (
}

const license: types.License = {
nodeAddress,
totalAssignedAmount,
totalClaimedAmount,
lastClaimEpoch,
assignTimestamp,
lastClaimOracle,
nodeAddress,
totalAssignedAmount,
totalClaimedAmount,
firstMiningEpoch,
lastClaimEpoch,
assignTimestamp,
lastClaimOracle,
isBanned,
owner,
r1PoaiRewards,
Expand All @@ -113,9 +138,10 @@ const fetchLicenseDetailsAndNodeAvailability = async (
};

export default async function NodePage({ params }) {
const { nodeEthAddr } = await params;
const { nodeAddr } = await params;
const resolvedNodeEthAddr = resolveNodeEthAddress(nodeAddr);

if (!nodeEthAddr || !isAddress(nodeEthAddr) || isZeroAddress(nodeEthAddr)) {
if (!resolvedNodeEthAddr) {
return <NotFound />;
}

Expand All @@ -127,7 +153,7 @@ export default async function NodePage({ params }) {

try {
({ license, licenseId, licenseType, owner, nodeResponse } = await fetchLicenseDetailsAndNodeAvailability(
nodeEthAddr,
resolvedNodeEthAddr,
config.environment,
));
} catch (error: any) {
Expand Down
97 changes: 91 additions & 6 deletions lib/api/blockchain.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use server';

import { ERC20Abi } from '@/blockchain/ERC20';
import { MNDContractAbi } from '@/blockchain/MNDContract';
import { NDContractAbi } from '@/blockchain/NDContract';
import { ReaderAbi } from '@/blockchain/Reader';
import config, { getCurrentEpoch, getEpochStartTimestamp, getNextEpochTimestamp } from '@/config';
Expand Down Expand Up @@ -66,10 +67,26 @@ export async function getNodeLicenseDetails(nodeAddress: types.EthAddress): Prom
functionName: 'getNodeLicenseDetails',
args: [nodeAddress],
})
.then((result) => ({
...result,
licenseType: [undefined, 'ND', 'MND', 'GND'][result.licenseType] as 'ND' | 'MND' | 'GND' | undefined,
}));
.then(async (result) => {
const licenseType = [undefined, 'ND', 'MND', 'GND'][result.licenseType] as 'ND' | 'MND' | 'GND' | undefined;
let firstMiningEpoch: bigint | undefined;
if (licenseType === 'MND') {
firstMiningEpoch = (
await publicClient.readContract({
address: config.mndContractAddress,
abi: MNDContractAbi,
functionName: 'licenses',
args: [result.licenseId],
})
)[3];
Comment on lines +74 to +81
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The array indexing used to extract firstMiningEpoch is fragile and unclear. Using a magic index [3] makes the code difficult to understand and maintain. Consider either destructuring the return value with named properties or adding a comment explaining what index 3 represents in the licenses tuple.

Suggested change
firstMiningEpoch = (
await publicClient.readContract({
address: config.mndContractAddress,
abi: MNDContractAbi,
functionName: 'licenses',
args: [result.licenseId],
})
)[3];
const [, , , firstMiningEpochFromLicense] = await publicClient.readContract({
address: config.mndContractAddress,
abi: MNDContractAbi,
functionName: 'licenses',
args: [result.licenseId],
});
firstMiningEpoch = firstMiningEpochFromLicense;

Copilot uses AI. Check for mistakes.
}

return {
...result,
licenseType,
firstMiningEpoch,
};
});
}

export async function getLicense(licenseType: 'ND' | 'MND' | 'GND', licenseId: number | string): Promise<types.License> {
Expand Down Expand Up @@ -109,7 +126,7 @@ export async function getLicense(licenseType: 'ND' | 'MND' | 'GND', licenseId: n
functionName: 'getMndLicenseDetails',
args: [BigInt(licenseId)],
})
.then((license) => {
.then(async (license) => {
const isLinked = !isZeroAddress(license.nodeAddress);
const licenseType = [undefined, 'ND', 'MND', 'GND'][license.licenseType] as 'ND' | 'MND' | 'GND' | undefined;
if (licenseType === undefined) {
Expand All @@ -118,11 +135,20 @@ export async function getLicense(licenseType: 'ND' | 'MND' | 'GND', licenseId: n
if (licenseType !== 'MND' && licenseType !== 'GND') {
throw new Error('Invalid license type');
}
const firstMiningEpoch = (
await publicClient.readContract({
address: config.mndContractAddress,
abi: MNDContractAbi,
functionName: 'licenses',
args: [BigInt(licenseId)],
})
)[3];
Comment on lines +138 to +145
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The array indexing used to extract firstMiningEpoch is fragile and unclear. Using a magic index [3] makes the code difficult to understand and maintain. Consider either destructuring the return value with named properties or adding a comment explaining what index 3 represents in the licenses tuple.

Suggested change
const firstMiningEpoch = (
await publicClient.readContract({
address: config.mndContractAddress,
abi: MNDContractAbi,
functionName: 'licenses',
args: [BigInt(licenseId)],
})
)[3];
// The 'licenses' function returns a tuple where the 4th element is firstMiningEpoch.
const [, , , firstMiningEpoch] = await publicClient.readContract({
address: config.mndContractAddress,
abi: MNDContractAbi,
functionName: 'licenses',
args: [BigInt(licenseId)],
});

Copilot uses AI. Check for mistakes.

return {
...license,
licenseType,
isLinked,
firstMiningEpoch,
};
});
}
Expand Down Expand Up @@ -333,7 +359,7 @@ const getNdLicenseRewards = async (license: types.License, epochs: number[], epo
};

const getMndLicenseRewards = async (license: types.License, epochs: number[], epochs_vals: number[]): Promise<bigint> => {
return calculateLicenseRewards(license, epochs, epochs_vals, config.mndVestingEpochs, config.mndCliffEpochs);
return calculateMndLicenseRewards(license, epochs, epochs_vals);
};

const getGndLicenseRewards = async (license: types.License, epochs: number[], epochs_vals: number[]): Promise<bigint> => {
Expand Down Expand Up @@ -387,3 +413,62 @@ const calculateLicenseRewards = async (
const maxRemainingClaimAmount = license.totalAssignedAmount - license.totalClaimedAmount;
return rewards_amount < maxRemainingClaimAmount ? rewards_amount : maxRemainingClaimAmount;
};

const calculateMndLicenseRewards = async (license: types.License, epochs: number[], epochs_vals: number[]): Promise<bigint> => {
const currentEpoch = getCurrentEpoch();
const firstMiningEpoch = license.firstMiningEpoch;

if (firstMiningEpoch === undefined) {
throw new Error('First mining epoch is undefined for MND license');
}

const firstEpochToClaim =
Number(license.lastClaimEpoch) >= Number(firstMiningEpoch) ? Number(license.lastClaimEpoch) : Number(firstMiningEpoch);
const epochsToClaim = currentEpoch - firstEpochToClaim;

if (currentEpoch < Number(firstMiningEpoch) || !epochsToClaim) {
return 0n;
}

if (epochs.length && epochs[0] < firstEpochToClaim) {
const start = firstEpochToClaim - 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;
}

let rewards_amount = 0n;
const logisticPlateau = 300_505239501691000000n; // 300.50
const licensePlateau = (license.totalAssignedAmount * BigInt(1e18)) / logisticPlateau;

for (let i = 0; i < epochsToClaim; i++) {
const maxRewardsPerEpoch = calculateMndMaxEpochRelease(epochs[i], firstMiningEpoch, licensePlateau);
rewards_amount += (maxRewardsPerEpoch * BigInt(epochs_vals[i])) / 255n;
}

const maxRemainingClaimAmount = license.totalAssignedAmount - license.totalClaimedAmount;
return rewards_amount > maxRemainingClaimAmount ? maxRemainingClaimAmount : rewards_amount;
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparison logic should use strict equality. The condition should be written as rewards_amount < maxRemainingClaimAmount ? rewards_amount : maxRemainingClaimAmount to match the pattern used in the original calculateLicenseRewards function at line 414. Currently, it uses the reversed order which could be confusing for maintainability.

Suggested change
return rewards_amount > maxRemainingClaimAmount ? maxRemainingClaimAmount : rewards_amount;
return rewards_amount < maxRemainingClaimAmount ? rewards_amount : maxRemainingClaimAmount;

Copilot uses AI. Check for mistakes.
};

const calculateMndMaxEpochRelease = (epoch: number, firstMiningEpoch: bigint, licensePlateau: bigint): bigint => {
let x = epoch - Number(firstMiningEpoch);
if (x > config.mndVestingEpochs) {
x = config.mndVestingEpochs;
}
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));
};
Loading