diff --git a/apps/s03-indexer/schema.graphql b/apps/s03-indexer/schema.graphql index ad9aa44..ebcc170 100644 --- a/apps/s03-indexer/schema.graphql +++ b/apps/s03-indexer/schema.graphql @@ -1,82 +1,451 @@ -# To improve query performance, we strongly suggest adding indexes to any field that you plan to filter or sort by -# Add the `@index` or `@index(unique: true)` annotation after any non-key field -# https://academy.subquery.network/build/graphql.html#indexing-by-non-primary-key-field - -type Account @entity { - """ - id field is always required and must look like this - """ - id: ID! - """ - The first block on which we see a transfer involving this account - """ - firstSeenLedger: Int - """ - The most recent block on which we see a transfer involving this account - """ - lastSeenLedger: Int - sentTransfers: [Transfer] @derivedFrom(field: "from") # These are virtual properties to help us navigate to the correct foreign key of Transfer - receivedTransfers: [Transfer] @derivedFrom(field: "to") # These are virtual properties to help us navigate to the correct foreign key of Transfer - sentPayments: [Payment] @derivedFrom(field: "from") # These are virtual properties to help us navigate to the correct foreign key of Payment - receivedPayments: [Payment] @derivedFrom(field: "to") # These are virtual properties to help us navigate to the correct foreign key of Payment -} - -type Transfer @entity { - """ - id field is always required and must look like this - """ - id: ID! - """ - The ledger of the transfer - """ - ledger: Int! - """ - The date of the transfer - """ - date: Date! - """ - The contract that was transferred - """ - contract: String! - """ - The account that transfers are made from - """ - from: Account! - """ - The account that transfers are made to - """ - to: Account! - """ - Value that is transferred - """ - value: BigInt! -} - -type Payment @entity { - id: ID! - txHash: String! - """ - The account that payments are made from - """ - from: Account! - """ - The account that payments are made to - """ - to: Account! - """ - Amount that is transferred - """ +# SO4 protocol schema +# +# Protocol-scale values, token amounts, prices, USD 1e30 values, shares, and +# signed deltas are stored as strings unless they are native ledger/count values. +# Mappings should keep these values as decoded decimal/base-unit strings and +# must not downcast them to JavaScript numbers. + +""" +Tracked SO4 protocol, token, faucet, market, and referral contracts. +Fed by deployment manifests and first-seen contract events such as mkt_new, +token transfer/mint/burn, faucet activity, and referral administration events. +""" +type ProtocolContract @entity { + id: ID! + key: String! @index(unique: true) + address: String! @index(unique: true) + contractType: String! @index + name: String + network: String @index + firstSeenLedger: Int @index + firstSeenTimestamp: Date @index + transactionHash: String @index + markets: [Market] @derivedFrom(field: "contract") +} + +""" +Token metadata for collateral, index, market, faucet, and protocol tokens. +Fed by token contract metadata plus token/faucet mint, burn, and transfer events. +""" +type Token @entity { + id: ID! + contract: ProtocolContract + address: String! @index(unique: true) + symbol: String @index + name: String + decimals: Int + tokenType: String @index + firstSeenLedger: Int @index + firstSeenTimestamp: Date @index + transactionHash: String @index + transfers: [MarketTokenTransfer] @derivedFrom(field: "token") +} + +""" +SO4 market definition. +Fed by mkt_new and later market status/configuration events. +""" +type Market @entity { + id: ID! + key: String! @index(unique: true) + contract: ProtocolContract + marketToken: Token + indexToken: Token + longToken: Token + shortToken: Token + name: String + status: String! @index + createdBy: String @index + createdLedger: Int! @index + createdTimestamp: Date! @index + createdTransactionHash: String! @index + latestConfigSnapshot: MarketConfigSnapshot + configSnapshots: [MarketConfigSnapshot] @derivedFrom(field: "market") + deposits: [Deposit] @derivedFrom(field: "market") + withdrawals: [Withdrawal] @derivedFrom(field: "market") + orders: [Order] @derivedFrom(field: "market") + positions: [Position] @derivedFrom(field: "market") + positionChanges: [PositionChange] @derivedFrom(field: "market") + liquidations: [Liquidation] @derivedFrom(field: "market") + adlEvents: [AdlEvent] @derivedFrom(field: "market") + feeClaims: [FeeClaim] @derivedFrom(field: "market") + uiFeeAccruals: [UiFeeAccrual] @derivedFrom(field: "market") + fundingFeeClaims: [FundingFeeClaim] @derivedFrom(field: "market") +} + +""" +Point-in-time market configuration. +Fed by mkt_new and market configuration update events. +""" +type MarketConfigSnapshot @entity { + id: ID! + market: Market! @index + key: String! @index + version: Int @index + oracle: String + maxLeverage: String + minCollateralUsd: String + borrowingFactor: String + fundingFactor: String + depositFeeFactor: String + withdrawalFeeFactor: String + liquidationFeeUsd: String + rawConfig: String + ledger: Int! @index + timestamp: Date! @index + transactionHash: String! @index +} + +""" +Liquidity deposit lifecycle. +Fed by dep_create, dep_execute, and dep_cancel events. +""" +type Deposit @entity { + id: ID! + key: String! @index(unique: true) + market: Market! @index + account: String! @index + receiver: String @index + status: String! @index + longTokenAmount: String + shortTokenAmount: String + minMarketTokens: String + marketTokenAmount: String + executionFee: String + createdLedger: Int @index + createdTimestamp: Date @index + createdTransactionHash: String @index + executedLedger: Int @index + executedTimestamp: Date @index + executedTransactionHash: String @index + cancelledLedger: Int @index + cancelledTimestamp: Date @index + cancelledTransactionHash: String @index + cancellationReason: String +} + +""" +Liquidity withdrawal lifecycle. +Fed by wth_create, wth_execute, and wth_cancel events. +""" +type Withdrawal @entity { + id: ID! + key: String! @index(unique: true) + market: Market! @index + account: String! @index + receiver: String @index + status: String! @index + marketTokenAmount: String + minLongTokenAmount: String + minShortTokenAmount: String + longTokenAmount: String + shortTokenAmount: String + executionFee: String + createdLedger: Int @index + createdTimestamp: Date @index + createdTransactionHash: String @index + executedLedger: Int @index + executedTimestamp: Date @index + executedTransactionHash: String @index + cancelledLedger: Int @index + cancelledTimestamp: Date @index + cancelledTransactionHash: String @index + cancellationReason: String +} + +""" +Pool balance snapshot for market-side liquidity and accounting. +Fed by dep_execute, wth_execute, fee settlement, funding, liquidation, and ADL events. +""" +type PoolBalanceSnapshot @entity { + id: ID! + market: Market! @index + token: Token @index + side: String! @index + poolAmount: String! + reservedAmount: String + openInterest: String + pnlPoolUsd: String + feePoolAmount: String + ledger: Int! @index + timestamp: Date! @index + transactionHash: String! @index +} + +""" +Market and protocol-token transfer observability. +Fed by token/faucet transfer, mint, burn, and market-token transfer events. +""" +type MarketTokenTransfer @entity { + id: ID! + token: Token @index + contractAddress: String! @index + from: String @index + to: String @index + account: String @index + transferType: String! @index amount: String! + ledger: Int! @index + timestamp: Date! @index + transactionHash: String! @index +} + +""" +Trading order lifecycle. +Fed by ord_create, ord_update, ord_freeze, ord_execute, and ord_cancel events. +""" +type Order @entity { + id: ID! + key: String! @index(unique: true) + market: Market! @index + account: String! @index + receiver: String @index + positionKey: String @index + orderType: String! @index + status: String! @index + isLong: Boolean @index + collateralToken: Token @index + swapPath: String + sizeDeltaUsd: String + collateralDeltaAmount: String + triggerPrice: String + acceptablePrice: String + executionFee: String + referralCode: String @index + createdLedger: Int @index + createdTimestamp: Date @index + createdTransactionHash: String @index + updatedLedger: Int @index + updatedTimestamp: Date @index + updatedTransactionHash: String @index + frozenLedger: Int @index + frozenTimestamp: Date @index + frozenTransactionHash: String @index + executedLedger: Int @index + executedTimestamp: Date @index + executedTransactionHash: String @index + cancelledLedger: Int @index + cancelledTimestamp: Date @index + cancelledTransactionHash: String @index + cancellationReason: String } -type Credit @entity { +""" +Current position state keyed by account, market, collateral token, and side. +Fed by pos_inc, pos_dec, pos_close, liq_execute, and adl_reduce events. +""" +type Position @entity { id: ID! - account: Account! + key: String! @index(unique: true) + market: Market! @index + account: String! @index + collateralToken: Token @index + isLong: Boolean! @index + status: String! @index + sizeUsd: String + collateralAmount: String + averagePrice: String + entryFundingRate: String + reserveAmount: String + realizedPnlUsd: String + realizedPnlAmount: String + latestOrderKey: String @index + openedLedger: Int @index + openedTimestamp: Date @index + openedTransactionHash: String @index + updatedLedger: Int @index + updatedTimestamp: Date @index + updatedTransactionHash: String @index + closedLedger: Int @index + closedTimestamp: Date @index + closedTransactionHash: String @index + changes: [PositionChange] @derivedFrom(field: "position") + liquidations: [Liquidation] @derivedFrom(field: "position") + adlEvents: [AdlEvent] @derivedFrom(field: "position") +} + +""" +Immutable position mutation record. +Fed by pos_inc, pos_dec, pos_close, liq_execute, and adl_reduce events. +""" +type PositionChange @entity { + id: ID! + key: String! @index + market: Market! @index + position: Position @index + account: String! @index + order: Order @index + changeType: String! @index + status: String! @index + isLong: Boolean @index + sizeDeltaUsd: String + nextSizeUsd: String + collateralDeltaAmount: String + nextCollateralAmount: String + executionPrice: String + indexTokenPrice: String + pnlUsd: String + priceImpactUsd: String + borrowingFeeUsd: String + fundingFeeAmount: String + positionFeeAmount: String + ledger: Int! @index + timestamp: Date! @index + transactionHash: String! @index +} + +""" +Position liquidation execution record. +Fed by liq_execute and related liq_* lifecycle events. +""" +type Liquidation @entity { + id: ID! + key: String! @index(unique: true) + market: Market! @index + position: Position @index + account: String! @index + liquidator: String @index + collateralToken: Token @index + status: String! @index + isLong: Boolean @index + sizeDeltaUsd: String + collateralLiquidatedAmount: String + remainingCollateralAmount: String + liquidationPrice: String + pnlUsd: String + priceImpactUsd: String + liquidationFeeUsd: String + ledger: Int! @index + timestamp: Date! @index + transactionHash: String! @index +} + +""" +Automatic deleveraging reduction record. +Fed by adl_reduce and related adl_* events. +""" +type AdlEvent @entity { + id: ID! + key: String! @index(unique: true) + market: Market! @index + position: Position @index + account: String! @index + collateralToken: Token @index + status: String! @index + isLong: Boolean @index + sizeReductionUsd: String + collateralReductionAmount: String + executionPrice: String + pnlUsd: String + ledger: Int! @index + timestamp: Date! @index + transactionHash: String! @index +} + +""" +Protocol and affiliate fee claim. +Fed by fee_claim and market fee withdrawal events. +""" +type FeeClaim @entity { + id: ID! + key: String! @index + market: Market @index + token: Token @index + account: String! @index + receiver: String @index + feeType: String! @index amount: String! + amountUsd: String + status: String! @index + ledger: Int! @index + timestamp: Date! @index + transactionHash: String! @index } -type Debit @entity { +""" +UI fee accrual attributed to an order, position change, or market action. +Fed by ui_fee_accrual and order execution fee events. +""" +type UiFeeAccrual @entity { id: ID! - account: Account! + key: String! @index + market: Market @index + order: Order @index + account: String! @index + uiFeeReceiver: String! @index + token: Token @index amount: String! + amountUsd: String + ledger: Int! @index + timestamp: Date! @index + transactionHash: String! @index +} + +""" +Funding fee claim by trader or protocol recipient. +Fed by funding_fee_claim events. +""" +type FundingFeeClaim @entity { + id: ID! + key: String! @index + market: Market @index + position: Position @index + account: String! @index + receiver: String @index + token: Token @index + amount: String! + amountUsd: String + status: String! @index + ledger: Int! @index + timestamp: Date! @index + transactionHash: String! @index +} + +""" +Referral code registration and ownership state. +Fed by referral_code_register and referral_code_transfer events. +""" +type ReferralCode @entity { + id: ID! + code: String! @index(unique: true) + owner: String! @index + status: String! @index + createdLedger: Int! @index + createdTimestamp: Date! @index + createdTransactionHash: String! @index + transfers: [ReferralOwnershipTransfer] @derivedFrom(field: "referralCode") + traderLinks: [TraderReferral] @derivedFrom(field: "referralCode") +} + +""" +Trader to referral-code assignment. +Fed by trader_referral_set and referral registration flows. +""" +type TraderReferral @entity { + id: ID! + trader: String! @index + referralCode: ReferralCode! @index + referrer: String! @index + status: String! @index + createdLedger: Int! @index + createdTimestamp: Date! @index + createdTransactionHash: String! @index + updatedLedger: Int @index + updatedTimestamp: Date @index + updatedTransactionHash: String @index +} + +""" +Referral code ownership transfer history. +Fed by referral_code_transfer events. +""" +type ReferralOwnershipTransfer @entity { + id: ID! + referralCode: ReferralCode! @index + code: String! @index + previousOwner: String! @index + newOwner: String! @index + ledger: Int! @index + timestamp: Date! @index + transactionHash: String! @index } diff --git a/apps/s03-indexer/src/mappings/mappingHandlers.ts b/apps/s03-indexer/src/mappings/mappingHandlers.ts index a3969e0..c3620e6 100644 --- a/apps/s03-indexer/src/mappings/mappingHandlers.ts +++ b/apps/s03-indexer/src/mappings/mappingHandlers.ts @@ -1,92 +1,22 @@ -import { Account, Credit, Debit, Payment, Transfer } from "../types"; -import { - StellarOperation, - StellarEffect, - SorobanEvent, -} from "@subql/types-stellar"; -import { - AccountCredited, - AccountDebited, -} from "@stellar/stellar-sdk/lib/horizon/types/effects"; -import type { Horizon } from "@stellar/stellar-sdk"; +import { MarketTokenTransfer } from "../types"; +import { SorobanEvent } from "@subql/types-stellar"; import { xdr } from "@stellar/stellar-sdk"; import { Address, scValToBigInt } from "@stellar/stellar-base"; -export async function handleOperation( - op: StellarOperation, -): Promise { - logger.info(`Indexing operation ${op.id}, type: ${op.type}`); - - const fromAccount = await checkAndGetAccount(op.from, op.ledger!.sequence); - const toAccount = await checkAndGetAccount(op.to, op.ledger!.sequence); - - const payment = Payment.create({ - id: op.id, - fromId: fromAccount.id, - toId: toAccount.id, - txHash: op.transaction_hash, - amount: op.amount, - }); - - fromAccount.lastSeenLedger = op.ledger!.sequence; - toAccount.lastSeenLedger = op.ledger!.sequence; - await Promise.all([fromAccount.save(), toAccount.save(), payment.save()]); -} - -export async function handleCredit( - effect: StellarEffect, -): Promise { - logger.info(`Indexing effect ${effect.id}, type: ${effect.type}`); - - const account = await checkAndGetAccount( - effect.account, - effect.ledger!.sequence, - ); - - const credit = Credit.create({ - id: effect.id, - accountId: account.id, - amount: effect.amount, - }); - - account.lastSeenLedger = effect.ledger!.sequence; - await Promise.all([account.save(), credit.save()]); -} - -export async function handleDebit( - effect: StellarEffect, -): Promise { - logger.info(`Indexing effect ${effect.id}, type: ${effect.type}`); - - const account = await checkAndGetAccount( - effect.account, - effect.ledger!.sequence, - ); - - const debit = Debit.create({ - id: effect.id, - accountId: account.id, - amount: effect.amount, - }); - - account.lastSeenLedger = effect.ledger!.sequence; - await Promise.all([account.save(), debit.save()]); -} - export async function handleEvent(event: SorobanEvent): Promise { logger.info( - `New transfer event found at block ${event.ledger!.sequence.toString()}`, + `New SO4 contract event found at block ${event.ledger!.sequence.toString()}`, ); - // Get data from the event - // The transfer event has the following payload \[env, from, to\] + // Token and market-token transfers use topic shape [event_name, from, to]. + // Amounts are stored as strings to preserve protocol-scale precision. if (event.topic.length < 3) { logger.info(`Event ${event.id} does not match transfer topic shape, skipping`); return; } const { - topic: [env, from, to], + topic: [eventName, from, to], } = event; // Check if the topic values are actually addresses before decoding @@ -111,14 +41,10 @@ export async function handleEvent(event: SorobanEvent): Promise { return; } - const fromAccount = await checkAndGetAccount( - decodeAddress(from), - event.ledger!.sequence, - ); - const toAccount = await checkAndGetAccount( - decodeAddress(to), - event.ledger!.sequence, - ); + // Stellar StrKey addresses are case-sensitive uppercase base32. Keep the + // canonical decoded form so app filters can match the real account/contract. + const fromAccount = decodeAddress(from); + const toAccount = decodeAddress(to); // Check if event.value is an integer type before converting const valueType = event.value.switch().name; @@ -128,36 +54,25 @@ export async function handleEvent(event: SorobanEvent): Promise { return; } - // Create the new transfer entity - const contractAddress = event.contractId ? Buffer.from(event.contractId.contractId()).toString('hex') : ''; - const transfer = Transfer.create({ + // Store the emitting contract as its canonical "C..." StrKey so it lines up + // with the contract ids in config and with the decoded from/to addresses. + const contractAddress = event.contractId + ? Address.contract(event.contractId.contractId() as unknown as Buffer).toString() + : ""; + const transfer = MarketTokenTransfer.create({ id: event.id, + contractAddress, + from: fromAccount, + to: toAccount, + account: fromAccount, + transferType: decodeTopicName(eventName), + amount: scValToBigInt(event.value).toString(), ledger: event.ledger!.sequence, - date: new Date(event.ledgerClosedAt), - contract: contractAddress, - fromId: fromAccount.id, - toId: toAccount.id, - value: scValToBigInt(event.value), + timestamp: new Date(event.ledgerClosedAt), + transactionHash: event.txHash, }); - fromAccount.lastSeenLedger = event.ledger!.sequence; - toAccount.lastSeenLedger = event.ledger!.sequence; - await Promise.all([fromAccount.save(), toAccount.save(), transfer.save()]); -} - -async function checkAndGetAccount( - id: string, - ledgerSequence: number, -): Promise { - let account = await Account.get(id.toLowerCase()); - if (!account) { - // We couldn't find the account - account = Account.create({ - id: id.toLowerCase(), - firstSeenLedger: ledgerSequence, - }); - } - return account; + await transfer.save(); } // scValToNative not works, temp solution @@ -176,3 +91,14 @@ function decodeAddress(scVal: xdr.ScVal): string { throw new Error(`Unknown address type: ${addressType}`); } + +function decodeTopicName(scVal: xdr.ScVal): string { + const valueType = scVal.switch().name; + if (valueType === "scvSymbol") { + return scVal.sym().toString(); + } + if (valueType === "scvString") { + return scVal.str().toString(); + } + return valueType; +}