Skip to content

Commit 10c6cb6

Browse files
authored
Merge pull request #285 from bitstarkbridge/StellarEscrow225
feature:Event Indexing Optimization
2 parents fd2c9bb + a70b1c5 commit 10c6cb6

11 files changed

Lines changed: 479 additions & 115 deletions

File tree

api/src/contracts.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,18 @@ export function isTrade(value: unknown): value is Trade {
3838
);
3939
}
4040

41+
const EVENT_CATEGORIES = new Set<string>([
42+
'trade', 'arb', 'fee', 'tmpl', 'sub', 'gov', 'sys', 'ins', 'oracle',
43+
]);
44+
4145
export function isEvent(value: unknown): value is Event {
4246
return (
4347
isRecord(value) &&
4448
typeof value.id === 'string' &&
4549
typeof value.type === 'string' &&
50+
typeof value.category === 'string' &&
51+
EVENT_CATEGORIES.has(value.category) &&
52+
typeof value.schemaVersion === 'number' &&
4653
typeof value.tradeId === 'string' &&
4754
typeof value.timestamp === 'string' &&
4855
isRecord(value.data)

api/src/models.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,41 @@ export interface Trade {
88
timestamp: string;
99
}
1010

11+
/** High-level event categories matching the contract's topic layout. */
12+
export type EventCategory =
13+
| 'trade'
14+
| 'arb'
15+
| 'fee'
16+
| 'tmpl'
17+
| 'sub'
18+
| 'gov'
19+
| 'sys'
20+
| 'ins'
21+
| 'oracle';
22+
1123
export interface Event {
1224
id: string;
25+
/** Canonical snake_case event name, e.g. "trade_created" */
1326
type: string;
27+
/** High-level category for client-side filtering */
28+
category: EventCategory;
29+
/** Schema version from the contract payload (v field) */
30+
schemaVersion: number;
1431
tradeId: string;
1532
timestamp: string;
16-
data: Record<string, any>;
33+
data: Record<string, unknown>;
34+
}
35+
36+
/** Query parameters accepted by GET /events */
37+
export interface EventFilterParams {
38+
limit?: number;
39+
offset?: number;
40+
event_type?: string;
41+
category?: EventCategory;
42+
trade_id?: string;
43+
from_ledger?: number;
44+
to_ledger?: number;
45+
from_time?: string;
46+
to_time?: string;
47+
contract_id?: string;
1748
}

api/src/resources.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ApiClient } from './client';
2-
import { Event, Trade } from './models';
2+
import { Event, EventCategory, EventFilterParams, Trade } from './models';
33

44
export class TradesApi {
55
constructor(private client: ApiClient) {}
@@ -28,16 +28,29 @@ export class TradesApi {
2828
export class EventsApi {
2929
constructor(private client: ApiClient) {}
3030

31-
async getEvents(limit = 100, tradeId?: string): Promise<Event[]> {
32-
const params = new URLSearchParams({ limit: limit.toString() });
33-
if (tradeId) params.append('tradeId', tradeId);
31+
async getEvents(filters: EventFilterParams = {}): Promise<Event[]> {
32+
const params = new URLSearchParams();
33+
if (filters.limit !== undefined) params.set('limit', filters.limit.toString());
34+
if (filters.offset !== undefined) params.set('offset', filters.offset.toString());
35+
if (filters.event_type) params.set('event_type', filters.event_type);
36+
if (filters.category) params.set('category', filters.category);
37+
if (filters.trade_id) params.set('trade_id', filters.trade_id);
38+
if (filters.from_ledger !== undefined) params.set('from_ledger', filters.from_ledger.toString());
39+
if (filters.to_ledger !== undefined) params.set('to_ledger', filters.to_ledger.toString());
40+
if (filters.from_time) params.set('from_time', filters.from_time);
41+
if (filters.to_time) params.set('to_time', filters.to_time);
42+
if (filters.contract_id) params.set('contract_id', filters.contract_id);
3443
return this.client.get(`/events?${params}`);
3544
}
3645

3746
async getEventsByTrade(tradeId: string): Promise<Event[]> {
3847
return this.client.get(`/events/trade/${tradeId}`);
3948
}
4049

50+
async getEventsByCategory(category: EventCategory, limit = 50): Promise<Event[]> {
51+
return this.getEvents({ category, limit });
52+
}
53+
4154
async getEvent(id: string): Promise<Event> {
4255
return this.client.get(`/events/${id}`);
4356
}

contract/src/events.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,11 @@ pub fn emit_trade_created(env: &Env, trade_id: u64, seller: Address, buyer: Addr
145145
}
146146

147147
pub fn emit_compliance_failed(env: &Env, user: Address, reason: &soroban_sdk::String) {
148-
env.events().publish((symbol_short!("compl_fail"),), (user, reason.clone()));
148+
env.events().publish((cat_sys(), symbol_short!("compl_fail")), EvComplianceFailed { v: EVENT_VERSION, user, reason: reason.clone() });
149149
}
150150

151151
pub fn emit_compliance_passed(env: &Env, trade_id: u64, seller: Address, buyer: Address, amount: u64) {
152-
env.events().publish((symbol_short!("compl_pass"),), (trade_id, seller, buyer, amount));
152+
env.events().publish((cat_sys(), symbol_short!("compl_pass")), EvCompliancePassed { v: EVENT_VERSION, trade_id, seller, buyer, amount });
153153
}
154154

155155
pub fn emit_trade_funded(env: &Env, trade_id: u64) {
@@ -340,6 +340,19 @@ pub fn emit_oracle_unavailable(env: &Env, base: Address, quote: Address) {
340340
// Upgrade system events
341341
// ---------------------------------------------------------------------------
342342

343+
// ---------------------------------------------------------------------------
344+
// Compliance event payloads
345+
// ---------------------------------------------------------------------------
346+
347+
#[contracttype] #[derive(Clone, Debug)]
348+
pub struct EvComplianceFailed { pub v: u32, pub user: Address, pub reason: String }
349+
#[contracttype] #[derive(Clone, Debug)]
350+
pub struct EvCompliancePassed { pub v: u32, pub trade_id: u64, pub seller: Address, pub buyer: Address, pub amount: u64 }
351+
352+
// ---------------------------------------------------------------------------
353+
// Upgrade system events
354+
// ---------------------------------------------------------------------------
355+
343356
#[contracttype] #[derive(Clone, Debug)]
344357
pub struct EvUpgradeProposed { pub v: u32, pub proposed_by: Address, pub executable_after: u32, pub description: String }
345358
#[contracttype] #[derive(Clone, Debug)]
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
-- Event indexing optimization: add category, schema_version columns and supporting indexes.
2+
-- These columns are derived from the two-topic layout (category, event_name) emitted by the
3+
-- Soroban contract and enable efficient category-level filtering without JSON scanning.
4+
5+
ALTER TABLE events
6+
ADD COLUMN IF NOT EXISTS category VARCHAR(20) NOT NULL DEFAULT 'unknown',
7+
ADD COLUMN IF NOT EXISTS schema_version SMALLINT NOT NULL DEFAULT 1;
8+
9+
-- Backfill category from event_type for existing rows
10+
UPDATE events SET category = CASE
11+
WHEN event_type IN (
12+
'trade_created', 'trade_funded', 'trade_completed', 'trade_confirmed',
13+
'trade_cancelled', 'time_released', 'metadata_updated',
14+
'dispute_raised', 'dispute_resolved', 'partial_resolved'
15+
) THEN 'trade'
16+
WHEN event_type IN (
17+
'arbitrator_registered', 'arbitrator_removed',
18+
'arbitrator_rated', 'arbitrator_rep_updated'
19+
) THEN 'arb'
20+
WHEN event_type IN (
21+
'fee_updated', 'fees_withdrawn', 'fees_distributed', 'custom_fee_set',
22+
'tier_upgraded', 'tier_downgraded', 'tier_config_updated'
23+
) THEN 'fee'
24+
WHEN event_type IN (
25+
'template_created', 'template_updated', 'template_deactivated', 'template_trade'
26+
) THEN 'tmpl'
27+
WHEN event_type IN (
28+
'subscribed', 'subscription_renewed', 'subscription_cancelled'
29+
) THEN 'sub'
30+
WHEN event_type IN (
31+
'proposal_created', 'vote_cast', 'proposal_executed', 'delegated'
32+
) THEN 'gov'
33+
WHEN event_type IN (
34+
'insurance_provider_registered', 'insurance_provider_removed',
35+
'insurance_purchased', 'insurance_claimed'
36+
) THEN 'ins'
37+
WHEN event_type IN (
38+
'oracle_registered', 'oracle_removed',
39+
'oracle_price_fetched', 'oracle_unavailable'
40+
) THEN 'oracle'
41+
ELSE 'sys'
42+
END
43+
WHERE category = 'unknown';
44+
45+
-- Backfill schema_version from the JSON payload where available
46+
UPDATE events
47+
SET schema_version = (data->>'v')::SMALLINT
48+
WHERE data->>'v' IS NOT NULL
49+
AND (data->>'v') ~ '^\d+$';
50+
51+
-- Indexes for the new columns
52+
CREATE INDEX IF NOT EXISTS idx_events_category
53+
ON events (category);
54+
55+
CREATE INDEX IF NOT EXISTS idx_events_category_ledger
56+
ON events (category, ledger DESC);
57+
58+
CREATE INDEX IF NOT EXISTS idx_events_schema_version
59+
ON events (schema_version);
60+
61+
-- Composite index for the most common query pattern: category + event_type + ledger range
62+
CREATE INDEX IF NOT EXISTS idx_events_category_type_ledger
63+
ON events (category, event_type, ledger DESC);
64+
65+
-- Timestamp range index (already exists in initial migration, kept for reference)
66+
-- CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events (timestamp DESC);
67+
68+
-- Partial index for trade events (highest query volume)
69+
CREATE INDEX IF NOT EXISTS idx_events_trade_category
70+
ON events (ledger DESC, timestamp DESC)
71+
WHERE category = 'trade';

0 commit comments

Comments
 (0)