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
25 changes: 25 additions & 0 deletions prisma/schema/curve-update.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/// Timelocked bonding-curve parameter update proposal indexed from Soroban.
model CurveUpdate {
id String @id @default(cuid())
keyId String
proposalId String
status String @default("pending") // pending | completed | cancelled
proposedParams Json
eta DateTime
proposedAt DateTime
completedAt DateTime?
outcome String?
proposedTxHash String
proposedEventIndex Int
executedTxHash String?
executedEventIndex Int?
ledger Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@unique([keyId, proposalId])
@@unique([proposedTxHash, proposedEventIndex])
@@unique([executedTxHash, executedEventIndex])
@@index([keyId, status, eta])
@@map("curve_updates")
}
47 changes: 47 additions & 0 deletions prisma/schema/key-launch-auction.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/// On-chain launch auction current state.
model KeyLaunchAuction {
id String @id @default(cuid())
auctionId String @unique
keyId String @unique
status String @default("active")
reserve Decimal @db.Decimal(36, 7)
highestBid Decimal? @db.Decimal(36, 7)
highestBidder String?
clearingPrice Decimal? @db.Decimal(36, 7)
startsAt DateTime
endsAt DateTime
settledAt DateTime?
createdTxHash String
createdEventIndex Int
settledTxHash String?
settledEventIndex Int?
ledger Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

bids KeyLaunchAuctionBid[]

@@unique([createdTxHash, createdEventIndex])
@@unique([settledTxHash, settledEventIndex])
@@index([keyId, status])
@@map("key_auctions")
}

