Skip to content
Merged
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ REVENUE_DISTRIBUTION_CYCLE_DAYS=7
# Comma-separated Stellar addresses of the three admin wallets.
ADMIN_MULTISIG_WALLETS=

# Key sunset watch (#931): days of inactivity before a key is "near threshold"
# in GET /keys/sunset-watch. Keys flagged on-chain are always included.
KEY_SUNSET_INACTIVITY_THRESHOLD_DAYS=30

# Creator and indexer tuning
INDEXER_JITTER_FACTOR=0.1
BACKGROUND_JOB_LOCK_TTL_MS=300000
Expand Down
3 changes: 3 additions & 0 deletions prisma/schema/creator.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ model CreatorProfile {
buybackPriceXlm Decimal? @db.Decimal(20, 7)
/// Buyback window expiry; buybacks are rejected with 410 after this time (#882).
buybackExpiresAt DateTime?
/// When the on-chain KeySunsetFlagged event was processed for this key (#931).
/// Null while the key has not been flagged for sunset.
sunsetFlaggedAt DateTime?
/// Buy cooldown between purchases by the same wallet, in ledgers (0 = no cooldown).
cooldownLedgers Int @default(0)
perks Json?
Expand Down
80 changes: 80 additions & 0 deletions prisma/schema/governance-delegation.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// prisma/schema/governance-delegation.prisma
// Delegation read model for governance voting (#933).
//
// VoteDelegation is the current-state table: one row per (delegator, keyId)
// pair, tracking the active delegate and whether the delegation has been
// revoked. Only rows where isActive = true count toward vote weight.
//
// VoteDelegationHistory is the append-only audit log: every DelegationSet and
// DelegationRevoked on-chain event appends a row so
// GET /governance/delegation/:wallet/history can return full provenance.
//
// Idempotency keys:
// DelegationSet — unique(txHash, eventIndex) on VoteDelegationHistory
// DelegationRevoked — same unique(txHash, eventIndex) on VoteDelegationHistory;
// the SET handler also guards on VoteDelegation.txHash
// for the upsert.

model VoteDelegation {
id String @id @default(cuid())

/// Wallet that is delegating its vote weight.
delegatorWallet String

/// Creator key scope this delegation applies to.
/// A wallet may hold different keys and delegate each independently.
keyId String

/// Wallet receiving the delegated weight.
delegateeWallet String

/// False once a DelegationRevoked event has been processed.
isActive Boolean @default(true)

/// Ledger of the most recent state-changing event (set or revoke).
ledger Int

/// Transaction hash of the most recent state-changing event.
/// Used as an optimistic concurrency guard so out-of-order replays
/// cannot clobber a newer state with an older one.
txHash String

/// On-chain timestamp of the most recent state-changing event.
occurredAt DateTime

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

@@unique([delegatorWallet, keyId])
@@index([delegateeWallet, isActive])
@@index([delegatorWallet])
@@index([keyId, isActive])
@@map("vote_delegations")
}

/// Append-only log of every delegation change event.
/// One row per DelegationSet or DelegationRevoked contract event.
model VoteDelegationHistory {
id String @id @default(cuid())

delegatorWallet String
keyId String

/// For DelegationSet events; null for DelegationRevoked entries.
delegateeWallet String?

/// 'set' | 'revoked'
action String

ledger Int
txHash String
eventIndex Int
occurredAt DateTime
createdAt DateTime @default(now())

@@unique([txHash, eventIndex])
@@index([delegatorWallet, occurredAt(sort: Desc)])
@@index([delegateeWallet, occurredAt(sort: Desc)])
@@index([keyId])
@@map("vote_delegation_history")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Migration: add_key_sunset_flagged_at (#931)
--
-- Adds sunsetFlaggedAt to CreatorProfile so the indexer can persist the
-- timestamp when a KeySunsetFlagged on-chain event is processed. The column
-- is nullable; NULL means the key has never been flagged for sunset.
--
-- Also extends the ActivityType enum with KEY_SUNSET_FLAGGED so the event
-- can be written to the Activity audit trail.

ALTER TABLE "CreatorProfile" ADD COLUMN "sunsetFlaggedAt" TIMESTAMP(3);

ALTER TYPE "ActivityType" ADD VALUE IF NOT EXISTS 'KEY_SUNSET_FLAGGED';
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
-- Migration: add_staking_nfts (#932)
--
-- Creates the staking NFT read model:
-- staking_nfts — one row per minted stake receipt NFT
-- staking_nft_transfers — append-only ownership history per NFT
--
-- Also extends the ActivityType enum with the two new staking event types so
-- the indexer can write audit-trail Activity records.

-- staking_nfts ──────────────────────────────────────────────────────────────

CREATE TABLE "staking_nfts" (
"id" TEXT NOT NULL,
"tokenId" TEXT NOT NULL,
"ownerAddress" TEXT NOT NULL,
"keyId" TEXT NOT NULL,
"stakedAmount" DECIMAL(30, 7) NOT NULL,
"lockExpiryLedger" INTEGER,
"lockExpiresAt" TIMESTAMP(3),
"burned" BOOLEAN NOT NULL DEFAULT FALSE,
"burnedAt" TIMESTAMP(3),
"mintLedger" INTEGER NOT NULL,
"mintTxHash" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

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

CREATE UNIQUE INDEX "staking_nfts_tokenId_key" ON "staking_nfts" ("tokenId");
CREATE UNIQUE INDEX "staking_nfts_mintTxHash_key" ON "staking_nfts" ("mintTxHash");
CREATE INDEX "staking_nfts_ownerAddress_idx" ON "staking_nfts" ("ownerAddress");
CREATE INDEX "staking_nfts_keyId_idx" ON "staking_nfts" ("keyId");
CREATE INDEX "staking_nfts_ownerAddress_burned_idx" ON "staking_nfts" ("ownerAddress", "burned");

-- staking_nft_transfers ─────────────────────────────────────────────────────

CREATE TABLE "staking_nft_transfers" (
"id" TEXT NOT NULL,
"nftId" TEXT NOT NULL,
"fromAddress" TEXT,
"toAddress" TEXT NOT NULL,
"ledger" INTEGER NOT NULL,
"txHash" TEXT NOT NULL,
"eventIndex" INTEGER NOT NULL,
"occurredAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

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

CREATE UNIQUE INDEX "staking_nft_transfers_txHash_eventIndex_key"
ON "staking_nft_transfers" ("txHash", "eventIndex");

CREATE INDEX "staking_nft_transfers_nftId_occurredAt_idx"
ON "staking_nft_transfers" ("nftId", "occurredAt" DESC);

CREATE INDEX "staking_nft_transfers_toAddress_idx"
ON "staking_nft_transfers" ("toAddress");

ALTER TABLE "staking_nft_transfers"
ADD CONSTRAINT "staking_nft_transfers_nftId_fkey"
FOREIGN KEY ("nftId") REFERENCES "staking_nfts" ("id")
ON DELETE CASCADE ON UPDATE CASCADE;

-- ActivityType enum extensions ──────────────────────────────────────────────

ALTER TYPE "ActivityType" ADD VALUE IF NOT EXISTS 'STAKE_NFT_MINTED';
ALTER TYPE "ActivityType" ADD VALUE IF NOT EXISTS 'STAKE_NFT_TRANSFERRED';
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
-- Migration: add_governance_delegation (#933)
--
-- Creates:
-- vote_delegations — current-state table (one row per delegator/keyId pair)
-- vote_delegation_history — append-only audit log of every delegation event
--
-- Extends ActivityType enum with the three new delegation/vote event values.

-- vote_delegations ──────────────────────────────────────────────────────────

CREATE TABLE "vote_delegations" (
"id" TEXT NOT NULL,
"delegatorWallet" TEXT NOT NULL,
"keyId" TEXT NOT NULL,
"delegateeWallet" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT TRUE,
"ledger" INTEGER NOT NULL,
"txHash" TEXT NOT NULL,
"occurredAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

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

CREATE UNIQUE INDEX "vote_delegations_delegatorWallet_keyId_key"
ON "vote_delegations" ("delegatorWallet", "keyId");

CREATE INDEX "vote_delegations_delegateeWallet_isActive_idx"
ON "vote_delegations" ("delegateeWallet", "isActive");

CREATE INDEX "vote_delegations_delegatorWallet_idx"
ON "vote_delegations" ("delegatorWallet");

CREATE INDEX "vote_delegations_keyId_isActive_idx"
ON "vote_delegations" ("keyId", "isActive");

-- vote_delegation_history ───────────────────────────────────────────────────

CREATE TABLE "vote_delegation_history" (
"id" TEXT NOT NULL,
"delegatorWallet" TEXT NOT NULL,
"keyId" TEXT NOT NULL,
"delegateeWallet" TEXT,
"action" TEXT NOT NULL,
"ledger" INTEGER NOT NULL,
"txHash" TEXT NOT NULL,
"eventIndex" INTEGER NOT NULL,
"occurredAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

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

CREATE UNIQUE INDEX "vote_delegation_history_txHash_eventIndex_key"
ON "vote_delegation_history" ("txHash", "eventIndex");

CREATE INDEX "vote_delegation_history_delegatorWallet_occurredAt_idx"
ON "vote_delegation_history" ("delegatorWallet", "occurredAt" DESC);

CREATE INDEX "vote_delegation_history_delegateeWallet_occurredAt_idx"
ON "vote_delegation_history" ("delegateeWallet", "occurredAt" DESC);

CREATE INDEX "vote_delegation_history_keyId_idx"
ON "vote_delegation_history" ("keyId");

-- ActivityType enum extensions ──────────────────────────────────────────────

ALTER TYPE "ActivityType" ADD VALUE IF NOT EXISTS 'GOVERNANCE_VOTE_CAST';
ALTER TYPE "ActivityType" ADD VALUE IF NOT EXISTS 'GOVERNANCE_DELEGATION_SET';
ALTER TYPE "ActivityType" ADD VALUE IF NOT EXISTS 'GOVERNANCE_DELEGATION_REVOKED';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Migration: add_trade_payment_asset (#934)
--
-- Adds paymentAsset to the Trade table to track which asset was used
-- for each key purchase. Existing rows default to 'XLM'.
-- Adds targeted indexes for aggregation queries.

ALTER TABLE "Trade"
ADD COLUMN "paymentAsset" TEXT NOT NULL DEFAULT 'XLM';

CREATE INDEX "Trade_paymentAsset_idx" ON "Trade" ("paymentAsset");
CREATE INDEX "Trade_creatorId_paymentAsset_idx" ON "Trade" ("creatorId", "paymentAsset");
51 changes: 51 additions & 0 deletions prisma/schema/staking.prisma
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// prisma/schema/staking.prisma
// Staking reward multiplier tiers and positions configuration (#942).
// StakingNft and StakingNftTransfer models (#932) are also defined here.

model StakingMultiplierTier {
id String @id @default(cuid())
Expand Down Expand Up @@ -31,3 +32,53 @@ model StakingPosition {
@@index([keyId])
@@map("staking_positions")
}

/// Tradeable stake receipt NFT (#932).
model StakingNft {
id String @id @default(cuid())
/// On-chain token ID (unique per network).
tokenId String @unique
/// Current holder wallet.
ownerAddress String
/// Creator key the staked tokens belong to.
keyId String
/// Number of creator keys locked in this position.
stakedAmount Decimal @db.Decimal(30, 7)
lockExpiryLedger Int?
lockExpiresAt DateTime?
burned Boolean @default(false)
burnedAt DateTime?
mintLedger Int
/// Mint tx hash — idempotency key for the indexer.
mintTxHash String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

transfers StakingNftTransfer[]

@@index([ownerAddress])
@@index([keyId])
@@index([ownerAddress, burned])
@@map("staking_nfts")
}

/// Append-only ownership history for a staking NFT (#932).
model StakingNftTransfer {
id String @id @default(cuid())
nftId String
/// Null on the initial mint.
fromAddress String?
toAddress String
ledger Int
txHash String
eventIndex Int
occurredAt DateTime
createdAt DateTime @default(now())

nft StakingNft @relation(fields: [nftId], references: [id], onDelete: Cascade)

@@unique([txHash, eventIndex])
@@index([nftId, occurredAt(sort: Desc)])
@@index([toAddress])
@@map("staking_nft_transfers")
}
23 changes: 14 additions & 9 deletions prisma/schema/trade.prisma
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
// prisma/schema/trade.prisma

model Trade {
id String @id @default(cuid())
buyer String
creatorId String
quantity String
price String
ledger Int
txHash String
timestamp DateTime
createdAt DateTime @default(now())
id String @id @default(cuid())
buyer String
creatorId String
quantity String
price String
ledger Int
txHash String
timestamp DateTime
/// Payment asset used for this purchase (e.g. 'XLM', 'USDC').
/// Defaults to 'XLM' for trades recorded before multi-currency support (#934).
paymentAsset String @default("XLM")
createdAt DateTime @default(now())

@@unique([ledger, txHash])
@@index([creatorId])
@@index([buyer])
@@index([ledger])
@@index([paymentAsset])
@@index([creatorId, paymentAsset])
}
10 changes: 10 additions & 0 deletions src/config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,16 @@ export const envSchema = z
.positive()
.default(5),

// Key sunset watch (#931): number of consecutive inactive days before a
// key is considered "near threshold" and surfaced by GET /keys/sunset-watch.
// Keys whose on-chain KeySunsetFlagged event has been processed always
// appear regardless of this threshold. Defaults to 30 days.
KEY_SUNSET_INACTIVITY_THRESHOLD_DAYS: z.coerce
.number()
.int()
.positive()
.default(30),

// Request body size limits (see docs/body-size-limits.md).
// Accepts any size string understood by the `bytes` package used
// internally by body-parser (e.g. '100kb', '1mb', '10mb').
Expand Down
Loading
Loading