Skip to content
Merged

Main #39

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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=<your-anon-key>
# Server-only β€” used by scripts, seeders and admin mutations.
# NEVER expose this to the browser.
SUPABASE_SERVICE_ROLE_KEY=<your-service-role-key>
STELLAR_HORIZON_URL=https://horizon.stellar.org
34 changes: 34 additions & 0 deletions src/@types/activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export type EventType =
| 'contract_deploy'
| 'payment'
| 'trust_change'
| 'contract_invoke'
| 'data_entry'
| 'account_create';

export type Significance = 'low' | 'medium' | 'high' | 'critical';

export interface MonitoredAccount {
id: string;
stellar_address: string;
label: string;
account_type: string;
monitor_events: string[];
active: boolean;
last_cursor?: string | null;
created_at: string;
}

export interface ActivityEvent {
id: string;
account_id: string;
source_account: string;
event_type: EventType;
significance: Significance;
raw_data: any;

Check failure on line 28 in src/@types/activity.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

Unexpected any. Specify a different type
summary?: string;
processed: boolean;
draft_article_id?: string | null;
detected_at: string;
tx_hash?: string | null;
}
Comment on lines +22 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

Replace any with a JSON-safe type.

raw_data is persisted JSON, so any weakens the new contract and already fails lint. unknown (or the concrete payload shape) is safer here.

Possible fix
-  raw_data: any;
+  raw_data: unknown;
πŸ“ 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
export interface ActivityEvent {
id: string;
account_id: string;
source_account: string;
event_type: EventType;
significance: Significance;
raw_data: any;
summary?: string;
processed: boolean;
draft_article_id?: string | null;
detected_at: string;
tx_hash?: string | null;
}
export interface ActivityEvent {
id: string;
account_id: string;
source_account: string;
event_type: EventType;
significance: Significance;
raw_data: unknown;
summary?: string;
processed: boolean;
draft_article_id?: string | null;
detected_at: string;
tx_hash?: string | null;
}
🧰 Tools
πŸͺ› ESLint

[error] 28-28: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

πŸͺ› GitHub Actions: CI / 0_lint-and-build.txt

[error] 28-28: ESLint (@typescript-eslint/no-explicit-any): Unexpected any. Specify a different type

πŸͺ› GitHub Actions: CI / lint-and-build

[error] 28-28: ESLint (@typescript-eslint/no-explicit-any): Unexpected any. Specify a different type.

πŸͺ› GitHub Check: lint-and-build

[failure] 28-28:
Unexpected any. Specify a different type

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/`@types/activity.ts around lines 22 - 34, The ActivityEvent type still
uses raw_data: any, which weakens the JSON contract and breaks linting. Update
the ActivityEvent interface in the activity types definition to use unknown or a
JSON-safe payload type instead of any, and make sure any code that reads
raw_data narrows or validates it before use.

Sources: Linters/SAST tools, Pipeline failures

14 changes: 14 additions & 0 deletions src/app/admin/(protected)/activity/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { createAdminClient } from '@/lib/supabase/admin';
import { ActivityFeed } from '@/components/modules/admin/activity/ActivityFeed';

export default async function ActivityPage() {
const supabase = createAdminClient();
const { data: events } = await supabase.from('activity_events').select('*').order('detected_at', { ascending: false }).limit(50);
return (
<div>
<h1>Admin β€” Activity</h1>
<ActivityFeed events={(events || []) as any} />

Check failure on line 11 in src/app/admin/(protected)/activity/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-build

Unexpected any. Specify a different type
Comment on lines +7 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Don't hide query failures behind an empty feed.

If the Supabase query errors, this silently renders no activity and looks like a real empty state. Throw or surface error so operators can distinguish "no data" from "failed to load data".

Possible fix
-  const { data: events } = await supabase.from('activity_events').select('*').order('detected_at', { ascending: false }).limit(50);
+  const { data: events, error } = await supabase.from('activity_events').select('*').order('detected_at', { ascending: false }).limit(50);
+  if (error) throw error;
πŸ“ 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 { data: events } = await supabase.from('activity_events').select('*').order('detected_at', { ascending: false }).limit(50);
return (
<div>
<h1>Admin β€” Activity</h1>
<ActivityFeed events={(events || []) as any} />
const { data: events, error } = await supabase.from('activity_events').select('*').order('detected_at', { ascending: false }).limit(50);
if (error) throw error;
return (
<div>
<h1>Admin β€” Activity</h1>
<ActivityFeed events={(events || []) as any} />
🧰 Tools
πŸͺ› ESLint

[error] 11-11: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

πŸͺ› GitHub Check: lint-and-build

[failure] 11-11:
Unexpected any. Specify a different type

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/admin/`(protected)/activity/page.tsx around lines 7 - 11, The admin
activity page is swallowing Supabase query failures by falling back to an empty
list in the activity feed. Update the `page.tsx` flow around the
`supabase.from('activity_events')` query and `ActivityFeed` rendering to check
for `error` and throw or otherwise surface it instead of defaulting to `[]`, so
`events` only renders when the query succeeds and operators can distinguish an
empty result from a load failure.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