/// Immutable bid history for a launch auction.
model KeyLaunchAuctionBid {
id String @id @default(cuid())
auctionId String
bidder String
amount Decimal @db.Decimal(36, 7)
placedAt DateTime
txHash String
eventIndex Int
ledger Int
createdAt DateTime @default(now())

auction KeyLaunchAuction @relation(fields: [auctionId], references: [auctionId], onDelete: Cascade)

@@unique([txHash, eventIndex])
@@index([auctionId, placedAt(sort: Desc)])
@@map("key_auction_bids")
}
19 changes: 19 additions & 0 deletions prisma/schema/key-retirement.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// Append-only retirement/deprecation lifecycle event log.
model KeyRetirement {
id String @id @default(cuid())
keyId String
eventType String
status String
buybackPrice Decimal? @db.Decimal(36, 7)
noticeExpiry DateTime?
reason String?
occurredAt DateTime
txHash String
eventIndex Int
ledger Int
createdAt DateTime @default(now())

@@unique([txHash, eventIndex])
@@index([keyId, occurredAt(sort: Desc)])
@@map("key_retirement")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE TABLE "curve_updates" (
"id" TEXT NOT NULL,
"keyId" TEXT NOT NULL,
"proposalId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"proposedParams" JSONB NOT NULL,
"eta" TIMESTAMP(3) NOT NULL,
"proposedAt" TIMESTAMP(3) NOT NULL,
"completedAt" TIMESTAMP(3),
"outcome" TEXT,
"proposedTxHash" TEXT NOT NULL,
"proposedEventIndex" INTEGER NOT NULL,
"executedTxHash" TEXT,
"executedEventIndex" INTEGER,
"ledger" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "curve_updates_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "curve_updates_keyId_proposalId_key" ON "curve_updates"("keyId", "proposalId");
CREATE UNIQUE INDEX "curve_updates_proposedTxHash_proposedEventIndex_key" ON "curve_updates"("proposedTxHash", "proposedEventIndex");
CREATE UNIQUE INDEX "curve_updates_executedTxHash_executedEventIndex_key" ON "curve_updates"("executedTxHash", "executedEventIndex");
CREATE INDEX "curve_updates_keyId_status_eta_idx" ON "curve_updates"("keyId", "status", "eta");
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE TABLE "milestone_claims" (
"id" TEXT NOT NULL,
"keyId" TEXT NOT NULL,
"wallet" TEXT NOT NULL,
"milestoneDays" INTEGER NOT NULL,
"rewardAmount" DECIMAL(36,7),
"claimedAt" TIMESTAMP(3) NOT NULL,
"txHash" TEXT NOT NULL,
"eventIndex" INTEGER NOT NULL,
"ledger" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "milestone_claims_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "milestone_claims_txHash_eventIndex_key" ON "milestone_claims"("txHash", "eventIndex");
CREATE UNIQUE INDEX "milestone_claims_keyId_wallet_milestoneDays_key" ON "milestone_claims"("keyId", "wallet", "milestoneDays");
CREATE INDEX "milestone_claims_wallet_keyId_idx" ON "milestone_claims"("wallet", "keyId");
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
CREATE TABLE "key_retirement" (
"id" TEXT NOT NULL,
"keyId" TEXT NOT NULL,
"eventType" TEXT NOT NULL,
"status" TEXT NOT NULL,
"buybackPrice" DECIMAL(36,7),
"noticeExpiry" TIMESTAMP(3),
"reason" TEXT,
"occurredAt" TIMESTAMP(3) NOT NULL,
"txHash" TEXT NOT NULL,
"eventIndex" INTEGER NOT NULL,
"ledger" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "key_retirement_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "key_retirement_txHash_eventIndex_key" ON "key_retirement"("txHash", "eventIndex");
CREATE INDEX "key_retirement_keyId_occurredAt_idx" ON "key_retirement"("keyId", "occurredAt" DESC);
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
CREATE TABLE "key_auctions" (
"id" TEXT NOT NULL,
"auctionId" TEXT NOT NULL,
"keyId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'active',
"reserve" DECIMAL(36,7) NOT NULL,
"highestBid" DECIMAL(36,7),
"highestBidder" TEXT,
"clearingPrice" DECIMAL(36,7),
"startsAt" TIMESTAMP(3) NOT NULL,
"endsAt" TIMESTAMP(3) NOT NULL,
"settledAt" TIMESTAMP(3),
"createdTxHash" TEXT NOT NULL,
"createdEventIndex" INTEGER NOT NULL,
"settledTxHash" TEXT,
"settledEventIndex" INTEGER,
"ledger" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "key_auctions_pkey" PRIMARY KEY ("id")
);

CREATE TABLE "key_auction_bids" (
"id" TEXT NOT NULL,
"auctionId" TEXT NOT NULL,
"bidder" TEXT NOT NULL,
"amount" DECIMAL(36,7) NOT NULL,
"placedAt" TIMESTAMP(3) NOT NULL,
"txHash" TEXT NOT NULL,
"eventIndex" INTEGER NOT NULL,
"ledger" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "key_auction_bids_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "key_auctions_auctionId_key" ON "key_auctions"("auctionId");
CREATE UNIQUE INDEX "key_auctions_keyId_key" ON "key_auctions"("keyId");
CREATE UNIQUE INDEX "key_auctions_createdTxHash_createdEventIndex_key" ON "key_auctions"("createdTxHash", "createdEventIndex");
CREATE UNIQUE INDEX "key_auctions_settledTxHash_settledEventIndex_key" ON "key_auctions"("settledTxHash", "settledEventIndex");
CREATE INDEX "key_auctions_keyId_status_idx" ON "key_auctions"("keyId", "status");
CREATE UNIQUE INDEX "key_auction_bids_txHash_eventIndex_key" ON "key_auction_bids"("txHash", "eventIndex");
CREATE INDEX "key_auction_bids_auctionId_placedAt_idx" ON "key_auction_bids"("auctionId", "placedAt" DESC);
ALTER TABLE "key_auction_bids" ADD CONSTRAINT "key_auction_bids_auctionId_fkey" FOREIGN KEY ("auctionId") REFERENCES "key_auctions"("auctionId") ON DELETE CASCADE ON UPDATE CASCADE;
18 changes: 18 additions & 0 deletions prisma/schema/milestone-claim.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// Holder tenure milestone reward claimed on-chain.
model MilestoneClaim {
id String @id @default(cuid())
keyId String
wallet String
milestoneDays Int
rewardAmount Decimal? @db.Decimal(36, 7)
claimedAt DateTime
txHash String
eventIndex Int
ledger Int
createdAt DateTime @default(now())

@@unique([txHash, eventIndex])
@@unique([keyId, wallet, milestoneDays])
@@index([wallet, keyId])
@@map("milestone_claims")
}
2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import tradersRouter from './modules/traders/traders.routes';
import nonceRouter from './modules/nonce/nonce.routes';
import swapsRouter from './modules/swaps/swaps.routes';
import keysRouter from './modules/keys/keys.routes';
import milestonesRouter from './modules/keys/milestones.routes';
import internalRouter from './modules/admin/sequencer.routes';
import eventsRouter from './modules/events/events.routes';
import { corsMiddleware } from './middlewares/cors.middleware';
Expand Down Expand Up @@ -116,6 +117,7 @@ app.use('/staking', routeBodySizeLimit('default'), stakingRouter);
app.use('/trades', routeBodySizeLimit('default'), tradesRouter);
app.use('/traders', routeBodySizeLimit('default'), tradersRouter);
app.use('/keys', routeBodySizeLimit('default'), keysRouter);
app.use('/milestones', routeBodySizeLimit('default'), milestonesRouter);
app.use('/swaps', routeBodySizeLimit('default'), swapsRouter);
app.use('/events', routeBodySizeLimit('default'), eventsRouter);

Expand Down
2 changes: 2 additions & 0 deletions src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import tradersRouter from './traders/traders.routes';
import nonceRouter from './nonce/nonce.routes';
import swapsRouter from './swaps/swaps.routes';
import eventsRouter from './events/events.routes';
import milestonesRouter from './keys/milestones.routes';

import { BASE as CREATORS_BASE } from '../constants/creator.constants';

Expand Down Expand Up @@ -141,5 +142,6 @@ router.use('/trades', routeBodySizeLimit('default'), tradesRouter);
router.use('/traders', routeBodySizeLimit('default'), tradersRouter);
router.use('/nonce', routeBodySizeLimit('default'), nonceRouter);
router.use('/swaps', routeBodySizeLimit('default'), swapsRouter);
router.use('/milestones', routeBodySizeLimit('default'), milestonesRouter);

export default router;
127 changes: 127 additions & 0 deletions src/modules/indexer/curve-update-indexer.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { prisma } from '../../utils/prisma.utils';
import { cacheInvalidate } from '../../utils/redis.utils';
import { logger } from '../../utils/logger.utils';
import {
IndexerChainEvent,
processIndexerChainEvents,
} from '../../utils/indexer-event-processor.utils';
import { curveUpdateCacheKeys } from '../keys/curve-updates.service';

export const CURVE_UPDATE_EVENT_TYPES = new Set([
'CurveUpdateProposed',
'CURVE_UPDATE_PROPOSED',
'CurveUpdateExecuted',
'CURVE_UPDATE_EXECUTED',
]);

const asDate = (value: unknown, fallback = new Date()): Date => {
if (
typeof value === 'number' ||
(typeof value === 'string' && /^\d+$/.test(value))
) {
const n = Number(value);
const date = new Date(n < 1e12 ? n * 1000 : n);
if (!Number.isNaN(date.getTime())) return date;
}
const date = value ? new Date(String(value)) : fallback;
return Number.isNaN(date.getTime()) ? fallback : date;
};

export async function processCurveUpdateEvents(
events: IndexerChainEvent[]
): Promise<void> {
await processIndexerChainEvents(events, async event => {
if (!CURVE_UPDATE_EVENT_TYPES.has(event.eventType)) return;
const keyId = String(event.keyId ?? event.creatorId ?? '').trim();
const proposalId = String(
event.proposalId ?? event.updateId ?? ''
).trim();
if (!keyId || !proposalId)
throw new Error(
`Invalid ${event.eventType}: keyId and proposalId are required`
);

if (
event.eventType === 'CurveUpdateProposed' ||
event.eventType === 'CURVE_UPDATE_PROPOSED'
) {
const eta = asDate(
event.eta ?? event.executeAfter ?? event.executionTime
);
const proposedAt = asDate(
event.proposedAt ?? event.occurredAt ?? event.timestamp
);
const proposedParams =
event.proposedParams ?? event.params ?? event.parameters;
if (!proposedParams || typeof proposedParams !== 'object')
throw new Error(
`Invalid ${event.eventType}: proposed params are required`
);
await prisma.curveUpdate.upsert({
where: { keyId_proposalId: { keyId, proposalId } },
create: {
keyId,
proposalId,
proposedParams,
eta,
proposedAt,
proposedTxHash: event.txHash,
proposedEventIndex: event.eventIndex,
ledger: Number(event.ledger ?? 0),
},
update: {},
});
} else {
const outcome = String(
event.outcome ??
event.result ??
(event.cancelled ? 'cancelled' : 'executed')
).toLowerCase();
const existing = await prisma.curveUpdate.findUnique({
where: { keyId_proposalId: { keyId, proposalId } },
select: { executedTxHash: true, executedEventIndex: true },
});
if (!existing) {
throw new Error(
`${event.eventType} references unknown proposal ${keyId}/${proposalId}`
);
}
if (
existing.executedTxHash === event.txHash &&
existing.executedEventIndex === event.eventIndex
) {
await cacheInvalidate(
curveUpdateCacheKeys.pending(keyId),
curveUpdateCacheKeys.history(keyId)
);
return;
}
await prisma.curveUpdate.update({
where: { keyId_proposalId: { keyId, proposalId } },
data: {
status:
outcome === 'cancelled' ||
outcome === 'canceled' ||
event.success === false
? 'cancelled'
: 'completed',
outcome,
completedAt: asDate(
event.executedAt ?? event.occurredAt ?? event.timestamp
),
executedTxHash: event.txHash,
executedEventIndex: event.eventIndex,
ledger: Number(event.ledger ?? 0),
},
});
}
await cacheInvalidate(
curveUpdateCacheKeys.pending(keyId),
curveUpdateCacheKeys.history(keyId)
);
logger.info(
{ keyId, proposalId, eventType: event.eventType },
'Curve update event indexed'
);
});
}
Loading
Loading