Skip to content
Open
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ NONCE_CACHE_TTL_SECONDS=30
# LP position registry contract
LP_CONTRACT_ID=

# Staking contract (#1086): deployed staking contract for live get_stake_rewards
# simulation calls. When unset, accrued rewards degrade gracefully to '0'.
STAKING_CONTRACT_ID=

# Webhook settings
WEBHOOK_MAX_PER_CREATOR=5
WEBHOOK_RETRY_MAX_ATTEMPTS=3
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-- Staking epoch indexer: stakes and epoch_rewards tables (#1086)

CREATE TABLE "stakes" (
"id" TEXT NOT NULL,
"wallet" TEXT NOT NULL,
"keyId" TEXT NOT NULL,
"amount" DECIMAL(20,7) NOT NULL,
"startEpoch" INTEGER NOT NULL,
"epochsTotal" INTEGER,
"removedAt" TIMESTAMP(3),
"ledger" INTEGER NOT NULL,
"txHash" TEXT NOT NULL,
"eventIndex" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "stakes_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "stakes_txHash_eventIndex_key"
ON "stakes"("txHash", "eventIndex");

CREATE INDEX "stakes_wallet_removedAt_idx"
ON "stakes"("wallet", "removedAt");

CREATE INDEX "stakes_keyId_idx"
ON "stakes"("keyId");

-- ──────────────────────────────────────────────────────────────────────────────

CREATE TABLE "epoch_rewards" (
"id" TEXT NOT NULL,
"epoch" INTEGER NOT NULL,
"keyId" TEXT NOT NULL,
"totalDistributed" DECIMAL(20,7) NOT NULL,
"ledger" INTEGER NOT NULL,
"txHash" TEXT NOT NULL,
"eventIndex" INTEGER NOT NULL,
"distributedAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "epoch_rewards_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "epoch_rewards_txHash_eventIndex_key"
ON "epoch_rewards"("txHash", "eventIndex");

CREATE INDEX "epoch_rewards_keyId_epoch_idx"
ON "epoch_rewards"("keyId", "epoch");

CREATE INDEX "epoch_rewards_epoch_idx"
ON "epoch_rewards"("epoch");
66 changes: 66 additions & 0 deletions prisma/schema/staking-epoch.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// prisma/schema/staking-epoch.prisma
// Staking epoch indexer models (#1086):
// - Stake — indexed from StakeCreated / RewardsClaimed contract events
// - EpochReward — indexed from EpochRewardDistributed contract events

/// A single staking position indexed from on-chain StakeCreated events.
/// Unstaking is recorded by setting `removedAt` when a RewardsClaimed event
/// signals the position is closed.
///
/// Idempotent via the (txHash, eventIndex) unique constraint so replays are safe.
model Stake {
id String @id @default(cuid())
/// Wallet that owns the stake.
wallet String
/// Creator key the staked tokens belong to.
keyId String
/// Amount of tokens staked (Decimal(20,7) = stroop-level precision).
amount Decimal @db.Decimal(20, 7)
/// Epoch in which this stake was created.
startEpoch Int
/// Total epochs this position is locked for; null means no fixed duration.
epochsTotal Int?
/// Set when the stake is removed (RewardsClaimed with closePosition=true).
removedAt DateTime?

// Chain correlation for the creating event
ledger Int
txHash String
eventIndex Int

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@unique([txHash, eventIndex])
@@index([wallet, removedAt])
@@index([keyId])
@@map("stakes")
}

/// Per-epoch reward distribution record indexed from EpochRewardDistributed events.
/// One row per on-chain distribution event; the (txHash, eventIndex) pair
/// makes replays idempotent.
model EpochReward {
id String @id @default(cuid())
/// The epoch sequence number this distribution covers.
epoch Int
/// Creator key the rewards are distributed for.
keyId String
/// Total tokens distributed in this epoch for this key.
totalDistributed Decimal @db.Decimal(20, 7)

// Chain correlation
ledger Int
txHash String
eventIndex Int

/// On-chain event timestamp.
distributedAt DateTime

createdAt DateTime @default(now())

@@unique([txHash, eventIndex])
@@index([keyId, epoch])
@@index([epoch])
@@map("epoch_rewards")
}
5 changes: 5 additions & 0 deletions src/config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,11 @@ export const envSchema = z
// LP position registry contract
LP_CONTRACT_ID: optionalNonEmptyString,

// Staking contract (#1086): deployed staking contract used for live
// get_stake_rewards simulation calls. When unset, accrued rewards
// degrade gracefully to '0' rather than failing.
STAKING_CONTRACT_ID: optionalNonEmptyString,

// Invoice protocol adapters. Unconfigured reads/execution fail closed.
ISSUER_REPUTATION_CONTRACT_ID: optionalNonEmptyString,
INVOICE_TRANSFER_CONTRACT_ID: optionalNonEmptyString,
Expand Down
3 changes: 3 additions & 0 deletions src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import referralRouter from './referrals/referrals.routes';
import referralEarningsRouter from './referrals/referral-earnings.routes';
import contractsRouter from './contracts/contract.routes';
import stakingRouter from './staking/staking.routes';
import { stakingEpochRouter, keyEpochRouter } from './staking/staking-epoch.routes';
import sellersRouter from './sellers/sellers.routes';
import governanceRouter from './governance/governance.routes';
import factoryRouter from './factory/factory.routes';
Expand Down Expand Up @@ -129,6 +130,8 @@ router.use(
watchlistRouter
);
router.use('/staking', routeBodySizeLimit('default'), stakingRouter);
router.use('/staking', routeBodySizeLimit('default'), stakingEpochRouter);
router.use('/keys', routeBodySizeLimit('default'), keyEpochRouter);
router.use('/sellers', routeBodySizeLimit('default'), sellersRouter);
router.use('/governance', routeBodySizeLimit('default'), governanceRouter);
router.use('/factory', routeBodySizeLimit('default'), factoryRouter);
Expand Down
9 changes: 9 additions & 0 deletions src/modules/indexer/indexer-pipeline.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,15 @@ export async function processIndexerEvents(
await processLpEvents(lpEvents);
}

const { STAKING_EPOCH_EVENT_TYPES, processStakingEpochEvents } =
await import('./staking-epoch-indexer.service');
const stakingEpochEvents = events.filter(event =>
STAKING_EPOCH_EVENT_TYPES.has(event.eventType)
);
if (stakingEpochEvents.length > 0) {
await processStakingEpochEvents(stakingEpochEvents);
}

if (options.advanceCursor !== false)
await advanceIndexerCursorForBatch(events);
}
Expand Down
Loading
Loading