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
2 changes: 2 additions & 0 deletions aastar/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { AdminModule } from "./admin/admin.module";
import { SaleModule } from "./sale/sale.module";
import { IndexerModule } from "./indexer/indexer.module";
import { SsoModule } from "./sso/sso.module";
import { MyTaskIndexModule } from "./mytask-index/mytask-index.module";

@Module({
imports: [
Expand All @@ -48,6 +49,7 @@ import { SsoModule } from "./sso/sso.module";
SaleModule,
IndexerModule,
SsoModule,
MyTaskIndexModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
74 changes: 74 additions & 0 deletions aastar/src/mytask-index/mytask-index.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { AbiEvent } from "viem";

/**
* Indexer source key for MyTask challenge events. Stable public identifier used
* by IndexerService.registerSource / queryEvents and the /mytask/challenges read
* endpoints. Do not rename without a persistence migration (it keys the cursor).
*/
export const MYTASK_CHALLENGES_SOURCE = "mytask-challenges";

/**
* TaskEscrowV2 default address (MT-8 redeploy, Sepolia, verified on-chain).
* Overridable via env MYTASK_ESCROW_ADDRESS. Set the env to an empty string to
* disable MyTask indexing entirely (the source is then skipped at init).
* Source of truth: ~/Dev/mycelium/MyTask/contracts + broadcast/DeploySepolia.
*/
export const MYTASK_ESCROW_DEFAULT_ADDRESS = "0x171234DD282eF2909ec20dafC3F81deBa6761178";

/**
* Default first block to scan. This is the TaskEscrowV2 deployment block on
* Sepolia (from broadcast/DeploySepolia.s.sol/11155111/run-latest.json receipt),
* so the indexer never wastes RPC scanning pre-deployment history. Override via
* env MYTASK_INDEX_FROM_BLOCK; if a fresh redeploy moves the contract, update
* this to the new deployment block to keep catch-up cheap.
*/
export const MYTASK_INDEX_DEFAULT_FROM_BLOCK = 11269271;

/**
* Event ABI fragments — copied 1:1 from TaskEscrowV2.sol so viem decodes the
* indexed topics and data correctly. A single wrong name/type/indexed flag
* silently breaks decoding, so these must mirror the contract exactly:
*
* event TaskChallenged(bytes32 indexed taskId, address indexed challenger, uint256 stake);
* event ChallengeResolved(bytes32 indexed taskId, bool challengeAccepted);
*/
export const TASK_CHALLENGED_EVENT = {
type: "event",
name: "TaskChallenged",
inputs: [
{ name: "taskId", type: "bytes32", indexed: true },
{ name: "challenger", type: "address", indexed: true },
{ name: "stake", type: "uint256", indexed: false },
],
} as const satisfies AbiEvent;

export const CHALLENGE_RESOLVED_EVENT = {
type: "event",
name: "ChallengeResolved",
inputs: [
{ name: "taskId", type: "bytes32", indexed: true },
{ name: "challengeAccepted", type: "bool", indexed: false },
],
} as const satisfies AbiEvent;

/** ABI passed to the indexer source (only its event items are used). */
export const MYTASK_CHALLENGES_ABI = [TASK_CHALLENGED_EVENT, CHALLENGE_RESOLVED_EVENT];

export const MYTASK_CHALLENGES_EVENT_NAMES = ["TaskChallenged", "ChallengeResolved"];

/** bytes32 hex (0x + 64 hex chars) — the taskId query-param shape. */
export const BYTES32_RE = /^0x[0-9a-fA-F]{64}$/;

/** Upper bound for the recent-challenges list (mirrors indexer's clamp style). */
export const MAX_RECENT_LIMIT = 100;
export const DEFAULT_RECENT_LIMIT = 20;

/** Page size when draining the indexer (matches IndexerService MAX_QUERY_LIMIT). */
export const INDEX_PAGE_SIZE = 200;

/**
* Safety cap on how many events we drain from the indexer for an in-module
* filter/scan. A-8's getEvents has no arg-level (taskId) filter, so we page and
* filter here; this bounds the work if the source grows very large.
*/
export const MAX_SCAN_EVENTS = 5000;
44 changes: 44 additions & 0 deletions aastar/src/mytask-index/mytask-index.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { BadRequestException, Controller, Get, Query } from "@nestjs/common";
import { ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger";
import { BYTES32_RE, DEFAULT_RECENT_LIMIT, MAX_RECENT_LIMIT } from "./mytask-index.constants";
import { MyTaskIndexService } from "./mytask-index.service";

/** Strict decimal-integer parse (mirrors indexer.controller). */
function parseStrictInt(name: string, value?: string): number | undefined {
if (value === undefined || value === "") return undefined;
if (!/^\d+$/.test(value)) {
throw new BadRequestException(`Query param "${name}" must be a non-negative integer`);
}
return Number.parseInt(value, 10);
}

@ApiTags("mytask")
@Controller("mytask")
export class MyTaskIndexController {
constructor(private readonly service: MyTaskIndexService) {}

@Get("challenges")
@ApiOperation({
summary: "Challengers of a task (public read-only) — from indexed TaskChallenged events",
})
@ApiQuery({ name: "taskId", required: true, description: "Task id (0x-prefixed bytes32)" })
async getChallenges(@Query("taskId") taskId?: string) {
if (!taskId || !BYTES32_RE.test(taskId)) {
throw new BadRequestException('Query param "taskId" must be a 0x-prefixed bytes32');
}
return this.service.getChallengesByTaskId(taskId);
}

@Get("challenges/recent")
@ApiOperation({ summary: "Most recent challenges across all tasks (public read-only)" })
@ApiQuery({
name: "limit",
required: false,
description: `Page size (default ${DEFAULT_RECENT_LIMIT}, max ${MAX_RECENT_LIMIT})`,
})
async getRecentChallenges(@Query("limit") limit?: string) {
const parsed = parseStrictInt("limit", limit) ?? DEFAULT_RECENT_LIMIT;
const clamped = Math.min(Math.max(parsed, 1), MAX_RECENT_LIMIT);
return this.service.getRecentChallenges(clamped);
}
}
17 changes: 17 additions & 0 deletions aastar/src/mytask-index/mytask-index.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Module } from "@nestjs/common";
import { IndexerModule } from "../indexer/indexer.module";
import { MyTaskIndexController } from "./mytask-index.controller";
import { MyTaskIndexService } from "./mytask-index.service";

/**
* First consumer of the shared A-8 IndexerService. Imports IndexerModule to
* obtain the (exported) IndexerService, registers TaskEscrowV2's challenge
* events as a source at init, and serves the /mytask/challenges read endpoints.
*/
@Module({
imports: [IndexerModule],
controllers: [MyTaskIndexController],
providers: [MyTaskIndexService],
exports: [MyTaskIndexService],
})
export class MyTaskIndexModule {}
Loading
Loading