Skip to content
Open
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
77 changes: 77 additions & 0 deletions indexer-api-gateway/src/api/services/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
SearchPolicyParamsDTO,
SearchPolicyResultDTO,
MessageDTO,
ProjectTonnagePageDTO,
VcTreeDTO,
} from '#dto';
import { PolicyFiltersDTO } from '../../dto/policy-filters.dto.js';
import { ComparePoliciesDTO } from '../../dto/compare-policies.dto.js';
Expand Down Expand Up @@ -125,6 +127,81 @@ export class AnalyticsApi extends ApiClient {
return await ZipUtils.unZipJson(zip);
}

/**
* Get project tonnage — aggregate token mint data per policy project.
* Supports filtering by policyId, owner, topicId and pagination.
* Addresses #4509 (project/tonnage API for eCommerce consumers).
*/
@Get('/projects')
@ApiOperation({
summary: 'Get projects with aggregate token issuance (tonnage)',
description:
'Returns a paginated list of policies with aggregated Mint Token VC data. ' +
'Useful for eCommerce consumers to discover consistent suppliers, ' +
'track project developer track records, and filter by methodology/geography.',
})
@ApiOkResponse({
description: 'Project tonnage page',
type: ProjectTonnagePageDTO,
})
@ApiInternalServerErrorResponse({
description: 'Internal server error',
type: InternalServerErrorDTO,
})
@HttpCode(HttpStatus.OK)
async getProjects(
@Query('pageIndex') pageIndex?: number,
@Query('pageSize') pageSize?: number,
@Query('orderField') orderField?: string,
@Query('orderDir') orderDir?: string,
@Query('policyId') policyId?: string,
@Query('owner') owner?: string,
@Query('topicId') topicId?: string,
@Query('minMinted') minMinted?: number,
): Promise<ProjectTonnagePageDTO> {
return await this.send(IndexerMessageAPI.GET_ANALYTICS_PROJECTS, {
pageIndex,
pageSize,
orderField,
orderDir,
policyId,
owner,
topicId,
minMinted,
});
}

/**
* Get VC document relationship tree rooted at a given message.
* Addresses #4509 (Tree API for consumer eCommerce transactions).
*/
@Get('/vc-tree/:messageId')
@ApiOperation({
summary: 'Get VC document relationship tree',
description:
'Returns a full hierarchical tree of VC documents starting from the given ' +
'message ID, traversing relationships recursively. Useful for eCommerce ' +
'consumers to understand the full trust chain of a carbon credit.',
})
@ApiOkResponse({
description: 'VC document tree',
type: VcTreeDTO,
})
@ApiInternalServerErrorResponse({
description: 'Internal server error',
type: InternalServerErrorDTO,
})
@HttpCode(HttpStatus.OK)
async getVcTree(
@Param('messageId') messageId: string,
@Query('maxDepth') maxDepth?: number,
): Promise<VcTreeDTO> {
return await this.send(IndexerMessageAPI.GET_ANALYTICS_VC_TREE, {
messageId,
maxDepth: maxDepth ? Number(maxDepth) : 10,
});
}

/**
* Get policy derivations
*/
Expand Down
4 changes: 3 additions & 1 deletion indexer-api-gateway/src/dto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ export * from './schema-tree.dto.js';
export * from './internal-server-error.dto.js';
export * from './search-policy.dto.js';
export * from './message.dto.js';
export * from './set-loading-priority.dto.js';
export * from './set-loading-priority.dto.js';
export * from './project-tonnage.dto.js';
export * from './vc-tree.dto.js';
67 changes: 67 additions & 0 deletions indexer-api-gateway/src/dto/project-tonnage.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

/**
* Individual token issuance record within a project
*/
export class ProjectTokenIssuanceDTO {
@ApiProperty({ description: 'Token identifier', example: '0.0.12345' })
tokenId: string;

@ApiProperty({ description: 'Token name', example: 'Carbon Credit Token' })
tokenName: string;

@ApiProperty({ description: 'Total amount minted (sum of all mint VCs)', example: 150000 })
totalMinted: number;

@ApiProperty({ description: 'Number of mint events', example: 12 })
mintEventCount: number;

@ApiPropertyOptional({ description: 'Most recent mint consensus timestamp', example: '1706823227.586179534' })
lastMintTimestamp?: string;
}

