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
44 changes: 22 additions & 22 deletions packages/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,43 @@ datasource db {
}

model Organization {
id String @id
name String
admin String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id // Unique identifier for the organization (Symbol on-chain)
name String // Display name of the organization
admin String // Primary administrator wallet address
createdAt DateTime @default(now()) // When the organization was first indexed
updatedAt DateTime @updatedAt // Last time the metadata was updated

@@index([createdAt])
}

model Transaction {
id String @default(cuid())
txHash String // Blockchain transaction hash
eventIndex Int // Index of the event within the transaction
walletAddress String
volumeUSD Decimal
createdAt DateTime // Explicitly set to ledger closure time for partitioning
type String // e.g., "PAYOUT_ALLOCATED", "ORG_FUNDED", "PAYOUT_CLAIMED"
id String @default(cuid()) // Internal tracking ID
txHash String // Stellar blockchain transaction hash
eventIndex Int // Position of the event within the transaction
walletAddress String // Actor associated with the transaction
volumeUSD Decimal // Value of the transaction in USD at the time of ledger closure
createdAt DateTime // Exact timestamp of ledger closure (Partition Key)
type String // Type of event (e.g., "PAYOUT_ALLOCATED")
ledger Int // Ledger sequence number
rawData String? // JSON-encoded raw event data for debugging
rawData String? // Original JSON payload from the Soroban RPC

@@id([id, createdAt])
@@unique([txHash, eventIndex, createdAt]) // Required for partitioning
@@unique([txHash, eventIndex, createdAt]) // Ensures no duplicate events are indexed
@@index([createdAt])
@@index([walletAddress])
@@index([txHash])
@@index([ledger])
}

model PayoutEvent {
id String @default(cuid())
orgId String
maintainer String
amountStroops BigInt
amountXlm Decimal
ledger Int
txHash String
createdAt DateTime // Partition key
id String @default(cuid()) // Internal tracking ID
orgId String // Associated organization ID
maintainer String // Recipient maintainer address
amountStroops BigInt // Payout amount in stroops (10^-7 XLM)
amountXlm Decimal // Payout amount converted to XLM
ledger Int // Ledger sequence number
txHash String // Stellar transaction hash
createdAt DateTime // Timestamp of the payout (Partition Key)

@@id([id, createdAt])
@@index([orgId])
Expand Down
41 changes: 40 additions & 1 deletion packages/backend/src/services/WebhookService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@ import { Queue } from "bullmq";
import { redis } from "./cache.js";

export interface WebhookJobData {
/** The unique ID of the organization to notify. */
organizationId: string;
/** The name of the event being dispatched (e.g., 'payout_claimed'). */
event: string;
/** The JSON-serializable payload data for the webhook. */
data: any;
}

/**
* Service for managing webhook configurations and dispatching events via BullMQ.
*/
export class WebhookService {
/** The BullMQ queue for background webhook delivery. */
private webhookQueue: Queue;

constructor() {
Expand All @@ -27,14 +34,29 @@ export class WebhookService {
});
}

/**
* Generates a cryptographically secure random secret for webhook signing.
* @returns A 64-character hex string.
*/
private generateWebhookSecret(): string {
return randomBytes(32).toString('hex');
}

/**
* Calculates a SHA-256 HMAC signature for a webhook payload.
* @param payload The raw stringified JSON payload.
* @param secret The organization's webhook secret.
* @returns The hex-encoded signature.
*/
calculateSignature(payload: string, secret: string): string {
return createHash('sha256').update(payload).update(secret).digest('hex');
}

/**
* Ensures an organization has a webhook secret, generating one if necessary.
* @param organizationId The organization to generate a secret for.
* @returns The organization's secret.
*/
async generateSecretForOrganization(organizationId: string): Promise<string> {
const existingConfig = await webhookRepository.getConfig(organizationId);

Expand All @@ -47,17 +69,29 @@ export class WebhookService {
return newSecret;
}

/**
* Retrieves the current webhook configuration for an organization.
* @param organizationId The ID of the organization.
*/
async getConfig(organizationId: string) {
return webhookRepository.getConfig(organizationId);
}

/**
* Updates or creates a webhook URL configuration for an organization.
* @param organizationId The ID of the organization.
* @param url The external HTTP POST endpoint.
*/
async updateConfig(organizationId: string, url: string) {
const secret = await this.generateSecretForOrganization(organizationId);
return webhookRepository.upsertConfig(organizationId, url, secret);
}

/**
* Dispatches a webhook asynchronously using BullMQ.
* @param organizationId The organization to notify.
* @param event The event name.
* @param data The payload data.
*/
async queueWebhook(organizationId: string, event: string, data: any) {
const config = await webhookRepository.getConfig(organizationId);
Expand All @@ -73,7 +107,12 @@ export class WebhookService {
}

/**
* Specifically handles PayoutClaimed webhooks.
* Specifically handles PayoutClaimed webhooks by queuing a background job.
* @param organizationId The ID of the organization.
* @param maintainer The address of the maintainer who claimed the payout.
* @param amountStroops The payout amount in stroops.
* @param txHash The transaction hash on the Stellar network.
* @param ledger The ledger sequence number.
*/
async dispatchPayoutClaimed(
organizationId: string,
Expand Down
Loading
Loading