Remove the as any cast.

This is the lint failure on this file, and it defeats the ActivityEvent boundary. Pass a typed row array instead of erasing the result type here.

Possible fix
-      <ActivityFeed events={(events || []) as any} />
+      <ActivityFeed events={events ?? []} />
πŸ“ 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
<ActivityFeed events={(events || []) as any} />
<ActivityFeed events={events ?? []} />
🧰 Tools
πŸͺ› ESLint

[error] 11-11: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

πŸͺ› GitHub Check: lint-and-build

[failure] 11-11:
Unexpected any. Specify a different type

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/admin/`(protected)/activity/page.tsx at line 11, The ActivityFeed
call is bypassing the ActivityEvent type boundary by casting events to any.
Update the page component to pass a properly typed row array into ActivityFeed
by using the actual result type from the events source and removing the unsafe
cast; use the ActivityFeed and events symbols to locate the prop wiring and
preserve type safety end to end.

Sources: Linters/SAST tools, Pipeline failures

</div>
);
}
17 changes: 17 additions & 0 deletions src/components/modules/admin/activity/ActivityFeed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import type { ActivityEvent } from '@/@types/activity';

export function ActivityFeed({ events }: { events: ActivityEvent[] }) {
return (
<div>
<h2>Activity Feed</h2>
<ul>
{events.map((e) => (
<li key={e.id}>
<strong>{e.event_type}</strong> β€” {e.summary} β€” <em>{e.significance}</em>
</li>
))}
</ul>
</div>
);
}
43 changes: 43 additions & 0 deletions src/lib/stellar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import fetch from 'node-fetch';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify whether node-fetch is declared for the runtimes that import src/lib/stellar/index.ts.
set -euo pipefail

rg -n "from ['\"]node-fetch['\"]|require\\(['\"]node-fetch['\"]\\)" .

fd -t f '^(package\.json|deno\.jsonc?|import_map\.json)$' . --exec sh -c '
  echo "--- $1"
  sed -n "1,220p" "$1"
' sh {}

Repository: ACTA-Team/news-acta

Length of output: 1472


Avoid node-fetch; use native fetch.

node-fetch is imported in src/lib/stellar/index.ts but is missing from package.json. As this project uses Next.js, the global fetch API is natively available, rendering the external dependency unnecessary.

Remove the unused import to prevent potential build failures:

Proposed diff
-import fetch from 'node-fetch';
πŸ“ 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
import fetch from 'node-fetch';
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/stellar/index.ts` at line 1, Remove the unnecessary node-fetch import
from the stellar module and use the native global fetch instead. Update
src/lib/stellar/index.ts by deleting the import statement and make sure any
references in the module rely on the built-in fetch API so the code stays
compatible with Next.js without requiring an extra dependency.