/**
* Project tonnage summary — aggregate mint data per policy project
*/
export class ProjectTonnageDTO {
@ApiProperty({ description: 'Policy message identifier', example: '1706823227.586179534' })
policyId: string;

@ApiPropertyOptional({ description: 'Policy name', example: 'ACM0007 Methodology' })
policyName?: string;

@ApiPropertyOptional({ description: 'Registry DID or owner', example: 'did:hedera:mainnet:....' })
owner?: string;

@ApiProperty({ description: 'Total minted credits across all tokens', example: 500000 })
totalMinted: number;

@ApiProperty({ description: 'Total number of mint events', example: 42 })
mintEventCount: number;

@ApiProperty({ description: 'Token breakdown per token ID', type: [ProjectTokenIssuanceDTO] })
tokens: ProjectTokenIssuanceDTO[];

@ApiPropertyOptional({ description: 'Geography / coordinates if available', example: '33.33|77.77' })
coordinates?: string;

@ApiPropertyOptional({ description: 'Project description topic ID', example: '0.0.98765' })
topicId?: string;
}

/**
* Paginated response for project tonnage
*/
export class ProjectTonnagePageDTO {
@ApiProperty({ description: 'List of projects with tonnage data', type: [ProjectTonnageDTO] })
items: ProjectTonnageDTO[];

@ApiProperty({ description: 'Total count of matching projects', example: 150 })
total: number;

@ApiProperty({ description: 'Current page index (0-based)', example: 0 })
pageIndex: number;

@ApiProperty({ description: 'Page size', example: 25 })
pageSize: number;
}
56 changes: 56 additions & 0 deletions indexer-api-gateway/src/dto/vc-tree.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

/**
* A single node in the VC document tree
*/
export class VcTreeNodeDTO {
@ApiProperty({ description: 'Message consensus timestamp (unique ID)', example: '1706823227.586179534' })
messageId: string;

@ApiProperty({ description: 'Document type', example: 'VC_DOCUMENT' })
type: string;

@ApiPropertyOptional({ description: 'Schema name', example: 'Monitoring Report' })
schemaName?: string;

@ApiPropertyOptional({ description: 'Schema identifier', example: '1706823227.586179534' })
schemaId?: string;

@ApiPropertyOptional({ description: 'Issuer DID', example: 'did:hedera:mainnet:...' })
issuer?: string;

@ApiPropertyOptional({ description: 'Policy identifier this document belongs to', example: '1706823227.586179534' })
policyId?: string;

@ApiPropertyOptional({ description: 'Topic ID', example: '0.0.12345' })
topicId?: string;

@ApiPropertyOptional({ description: 'Consensus timestamp (creation time)', example: '1706823227.586179534' })
consensusTimestamp?: string;

@ApiPropertyOptional({ description: 'Token amount if this is a Mint VC', example: 5000 })
tokenAmount?: number;

@ApiPropertyOptional({ description: 'Token ID if this is a Mint VC', example: '0.0.12345' })
tokenId?: string;

@ApiProperty({ description: 'Child nodes', type: () => [VcTreeNodeDTO] })
children: VcTreeNodeDTO[];
}

/**
* Full VC document relationship tree rooted at a given message
*/
export class VcTreeDTO {
@ApiProperty({ description: 'Root message identifier', example: '1706823227.586179534' })
rootId: string;

@ApiProperty({ description: 'Tree depth', example: 4 })
depth: number;

@ApiProperty({ description: 'Total number of nodes in the tree', example: 18 })
nodeCount: number;

@ApiProperty({ description: 'Root node of the tree', type: VcTreeNodeDTO })
root: VcTreeNodeDTO;
}
2 changes: 2 additions & 0 deletions indexer-common/src/messages/message-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export enum IndexerMessageAPI {

// #region ANALYTICS
GET_ANALYTICS_SEARCH_POLICY = 'INDEXER_API_GET_ANALYTICS_SEARCH_POLICY',
GET_ANALYTICS_PROJECTS = 'INDEXER_API_GET_ANALYTICS_PROJECTS',
GET_ANALYTICS_VC_TREE = 'INDEXER_API_GET_ANALYTICS_VC_TREE',
// #endregion

UPDATE_FILES = 'INDEXER_API_UPDATE_FILES',
Expand Down
2 changes: 2 additions & 0 deletions indexer-frontend/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { LabelDocumentDetailsComponent } from '@views/details/label-document-det
import { FormulasComponent } from '@views/collections/formulas/formulas.component';
import { FormulaDetailsComponent } from '@views/details/formula-details/formula-details.component';
import { PriorityQueueComponent } from '@views/priority-queue/priority-queue.component';
import { ProjectsComponent } from '@views/projects/projects.component';
import { SchemasPackagesComponent } from '@views/collections/schemas-packages/schemas-packages.component';
import { SchemasPackageDetailsComponent } from '@views/details/schemas-packages-details/schemas-packages-details.component';

Expand All @@ -65,6 +66,7 @@ export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'search', component: SearchViewComponent },
{ path: 'priority-queue', component: PriorityQueueComponent },
{ path: 'projects', component: ProjectsComponent },

