Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix bugs #408

Merged
merged 3 commits into from
Nov 18, 2024
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
91 changes: 3 additions & 88 deletions src/controllers/nft.controller.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,17 @@
import { Request, Response } from 'express';
import { catchAsync } from '../utils';
import parentLogger from '../config/logger';
import { moduleService, platformService, ociService } from '../services';
import { ApiError } from '../utils';
import { IModule, IPlatform } from '@togethercrew.dev/db';
import { HydratedDocument } from 'mongoose';
import * as Neo4j from '../neo4j';
import { NEO4J_PLATFORM_INFO } from '../constants/neo4j.constant';
import { SupportedNeo4jPlatforms } from '../types/neo4j.type';
import { nftService } from '../services';

const logger = parentLogger.child({ module: 'NftController' });

const getReputationScore = catchAsync(async function (req: Request, res: Response) {
const { tokenId, address } = req.params;
const supportedPlatforms = ['discord', 'discourse'];

let repuationScore;
logger.debug(tokenId, address);
const profiles: Array<any> = await getProfilesOnAllSupportedChains(address);
logger.debug(profiles);
const dynamicNftModule = await moduleService.getModuleByFilter({ 'options.platforms.0.metadata.tokenId': tokenId });
logger.debug(dynamicNftModule);

for (let i = 0; i < supportedPlatforms.length; i++) {
const platform = await platformService.getPlatformByFilter({
name: supportedPlatforms[i],
community: dynamicNftModule?.community,
});
logger.debug({ i, platform, supportedPlatforms: supportedPlatforms[i] });
for (let j = 0; j < profiles.length; j++) {
const profile = profiles[j];
logger.debug({ i, j, profile, supportedPlatforms: supportedPlatforms[i] });
const temp = platform?.name as SupportedNeo4jPlatforms;
if (profile.profile.provider === supportedPlatforms[i]) {
const reputationScoreQuery = `
MATCH (:${NEO4J_PLATFORM_INFO[temp].member} {id: "${profile.profile.id}"})-[r:HAVE_METRICS {platformId: "${platform?.id}"}]->(a)
WITH r.date as metrics_date, r.closenessCentrality as memberScore
ORDER BY metrics_date DESC
LIMIT 1
MATCH (user:${NEO4J_PLATFORM_INFO[temp].member})-[user_r:HAVE_METRICS {platformId: "${platform?.id}", date: metrics_date}]->(user)
WITH memberScore, MAX(user_r.closenessCentrality) as maxScore
RETURN memberScore / maxScore AS reputation_score
`;

const neo4jData = await Neo4j.read(reputationScoreQuery);
const { records } = neo4jData;
logger.debug(records);

const reputationScoreResponse = records[0];

logger.debug(reputationScoreResponse);

const { _fieldLookup, _fields } = reputationScoreResponse as unknown as {
_fieldLookup: Record<string, number>;
_fields: number[];
};

repuationScore = _fields[_fieldLookup['reputation_score']];
logger.debug(repuationScore);
}
}
}
return repuationScore;
const reputationScore = await nftService.getReputationScore(tokenId, address);
res.send(reputationScore);
Comment on lines +11 to +12
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add input validation and error handling

The simplified implementation could benefit from some essential safeguards:

  1. Input validation for tokenId and address
  2. Explicit error handling for service failures
  3. Type checking of the response

Consider implementing these improvements:

 const getReputationScore = catchAsync(async function (req: Request, res: Response) {
   const { tokenId, address } = req.params;
-  logger.debug(tokenId, address);
+  logger.debug({ tokenId, address }, 'Getting reputation score');
+
+  if (!tokenId || !address) {
+    throw new Error('Missing required parameters: tokenId and address');
+  }
+
   const reputationScore = await nftService.getReputationScore(tokenId, address);
-  res.send(reputationScore);
+  
+  if (reputationScore === null || reputationScore === undefined) {
+    throw new Error('Failed to retrieve reputation score');
+  }
+
+  logger.debug({ reputationScore }, 'Retrieved reputation score');
+  res.json({ success: true, data: reputationScore });
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const reputationScore = await nftService.getReputationScore(tokenId, address);
res.send(reputationScore);
const getReputationScore = catchAsync(async function (req: Request, res: Response) {
const { tokenId, address } = req.params;
logger.debug({ tokenId, address }, 'Getting reputation score');
if (!tokenId || !address) {
throw new Error('Missing required parameters: tokenId and address');
}
const reputationScore = await nftService.getReputationScore(tokenId, address);
if (reputationScore === null || reputationScore === undefined) {
throw new Error('Failed to retrieve reputation score');
}
logger.debug({ reputationScore }, 'Retrieved reputation score');
res.json({ success: true, data: reputationScore });
});

});

async function getProfilesOnAllSupportedChains(address: string) {
let profiles: Array<any> = [];
const supportedChainIds = [11155111];
for (let i = 0; i < supportedChainIds.length; i++) {
const chainProfiles = await ociService.getProfiles(address, supportedChainIds[i]);
profiles = profiles.concat(chainProfiles);
}
return profiles;
}

function shouldProfilesExist(profiles: Array<any>) {
if (profiles.length < 0) {
throw new ApiError(400, 'User has no any onchain profiles');
}
}

function shouldDynamicNftModuleExist(dynamicNftModule: HydratedDocument<IModule> | null) {
if (!dynamicNftModule) {
throw new ApiError(400, "There's not any assoicated dynamic nft module to the token Id");
}
}

function shouldPlatformExist(platform: HydratedDocument<IPlatform> | null) {
if (!platform) {
throw new ApiError(400, "There's not any platform connected for requested platform");
}
}

function shouldProfileExist(profile: any) {
if (!profile) {
throw new ApiError(400, "There's not any user oncahin profile for requested platform");
}
}
export default {
getReputationScore,
};
2 changes: 1 addition & 1 deletion src/routes/v1/nft.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { validate } from '../../middlewares';
const router = express.Router();

// Routes
router.post(
router.get(
'/:tokenId/:address/reputation-score',
validate(nftValidation.getReputationScore),
nftController.getReputationScore,
Expand Down
3 changes: 3 additions & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import githubService from './github';
import notionService from './notion';
import discourseService from './discourse';
import ociService from './oci.service';
import nftService from './nft.service';

export {
userService,
authService,
Expand All @@ -33,4 +35,5 @@ export {
notionService,
discourseService,
ociService,
nftService,
};
115 changes: 96 additions & 19 deletions src/services/nft.service.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,112 @@
import { HydratedDocument, Types } from 'mongoose';
import httpStatus from 'http-status';
import { Platform, IPlatform } from '@togethercrew.dev/db';
import { IPlatform, IModule, PlatformNames } from '@togethercrew.dev/db';
import ApiError from '../utils/ApiError';
import sagaService from './saga.service';
import discourseService from './discourse';
import { Snowflake } from 'discord.js';
import { analyzerAction, analyzerWindow } from '../config/analyzer.statics';
import { PlatformNames } from '@togethercrew.dev/db';
import * as Neo4j from '../neo4j';
import { NEO4J_PLATFORM_INFO } from '../constants/neo4j.constant';
import { SupportedNeo4jPlatforms } from '../types/neo4j.type';
import parentLogger from '../config/logger';
import moduleService from './module.service';
import platformService from './platform.service';
import ociService from './oci.service';

const logger = parentLogger.child({ module: 'NftService' });

const supportedPlatforms = [PlatformNames.Discord, PlatformNames.Discourse];

/**
* get reputation score
* @param {IPlatform} PlatformBody
* @returns {Promise<HydratedDocument<IPlatform>>}
*/
const getReputationScore = async (PlatformBody: IPlatform): Promise<HydratedDocument<IPlatform>> => {
if (PlatformBody.name === PlatformNames.Discord || PlatformBody.name === PlatformNames.Discourse) {
if (PlatformBody.metadata) {
PlatformBody.metadata = {
action: analyzerAction,
window: analyzerWindow,
...PlatformBody.metadata,
};
const getReputationScore = async (tokenId: string, address: string) => {
const dynamicNftModule = await moduleService.getModuleByFilter({ 'options.platforms.0.metadata.tokenId': tokenId });
logger.debug(dynamicNftModule);
throwErrorIfDynamicNftModuleDoesNotExist(dynamicNftModule);

const profiles: Array<any> = await getProfiles(address);
logger.debug(profiles);
throwErrorIfUserHasNoOnChainProfiles(profiles);

let reputationScore = 0;
for (const profile of profiles) {
if (supportedPlatforms.includes(profile.profile.provider)) {
const platform = await platformService.getPlatformByFilter({
name: profile.profile.provider,
community: dynamicNftModule?.community,
});
if (platform) {
reputationScore = (await calculateReputationScoreForProfile(profile, platform)) + reputationScore;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix reputation score calculation logic

The current implementation adds reputation scores together without averaging, which could lead to inflated scores for users with multiple profiles.

Apply this fix:

-        reputationScore = (await calculateReputationScoreForProfile(profile, platform)) + reputationScore;
+        const profileScore = await calculateReputationScoreForProfile(profile, platform);
+        reputationScore = Math.max(reputationScore, profileScore);  // Take highest score across platforms
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
reputationScore = (await calculateReputationScoreForProfile(profile, platform)) + reputationScore;
const profileScore = await calculateReputationScoreForProfile(profile, platform);
reputationScore = Math.max(reputationScore, profileScore); // Take highest score across platforms

logger.debug(`Reputation Score: ${reputationScore}`);
}
}
}
const platform = await Platform.create(PlatformBody);
if (PlatformBody.name === PlatformNames.Discord) {
await sagaService.createAndStartFetchMemberSaga(platform._id);
}
return platform;
return {
reputationScore,
communintyId: dynamicNftModule?.id,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix typo in property key 'communintyId'

The property key 'communintyId' in the return object is misspelled. It should be 'communityId'.

Apply this diff to correct the typo:

 return {
   reputationScore,
-  communintyId: dynamicNftModule?.id,
+  communityId: dynamicNftModule?.id,
 };

Committable suggestion skipped: line range outside the PR's diff.

};
};

async function getProfiles(address: string) {
let profiles: Array<any> = [];
const supportedChainIds = [11155111];
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Move chain ID to configuration

The supported chain ID is hard-coded. This should be moved to a configuration file for better maintainability.

Create a config file and move the chain IDs there:

// src/config/blockchain.config.ts
export const SUPPORTED_CHAIN_IDS = {
  SEPOLIA: 11155111,
  // Add other chains as needed
};

for (let i = 0; i < supportedChainIds.length; i++) {
const chainProfiles = await ociService.getProfiles(address, supportedChainIds[i]);
profiles = profiles.concat(chainProfiles);
Comment on lines +54 to +55
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for OCI service calls

Exceptions from ociService.getProfiles could disrupt the entire process. Implement error handling to make the function more robust.

Example implementation:

 for (let i = 0; i < supportedChainIds.length; i++) {
-  const chainProfiles = await ociService.getProfiles(address, supportedChainIds[i]);
-  profiles = profiles.concat(chainProfiles);
+  try {
+    const chainProfiles = await ociService.getProfiles(address, supportedChainIds[i]);
+    profiles = profiles.concat(chainProfiles);
+  } catch (error) {
+    logger.error(`Error fetching profiles for chain ID ${supportedChainIds[i]}: ${error.message}`);
+    // Optionally, handle the error or continue to the next iteration
+  }
 }

Committable suggestion skipped: line range outside the PR's diff.

}
return profiles;
}

async function calculateReputationScoreForProfile(profile: any, platform: any): Promise<number> {
const platformName = platform.name as SupportedNeo4jPlatforms;
const memberLabel = NEO4J_PLATFORM_INFO[platformName].member;
const platformId = platform.id;
const profileId = profile.profile.id;

const reputationScoreQuery = buildReputationScoreQuery(profileId, platformId, memberLabel);
const neo4jData = await Neo4j.read(reputationScoreQuery);

return extractReputationScoreFromNeo4jData(neo4jData);
}

function buildReputationScoreQuery(profileId: string, platformId: string, memberLabel: string): string {
return `
MATCH (:${memberLabel} {id: "${profileId}"})-[r:HAVE_METRICS {platformId: "${platformId}"}]->(a)
WITH r.date as metrics_date, r.closenessCentrality as memberScore
ORDER BY metrics_date DESC
LIMIT 1
MATCH (user:${memberLabel})-[user_r:HAVE_METRICS {platformId: "${platformId}", date: metrics_date}]->(user)
WITH memberScore, MAX(user_r.closenessCentrality) as maxScore
RETURN memberScore / maxScore AS reputation_score
`;
}
Comment on lines +72 to +82
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Prevent potential Cypher injection by using parameterized queries

The buildReputationScoreQuery function constructs a Cypher query by directly interpolating profileId and platformId. This can lead to Cypher injection if these values are tampered with. Use parameterized queries to enhance security.

Refactor the code to use parameters:

-function buildReputationScoreQuery(profileId: string, platformId: string, memberLabel: string): string {
-  return `
-    MATCH (:${memberLabel} {id: "${profileId}"})-[r:HAVE_METRICS {platformId: "${platformId}"}]->(a)
+function buildReputationScoreQuery(memberLabel: string): string {
+  return `
+    MATCH (:${memberLabel} {id: $profileId})-[r:HAVE_METRICS {platformId: $platformId}]->(a)
     WITH r.date as metrics_date, r.closenessCentrality as memberScore
     ORDER BY metrics_date DESC
     LIMIT 1
-    MATCH (user:${memberLabel})-[user_r:HAVE_METRICS {platformId: "${platformId}", date: metrics_date}]->(user)
+    MATCH (user:${memberLabel})-[user_r:HAVE_METRICS {platformId: $platformId, date: metrics_date}]->(user)
     WITH memberScore, MAX(user_r.closenessCentrality) as maxScore
     RETURN memberScore / maxScore AS reputation_score
   `;
 }

When executing the query, pass parameters separately:

 async function calculateReputationScoreForProfile(profile: any, platform: any): Promise<number> {
   const platformName = platform.name as SupportedNeo4jPlatforms;
   const memberLabel = NEO4J_PLATFORM_INFO[platformName].member;
   const platformId = platform.id;
   const profileId = profile.profile.id;

-  const reputationScoreQuery = buildReputationScoreQuery(profileId, platformId, memberLabel);
-  const neo4jData = await Neo4j.read(reputationScoreQuery);
+  const reputationScoreQuery = buildReputationScoreQuery(memberLabel);
+  const params = { profileId, platformId };
+  const neo4jData = await Neo4j.run(reputationScoreQuery, params);

   return extractReputationScoreFromNeo4jData(neo4jData);
 }

Ensure your Neo4j driver supports parameterized queries.


function extractReputationScoreFromNeo4jData(neo4jData: any): number {
const { records } = neo4jData;
logger.debug(`Neo4j Records: ${JSON.stringify(records)}`);

if (records.length === 0) {
return 0;
}

const reputationScoreResponse = records[0];
const reputationScore = reputationScoreResponse.get('reputation_score');

return reputationScore || 0;
}

function throwErrorIfUserHasNoOnChainProfiles(profiles: Array<any>) {
if (profiles.length === 0) {
throw new ApiError(400, 'User does not have any on-chain profiles.');
}
}

function throwErrorIfDynamicNftModuleDoesNotExist(dynamicNftModule: HydratedDocument<IModule> | null) {
if (!dynamicNftModule) {
throw new ApiError(400, 'There is no associated dynamic NFT module for the provided token ID.');
}
}

export default {
getReputationScore,
};