export interface HorizonOperation {
id: string;
paging_token: string;
type: string;
transaction_hash?: string;
source_account?: string;
[key: string]: any;

Check failure on line 9 in src/lib/stellar/index.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

Unexpected any. Specify a different type

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/lib/stellar/index.ts

Repository: ACTA-Team/news-acta

Length of output: 2590


🏁 Script executed:

python3 - <<'PY'
import json

# Simulate the TypeScript index signature behavior
# The original: [key: string]: any
# TS Behavior: A property accessed with [string] can accept value of type 'any'.
# Replacing with 'unknown': 
#   1. The interface becomes: { [key: string]: unknown }.
#   2. Assigning an object `data` (JSON) to a variable of this type is valid (record).
#   3. The issue is `map((r: any) => r as HorizonOperation)`. 
#      If the interface has `[key: string]: any`, and data comes in, mapping `r` to `HorizonOperation` is fine.
#      If we change index to `unknown`, the variable `r` inside map (inferred from `records`) would be `unknown`.
#      `r as HorizonOperation` on `unknown` is valid.
# 4. However, the proposed fix in the original comment suggests a structural type `HorizonOperationsResponse`.
# 5. Let's verify if the specific `no-explicit-any` check flags the index signature.
#    Yes, in strict mode `any` in an index signature triggers `no-explicit-any` if it's not a workaround.

# But wait, does the map usage need fixing?
# Line 25: const ops: HorizonOperation[] = (data._embedded?.records || []).map((r: any) => r as HorizonOperation);
# If we fix line 9 to 'unknown', line 25's `(r: any)` is still an explicit `any`.
# The original comment says "lines 9-9 and 25-25".
# If index signature is `any`, it flags line 9.
# If parameter `r` is `any`, it flags line 25.

# Proposed change:
# Line 9: [key: string]: unknown; -> This is a known workaround for unknown additional props but often requires type narrowing when accessing.
# BUT, the `no-explicit-any` rule *specifically* prohibits `any`. `unknown` is the replacement.
# However, changing line 9 to `unknown` makes line 25's `r` type `unknown` (implicitly) or `any` (explicitly).
# The proposed fix in the original comment adds an interface structure for better typing.

# Let's check if the proposed fix is better practice.
# Current: `[key: string]: any` allows everything.
# Better: `Record<string, unknown>` or specific types.
# The code uses `r as HorizonOperation`. If `data._embedded.records` has `unknown` entries, we still need to cast or verify.
# The `any` on index signature is definitely a lint violation if `no-explicit-any` is on.
# The `any` on the map parameter `r` is ALSO a lint violation.

# Conclusion: The issue is real on both lines.
# Line 9: `[key: string]: any` -> Replace with `Record<string, unknown>` or `[key: string]: unknown`.
# Line 25: `r: any` -> Remove the type annotation and let TS infer `unknown` from the array, or use the cast directly.
# The original comment suggests `[key: string]: unknown` and updating the response type.

# I will rewrite the comment to include both fixes (line 9 and 25) clearly.
print("Verification complete: Both line 9 and 25 contain explicit 'any' triggering lint rules.")
PY

Repository: ACTA-Team/news-acta

Length of output: 149


Replace explicit any with unknown and remove unnecessary type annotation.

Update the HorizonOperation index signature and remove the explicit any type from the map callback to satisfy @typescript-eslint/no-explicit-any.

src/lib/stellar/index.ts

export interface HorizonOperation {
  // ...
-  [key: string]: any;
+  [key: string]: unknown;
}
- const ops: HorizonOperation[] = (data._embedded?.records || []).map((r: any) => r as HorizonOperation);
+ const ops: HorizonOperation[] = (data._embedded?.records || []).map((r) => r as HorizonOperation);

The parameter r will be inferred as unknown from the array source. The subsequent as HorizonOperation cast remains valid to narrow the type before use.

🧰 Tools
πŸͺ› ESLint

[error] 9-9: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

πŸͺ› GitHub Check: lint-and-build

[failure] 9-9:
Unexpected any. Specify a different type

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/stellar/index.ts` at line 9, The `HorizonOperation` shape currently
uses an explicit `any` index signature and the map callback also relies on an
unnecessary explicit `any`, which should be removed to satisfy the lint rule.
Update the `HorizonOperation` type in `index.ts` to use `unknown` for the index
signature, and remove the explicit type annotation from the `r` parameter in the
mapping logic so it can be inferred from the array source; keep the existing `as
HorizonOperation` cast where the value is narrowed for use.

Source: Linters/SAST tools

}

export async function fetchOperationsSince(
horizonUrl: string,
account: string,
cursor?: string
): Promise<{ operations: HorizonOperation[]; nextCursor?: string }> {
const url = new URL(`${horizonUrl.replace(/\/$/, '')}/accounts/${account}/operations`);
url.searchParams.set('limit', '200');
url.searchParams.set('order', 'asc');
if (cursor) url.searchParams.set('cursor', cursor);

const res = await fetch(url.toString());
if (!res.ok) throw new Error(`Horizon fetch failed: ${res.status}`);
Comment on lines +22 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

Bound the Horizon request with a timeout

The fetch call currently lacks a timeout safeguard. A stalled Horizon request can block the scanner platform timeout and prevent subsequent accounts from being scanned.

Suggested implementation
  const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 10_000);
- const res = await fetch(url.toString());
+ const res = await fetch(url.toString(), { signal: controller.signal }).finally(() => {
+   clearTimeout(timeout);
+ });
  if (!res.ok) throw new Error(`Horizon fetch failed: ${res.status}`);
πŸ“ 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 res = await fetch(url.toString());
if (!res.ok) throw new Error(`Horizon fetch failed: ${res.status}`);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
const res = await fetch(url.toString(), { signal: controller.signal }).finally(() => {
clearTimeout(timeout);
});
if (!res.ok) throw new Error(`Horizon fetch failed: ${res.status}`);
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/stellar/index.ts` around lines 22 - 23, The Horizon request in the
stellar fetch path has no timeout, so a stalled call can block scanning. Update
the fetch logic in the stellar index flow to use an AbortController (or
equivalent timeout-aware request wrapper) with a reasonable deadline, and ensure
the timeout is cleared after completion. Keep the existing error handling around
the fetch result, and make the timeout failure surface as a clear request error
from the same Horizon request code path.

const data = await res.json();
const ops: HorizonOperation[] = (data._embedded?.records || []).map((r: any) => r as HorizonOperation);

Check failure on line 25 in src/lib/stellar/index.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

Unexpected any. Specify a different type
const nextCursor = ops.length ? ops[ops.length - 1].paging_token : undefined;
return { operations: ops, nextCursor };
}