//Collections
{ path: 'registries', component: RegistriesComponent },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export class HeaderComponent {
label: 'header.policies',
routerLink: '/policies',
},
{
label: 'Projects & Tonnage',
routerLink: '/projects',
},
{
label: 'header.tools',
routerLink: '/tools',
Expand Down
78 changes: 78 additions & 0 deletions indexer-frontend/src/app/services/analytics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,56 @@ import { API_BASE_URL } from './api';
import { ApiUtils } from './utils';
import { Page, PageFilters, Policy } from '@indexer/interfaces';

/** Aggregate token issuance per individual token within a project */
export interface ProjectTokenIssuance {
tokenId: string;
tokenName: string;
totalMinted: number;
mintEventCount: number;
lastMintTimestamp?: string;
}

/** Aggregate mint data per policy project */
export interface ProjectTonnage {
policyId: string;
policyName?: string;
owner?: string;
totalMinted: number;
mintEventCount: number;
tokens: ProjectTokenIssuance[];
coordinates?: string;
topicId?: string;
}

export interface ProjectTonnagePage {
items: ProjectTonnage[];
total: number;
pageIndex: number;
pageSize: number;
}

/** A single node in a VC relationship tree */
export interface VcTreeNode {
messageId: string;
type: string;
schemaName?: string;
schemaId?: string;
issuer?: string;
policyId?: string;
topicId?: string;
consensusTimestamp?: string;
tokenAmount?: number;
tokenId?: string;
children: VcTreeNode[];
}

export interface VcTree {
rootId: string;
depth: number;
nodeCount: number;
root: VcTreeNode;
}

@Injectable({ providedIn: 'root' })
export class AnalyticsService {
private readonly url = `${API_BASE_URL}/analytics`;
Expand All @@ -19,4 +69,32 @@ export class AnalyticsService {
const options = ApiUtils.getOptions(filters);
return this.http.get<Page<Policy>>(`${this.url}/derivations/${messageId}`, options) as any;
}

/**
* Get project tonnage — aggregate Mint Token data per policy project.
* Supports filtering by policyId, owner, topicId, minMinted and pagination.
* Addresses #4509: project/tonnage API for eCommerce consumers.
*/
public getProjects(filters: {
pageIndex?: number;
pageSize?: number;
orderField?: string;
orderDir?: string;
policyId?: string;
owner?: string;
topicId?: string;
minMinted?: number;
} = {}): Observable<ProjectTonnagePage> {
const options = ApiUtils.getOptions(filters as any);
return this.http.get<ProjectTonnagePage>(`${this.url}/projects`, options);
}

/**
* Get VC document relationship tree rooted at the given message ID.
* Addresses #4509: Tree API for consumer eCommerce transactions.
*/
public getVcTree(messageId: string, maxDepth: number = 10): Observable<VcTree> {
const options = ApiUtils.getOptions({ maxDepth } as any);
return this.http.get<VcTree>(`${this.url}/vc-tree/${messageId}`, options);
}
}
41 changes: 41 additions & 0 deletions indexer-frontend/src/app/views/projects/projects.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<div class="page-content">
<div class="content">
<h1 class="content__header">Projects &amp; Tonnage</h1>
<p class="content__subtitle" style="color:#888;font-size:13px;margin:-12px 0 16px;">
Aggregate token issuance (mint credits) per policy project — for eCommerce consumers and supply discovery.
Fixes Indexer use-case #7: project developer track record via role-ID relationships.
</p>
<div class="content__table">
<app-table
class="collection-container__table"
[loading]="loading"
[columns]="columns"
[data]="items"
[pageIndex]="pageIndex"
[pageSize]="pageSize"
[total]="total"
(onPage)="onPage($event)"
[sortColumn]="orderField"
[sortDirection]="orderDir"
(onSort)="onSort($event)"
(onRowClick)="onRowClick($event)">
<div class="filters">
<div class="filters__fields">
<p-inputGroup>
<p-inputGroupAddon>Owner / Registry</p-inputGroupAddon>
<input pInputText placeholder="DID or owner address" [formControl]="ownerFilter" />
</p-inputGroup>
<p-inputGroup>
<p-inputGroupAddon>Policy ID</p-inputGroupAddon>
<input pInputText placeholder="consensus timestamp" [formControl]="policyFilter" />
</p-inputGroup>
<p-inputGroup>
<p-inputGroupAddon>Min Minted</p-inputGroupAddon>
<input pInputText type="number" placeholder="e.g. 10000" [formControl]="minMintedFilter" />
</p-inputGroup>
</div>
</div>
</app-table>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Projects & Tonnage view styles
// Inherits base-grid component styles
Loading
Loading