Some checks failed
CI / Frontend Lint (push) Failing after 6s
CI / Frontend Type Check (push) Failing after 6s
CI / Frontend Build (push) Failing after 6s
CI / Frontend E2E Tests (push) Failing after 9s
CI / Orchestrator Build (push) Failing after 6s
CI / Contracts Compile (push) Failing after 6s
CI / Contracts Test (push) Failing after 6s
Security Scan / Dependency Vulnerability Scan (push) Failing after 4s
Security Scan / OWASP ZAP Scan (push) Failing after 4s
282 lines
9.3 KiB
TypeScript
282 lines
9.3 KiB
TypeScript
/**
|
|
* CurrenciCombo/orchestrator API client — consumed by the portal's
|
|
* /transactions page.
|
|
*
|
|
* When `VITE_ORCHESTRATOR_URL` is unset or the endpoint is unreachable,
|
|
* every call falls back to deterministic demo data so the page still
|
|
* renders something meaningful. This mirrors how the other services
|
|
* treat unreachable backends (chain138, explorer, dbisCore).
|
|
*
|
|
* The shape of the data matches the orchestrator's API (see
|
|
* orchestrator/src/api/plans.ts + orchestrator/src/api/eventBus.ts).
|
|
* Re-sync if the orchestrator evolves.
|
|
*/
|
|
|
|
import { endpoints } from '../config/endpoints';
|
|
|
|
export type TransactionState =
|
|
| 'DRAFT'
|
|
| 'INITIATED'
|
|
| 'PRECONDITIONS_PENDING'
|
|
| 'READY_FOR_PREPARE'
|
|
| 'PREPARED'
|
|
| 'EXECUTING'
|
|
| 'PARTIALLY_EXECUTED'
|
|
| 'VALIDATING'
|
|
| 'COMMITTED'
|
|
| 'ABORTED'
|
|
| 'UNWIND_PENDING'
|
|
| 'CLOSED';
|
|
|
|
export const TRANSACTION_STATES: TransactionState[] = [
|
|
'DRAFT',
|
|
'INITIATED',
|
|
'PRECONDITIONS_PENDING',
|
|
'READY_FOR_PREPARE',
|
|
'PREPARED',
|
|
'EXECUTING',
|
|
'PARTIALLY_EXECUTED',
|
|
'VALIDATING',
|
|
'COMMITTED',
|
|
'ABORTED',
|
|
'UNWIND_PENDING',
|
|
'CLOSED',
|
|
];
|
|
|
|
export interface PlanSummary {
|
|
plan_id: string;
|
|
status: TransactionState;
|
|
actor_id: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
instrument_hint?: string;
|
|
}
|
|
|
|
export interface StateTransition {
|
|
from_state: TransactionState | null;
|
|
to_state: TransactionState;
|
|
actor_id: string;
|
|
actor_role: string;
|
|
reason: string | null;
|
|
occurred_at: string;
|
|
}
|
|
|
|
export interface PlanStateDetail {
|
|
plan_id: string;
|
|
current_state: TransactionState;
|
|
transitions: StateTransition[];
|
|
}
|
|
|
|
export interface PlanEvent {
|
|
id: number;
|
|
plan_id: string;
|
|
type: string;
|
|
payload: Record<string, unknown>;
|
|
signature: string;
|
|
prev_hash: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
type OrchestratorStatus = 'live' | 'mocked' | 'degraded';
|
|
|
|
export interface OrchestratorProbe {
|
|
status: OrchestratorStatus;
|
|
latencyMs: number | null;
|
|
error?: string;
|
|
}
|
|
|
|
const base = endpoints.orchestrator.baseUrl;
|
|
const deployed = endpoints.orchestrator.deployed;
|
|
|
|
async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const res = await fetch(`${base}${path}`, {
|
|
...init,
|
|
headers: { Accept: 'application/json', ...(init?.headers ?? {}) },
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error(`orchestrator ${res.status} on ${path}`);
|
|
}
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
export async function probeOrchestrator(): Promise<OrchestratorProbe> {
|
|
if (!deployed) return { status: 'mocked', latencyMs: null };
|
|
const t0 = performance.now();
|
|
try {
|
|
const res = await fetch(`${base}/health`, { method: 'GET' });
|
|
const latencyMs = Math.round(performance.now() - t0);
|
|
if (!res.ok) return { status: 'degraded', latencyMs, error: `HTTP ${res.status}` };
|
|
return { status: 'live', latencyMs };
|
|
} catch (err) {
|
|
return {
|
|
status: 'degraded',
|
|
latencyMs: null,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function listPlans(): Promise<{ plans: PlanSummary[]; source: OrchestratorStatus }> {
|
|
if (!deployed) return { plans: demoPlans(), source: 'mocked' };
|
|
try {
|
|
const data = await fetchJson<{ plans: PlanSummary[] }>('/api/plans');
|
|
return { plans: data.plans ?? [], source: 'live' };
|
|
} catch {
|
|
return { plans: demoPlans(), source: 'degraded' };
|
|
}
|
|
}
|
|
|
|
export async function getPlanState(
|
|
planId: string,
|
|
): Promise<{ detail: PlanStateDetail; source: OrchestratorStatus }> {
|
|
if (!deployed) return { detail: demoPlanState(planId), source: 'mocked' };
|
|
try {
|
|
const detail = await fetchJson<PlanStateDetail>(`/api/plans/${encodeURIComponent(planId)}/state`);
|
|
return { detail, source: 'live' };
|
|
} catch {
|
|
return { detail: demoPlanState(planId), source: 'degraded' };
|
|
}
|
|
}
|
|
|
|
export async function getPlanEvents(
|
|
planId: string,
|
|
): Promise<{ events: PlanEvent[]; source: OrchestratorStatus }> {
|
|
if (!deployed) return { events: demoPlanEvents(planId), source: 'mocked' };
|
|
try {
|
|
const data = await fetchJson<{ events: PlanEvent[] }>(
|
|
`/api/plans/${encodeURIComponent(planId)}/events`,
|
|
);
|
|
return { events: data.events ?? [], source: 'live' };
|
|
} catch {
|
|
return { events: demoPlanEvents(planId), source: 'degraded' };
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Demo data — used only when VITE_ORCHESTRATOR_URL is unset. Lets the
|
|
// /transactions page demonstrate the 12-state machine visualisation
|
|
// without needing a deployed orchestrator.
|
|
// ---------------------------------------------------------------------
|
|
|
|
function demoPlans(): PlanSummary[] {
|
|
const now = Date.now();
|
|
return [
|
|
{
|
|
plan_id: 'demo-sblc-001',
|
|
status: 'VALIDATING',
|
|
actor_id: 'ops.alice',
|
|
created_at: new Date(now - 1000 * 60 * 55).toISOString(),
|
|
updated_at: new Date(now - 1000 * 30).toISOString(),
|
|
instrument_hint: 'MT760 / EIB beneficiary format',
|
|
},
|
|
{
|
|
plan_id: 'demo-pay-014',
|
|
status: 'COMMITTED',
|
|
actor_id: 'ops.bob',
|
|
created_at: new Date(now - 1000 * 60 * 60 * 3).toISOString(),
|
|
updated_at: new Date(now - 1000 * 60 * 7).toISOString(),
|
|
instrument_hint: 'pacs.009 FI-to-FI',
|
|
},
|
|
{
|
|
plan_id: 'demo-sblc-003',
|
|
status: 'ABORTED',
|
|
actor_id: 'ops.alice',
|
|
created_at: new Date(now - 1000 * 60 * 60 * 8).toISOString(),
|
|
updated_at: new Date(now - 1000 * 60 * 60 * 2).toISOString(),
|
|
instrument_hint: 'MT202 COV',
|
|
},
|
|
{
|
|
plan_id: 'demo-draft-029',
|
|
status: 'DRAFT',
|
|
actor_id: null,
|
|
created_at: new Date(now - 1000 * 60 * 4).toISOString(),
|
|
updated_at: new Date(now - 1000 * 60 * 4).toISOString(),
|
|
instrument_hint: 'Pending review',
|
|
},
|
|
];
|
|
}
|
|
|
|
function demoPlanState(planId: string): PlanStateDetail {
|
|
const plan = demoPlans().find((p) => p.plan_id === planId) ?? demoPlans()[0];
|
|
const base = new Date(plan.created_at).getTime();
|
|
const mk = (i: number, from: TransactionState | null, to: TransactionState, role: string, actor: string, reason: string) => ({
|
|
from_state: from,
|
|
to_state: to,
|
|
actor_id: actor,
|
|
actor_role: role,
|
|
reason,
|
|
occurred_at: new Date(base + i * 1000 * 60 * 5).toISOString(),
|
|
});
|
|
|
|
if (plan.status === 'COMMITTED') {
|
|
return {
|
|
plan_id: plan.plan_id,
|
|
current_state: 'COMMITTED',
|
|
transitions: [
|
|
mk(0, null, 'DRAFT', 'submitter', 'ops.bob', 'plan created'),
|
|
mk(1, 'DRAFT', 'INITIATED', 'submitter', 'ops.bob', 'initiation'),
|
|
mk(2, 'INITIATED', 'PRECONDITIONS_PENDING', 'coordinator', 'system', 'await controls'),
|
|
mk(3, 'PRECONDITIONS_PENDING', 'READY_FOR_PREPARE', 'coordinator', 'system', 'preconditions satisfied'),
|
|
mk(4, 'READY_FOR_PREPARE', 'PREPARED', 'approver', 'ops.chen', 'approve — prepare (SoD)'),
|
|
mk(5, 'PREPARED', 'EXECUTING', 'releaser', 'ops.dey', 'release — execute (SoD)'),
|
|
mk(6, 'EXECUTING', 'VALIDATING', 'coordinator', 'system', 'both legs dispatched'),
|
|
mk(7, 'VALIDATING', 'COMMITTED', 'validator', 'ops.eve', 'reconciled + committed (SoD)'),
|
|
],
|
|
};
|
|
}
|
|
if (plan.status === 'ABORTED') {
|
|
return {
|
|
plan_id: plan.plan_id,
|
|
current_state: 'ABORTED',
|
|
transitions: [
|
|
mk(0, null, 'DRAFT', 'submitter', 'ops.alice', 'plan created'),
|
|
mk(1, 'DRAFT', 'INITIATED', 'submitter', 'ops.alice', 'initiation'),
|
|
mk(2, 'INITIATED', 'PRECONDITIONS_PENDING', 'coordinator', 'system', 'await controls'),
|
|
mk(3, 'PRECONDITIONS_PENDING', 'READY_FOR_PREPARE', 'coordinator', 'system', 'preconditions satisfied'),
|
|
mk(4, 'READY_FOR_PREPARE', 'PREPARED', 'approver', 'ops.chen', 'approve — prepare (SoD)'),
|
|
mk(5, 'PREPARED', 'EXECUTING', 'releaser', 'ops.dey', 'release — execute (SoD)'),
|
|
mk(6, 'EXECUTING', 'VALIDATING', 'coordinator', 'system', 'both legs dispatched'),
|
|
mk(7, 'VALIDATING', 'ABORTED', 'validator', 'ops.eve', 'amount mismatch on camt.054'),
|
|
],
|
|
};
|
|
}
|
|
if (plan.status === 'DRAFT') {
|
|
return {
|
|
plan_id: plan.plan_id,
|
|
current_state: 'DRAFT',
|
|
transitions: [mk(0, null, 'DRAFT', 'submitter', 'ops.frank', 'plan created')],
|
|
};
|
|
}
|
|
return {
|
|
plan_id: plan.plan_id,
|
|
current_state: 'VALIDATING',
|
|
transitions: [
|
|
mk(0, null, 'DRAFT', 'submitter', 'ops.alice', 'plan created'),
|
|
mk(1, 'DRAFT', 'INITIATED', 'submitter', 'ops.alice', 'initiation'),
|
|
mk(2, 'INITIATED', 'PRECONDITIONS_PENDING', 'coordinator', 'system', 'await controls'),
|
|
mk(3, 'PRECONDITIONS_PENDING', 'READY_FOR_PREPARE', 'coordinator', 'system', 'preconditions satisfied'),
|
|
mk(4, 'READY_FOR_PREPARE', 'PREPARED', 'approver', 'ops.chen', 'approve — prepare (SoD)'),
|
|
mk(5, 'PREPARED', 'EXECUTING', 'releaser', 'ops.dey', 'release — execute (SoD)'),
|
|
mk(6, 'EXECUTING', 'VALIDATING', 'coordinator', 'system', 'both legs dispatched, awaiting reconciliation'),
|
|
],
|
|
};
|
|
}
|
|
|
|
function demoPlanEvents(planId: string): PlanEvent[] {
|
|
const detail = demoPlanState(planId);
|
|
return detail.transitions.map((t, i) => ({
|
|
id: i + 1,
|
|
plan_id: planId,
|
|
type: `state.${t.to_state.toLowerCase()}`,
|
|
payload: {
|
|
from: t.from_state,
|
|
to: t.to_state,
|
|
actor_role: t.actor_role,
|
|
reason: t.reason,
|
|
},
|
|
signature: `demo-sig-${i.toString(16).padStart(4, '0')}`,
|
|
prev_hash: i === 0 ? null : `demo-hash-${(i - 1).toString(16).padStart(4, '0')}`,
|
|
created_at: t.occurred_at,
|
|
}));
|
|
}
|