diff --git a/src/dataLoaders/ProjectsDataSource.ts b/src/dataLoaders/ProjectsDataSource.ts index 6127a5d..577f011 100644 --- a/src/dataLoaders/ProjectsDataSource.ts +++ b/src/dataLoaders/ProjectsDataSource.ts @@ -173,14 +173,17 @@ export default class ProjectsDataSource { ids: RepoDriverId[], chain: DbSchema, ): Promise { - return ( - await (this._batchProjectsByIds.loadMany( - ids.map((id) => ({ - accountId: id, - chains: [chain], - })), - ) as Promise) - ).filter((p) => p.chain === chain); + const results = await this._batchProjectsByIds.loadMany( + ids.map((id) => ({ + accountId: id, + chains: [chain], + })), + ); + + // Filter out errors and undefined values before accessing properties + return results + .filter((p): p is ProjectDataValues => p != null && !(p instanceof Error)) + .filter((p) => p.chain === chain); } public async getEarnedFunds( diff --git a/src/dataLoaders/SupportDataSource.ts b/src/dataLoaders/SupportDataSource.ts index 2020ee8..515570c 100644 --- a/src/dataLoaders/SupportDataSource.ts +++ b/src/dataLoaders/SupportDataSource.ts @@ -124,6 +124,22 @@ export default class SupportDataSource { ).filter((support) => support.chain === chain); } + public async getOneTimeDonationSupportByAccountIdsOnChain( + accountIds: AccountId[], + chain: DbSchema, + ): Promise { + const results = await Promise.all( + accountIds.map((accountId) => + this._batchOneTimeDonationSupportByAccountIds.load({ + accountId, + chains: [chain], + }), + ), + ); + + return results.flat().filter((support) => support.chain === chain); + } + public async getStreamSupportByAccountIdOnChain( accountId: AccountId, chain: DbSchema, diff --git a/src/ecosystem/ecosystemResolvers.ts b/src/ecosystem/ecosystemResolvers.ts index 046becd..6d52eab 100644 --- a/src/ecosystem/ecosystemResolvers.ts +++ b/src/ecosystem/ecosystemResolvers.ts @@ -19,7 +19,9 @@ import assert, { assertIsRepoDriverId, assertMany, isNftDriverId, + isRepoSubAccountDriverId, } from '../utils/assert'; +import { calcParentRepoDriverId } from '../utils/repoSubAccountIdUtils'; import { resolveTotalEarned } from '../common/commonResolverLogic'; import { chainToDbSchema } from '../utils/chainSchemaMappings'; import { getLatestMetadataHashOnChain } from '../utils/getLatestAccountMetadata'; @@ -128,10 +130,27 @@ const ecosystemResolvers = { const linkedIdentityReceivers = splitReceiversByReceiverAccountType.get('linked_identity') || []; - const projectIds = - projectReceivers.length > 0 - ? (projectReceivers.map((r) => r.receiverAccountId) as RepoDriverId[]) // Events processors ensure that all project IDs are RepoDriverIds. - : []; + // Get parent project IDs - transform sub-account IDs to parent IDs + // Detect ID type instead of relying solely on splitsToRepoDriverSubAccount flag + // Create a map to cache transformations and avoid duplicate async calls + const receiverToParentIdMap = new Map(); + + if (projectReceivers.length > 0) { + await Promise.all( + projectReceivers.map(async (r) => { + const parentId = isRepoSubAccountDriverId(r.receiverAccountId) + ? await calcParentRepoDriverId( + r.receiverAccountId, + ecosystemChain, + ) + : (r.receiverAccountId as RepoDriverId); + receiverToParentIdMap.set(r.receiverAccountId, parentId); + }), + ); + } + + // Deduplicate project IDs (multiple sub-accounts can share the same parent) + const projectIds = Array.from(new Set(receiverToParentIdMap.values())); const [projects, subLists] = await Promise.all([ projectReceivers.length > 0 @@ -163,9 +182,25 @@ const ecosystemResolvers = { const projectDependencies = await Promise.all( projectReceivers.map(async (s) => { - assertIsRepoDriverId(s.receiverAccountId); + // Detect the ID type to determine if this is a sub-account + const isSubAccount = isRepoSubAccountDriverId(s.receiverAccountId); + + // When the ID is a RepoSubAccountDriver ID, we need to get the parent project ID + // Otherwise, the receiver ID is already the project ID + if (!isSubAccount) { + assertIsRepoDriverId(s.receiverAccountId); + } + + // Get the parent project ID from the cached map (already calculated above) + const projectId = receiverToParentIdMap.get(s.receiverAccountId); + + if (!projectId) { + return shouldNeverHappen( + `Expected parent ID to be cached for ${s.receiverAccountId}`, + ); + } - const project = projectsMap.get(s.receiverAccountId); + const project = projectsMap.get(projectId); return { ...s, diff --git a/src/project/projectResolvers.ts b/src/project/projectResolvers.ts index bde959d..4afd2fc 100644 --- a/src/project/projectResolvers.ts +++ b/src/project/projectResolvers.ts @@ -35,6 +35,7 @@ import { assertMany, isGitHubUrl, isRepoDriverId, + isRepoSubAccountDriverId, } from '../utils/assert'; import { resolveTotalEarned } from '../common/commonResolverLogic'; import { validateChainsQueryArg } from '../utils/commonInputValidators'; @@ -48,6 +49,10 @@ import { calcSubRepoDriverId } from '../utils/repoSubAccountIdUtils'; import toGqlLinkedIdentity from '../linked-identity/linkedIdentityUtils'; import { getGitHubRepoByUrl } from './github'; import { PUBLIC_ERROR_CODES } from '../utils/formatError'; +import { + getProjectSplitSupport, + getProjectOneTimeDonationSupport, +} from './projectSupportHelpers'; const projectResolvers = { Query: { @@ -83,7 +88,8 @@ const projectResolvers = { { id, chains }: { id: RepoDriverId; chains?: SupportedChain[] }, { dataSources: { projectsDataSource } }: Context, ): Promise => { - if (!isRepoDriverId(id)) { + // Accept both repo driver IDs and repo sub-account driver IDs + if (!isRepoDriverId(id) && !isRepoSubAccountDriverId(id)) { return null; } @@ -396,11 +402,11 @@ const projectResolvers = { }, }: Context, ) => { - const splitReceivers = - await supportDataSource.getSplitSupportByReceiverIdOnChain( - projectId, - projectChain, - ); + const splitReceivers = await getProjectSplitSupport( + projectId, + projectChain, + supportDataSource, + ); const supportItems = await Promise.all( splitReceivers.map(async (receiver) => { @@ -492,11 +498,11 @@ const projectResolvers = { const support = supportItems.filter((item) => item !== null); - const oneTimeDonationSupport = - await supportDataSource.getOneTimeDonationSupportByAccountIdOnChain( - projectId, - projectChain, - ); + const oneTimeDonationSupport = await getProjectOneTimeDonationSupport( + projectId, + projectChain, + supportDataSource, + ); return [...support, ...oneTimeDonationSupport]; }, @@ -539,11 +545,11 @@ const projectResolvers = { }, }: Context, ) => { - const splitsReceivers = - await supportDataSource.getSplitSupportByReceiverIdOnChain( - projectId, - projectChain, - ); + const splitsReceivers = await getProjectSplitSupport( + projectId, + projectChain, + supportDataSource, + ); const supportItems = await Promise.all( splitsReceivers.map(async (s) => { @@ -632,12 +638,11 @@ const projectResolvers = { const support = supportItems.filter((item) => item !== null); - // `GivenEventModelDataValues`s that represent one time donations to the Project. - const oneTimeDonationSupport = - await supportDataSource.getOneTimeDonationSupportByAccountIdOnChain( - projectId, - projectChain, - ); + const oneTimeDonationSupport = await getProjectOneTimeDonationSupport( + projectId, + projectChain, + supportDataSource, + ); return [...support, ...oneTimeDonationSupport]; }, diff --git a/src/project/projectSupportHelpers.ts b/src/project/projectSupportHelpers.ts new file mode 100644 index 0000000..7dea75e --- /dev/null +++ b/src/project/projectSupportHelpers.ts @@ -0,0 +1,67 @@ +import type { AccountId, DbSchema } from '../common/types'; +import { isRepoDriverId } from '../utils/assert'; +import { calcSubRepoDriverId } from '../utils/repoSubAccountIdUtils'; +import type SupportDataSource from '../dataLoaders/SupportDataSource'; +import type { SplitsReceiverModelDataValues } from '../models/SplitsReceiverModel'; +import type { GivenEventModelDataValues } from '../given-event/GivenEventModel'; + +/** + * Gets account IDs to query for project support (main account + sub-account if applicable) + */ +export async function getProjectAccountIdsToQuery( + projectId: AccountId, + projectChain: DbSchema, +): Promise { + const accountIds = [projectId]; + + if (isRepoDriverId(projectId)) { + const subAccountId = await calcSubRepoDriverId(projectId, projectChain); + accountIds.push(subAccountId); + } + + return accountIds; +} + +/** + * Queries split support for a project (including both main and sub-account) + */ +export async function getProjectSplitSupport( + projectId: AccountId, + projectChain: DbSchema, + supportDataSource: SupportDataSource, +): Promise { + const accountIdsForSplitSupport = await getProjectAccountIdsToQuery( + projectId, + projectChain, + ); + + const splitReceiversResults = await Promise.all( + accountIdsForSplitSupport.map((accountId) => + supportDataSource.getSplitSupportByReceiverIdOnChain( + accountId, + projectChain, + ), + ), + ); + + return splitReceiversResults.flat(); +} + +/** + * Queries one-time donation support for a project (including both main and sub-account) + */ +export async function getProjectOneTimeDonationSupport( + projectId: AccountId, + projectChain: DbSchema, + supportDataSource: SupportDataSource, +): Promise { + const accountIdsToQuery = await getProjectAccountIdsToQuery( + projectId, + projectChain, + ); + + return supportDataSource.getOneTimeDonationSupportByAccountIdsOnChain( + accountIdsToQuery, + projectChain, + ); +} diff --git a/src/utils/repoSubAccountIdUtils.ts b/src/utils/repoSubAccountIdUtils.ts index 858c535..9c41082 100644 --- a/src/utils/repoSubAccountIdUtils.ts +++ b/src/utils/repoSubAccountIdUtils.ts @@ -1,5 +1,9 @@ import dripsContracts from '../common/dripsContracts'; -import type { DbSchema, RepoDriverId } from '../common/types'; +import type { + DbSchema, + RepoDriverId, + RepoSubAccountDriverId, +} from '../common/types'; import { assertIsRepoDriverId, assertIsRepoSubAccountDriverId } from './assert'; import { dbSchemaToChain } from './chainSchemaMappings'; import shouldNeverHappen from './shouldNeverHappen'; @@ -50,6 +54,10 @@ export async function calcParentRepoDriverId( export async function calcSubRepoDriverId( parentId: string, chain: DbSchema, -): Promise { - return transformRepoDriverId(parentId, 'toSub', chain); +): Promise { + return transformRepoDriverId( + parentId, + 'toSub', + chain, + ) as unknown as Promise; }