export function classifyOperation(op: HorizonOperation): { eventType: string; significance: string; summary?: string } {
const t = op.type;
if (t === 'create_account') return { eventType: 'account_create', significance: 'low', summary: `Account created: ${op.account}` };
if (t === 'payment' || t === 'payment_strict_receive') {
const amount = op.amount || op.value || '0';
const significance = Number(amount) > 100 ? 'high' : 'medium';
return { eventType: 'payment', significance, summary: `Payment ${amount} ${op.asset_type || 'XLM'}` };
}
Comment on lines +33 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟑 Minor

🧩 Analysis chain

🌐 Web query:

Stellar Horizon operation type payment_strict_send operation record type

πŸ’‘ Result:

In the Stellar Horizon API, the operation record type for a path payment where the source amount is fixed is represented by the string "path_payment_strict_send" [1][2]. When querying Horizon for operations, this operation is identified in JSON responses by the following key fields: - type: "path_payment_strict_send" [1] - type_i: 13 [1] The path_payment_strict_send operation is used to send a payment from one account to another through a path of order books, where the sender designates the exact amount of the source asset to be sent [1]. This contrasts with a standard path_payment_strict_receive operation, where the receiver designates the exact amount they wish to receive [2]. Key properties present in the operation record include: - source_account: The account originating the payment [1]. - source_asset: The asset type and details (code/issuer) being sent [1]. - source_amount: The exact amount of the source asset to be sent [1]. - destination: The account receiving the payment [1]. - dest_asset: The asset the destination account receives [1]. - dest_min: The minimum amount of the destination asset the sender is willing to accept [1]. - path: An array of assets representing the path taken through the order books [1]. For developers, these operations are handled via Stellar SDKs (such as stellar-sdk for Python or JavaScript), which provide dedicated objects and builders (e.g., PathPaymentStrictSend) that map to the underlying XDR operation type PATH_PAYMENT_STRICT_SEND [3][4][5][6].

Citations:


🏁 Script executed:

cat -n src/lib/stellar/index.ts | sed -n '30,50p'

Repository: ACTA-Team/news-acta

Length of output: 1407


Include payment_strict_send in payment classification logic.

The operation type payment_strict_send falls through to the default handler, assigning it a hardcoded significance of 'low'. This likely bypasses intended significance thresholds for actual payments made via strict send.

Update the condition at line 33 to match strict send payments:

-  if (t === 'payment' || t === 'payment_strict_receive') {
+  if (t === 'payment' || t === 'payment_strict_receive' || t === 'payment_strict_send') {
πŸ“ 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
if (t === 'payment' || t === 'payment_strict_receive') {
const amount = op.amount || op.value || '0';
const significance = Number(amount) > 100 ? 'high' : 'medium';
return { eventType: 'payment', significance, summary: `Payment ${amount} ${op.asset_type || 'XLM'}` };
}
if (t === 'payment' || t === 'payment_strict_receive' || t === 'payment_strict_send') {
const amount = op.amount || op.value || '0';
const significance = Number(amount) > 100 ? 'high' : 'medium';
return { eventType: 'payment', significance, summary: `Payment ${amount} ${op.asset_type || 'XLM'}` };
}
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/stellar/index.ts` around lines 33 - 37, The payment classification in
the logic around the t check currently handles only payment and
payment_strict_receive, so payment_strict_send falls through to the default
low-significance path. Update the condition in the payment handling branch to
include payment_strict_send, and keep using the same amount-based significance
calculation and summary formatting in the existing payment return path.

if (t === 'change_trust') return { eventType: 'trust_change', significance: 'medium', summary: `Trustline change` };
if (t === 'manage_data') return { eventType: 'data_entry', significance: 'low', summary: `Data entry updated` };
if (t === 'invoke_host_function' || t === 'invoke_contract') return { eventType: 'contract_invoke', significance: 'high', summary: `Contract invoked` };
if (t === 'deploy_contract') return { eventType: 'contract_deploy', significance: 'high', summary: `Contract deployed` };
return { eventType: t, significance: 'low', summary: `Operation ${t}` };
}
55 changes: 55 additions & 0 deletions supabase/functions/activity_scanner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createAdminClient } from '../../../src/lib/supabase/admin';
import { fetchOperationsSince, classifyOperation } from '../../../src/lib/stellar';

const HORIZON_URL = process.env.STELLAR_HORIZON_URL || 'https://horizon.stellar.org';

export default async function handler() {
const supabase = createAdminClient();

// fetch active monitored accounts
const { data: accounts } = await supabase.from('monitored_accounts').select('*').eq('active', true);
if (!accounts) return { processed: 0 };
Comment on lines +10 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n supabase/functions/activity_scanner/index.ts

Repository: ACTA-Team/news-acta

Length of output: 2461


🏁 Script executed:

cd /workspace/supabase/functions/activity_scanner
cat -n index.ts

Repository: ACTA-Team/news-acta

Length of output: 290


🏁 Script executed:

cat -n "supabase/functions/activity_scanner/index.ts"

Repository: ACTA-Team/news-acta

Length of output: 2461


🏁 Script executed:

cat -n "src/lib/supabase/admin.ts"

Repository: ACTA-Team/news-acta

Length of output: 644


🏁 Script executed:

python3 - <<'PY'
import json

# Simulating Supabase client response for select().eq()
# If query fails (e.g., network issue, permission denied, DB error), 
# the standard supabase-js response is: error != null, and data is null.
# If query succeeds but finds no rows, response is: error == null, data == [].

# Current Code Logic at lines 10-11:
#  const { data: accounts } = await supabase.from('monitored_accounts').select('*').eq('active', true);
#  if (!accounts) return { processed: 0 };

# Scenario 1: Query FAILED (e.g., 500 error)
# Response: { data: null, error: { message: '...', ... } }
# Variable accounts = null
# if (!accounts) is TRUE -> Returns { processed: 0 } silently.
# CONSEQUENCE: The caller (e.g., Scheduled Function) sees success but 0 processed. 
# An outage is masked as "nothing to do".

# Scenario 2: Query SUCCESS, no active accounts
# Response: { data: [], error: null }
# Variable accounts = []
# typeof [] is "object", ![] is FALSE.
# Loop `for (const acct of accounts)` iterates 0 times.
# Returns { processed: 0 } explicitly.
# CONSEQUENCE: Correctly reflects "no work done".

# The bug is specific to Scenario 1 (Error vs No Results).
# The proposed fix in the original comment checks for `accountsError` explicitly.

# Let's verify if the file has any import for `SupabaseResponse` type or similar 
# to see if error destructuring is expected elsewhere.
# We also need to confirm if the current code throws on error or swallows it.

print("Analysis Result:")
print("The current code destructures ONLY `data`.")
print("When Supabase query fails, `data` is `null`.")
print("The check `if (!accounts)` treats `null` (error) and potentially other falsy values identically to 'no data'.")
print("However, if the query returns an empty array (success), `data` is `[]`.")
print("'!' converts `[]` to `false`, so `if (!accounts)` is FALSE for empty results.")
print("Wait, let's re-verify JS truthy/falsy.")
print(f"Boolean({[]}): {bool([])}") # This is True (Objects are truthy)
# Actually `![]` is false. So `if (!accounts)` with accounts=[] is FALSE.
# The code proceeds to `for (const acct of [])` which does nothing.
# It returns `{ processed: 0 }` at the end. That is fine.

# But for Error: accounts = null. `!null` is True.
# Code returns `{ processed: 0 }` immediately. 
# Error is ignored. No alerting, no propagation.

# Conclusion: The original review comment is correct. 
# The missing error check swallows database failures.
PY

Repository: ACTA-Team/news-acta

Length of output: 580


🏁 Script executed:

node - <<'JS'
// Verify the truthiness of an empty array in JavaScript
const emptyArr = [];
console.log("Boolean([]):", Boolean(emptyArr));
console.log("![]:", !emptyArr);
console.log("Type of []:", typeof emptyArr);

// Verify the logic of the current code
// Scenario: Query fails
const errorData = null;
console.log("\nScenario: Query Failed (data is null)");
// Simulating: const { data: accounts } = { data: null, error: ... };
const accountsErrorCase = errorData;
if (!accountsErrorCase) {
  console.log("-> Current: Treates as 'no work', returns { processed: 0 }");
} else {
  console.log("-> Current: Proceeds to loop");
}

// Scenario: Query succeeds, no rows
const successData = [];
console.log("\nScenario: Success, empty results (data is [])");
console.log("Current check '!successData':", !successData);
if (!successData) {
  console.log("-> Current: Treats as 'no work'");
} else {
  console.log("-> Current: Proceeds to loop");
  for (const item of successData) {
    console.log("Processing item");
  }
  console.log("-> Loop finished empty, returns { processed: 0 }");
}
JS

Repository: ACTA-Team/news-acta

Length of output: 464


🏁 Script executed:

sed -n '18p;27p;54p' "supabase/functions/activity_scanner/index.ts"

Repository: ACTA-Team/news-acta

Length of output: 269


🏁 Script executed:

grep -n "error" "supabase/functions/activity_scanner/index.ts"

Repository: ACTA-Team/news-acta

Length of output: 222


Hide ingestion outages when monitored_accounts query fails.

Destructure the error property from the query response and throw it if present. Currently, if the database query fails, data is null, causing the check if (!accounts) to trigger, which silently returns { processed: 0 } instead of surfacing the failure.

Proposed fix
-  const { data: accounts } = await supabase.from('monitored_accounts').select('*').eq('active', true);
-  if (!accounts) return { processed: 0 };
+  const { data: accounts, error: accountsError } = await supabase
+    .from('monitored_accounts')
+    .select('*')
+    .eq('active', true);
+
+  if (accountsError) throw accountsError;
+  if (!accounts?.length) return { processed: 0 };
πŸ“ 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 { data: accounts } = await supabase.from('monitored_accounts').select('*').eq('active', true);
if (!accounts) return { processed: 0 };
const { data: accounts, error: accountsError } = await supabase
.from('monitored_accounts')
.select('*')
.eq('active', true);
if (accountsError) throw accountsError;
if (!accounts?.length) return { processed: 0 };
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/activity_scanner/index.ts` around lines 10 - 11, The
monitored_accounts fetch in activity_scanner currently treats any null data as
an empty result, which hides real query failures. Update the supabase query
handling in the activity scanning flow to destructure the error alongside data,
and in the same block throw or propagate that error before checking accounts.
Keep the existing processed fallback only for the true no-data case, and make
the change in the logic around the monitored_accounts query response.


let processed = 0;

for (const acct of accounts) {
try {
const { operations, nextCursor } = await fetchOperationsSince(HORIZON_URL, acct.stellar_address, acct.last_cursor || undefined);
for (const op of operations) {
const classification = classifyOperation(op as any);

Check failure on line 19 in supabase/functions/activity_scanner/index.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

Unexpected any. Specify a different type
// basic dedupe: by tx_hash
const txHash = (op as any).transaction_hash || null;

Check failure on line 21 in supabase/functions/activity_scanner/index.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

Unexpected any. Specify a different type
Comment on lines +19 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n supabase/functions/activity_scanner/index.ts

Repository: ACTA-Team/news-acta

Length of output: 2461


🏁 Script executed:

grep -r "type HorizonOperation" --include="*.ts"

Repository: ACTA-Team/news-acta

Length of output: 157


🏁 Script executed:

grep -r "function classifyOperation" --include="*.ts" -A 5 --include="*.d.ts"

Repository: ACTA-Team/news-acta

Length of output: 758


🏁 Script executed:

cat -n src/lib/stellar/index.ts

Repository: ACTA-Team/news-acta

Length of output: 2590


Remove the redundant any casts and unnecessary operator usage.

fetchOperationsSince returns HorizonOperation[], so op is already properly typed. HorizonOperation includes transaction_hash and allows index access any, making the as any casts redundant.

        const classification = classifyOperation(op);
        // basic dedupe: by tx_hash
-       const txHash = (op as any).transaction_hash || null;
+       const txHash = op.transaction_hash ?? null;

Also, note that classifyOperation accesses op.account (line 32 of stellar/index.ts), which is not explicitly defined in the HorizonOperation interface, yet the code relies on the [key: string]: any index signature. Ensure that dynamic property access is handled consistently.

πŸ“ 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 classification = classifyOperation(op as any);
// basic dedupe: by tx_hash
const txHash = (op as any).transaction_hash || null;
const classification = classifyOperation(op);
// basic dedupe: by tx_hash
const txHash = op.transaction_hash ?? null;
🧰 Tools
πŸͺ› ESLint

[error] 19-19: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 21-21: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

πŸͺ› GitHub Check: lint-and-build

[failure] 21-21:
Unexpected any. Specify a different type


[failure] 19-19:
Unexpected any. Specify a different type

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/activity_scanner/index.ts` around lines 19 - 21, Remove
the redundant `as any` casts in `activity_scanner/index.ts` by using the
existing `HorizonOperation` type from `fetchOperationsSince` directly; update
the `classifyOperation` call and `transaction_hash` access to rely on the typed
operation object. Also review `classifyOperation` in `stellar/index.ts` so
dynamic fields like `account` are accessed consistently through the interface’s
index signature rather than unnecessary casts, keeping the typing aligned across
both functions.

Source: Linters/SAST tools

const { data: existing } = await supabase
.from('activity_events')
.select('id')
.eq('tx_hash', txHash)
.limit(1);
if (existing && existing.length) continue;
Comment on lines +20 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ—„οΈ Data Integrity & Integration | 🟠 Major | πŸ—οΈ Heavy lift

Make dedupe operation-scoped and atomic.

Dedupe by tx_hash drops additional operations from the same transaction, and the select-then-insert pattern can race across concurrent scanner runs. Store a Horizon operation identifier/paging token and enforce a unique constraint/upsert around that key.

🧰 Tools
πŸͺ› ESLint

[error] 21-21: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

πŸͺ› GitHub Check: lint-and-build

[failure] 21-21:
Unexpected any. Specify a different type

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/activity_scanner/index.ts` around lines 20 - 27, The
dedupe in activity_scanner/index.ts is too broad and non-atomic: it currently
uses transaction_hash in the existing lookup, which can suppress distinct
operations from the same transaction and can race between concurrent runs.
Update the scan/insert flow to use a Horizon operation-scoped identifier or
paging token in the activity_events record, and enforce uniqueness on that key
instead of tx_hash. Then switch the create path to an atomic
upsert/insert-with-conflict-handling around the same identifier so duplicate
scanner runs cannot insert the same operation twice.


await supabase.from('activity_events').insert([
{
account_id: acct.id,
source_account: op.source_account || acct.stellar_address,
event_type: classification.eventType,
significance: classification.significance,
raw_data: op,
summary: classification.summary,
tx_hash: txHash,
detected_at: new Date().toISOString(),
},
]);
processed++;
}

// update cursor if we got one
if (nextCursor) {
await supabase.from('monitored_accounts').update({ last_cursor: nextCursor }).eq('id', acct.id);
Comment on lines +29 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ—„οΈ Data Integrity & Integration | 🟠 Major | ⚑ Quick win

Check write errors before incrementing or advancing the cursor.

Failed inserts/updates are currently ignored, so processed can be inflated and last_cursor can advance past events that were never persisted.

Proposed fix
-        await supabase.from('activity_events').insert([
+        const { error: insertError } = await supabase.from('activity_events').insert([
           {
             account_id: acct.id,
             source_account: op.source_account || acct.stellar_address,
@@
             detected_at: new Date().toISOString(),
           },
         ]);
+        if (insertError) throw insertError;
         processed++;
@@
-        await supabase.from('monitored_accounts').update({ last_cursor: nextCursor }).eq('id', acct.id);
+        const { error: cursorError } = await supabase
+          .from('monitored_accounts')
+          .update({ last_cursor: nextCursor })
+          .eq('id', acct.id);
+        if (cursorError) throw cursorError;
πŸ“ 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
await supabase.from('activity_events').insert([
{
account_id: acct.id,
source_account: op.source_account || acct.stellar_address,
event_type: classification.eventType,
significance: classification.significance,
raw_data: op,
summary: classification.summary,
tx_hash: txHash,
detected_at: new Date().toISOString(),
},
]);
processed++;
}
// update cursor if we got one
if (nextCursor) {
await supabase.from('monitored_accounts').update({ last_cursor: nextCursor }).eq('id', acct.id);
const { error: insertError } = await supabase.from('activity_events').insert([
{
account_id: acct.id,
source_account: op.source_account || acct.stellar_address,
event_type: classification.eventType,
significance: classification.significance,
raw_data: op,
summary: classification.summary,
tx_hash: txHash,
detected_at: new Date().toISOString(),
},
]);
if (insertError) throw insertError;
processed++;
}
// update cursor if we got one
if (nextCursor) {
const { error: cursorError } = await supabase
.from('monitored_accounts')
.update({ last_cursor: nextCursor })
.eq('id', acct.id);
if (cursorError) throw cursorError;
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/activity_scanner/index.ts` around lines 29 - 46, The
activity scanner currently ignores failures from the activity_events insert and
monitored_accounts cursor update, so processed can be incremented and
last_cursor advanced even when persistence fails. In the activity_scanner flow
around the supabase.from('activity_events') insert and the subsequent cursor
update, check the returned error/result before incrementing processed or setting
last_cursor, and only advance state after a successful write; otherwise
log/handle the failure and leave the cursor unchanged so the same events can be
retried.

}
} catch (err) {
// log and continue
console.error('scan error', acct.stellar_address, err);
}
}

return { processed };
}
26 changes: 26 additions & 0 deletions supabase/migrations/0003_monitored_accounts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- =============================================================================
-- Add monitored_accounts table for automated activity monitoring
-- =============================================================================

create table if not exists public.monitored_accounts (
id uuid primary key default gen_random_uuid(),
stellar_address text unique not null,
label text not null,
account_type text not null,
monitor_events text[] not null default '{}',
active boolean not null default true,
last_cursor text,
created_at timestamptz not null default now()
);

create index if not exists monitored_accounts_stellar_address_idx
on public.monitored_accounts (stellar_address);

alter table public.monitored_accounts enable row level security;

create policy "admins can manage monitored accounts"
on public.monitored_accounts
for all
to authenticated
using (public.is_admin())
with check (public.is_admin());
32 changes: 32 additions & 0 deletions supabase/migrations/0004_activity_events.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
-- =============================================================================
-- Add activity_events table to store detected Stellar activity
-- =============================================================================

create table if not exists public.activity_events (
id uuid primary key default gen_random_uuid(),
account_id uuid not null references public.monitored_accounts(id) on delete cascade,
source_account text not null,
event_type text not null,
significance text not null,
raw_data jsonb not null default '{}'::jsonb,
summary text,
processed boolean not null default false,
draft_article_id uuid,
detected_at timestamptz not null default now(),
tx_hash text
);
Comment on lines +5 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ—„οΈ Data Integrity & Integration | 🟠 Major | ⚑ Quick win

Constrain event_type and significance in the database.

The TS layer already limits these fields to a fixed set, but the table still accepts arbitrary text. That lets malformed rows slip in and breaks the contract this schema is trying to establish.

Possible fix
 create table if not exists public.activity_events (
   id uuid primary key default gen_random_uuid(),
   account_id uuid not null references public.monitored_accounts(id) on delete cascade,
   source_account text not null,
-  event_type text not null,
-  significance text not null,
+  event_type text not null check (event_type in ('contract_deploy', 'payment', 'trust_change', 'contract_invoke', 'data_entry', 'account_create')),
+  significance text not null check (significance in ('low', 'medium', 'high', 'critical')),
   raw_data jsonb not null default '{}'::jsonb,
πŸ“ 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
create table if not exists public.activity_events (
id uuid primary key default gen_random_uuid(),
account_id uuid not null references public.monitored_accounts(id) on delete cascade,
source_account text not null,
event_type text not null,
significance text not null,
raw_data jsonb not null default '{}'::jsonb,
summary text,
processed boolean not null default false,
draft_article_id uuid,
detected_at timestamptz not null default now(),
tx_hash text
);
create table if not exists public.activity_events (
id uuid primary key default gen_random_uuid(),
account_id uuid not null references public.monitored_accounts(id) on delete cascade,
source_account text not null,
event_type text not null check (event_type in ('contract_deploy', 'payment', 'trust_change', 'contract_invoke', 'data_entry', 'account_create')),
significance text not null check (significance in ('low', 'medium', 'high', 'critical')),
raw_data jsonb not null default '{}'::jsonb,
summary text,
processed boolean not null default false,
draft_article_id uuid,
detected_at timestamptz not null default now(),
tx_hash text
);
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/0004_activity_events.sql` around lines 5 - 17, The
activity_events table currently leaves event_type and significance as
unrestricted text, so add database-level constraints in the migration to match
the TypeScript allowed values. Update the public.activity_events definition to
constrain both columns, using CHECK constraints or enum-backed types, and keep
the change localized around the activity_events table creation so the schema
contract is enforced at the database layer.


create index if not exists activity_events_account_id_detected_at_idx
on public.activity_events (account_id, detected_at desc);

create index if not exists activity_events_processed_idx
on public.activity_events (processed);

alter table public.activity_events enable row level security;

create policy "admins can manage activity events"
on public.activity_events
for all
to authenticated
using (public.is_admin())
with check (public.is_admin());
Loading