From bab104968ace7f7343f4d67804fb8f9b13aa8749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Sun, 21 Dec 2025 00:59:29 -0300 Subject: [PATCH 1/8] feat: add subgraph and cli specs first draft MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- .claude/settings.local.json | 8 + specs/HORIZON_CLI_SPEC.md | 186 +++++++++ specs/NETWORK_SUBGRAPH_SPEC.md | 718 +++++++++++++++++++++++++++++++++ 3 files changed, 912 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 specs/HORIZON_CLI_SPEC.md create mode 100644 specs/NETWORK_SUBGRAPH_SPEC.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..19774ae --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(grep:*)", + "Bash(find:*)" + ] + } +} diff --git a/specs/HORIZON_CLI_SPEC.md b/specs/HORIZON_CLI_SPEC.md new file mode 100644 index 0000000..81c0f7a --- /dev/null +++ b/specs/HORIZON_CLI_SPEC.md @@ -0,0 +1,186 @@ +# Horizon CLI Specification + +A CLI tool for service provider operators to manage stake and provisions on Graph Horizon. + +## Overview + +The CLI is designed for **operators** acting on behalf of service providers. The operator role exists as a security mechanism - operators use a hot private key to execute day-to-day operations while the service provider's cold key stays secure. + +All read operations pull data from the Horizon subgraph. Write operations are either: +- **Signed by the operator** - executed directly via RPC +- **Service provider only** - CLI generates ABI Ninja link + instructions (no signing) + +## Configuration + +| Field | Required | Default | +|-------|----------|---------| +| `operator_mnemonic` | Yes | - | +| `service_provider_address` | Yes | - | +| `rpc_endpoint` | Yes | - | +| `subgraph_endpoint` | Yes | - | +| `horizon_staking_address` | No | Arbitrum One deployed address | + +## Commands + +### stake + +Aggregate stake view and service-provider-only actions. + +#### `stake status [--service-provider ]` + +Displays: +- Total stake: sum of idle and provisioned +- Idle stake: stake not assigned to provisions +- Provisioned stake: stake assigned to provisions + +#### `stake deposit --tokens ` + +**Service provider only** - outputs ABI Ninja instructions: +``` +To deposit GRT: + +1. Open: https://abi.ninja//?methods=stake + +2. Connect wallet with service provider address: + + +3. Fill in parameters: + - tokens: + +4. Submit transaction +``` + +#### `stake unstake --tokens ` + +**Service provider only** - outputs ABI Ninja instructions for `unstake`. + +--- + +### operator + +Operator authorization management. + +#### `operator list [--service-provider ]` + +Lists all authorized operators per data service. Highlights the configured operator as `(active)`. + +``` +Data Service Operator +──────────────────────────────────────── +0x1234...5678 0xABCD...EF01 (active) +0x1234...5678 0x9876...5432 +0x5678...9ABC 0xABCD...EF01 (active) +``` + +#### `operator authorize ` + +**Service provider only** - outputs ABI Ninja instructions for `setOperator` with `allowed: true`. + +#### `operator revoke ` + +**Service provider only** - outputs ABI Ninja instructions for `setOperator` with `allowed: false`. + +--- + +### provision + +Provision listing, inspection, and management. + +#### `provision list [--service-provider ]` + +Table of all provisions: +- Data service address +- Tokens provisioned +- Tokens thawing +- Provision parameters (max verifier cut, thawing period) + +#### `provision status [--service-provider ]` + +Detailed provision view: +- Data service address +- Operators +- Tokens provisioned +- Tokens thawing +- Tokens available (non-thawing) +- Max verifier cut (current and pending if staged) +- Thawing period (current and pending if staged) +- Thaw requests summary (count, total shares thawing, ready to remove) +- Delegation pool (tokens, shares) +- Fee cuts by payment type + +#### `provision create --tokens --max-cut --thawing-period ` + +**Operator signed** - creates a new provision for the data service. + +Maps to: `HorizonStaking.provision(serviceProvider, verifier, tokens, maxVerifierCut, thawingPeriod)` + +#### `provision add --tokens ` + +**Operator signed** - adds tokens from idle stake to an existing provision. + +Maps to: `HorizonStaking.addToProvision(serviceProvider, verifier, tokens)` + +#### `provision thaw --tokens ` + +**Operator signed** - starts thawing tokens from a provision. + +Maps to: `HorizonStaking.thaw(serviceProvider, verifier, tokens)` + +#### `provision list-thaw [--service-provider ]` + +Lists thaw requests for a provision: + +``` +ID Shares Thawing Until Status +───────────────────────────────────────────────────────────────────── +0xabc123... 1,000.00 GRT 2024-03-15 14:30:00 pending +0xdef456... 500.00 GRT 2024-03-10 09:00:00 ready + +Current block timestamp: 2024-03-12 10:00:00 +``` + +Status is `ready` when `thawingUntil < currentBlockTimestamp`, otherwise `pending`. + +#### `provision remove [--n-requests ]` + +**Operator signed** - removes thawed tokens back to idle stake. + +Maps to: `HorizonStaking.deprovision(serviceProvider, verifier, nThawRequests)` + +If `--n-requests` is not specified, processes all ready thaw requests. + +#### `provision stage-parameters --max-cut --thawing-period ` + +**Service provider only** - outputs ABI Ninja instructions for staging new provision parameters. + +Maps to: `HorizonStaking.setProvisionParameters(serviceProvider, verifier, maxVerifierCut, thawingPeriod)` + +Note: Parameter changes require acceptance by the data service (verifier) before taking effect. The pending parameters will be visible in `provision status`. + +#### `provision set-cut --payment-type --cut ` + +**Operator signed** - sets the delegation fee cut for a payment type. + +Maps to: `HorizonStaking.setDelegationFeeCut(serviceProvider, verifier, paymentType, feeCut)` + +Payment types: +- `QueryFee` +- `IndexingFee` +- `IndexingRewards` + +--- + +## Data Sources + +### Subgraph Queries + +All read operations fetch data from the Horizon subgraph: +- Service provider stake (total, idle, provisioned) +- Provisions list and details +- Thaw requests +- Delegation pools +- Operator authorizations + +### RPC + +Used only for sending signed transactions (operator write operations). diff --git a/specs/NETWORK_SUBGRAPH_SPEC.md b/specs/NETWORK_SUBGRAPH_SPEC.md new file mode 100644 index 0000000..5219ecf --- /dev/null +++ b/specs/NETWORK_SUBGRAPH_SPEC.md @@ -0,0 +1,718 @@ +# Network Subgraph Specification + +A subgraph for indexing The Graph protocol's core staking and payments infrastructure. + +## Overview + +This subgraph indexes the Horizon protocol contracts to provide queryable data about: +- Service provider stake and provisions +- Delegations and delegation pools +- Thaw requests (deprovisioning and undelegating) +- Payment collections and escrow +- Payment collectors: GraphTally +- Operators + +The subgraph is **data-service agnostic** - it captures the concept of data services (verifiers) but contains nothing specific to any particular data service. Each data service can create their own subgraph for service-specific data. + +## Data Sources + +### Contracts + +| Contract | Purpose | +|----------|---------| +| `HorizonStaking` | Core staking, provisions, delegations, slashing | +| `GraphPayments` | Payment distribution | +| `PaymentsEscrow` | Escrow account management | +| `GraphTallyCollector` | RAV-based payment collection and signer authorization | + +## Entities + +### GraphNetwork + +Singleton entity for protocol-wide aggregates and parameters. + +```graphql +type GraphNetwork @entity { + id: ID! + + # Counts + "Active service providers" + countServiceProviders: Int! + "Active delegators" + countDelegators: Int! + "Active data services" + countDataServices: Int! + "Active provisions" + countProvisions: Int! + + # Stake aggregates + "Total tokens staked by service providers" + tokensStaked: BigInt! + "Total tokens delegated" + tokensDelegated: BigInt! + "Total tokens currently thawing from provisions" + tokensThawingFromProvisions: BigInt! + "Total tokens currently thawing from delegation pools" + tokensThawingFromDelegationPools: BigInt! + + # Slashing aggregates + "Total slash events" + countSlashEvents: Int! + "Total tokens slashed" + tokensSlashed: BigInt! + "Total tokens slashed from provisions" + tokensSlashedFromProvisions: BigInt! + "Total tokens slashed from delegation pools" + tokensSlashedFromDelegationPools: BigInt! + + # Payment collection aggregates + "Total tokens collected" + tokensCollected: BigInt! + "Total tokens burnt as protocol tax" + tokensCollectedByProtocolTax: BigInt! + "Total tokens collected by service providers" + tokensCollectedByServiceProviders: BigInt! + "Total tokens collected by delegators" + tokensCollectedByDelegators: BigInt! + "Total tokens collected by data services" + tokensCollectedByDataServices: BigInt! + + # Payment escrow aggregates + "Total tokens held in escrow" + tokensEscrowed: BigInt! + "Total tokens currently thawing in escrow" + tokensThawingFromEscrow: BigInt! + + # Protocol parameters (mutable) + "Max allowed thawing period (seconds)" + maxThawingPeriod: BigInt! + "Delegation slashing status" + delegationSlashingEnabled: Boolean! + + # Protocol parameters (immutable) + "Protocol tax on payments collected (PPM)" + protocolPaymentCut: BigInt! + "Withdrawal thawing period for payments escrow (seconds)" + escrowThawingPeriod: BigInt! + "Signer revocation thawing period for payments escrow(seconds)" + revokeSignerThawingPeriod: BigInt! +} +``` + +### ServiceProvider + +An account that stakes GRT to provide services. + +```graphql +type ServiceProvider @entity { + id: ID! + + # Relationships + provisions: [Provision!]! @derivedFrom(field: "serviceProvider") + operatorAuthorizations: [OperatorAuthorization!]! @derivedFrom(field: "serviceProvider") + + # Counts + "Active provisions" + countProvisions: Int! + "Active delegators" + countDelegators: Int! + "Active operators" + countOperators: Int! + "Pending thaw requests for the service provider" + countThawRequests: Int! + "Amount of times slashed" + countSlashEvents: Int! + + # Stake + "Tokens staked by the service provider" + tokensStaked: BigInt! + "Tokens provisioned to data services" + tokensProvisioned: BigInt! + "Tokens that are not locked in provisions" + tokensIdle: BigInt! + "Tokens currently thawing from provisions" + tokensThawing: BigInt! + + # Delegation + "Total tokens delegated to this service provider" + tokensDelegated: BigInt! + "Tokens currently thawing from delegation pools" + tokensDelegatedThawing: BigInt! + + # Slashing + "Total tokens slashed" + tokensSlashed: BigInt! + "Tokens slashed from provisions" + tokensSlashedFromProvisions: BigInt! + "Tokens slashed from delegation pools" + tokensSlashedFromDelegationPools: BigInt! + + # Payments + "Total tokens collected" + tokensCollected: BigInt! +} +``` + +### DataService + +Represents a data service (verifier) in the protocol. + +```graphql +type DataService @entity { + id: ID! + + # Relationships + provisions: [Provision!]! @derivedFrom(field: "dataService") + + # Counts + "Active service providers" + countServiceProviders: Int! + "Active delegators" + countDelegators: Int! + "Pending provision thaw requests" + countThawRequestsProvision: Int! + "Pending delegation thaw requests" + countThawRequestsDelegation: Int! + "Amount of times slashed" + countSlashEvents: Int! + + # Tokens + "Total tokens provisioned" + tokensProvisioned: BigInt! + "Total tokens delegated" + tokensDelegated: BigInt! + "Tokens currently thawing from provisions" + tokensThawingFromProvisions: BigInt! + "Tokens currently thawing from delegation pools" + tokensThawingFromDelegationPools: BigInt! + "Total tokens slashed" + tokensSlashed: BigInt! + "Total tokens collected" + tokensCollected: BigInt! + + # Parameters + "Max delegation multiplier on service provider stake" + delegationRatio: BigInt! + "Minimum tokens required for a provision" + minProvisionTokens: BigInt! + "Maximum tokens allowed for a provision" + maxProvisionTokens: BigInt! + "Minimum verifier cut (PPM)" + minVerifierCut: BigInt! + "Maximum verifier cut (PPM)" + maxVerifierCut: BigInt! + "Minimum thawing period (seconds)" + minThawingPeriod: BigInt! + "Maximum thawing period (seconds)" + maxThawingPeriod: BigInt! +} +``` + +### Provision + +Stake allocated by a service provider to a specific data service. + +```graphql +type Provision @entity { + id: ID! + + # References + "Service provider" + serviceProvider: ServiceProvider! + "Data service" + dataService: DataService! + "Delegation pool" + delegationPool: DelegationPool! + + # Relationships + thawRequests: [ThawRequest!]! @derivedFrom(field: "provision") + + # Counts + "Active delegators" + countDelegators: Int! + "Pending thaw requests" + countThawRequests: Int! + "Amount of times slashed" + countSlashEvents: Int! + + # Tokens + "Tokens in provision" + tokens: BigInt! + "Tokens currently thawing" + tokensThawing: BigInt! + "Shares representing thawing tokens" + sharesThawing: BigInt! + "Total tokens delegated" + tokensDelegated: BigInt! + "Total tokens slashed" + tokensSlashed: BigInt! + "Total tokens collected" + tokensCollected: BigInt! + + # Parameters + "Max verifier reward on slash (PPM)" + maxVerifierCut: BigInt! + "Thawing period for deprovisioning stake (seconds)" + thawingPeriod: BigInt! + + # Staged parameters + "Pending max verifier cut (PPM)" + maxVerifierCutPending: BigInt! + "Pending thawing period (seconds)" + thawingPeriodPending: BigInt! + "Timestamp when parameters were staged" + lastParametersStagedAt: BigInt! + + # Fee cuts + "Query fee cut for delegators (PPM)" + queryFeeCut: BigInt! + "Indexing fee cut for delegators (PPM)" + indexingFeeCut: BigInt! + "Indexing reward cut for delegators (PPM)" + indexingRewardCut: BigInt! + + # State + "Thawing nonce - incremented on slash to invalidate pending thaw requests" + thawingNonce: BigInt! +} +``` + +### DelegationPool + +Aggregate delegation pool for a provision. + +```graphql +type DelegationPool @entity { + id: ID! + + # References + "Provision" + provision: Provision! + + # Relationships + delegations: [Delegation!]! @derivedFrom(field: "pool") + + # Counts + "Active delegators" + countDelegators: Int! + "Pending thaw requests" + countThawRequests: Int! + "Amount of times slashed" + countSlashEvents: Int! + + # Tokens + "Total delegated tokens" + tokens: BigInt! + "Total shares issued" + shares: BigInt! + "Tokens being undelegated" + tokensThawing: BigInt! + "Shares representing thawing" + sharesThawing: BigInt! + "Total tokens slashed" + tokensSlashed: BigInt! + "Total tokens collected by delegators" + tokensCollected: BigInt! + + # State + "Thawing nonce - incremented on slash to invalidate pending thaw requests" + thawingNonce: BigInt! +} +``` + +### Delegator + +An account that delegates tokens to service providers. + +```graphql +type Delegator @entity { + id: ID! + + # Relationships + delegations: [Delegation!]! @derivedFrom(field: "delegator") + + # Counts + "Active delegations" + countDelegations: Int! + "Pending thaw requests" + countThawRequests: Int! + + # Tokens + "Total tokens delegated" + tokensDelegated: BigInt! + "Tokens currently thawing" + tokensThawing: BigInt! + "Total tokens collected" + tokensCollected: BigInt! +} +``` + +### Delegation + +Individual delegator's stake in a delegation pool. + +```graphql +type Delegation @entity { + id: ID! + + # References + "Delegator" + delegator: Delegator! + "Delegation pool" + pool: DelegationPool! + "Service provider" + serviceProvider: ServiceProvider! + "Data service" + dataService: DataService! + + # Relationships + thawRequests: [ThawRequest!]! @derivedFrom(field: "delegation") + + # Counts + "Pending thaw requests" + countThawRequests: Int! + + # Tokens + "Tokens delegated - note that this does not represent current delegation valuation" + tokensDelegated: BigInt! + "Delegator's shares in pool" + shares: BigInt! + "Tokens currently thawing" + tokensThawing: BigInt! + "Tokens collected by this delegator" + tokensCollected: BigInt! +} +``` + +### ThawRequest + +Pending deprovision or undelegate request. + +```graphql +enum ThawRequestType { + PROVISION + DELEGATION +} + +type ThawRequest @entity { + id: ID! + + # Type + "Thaw request type" + type: ThawRequestType! + + # References + "Service provider" + serviceProvider: ServiceProvider! + "Data service" + dataService: DataService! + "Owner address - service provider (provision) or delegator (delegation)" + owner: Bytes! + "Provision - for provision thaws" + provision: Provision + "Delegation - for delegation thaws" + delegation: Delegation + + # State + "Shares being thawed" + shares: BigInt! + "Timestamp when thaw completes" + thawingUntil: BigInt! + "Thawing nonce" + thawingNonce: BigInt! + + # Status + "False if invalidated by slashing" + valid: Boolean! + "True when tokens withdrawn" + fulfilled: Boolean! +} +``` + +### Operator + +An operator account that can be authorized to act on behalf of service providers. + +```graphql +type Operator @entity { + id: ID! + + # Relationships + authorizations: [OperatorAuthorization!]! @derivedFrom(field: "operator") + + # Counts + "Active authorizations" + countAuthorizations: Int! +} +``` + +### OperatorAuthorization + +Authorization for an operator to act on behalf of a service provider on a specific data service. + +```graphql +type OperatorAuthorization @entity { + id: ID! + + # References + "Operator" + operator: Operator! + "Service provider" + serviceProvider: ServiceProvider! + "Data service" + dataService: DataService! + + # State + "Current authorization status" + allowed: Boolean! +} +``` + +### EscrowAccount + +Escrow balance for a payer-collector-receiver tuple. + +```graphql +type EscrowAccount @entity { + id: ID! + + # References + "Payer address" + payer: Bytes! + "Collector address" + collector: Bytes! + "Receiver address" + receiver: Bytes! + + # Tokens + "Available tokens" + tokens: BigInt! + "Tokens currently thawing" + tokensThawing: BigInt! + "Timestamp when thawing completes (0 if not thawing)" + thawEndTimestamp: BigInt! +} +``` + +### PaymentCollection + +Record of a payment distribution event. + +```graphql +enum PaymentType { + QUERY_FEE + INDEXING_FEE + INDEXING_REWARDS +} + +type PaymentCollection @entity { + id: ID! + + # References + "Payer address" + payer: Bytes! + "Receiver address" + receiver: Bytes! + "Data service" + dataService: DataService! + + # Payment details + "Payment type" + paymentType: PaymentType! + "Total payment amount" + tokens: BigInt! + + # Distribution + "Protocol tax (burned)" + tokensProtocol: BigInt! + "Data service cut" + tokensDataService: BigInt! + "Delegation pool rewards" + tokensDelegationPool: BigInt! + "Receiver's share" + tokensReceiver: BigInt! + "Where receiver tokens went" + receiverDestination: Bytes! +} +``` + +### GraphTallySigner + +An account authorized to sign RAVs on behalf of payers. + +```graphql +type GraphTallySigner @entity { + id: ID! + + # Relationships + authorizations: [GraphTallySignerAuthorization!]! @derivedFrom(field: "signer") + + # Counts + "Active authorizations" + countAuthorizations: Int! +} +``` + +### GraphTallySignerAuthorization + +Authorization for a signer to sign RAVs on behalf of a payer. + +```graphql +type GraphTallySignerAuthorization @entity { + id: ID! + + # References + "Signer" + signer: GraphTallySigner! + "Payer address" + payer: Bytes! + + # State + "Current authorization status" + authorized: Boolean! + "Timestamp when thawing completes (0 if not thawing)" + thawEndTimestamp: BigInt! +} +``` + +### GraphTallyRAVCollection + +Record of a RAV redemption event. + +```graphql +type GraphTallyRAVCollection @entity { + id: ID! + + # References + "Payer address" + payer: Bytes! + "Service provider" + serviceProvider: ServiceProvider! + "Data service" + dataService: DataService! + + # Details + "Collection ID" + collectionId: Bytes! + "Timestamp in nanoseconds" + timestampNs: BigInt! + "Cumulative payment amount" + valueAggregate: BigInt! + "Tokens collected in this redemption" + tokensCollected: BigInt! +} +``` + +## Event Handlers + +### HorizonStaking + +| Event | Handler Action | +|-------|----------------| +| `ProvisionCreated` | Create `Provision`, `DelegationPool`, update `ServiceProvider` | +| `ProvisionIncreased` | Update `Provision.tokens`, `ServiceProvider.tokensProvisioned` | +| `ProvisionThawed` | Update `Provision.tokensThawing`, `sharesThawing` | +| `TokensDeprovisioned` | Update `Provision`, `ServiceProvider` | +| `ProvisionParametersStaged` | Update pending parameters on `Provision` | +| `ProvisionParametersSet` | Update active parameters on `Provision` | +| `ProvisionSlashed` | Update `Provision.tokens`, `GraphNetwork.totalSlashed` | +| `DelegationSlashed` | Update `DelegationPool.tokens`, `GraphNetwork.totalSlashed` | +| `TokensDelegated` | Create/update `Delegation`, update `DelegationPool` | +| `TokensUndelegated` | Update `Delegation.shares`, `DelegationPool` thawing fields | +| `DelegatedTokensWithdrawn` | Update `Delegation`, `DelegationPool` | +| `TokensToDelegationPoolAdded` | Update `DelegationPool.tokens` | +| `DelegationFeeCutSet` | Update fee cut fields on `Provision` | +| `ThawRequestCreated` | Create `ThawRequest` | +| `ThawRequestFulfilled` | Update `ThawRequest.fulfilled` | +| `ThawRequestsFulfilled` | Batch update `ThawRequest` entities | +| `OperatorSet` | Create/update `Operator` | +| `HorizonStakeWithdrawn` | Update `ServiceProvider.tokensStaked` | + +### GraphPayments + +| Event | Handler Action | +|-------|----------------| +| `GraphPaymentCollected` | Create `PaymentCollection` | + +### PaymentsEscrow + +| Event | Handler Action | +|-------|----------------| +| `Deposit` | Create/update `EscrowAccount.balance` | +| `Thaw` | Update `EscrowAccount` thawing fields | +| `CancelThaw` | Reset `EscrowAccount` thawing fields | +| `Withdraw` | Update `EscrowAccount.balance` | +| `EscrowCollected` | Update `EscrowAccount.balance` | + +## Example Queries + +### Service Provider Overview + +```graphql +query ServiceProviderOverview($id: ID!) { + serviceProvider(id: $id) { + tokensStaked + tokensProvisioned + tokensIdle + provisions { + dataService { id } + tokens + tokensThawing + maxVerifierCut + thawingPeriod + delegationPool { + tokens + shares + } + } + } +} +``` + +### Delegations for a Delegator + +```graphql +query DelegatorPositions($delegator: Bytes!) { + delegations(where: { delegator: $delegator, shares_gt: 0 }) { + serviceProvider { id } + dataService { id } + shares + pool { + tokens + shares + } + } +} +``` + +### Pending Thaw Requests + +```graphql +query PendingThaws($owner: Bytes!) { + thawRequests( + where: { owner: $owner, fulfilled: false, valid: true } + orderBy: thawingUntil + ) { + id + type + serviceProvider { id } + dataService { id } + shares + thawingUntil + } +} +``` + +### Protocol Stats + +```graphql +query ProtocolStats { + graphNetwork(id: "1") { + totalStake + totalDelegation + totalProvisions + totalSlashed + delegationSlashingEnabled + } +} +``` From 5ed9b76f88337ab3b7839cf264e8c2b7d0117910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Mon, 22 Dec 2025 18:06:28 -0300 Subject: [PATCH 2/8] fix: add missing events and stuff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- specs/NETWORK_SUBGRAPH_SPEC.md | 131 +++++++++++---------------------- 1 file changed, 42 insertions(+), 89 deletions(-) diff --git a/specs/NETWORK_SUBGRAPH_SPEC.md b/specs/NETWORK_SUBGRAPH_SPEC.md index 5219ecf..9053069 100644 --- a/specs/NETWORK_SUBGRAPH_SPEC.md +++ b/specs/NETWORK_SUBGRAPH_SPEC.md @@ -609,110 +609,63 @@ type GraphTallyRAVCollection @entity { | Event | Handler Action | |-------|----------------| -| `ProvisionCreated` | Create `Provision`, `DelegationPool`, update `ServiceProvider` | -| `ProvisionIncreased` | Update `Provision.tokens`, `ServiceProvider.tokensProvisioned` | -| `ProvisionThawed` | Update `Provision.tokensThawing`, `sharesThawing` | -| `TokensDeprovisioned` | Update `Provision`, `ServiceProvider` | +| `HorizonStakeDeposited` | Update `ServiceProvider.tokensStaked`, `GraphNetwork.tokensStaked` | +| `HorizonStakeLocked` | Update `ServiceProvider` | +| `HorizonStakeWithdrawn` | Update `ServiceProvider.tokensStaked`, `GraphNetwork.tokensStaked` | +| `ProvisionCreated` | Create `Provision`, `DelegationPool`, update `ServiceProvider`, `DataService`, `GraphNetwork` | +| `ProvisionIncreased` | Update `Provision`, `ServiceProvider`, `DataService`, `GraphNetwork` | +| `ProvisionThawed` | Update `Provision` thawing fields | +| `TokensDeprovisioned` | Update `Provision`, `ServiceProvider`, `DataService`, `GraphNetwork` | | `ProvisionParametersStaged` | Update pending parameters on `Provision` | | `ProvisionParametersSet` | Update active parameters on `Provision` | -| `ProvisionSlashed` | Update `Provision.tokens`, `GraphNetwork.totalSlashed` | -| `DelegationSlashed` | Update `DelegationPool.tokens`, `GraphNetwork.totalSlashed` | -| `TokensDelegated` | Create/update `Delegation`, update `DelegationPool` | -| `TokensUndelegated` | Update `Delegation.shares`, `DelegationPool` thawing fields | -| `DelegatedTokensWithdrawn` | Update `Delegation`, `DelegationPool` | +| `ProvisionSlashed` | Update `Provision`, `ServiceProvider`, `DataService`, `GraphNetwork` slashing fields | +| `DelegationSlashed` | Update `DelegationPool`, `Provision`, `ServiceProvider`, `DataService`, `GraphNetwork` slashing fields | +| `DelegationSlashingSkipped` | No action required | +| `VerifierTokensSent` | No action required (informational) | +| `TokensDelegated` | Create/update `Delegator`, `Delegation`, `DelegationPool`, `Provision`, `ServiceProvider`, `DataService`, `GraphNetwork` | +| `TokensUndelegated` | Update `Delegation`, `Delegator`, `DelegationPool` thawing fields | +| `DelegatedTokensWithdrawn` | Update `Delegation`, `Delegator`, `DelegationPool`, `Provision`, `ServiceProvider`, `DataService`, `GraphNetwork` | | `TokensToDelegationPoolAdded` | Update `DelegationPool.tokens` | | `DelegationFeeCutSet` | Update fee cut fields on `Provision` | -| `ThawRequestCreated` | Create `ThawRequest` | -| `ThawRequestFulfilled` | Update `ThawRequest.fulfilled` | -| `ThawRequestsFulfilled` | Batch update `ThawRequest` entities | -| `OperatorSet` | Create/update `Operator` | -| `HorizonStakeWithdrawn` | Update `ServiceProvider.tokensStaked` | +| `ThawRequestCreated` | Create `ThawRequest`, update counts on related entities | +| `ThawRequestFulfilled` | Update `ThawRequest.fulfilled`, update counts | +| `ThawRequestsFulfilled` | Batch update `ThawRequest` entities, update counts | +| `OperatorSet` | Create/update `Operator`, `OperatorAuthorization` | +| `MaxThawingPeriodSet` | Update `GraphNetwork.maxThawingPeriod` | +| `DelegationSlashingEnabled` | Update `GraphNetwork.delegationSlashingEnabled` | ### GraphPayments | Event | Handler Action | |-------|----------------| -| `GraphPaymentCollected` | Create `PaymentCollection` | +| `GraphPaymentCollected` | Create `PaymentCollection`, update `GraphNetwork`, `ServiceProvider`, `DataService`, `Provision`, `DelegationPool` token aggregates | ### PaymentsEscrow | Event | Handler Action | |-------|----------------| -| `Deposit` | Create/update `EscrowAccount.balance` | -| `Thaw` | Update `EscrowAccount` thawing fields | -| `CancelThaw` | Reset `EscrowAccount` thawing fields | -| `Withdraw` | Update `EscrowAccount.balance` | -| `EscrowCollected` | Update `EscrowAccount.balance` | +| `Deposit` | Create/update `EscrowAccount`, update `GraphNetwork.tokensEscrowed` | +| `Thaw` | Update `EscrowAccount` thawing fields, update `GraphNetwork.tokensThawingFromEscrow` | +| `CancelThaw` | Reset `EscrowAccount` thawing fields, update `GraphNetwork.tokensThawingFromEscrow` | +| `Withdraw` | Update `EscrowAccount.tokens`, update `GraphNetwork.tokensEscrowed` | +| `EscrowCollected` | Update `EscrowAccount.tokens`, update `GraphNetwork.tokensEscrowed` | -## Example Queries +### GraphTallyCollector -### Service Provider Overview - -```graphql -query ServiceProviderOverview($id: ID!) { - serviceProvider(id: $id) { - tokensStaked - tokensProvisioned - tokensIdle - provisions { - dataService { id } - tokens - tokensThawing - maxVerifierCut - thawingPeriod - delegationPool { - tokens - shares - } - } - } -} -``` - -### Delegations for a Delegator - -```graphql -query DelegatorPositions($delegator: Bytes!) { - delegations(where: { delegator: $delegator, shares_gt: 0 }) { - serviceProvider { id } - dataService { id } - shares - pool { - tokens - shares - } - } -} -``` - -### Pending Thaw Requests - -```graphql -query PendingThaws($owner: Bytes!) { - thawRequests( - where: { owner: $owner, fulfilled: false, valid: true } - orderBy: thawingUntil - ) { - id - type - serviceProvider { id } - dataService { id } - shares - thawingUntil - } -} -``` +| Event | Handler Action | +|-------|----------------| +| `SignerAuthorized` | Create/update `GraphTallySigner`, `GraphTallySignerAuthorization` | +| `SignerThawing` | Update `GraphTallySignerAuthorization.thawEndTimestamp` | +| `SignerRevoked` | Update `GraphTallySignerAuthorization.authorized` | +| `SignerThawCanceled` | Reset `GraphTallySignerAuthorization.thawEndTimestamp` | +| `PaymentCollected` | Create `GraphTallyRAVCollection`, update `GraphNetwork.countRAVsCollected` | +| `RAVCollected` | Update `GraphTallyRAVCollection` with RAV details | -### Protocol Stats +### DataService (ProvisionManager) -```graphql -query ProtocolStats { - graphNetwork(id: "1") { - totalStake - totalDelegation - totalProvisions - totalSlashed - delegationSlashingEnabled - } -} -``` +| Event | Handler Action | +|-------|----------------| +| `ProvisionTokensRangeSet` | Update `DataService.minProvisionTokens`, `DataService.maxProvisionTokens` | +| `DelegationRatioSet` | Update `DataService.delegationRatio` | +| `VerifierCutRangeSet` | Update `DataService.minVerifierCut`, `DataService.maxVerifierCut` | +| `ThawingPeriodRangeSet` | Update `DataService.minThawingPeriod`, `DataService.maxThawingPeriod` | From 685b42fec3075e71a943018e58270f78608dcf52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Tue, 23 Dec 2025 00:11:05 -0300 Subject: [PATCH 3/8] refactor: some changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- specs/NETWORK_SUBGRAPH_SPEC.md | 488 ++++++++++++++++++++++++--------- 1 file changed, 355 insertions(+), 133 deletions(-) diff --git a/specs/NETWORK_SUBGRAPH_SPEC.md b/specs/NETWORK_SUBGRAPH_SPEC.md index 9053069..730c1c0 100644 --- a/specs/NETWORK_SUBGRAPH_SPEC.md +++ b/specs/NETWORK_SUBGRAPH_SPEC.md @@ -33,6 +33,7 @@ Singleton entity for protocol-wide aggregates and parameters. ```graphql type GraphNetwork @entity { + "Singleton entity, always '1'" id: ID! # Counts @@ -44,6 +45,12 @@ type GraphNetwork @entity { countDataServices: Int! "Active provisions" countProvisions: Int! + "Active payers" + countPayers: Int! + "Active collectors" + countCollectors: Int! + "Active escrow accounts" + countEscrowAccounts: Int! # Stake aggregates "Total tokens staked by service providers" @@ -66,16 +73,16 @@ type GraphNetwork @entity { tokensSlashedFromDelegationPools: BigInt! # Payment collection aggregates - "Total tokens collected" + "Total tokens collected in the protocol" tokensCollected: BigInt! - "Total tokens burnt as protocol tax" - tokensCollectedByProtocolTax: BigInt! - "Total tokens collected by service providers" - tokensCollectedByServiceProviders: BigInt! - "Total tokens collected by delegators" - tokensCollectedByDelegators: BigInt! - "Total tokens collected by data services" - tokensCollectedByDataServices: BigInt! + "Tokens burned as protocol tax" + tokensDistributedAsProtocolTax: BigInt! + "Tokens distributed to service providers" + tokensDistributedToServiceProviders: BigInt! + "Tokens distributed to delegation pools" + tokensDistributedToDelegationPools: BigInt! + "Tokens distributed to data services" + tokensDistributedToDataServices: BigInt! # Payment escrow aggregates "Total tokens held in escrow" @@ -94,7 +101,7 @@ type GraphNetwork @entity { protocolPaymentCut: BigInt! "Withdrawal thawing period for payments escrow (seconds)" escrowThawingPeriod: BigInt! - "Signer revocation thawing period for payments escrow(seconds)" + "Signer revocation thawing period for payments escrow (seconds)" revokeSignerThawingPeriod: BigInt! } ``` @@ -105,23 +112,28 @@ An account that stakes GRT to provide services. ```graphql type ServiceProvider @entity { + "Service provider address" id: ID! # Relationships + "Provisions created by this service provider" provisions: [Provision!]! @derivedFrom(field: "serviceProvider") + "Operator authorizations for this service provider" operatorAuthorizations: [OperatorAuthorization!]! @derivedFrom(field: "serviceProvider") + "Escrow accounts where this service provider is the receiver" + escrowAccounts: [EscrowAccount!]! @derivedFrom(field: "serviceProvider") # Counts "Active provisions" countProvisions: Int! "Active delegators" countDelegators: Int! - "Active operators" - countOperators: Int! - "Pending thaw requests for the service provider" + "Pending thaw requests" countThawRequests: Int! - "Amount of times slashed" + "Slash events" countSlashEvents: Int! + "Active escrow accounts" + countEscrowAccounts: Int! # Stake "Tokens staked by the service provider" @@ -147,9 +159,31 @@ type ServiceProvider @entity { "Tokens slashed from delegation pools" tokensSlashedFromDelegationPools: BigInt! - # Payments - "Total tokens collected" + # Payment collection + "Total tokens collected by the service provider" tokensCollected: BigInt! + "Tokens burned as protocol tax" + tokensDistributedAsProtocolTax: BigInt! + "Tokens kept by the service provider" + tokensDistributedToServiceProvider: BigInt! + "Tokens distributed to delegation pools" + tokensDistributedToDelegationPools: BigInt! + "Tokens distributed to data services" + tokensDistributedToDataServices: BigInt! + + # Escrow + "Total tokens in escrow for this service provider" + tokensEscrowed: BigInt! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } ``` @@ -159,9 +193,11 @@ Represents a data service (verifier) in the protocol. ```graphql type DataService @entity { + "Data service (verifier) contract address" id: ID! # Relationships + "Provisions for this data service" provisions: [Provision!]! @derivedFrom(field: "dataService") # Counts @@ -173,7 +209,7 @@ type DataService @entity { countThawRequestsProvision: Int! "Pending delegation thaw requests" countThawRequestsDelegation: Int! - "Amount of times slashed" + "Slash events" countSlashEvents: Int! # Tokens @@ -187,8 +223,18 @@ type DataService @entity { tokensThawingFromDelegationPools: BigInt! "Total tokens slashed" tokensSlashed: BigInt! - "Total tokens collected" + + # Payment collection + "Total tokens collected by service providers for this data service" tokensCollected: BigInt! + "Tokens burned as protocol tax" + tokensDistributedAsProtocolTax: BigInt! + "Tokens distributed to service providers" + tokensDistributedToServiceProviders: BigInt! + "Tokens distributed to delegation pools" + tokensDistributedToDelegationPools: BigInt! + "Tokens kept by the data service" + tokensDistributedToDataService: BigInt! # Parameters "Max delegation multiplier on service provider stake" @@ -205,6 +251,16 @@ type DataService @entity { minThawingPeriod: BigInt! "Maximum thawing period (seconds)" maxThawingPeriod: BigInt! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } ``` @@ -214,6 +270,7 @@ Stake allocated by a service provider to a specific data service. ```graphql type Provision @entity { + "Concatenation of service provider address and data service address (serviceProvider-dataService)" id: ID! # References @@ -221,18 +278,19 @@ type Provision @entity { serviceProvider: ServiceProvider! "Data service" dataService: DataService! - "Delegation pool" - delegationPool: DelegationPool! # Relationships - thawRequests: [ThawRequest!]! @derivedFrom(field: "provision") + "Delegation pool for this provision" + delegationPool: DelegationPool! @derivedFrom(field: "provision") + "Thaw requests for this provision" + thawRequests: [ProvisionThawRequest!]! @derivedFrom(field: "provision") # Counts "Active delegators" countDelegators: Int! "Pending thaw requests" countThawRequests: Int! - "Amount of times slashed" + "Slash events" countSlashEvents: Int! # Tokens @@ -246,8 +304,18 @@ type Provision @entity { tokensDelegated: BigInt! "Total tokens slashed" tokensSlashed: BigInt! - "Total tokens collected" + + # Payment collection + "Total tokens collected by this provision" tokensCollected: BigInt! + "Tokens burned as protocol tax" + tokensDistributedAsProtocolTax: BigInt! + "Tokens distributed to service provider" + tokensDistributedToServiceProvider: BigInt! + "Tokens distributed to delegation pool" + tokensDistributedToDelegationPool: BigInt! + "Tokens distributed to data service" + tokensDistributedToDataService: BigInt! # Parameters "Max verifier reward on slash (PPM)" @@ -274,6 +342,59 @@ type Provision @entity { # State "Thawing nonce - incremented on slash to invalidate pending thaw requests" thawingNonce: BigInt! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! +} +``` + +### ProvisionThawRequest + +Pending deprovision request to remove stake from a provision. + +```graphql +type ProvisionThawRequest @entity { + "Thaw request ID from contract event" + id: ID! + + # References + "Provision being thawed" + provision: Provision! + "Service provider" + serviceProvider: ServiceProvider! + "Data service" + dataService: DataService! + + # State + "Shares being thawed" + shares: BigInt! + "Timestamp when thaw completes" + thawingUntil: BigInt! + "Thawing nonce at time of creation" + thawingNonce: BigInt! + + # Status + "False if invalidated by slashing" + valid: Boolean! + "True when tokens have been withdrawn" + fulfilled: Boolean! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } ``` @@ -283,21 +404,29 @@ Aggregate delegation pool for a provision. ```graphql type DelegationPool @entity { + "Same as Provision ID (serviceProvider-dataService)" id: ID! # References - "Provision" + "Provision this pool belongs to" provision: Provision! + "Service provider" + serviceProvider: ServiceProvider! + "Data service" + dataService: DataService! # Relationships + "Delegations in this pool" delegations: [Delegation!]! @derivedFrom(field: "pool") + "Thaw requests for delegations in this pool" + thawRequests: [DelegationThawRequest!]! @derivedFrom(field: "pool") # Counts "Active delegators" countDelegators: Int! "Pending thaw requests" countThawRequests: Int! - "Amount of times slashed" + "Slash events" countSlashEvents: Int! # Tokens @@ -317,6 +446,16 @@ type DelegationPool @entity { # State "Thawing nonce - incremented on slash to invalidate pending thaw requests" thawingNonce: BigInt! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } ``` @@ -326,9 +465,11 @@ An account that delegates tokens to service providers. ```graphql type Delegator @entity { + "Delegator address" id: ID! # Relationships + "Delegations by this delegator" delegations: [Delegation!]! @derivedFrom(field: "delegator") # Counts @@ -342,8 +483,16 @@ type Delegator @entity { tokensDelegated: BigInt! "Tokens currently thawing" tokensThawing: BigInt! - "Total tokens collected" - tokensCollected: BigInt! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } ``` @@ -353,6 +502,7 @@ Individual delegator's stake in a delegation pool. ```graphql type Delegation @entity { + "Concatenation of delegator, service provider, and data service addresses (delegator-serviceProvider-dataService)" id: ID! # References @@ -366,66 +516,79 @@ type Delegation @entity { dataService: DataService! # Relationships - thawRequests: [ThawRequest!]! @derivedFrom(field: "delegation") + "Thaw requests for this delegation" + thawRequests: [DelegationThawRequest!]! @derivedFrom(field: "delegation") # Counts "Pending thaw requests" countThawRequests: Int! # Tokens - "Tokens delegated - note that this does not represent current delegation valuation" + "Tokens delegated (input amount, not current valuation)" tokensDelegated: BigInt! "Delegator's shares in pool" shares: BigInt! "Tokens currently thawing" tokensThawing: BigInt! - "Tokens collected by this delegator" - tokensCollected: BigInt! + "Shares currently thawing" + sharesThawing: BigInt! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } ``` -### ThawRequest +### DelegationThawRequest -Pending deprovision or undelegate request. +Pending undelegation request to remove stake from a delegation. ```graphql -enum ThawRequestType { - PROVISION - DELEGATION -} - -type ThawRequest @entity { +type DelegationThawRequest @entity { + "Thaw request ID from contract event" id: ID! - # Type - "Thaw request type" - type: ThawRequestType! - # References + "Delegation being thawed" + delegation: Delegation! + "Delegator" + delegator: Delegator! + "Delegation pool" + pool: DelegationPool! "Service provider" serviceProvider: ServiceProvider! "Data service" dataService: DataService! - "Owner address - service provider (provision) or delegator (delegation)" - owner: Bytes! - "Provision - for provision thaws" - provision: Provision - "Delegation - for delegation thaws" - delegation: Delegation # State "Shares being thawed" shares: BigInt! "Timestamp when thaw completes" thawingUntil: BigInt! - "Thawing nonce" + "Thawing nonce at time of creation" thawingNonce: BigInt! # Status "False if invalidated by slashing" valid: Boolean! - "True when tokens withdrawn" + "True when tokens have been withdrawn" fulfilled: Boolean! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } ``` @@ -435,14 +598,26 @@ An operator account that can be authorized to act on behalf of service providers ```graphql type Operator @entity { + "Operator address" id: ID! # Relationships + "Authorizations granted to this operator" authorizations: [OperatorAuthorization!]! @derivedFrom(field: "operator") # Counts "Active authorizations" countAuthorizations: Int! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } ``` @@ -452,6 +627,7 @@ Authorization for an operator to act on behalf of a service provider on a specif ```graphql type OperatorAuthorization @entity { + "Concatenation of operator, service provider, and data service addresses (operator-serviceProvider-dataService)" id: ID! # References @@ -465,74 +641,127 @@ type OperatorAuthorization @entity { # State "Current authorization status" allowed: Boolean! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } ``` -### EscrowAccount +### Payer -Escrow balance for a payer-collector-receiver tuple. +An account that pays for services via GraphPayments. ```graphql -type EscrowAccount @entity { +type Payer @entity { + "Payer address" id: ID! - # References - "Payer address" - payer: Bytes! - "Collector address" - collector: Bytes! - "Receiver address" - receiver: Bytes! + # Relationships + "Escrow accounts funded by this payer" + escrowAccounts: [EscrowAccount!]! @derivedFrom(field: "payer") + + # Counts + "Active escrow accounts" + countEscrowAccounts: Int! # Tokens - "Available tokens" - tokens: BigInt! - "Tokens currently thawing" + "Total tokens in escrow" + tokensEscrowed: BigInt! + "Total tokens thawing" tokensThawing: BigInt! - "Timestamp when thawing completes (0 if not thawing)" - thawEndTimestamp: BigInt! + "Total tokens collected from escrow" + tokensCollected: BigInt! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } ``` -### PaymentCollection +### Collector -Record of a payment distribution event. +A contract that facilitates payment collection through GraphPayments (e.g., GraphTallyCollector). ```graphql -enum PaymentType { - QUERY_FEE - INDEXING_FEE - INDEXING_REWARDS +type Collector @entity { + "Collector contract address" + id: ID! + + # Relationships + "Escrow accounts using this collector" + escrowAccounts: [EscrowAccount!]! @derivedFrom(field: "collector") + + # Counts + "Active escrow accounts" + countEscrowAccounts: Int! + + # Tokens + "Total tokens in escrow" + tokensEscrowed: BigInt! + "Total tokens thawing" + tokensThawing: BigInt! + "Total tokens collected" + tokensCollected: BigInt! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } +``` + +### EscrowAccount + +Escrow balance for a payer-collector-serviceProvider tuple in GraphPayments. -type PaymentCollection @entity { +```graphql +type EscrowAccount @entity { + "Concatenation of payer, collector, and service provider addresses (payer-collector-serviceProvider)" id: ID! # References - "Payer address" - payer: Bytes! - "Receiver address" - receiver: Bytes! - "Data service" - dataService: DataService! + "Payer that deposited funds into the escrow account" + payer: Payer! + "Collector allowed to withdraw funds from the account" + collector: Collector! + "Service provider that can collect funds from the account" + serviceProvider: ServiceProvider! - # Payment details - "Payment type" - paymentType: PaymentType! - "Total payment amount" + # Tokens + "Available tokens" tokens: BigInt! + "Tokens currently thawing" + tokensThawing: BigInt! + "Timestamp when thawing completes (0 if not thawing)" + thawEndTimestamp: BigInt! - # Distribution - "Protocol tax (burned)" - tokensProtocol: BigInt! - "Data service cut" - tokensDataService: BigInt! - "Delegation pool rewards" - tokensDelegationPool: BigInt! - "Receiver's share" - tokensReceiver: BigInt! - "Where receiver tokens went" - receiverDestination: Bytes! + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } ``` @@ -542,14 +771,26 @@ An account authorized to sign RAVs on behalf of payers. ```graphql type GraphTallySigner @entity { + "Signer address" id: ID! # Relationships + "Authorizations granted to this signer" authorizations: [GraphTallySignerAuthorization!]! @derivedFrom(field: "signer") # Counts "Active authorizations" countAuthorizations: Int! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } ``` @@ -559,47 +800,30 @@ Authorization for a signer to sign RAVs on behalf of a payer. ```graphql type GraphTallySignerAuthorization @entity { + "Concatenation of signer and payer addresses (signer-payer)" id: ID! # References "Signer" signer: GraphTallySigner! "Payer address" - payer: Bytes! + payer: Payer! # State "Current authorization status" authorized: Boolean! "Timestamp when thawing completes (0 if not thawing)" thawEndTimestamp: BigInt! -} -``` - -### GraphTallyRAVCollection - -Record of a RAV redemption event. -```graphql -type GraphTallyRAVCollection @entity { - id: ID! - - # References - "Payer address" - payer: Bytes! - "Service provider" - serviceProvider: ServiceProvider! - "Data service" - dataService: DataService! - - # Details - "Collection ID" - collectionId: Bytes! - "Timestamp in nanoseconds" - timestampNs: BigInt! - "Cumulative payment amount" - valueAggregate: BigInt! - "Tokens collected in this redemption" - tokensCollected: BigInt! + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! } ``` @@ -627,9 +851,9 @@ type GraphTallyRAVCollection @entity { | `DelegatedTokensWithdrawn` | Update `Delegation`, `Delegator`, `DelegationPool`, `Provision`, `ServiceProvider`, `DataService`, `GraphNetwork` | | `TokensToDelegationPoolAdded` | Update `DelegationPool.tokens` | | `DelegationFeeCutSet` | Update fee cut fields on `Provision` | -| `ThawRequestCreated` | Create `ThawRequest`, update counts on related entities | -| `ThawRequestFulfilled` | Update `ThawRequest.fulfilled`, update counts | -| `ThawRequestsFulfilled` | Batch update `ThawRequest` entities, update counts | +| `ThawRequestCreated` | Create `ProvisionThawRequest` or `DelegationThawRequest`, update counts on related entities | +| `ThawRequestFulfilled` | Update thaw request's `fulfilled` field, update counts | +| `ThawRequestsFulfilled` | Batch update thaw request entities, update counts | | `OperatorSet` | Create/update `Operator`, `OperatorAuthorization` | | `MaxThawingPeriodSet` | Update `GraphNetwork.maxThawingPeriod` | | `DelegationSlashingEnabled` | Update `GraphNetwork.delegationSlashingEnabled` | @@ -638,17 +862,17 @@ type GraphTallyRAVCollection @entity { | Event | Handler Action | |-------|----------------| -| `GraphPaymentCollected` | Create `PaymentCollection`, update `GraphNetwork`, `ServiceProvider`, `DataService`, `Provision`, `DelegationPool` token aggregates | +| `GraphPaymentCollected` | Update `GraphNetwork`, `ServiceProvider`, `DataService`, `Provision`, `DelegationPool` payment collection aggregates | ### PaymentsEscrow | Event | Handler Action | |-------|----------------| -| `Deposit` | Create/update `EscrowAccount`, update `GraphNetwork.tokensEscrowed` | -| `Thaw` | Update `EscrowAccount` thawing fields, update `GraphNetwork.tokensThawingFromEscrow` | -| `CancelThaw` | Reset `EscrowAccount` thawing fields, update `GraphNetwork.tokensThawingFromEscrow` | -| `Withdraw` | Update `EscrowAccount.tokens`, update `GraphNetwork.tokensEscrowed` | -| `EscrowCollected` | Update `EscrowAccount.tokens`, update `GraphNetwork.tokensEscrowed` | +| `Deposit` | Create/update `Payer`, `Collector`, `EscrowAccount`, update `GraphNetwork.tokensEscrowed` | +| `Thaw` | Update `EscrowAccount`, `Payer`, `Collector` thawing fields, update `GraphNetwork.tokensThawingFromEscrow` | +| `CancelThaw` | Reset `EscrowAccount`, `Payer`, `Collector` thawing fields, update `GraphNetwork.tokensThawingFromEscrow` | +| `Withdraw` | Update `EscrowAccount`, `Payer`, `Collector`, `GraphNetwork.tokensEscrowed` | +| `EscrowCollected` | Update `EscrowAccount`, `Payer`, `Collector`, `GraphNetwork.tokensEscrowed` | ### GraphTallyCollector @@ -658,8 +882,6 @@ type GraphTallyRAVCollection @entity { | `SignerThawing` | Update `GraphTallySignerAuthorization.thawEndTimestamp` | | `SignerRevoked` | Update `GraphTallySignerAuthorization.authorized` | | `SignerThawCanceled` | Reset `GraphTallySignerAuthorization.thawEndTimestamp` | -| `PaymentCollected` | Create `GraphTallyRAVCollection`, update `GraphNetwork.countRAVsCollected` | -| `RAVCollected` | Update `GraphTallyRAVCollection` with RAV details | ### DataService (ProvisionManager) From afaf3380a2805d386a02a643fbcf383c675ec4f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Tue, 23 Dec 2025 00:42:05 -0300 Subject: [PATCH 4/8] fix: some more improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- specs/NETWORK_SUBGRAPH_SPEC.md | 68 +++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/specs/NETWORK_SUBGRAPH_SPEC.md b/specs/NETWORK_SUBGRAPH_SPEC.md index 730c1c0..bfa6f06 100644 --- a/specs/NETWORK_SUBGRAPH_SPEC.md +++ b/specs/NETWORK_SUBGRAPH_SPEC.md @@ -34,7 +34,7 @@ Singleton entity for protocol-wide aggregates and parameters. ```graphql type GraphNetwork @entity { "Singleton entity, always '1'" - id: ID! + id: Bytes! # Counts "Active service providers" @@ -113,11 +113,19 @@ An account that stakes GRT to provide services. ```graphql type ServiceProvider @entity { "Service provider address" - id: ID! + id: Bytes! # Relationships "Provisions created by this service provider" provisions: [Provision!]! @derivedFrom(field: "serviceProvider") + "Delegation pools for this service provider" + delegationPools: [DelegationPool!]! @derivedFrom(field: "serviceProvider") + "Delegations to this service provider" + delegations: [Delegation!]! @derivedFrom(field: "serviceProvider") + "Provision thaw requests for this service provider" + provisionThawRequests: [ProvisionThawRequest!]! @derivedFrom(field: "serviceProvider") + "Delegation thaw requests for this service provider" + delegationThawRequests: [DelegationThawRequest!]! @derivedFrom(field: "serviceProvider") "Operator authorizations for this service provider" operatorAuthorizations: [OperatorAuthorization!]! @derivedFrom(field: "serviceProvider") "Escrow accounts where this service provider is the receiver" @@ -194,11 +202,21 @@ Represents a data service (verifier) in the protocol. ```graphql type DataService @entity { "Data service (verifier) contract address" - id: ID! + id: Bytes! # Relationships "Provisions for this data service" provisions: [Provision!]! @derivedFrom(field: "dataService") + "Delegation pools for this data service" + delegationPools: [DelegationPool!]! @derivedFrom(field: "dataService") + "Delegations for this data service" + delegations: [Delegation!]! @derivedFrom(field: "dataService") + "Provision thaw requests for this data service" + provisionThawRequests: [ProvisionThawRequest!]! @derivedFrom(field: "dataService") + "Delegation thaw requests for this data service" + delegationThawRequests: [DelegationThawRequest!]! @derivedFrom(field: "dataService") + "Operator authorizations for this data service" + operatorAuthorizations: [OperatorAuthorization!]! @derivedFrom(field: "dataService") # Counts "Active service providers" @@ -270,8 +288,8 @@ Stake allocated by a service provider to a specific data service. ```graphql type Provision @entity { - "Concatenation of service provider address and data service address (serviceProvider-dataService)" - id: ID! + "Concatenation of service provider and data service addresses" + id: Bytes! # References "Service provider" @@ -362,7 +380,7 @@ Pending deprovision request to remove stake from a provision. ```graphql type ProvisionThawRequest @entity { "Thaw request ID from contract event" - id: ID! + id: Bytes! # References "Provision being thawed" @@ -404,8 +422,8 @@ Aggregate delegation pool for a provision. ```graphql type DelegationPool @entity { - "Same as Provision ID (serviceProvider-dataService)" - id: ID! + "Same as Provision ID (serviceProvider.concat(dataService))" + id: Bytes! # References "Provision this pool belongs to" @@ -466,11 +484,13 @@ An account that delegates tokens to service providers. ```graphql type Delegator @entity { "Delegator address" - id: ID! + id: Bytes! # Relationships "Delegations by this delegator" delegations: [Delegation!]! @derivedFrom(field: "delegator") + "Thaw requests by this delegator" + thawRequests: [DelegationThawRequest!]! @derivedFrom(field: "delegator") # Counts "Active delegations" @@ -502,8 +522,8 @@ Individual delegator's stake in a delegation pool. ```graphql type Delegation @entity { - "Concatenation of delegator, service provider, and data service addresses (delegator-serviceProvider-dataService)" - id: ID! + "Concatenation of delegator, service provider, and data service addresses" + id: Bytes! # References "Delegator" @@ -552,7 +572,7 @@ Pending undelegation request to remove stake from a delegation. ```graphql type DelegationThawRequest @entity { "Thaw request ID from contract event" - id: ID! + id: Bytes! # References "Delegation being thawed" @@ -599,7 +619,7 @@ An operator account that can be authorized to act on behalf of service providers ```graphql type Operator @entity { "Operator address" - id: ID! + id: Bytes! # Relationships "Authorizations granted to this operator" @@ -627,8 +647,8 @@ Authorization for an operator to act on behalf of a service provider on a specif ```graphql type OperatorAuthorization @entity { - "Concatenation of operator, service provider, and data service addresses (operator-serviceProvider-dataService)" - id: ID! + "Concatenation of operator, service provider, and data service addresses" + id: Bytes! # References "Operator" @@ -661,15 +681,19 @@ An account that pays for services via GraphPayments. ```graphql type Payer @entity { "Payer address" - id: ID! + id: Bytes! # Relationships "Escrow accounts funded by this payer" escrowAccounts: [EscrowAccount!]! @derivedFrom(field: "payer") + "GraphTally signer authorizations for this payer" + signerAuthorizations: [GraphTallySignerAuthorization!]! @derivedFrom(field: "payer") # Counts "Active escrow accounts" countEscrowAccounts: Int! + "Active signer authorizations" + countSignerAuthorizations: Int! # Tokens "Total tokens in escrow" @@ -698,7 +722,7 @@ A contract that facilitates payment collection through GraphPayments (e.g., Grap ```graphql type Collector @entity { "Collector contract address" - id: ID! + id: Bytes! # Relationships "Escrow accounts using this collector" @@ -734,8 +758,8 @@ Escrow balance for a payer-collector-serviceProvider tuple in GraphPayments. ```graphql type EscrowAccount @entity { - "Concatenation of payer, collector, and service provider addresses (payer-collector-serviceProvider)" - id: ID! + "Concatenation of payer, collector, and service provider addresses" + id: Bytes! # References "Payer that deposited funds into the escrow account" @@ -772,7 +796,7 @@ An account authorized to sign RAVs on behalf of payers. ```graphql type GraphTallySigner @entity { "Signer address" - id: ID! + id: Bytes! # Relationships "Authorizations granted to this signer" @@ -800,8 +824,8 @@ Authorization for a signer to sign RAVs on behalf of a payer. ```graphql type GraphTallySignerAuthorization @entity { - "Concatenation of signer and payer addresses (signer-payer)" - id: ID! + "Concatenation of signer and payer addresses" + id: Bytes! # References "Signer" From beab10497e98c9694ea8e204e6aaf905cfcf304a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Wed, 22 Apr 2026 17:22:27 -0300 Subject: [PATCH 5/8] spec: updated network subgraph spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- specs/NETWORK_SUBGRAPH_SPEC.md | 219 ++++++++++++++++----------------- 1 file changed, 106 insertions(+), 113 deletions(-) diff --git a/specs/NETWORK_SUBGRAPH_SPEC.md b/specs/NETWORK_SUBGRAPH_SPEC.md index bfa6f06..60cd7ab 100644 --- a/specs/NETWORK_SUBGRAPH_SPEC.md +++ b/specs/NETWORK_SUBGRAPH_SPEC.md @@ -2,6 +2,8 @@ A subgraph for indexing The Graph protocol's core staking and payments infrastructure. +This is an **aggregate state subgraph** - it tracks current state (balances, counts, parameters) rather than historical events. Entities are updated in place as events occur. Historical event tracking (e.g., individual stake deposits, slash events over time) is out of scope for the time being. + ## Overview This subgraph indexes the Horizon protocol contracts to provide queryable data about: @@ -9,10 +11,11 @@ This subgraph indexes the Horizon protocol contracts to provide queryable data a - Delegations and delegation pools - Thaw requests (deprovisioning and undelegating) - Payment collections and escrow -- Payment collectors: GraphTally - Operators -The subgraph is **data-service agnostic** - it captures the concept of data services (verifiers) but contains nothing specific to any particular data service. Each data service can create their own subgraph for service-specific data. +The subgraph is **data-service** and **collector** agnostic: +- Data services are discovered dynamically via staking events, but no data-service-specific parameters are tracked. Each data service can create their own subgraph for service-specific data. +- Collectors are discovered via escrow events. Collector-specific logic (e.g., signer authorizations) should be handled by collector-specific subgraphs (To be determined in the future if this will be included in this subgraph). ## Data Sources @@ -23,7 +26,6 @@ The subgraph is **data-service agnostic** - it captures the concept of data serv | `HorizonStaking` | Core staking, provisions, delegations, slashing | | `GraphPayments` | Payment distribution | | `PaymentsEscrow` | Escrow account management | -| `GraphTallyCollector` | RAV-based payment collection and signer authorization | ## Entities @@ -97,12 +99,12 @@ type GraphNetwork @entity { delegationSlashingEnabled: Boolean! # Protocol parameters (immutable) + # Note: These are constructor parameters with no setter events. + # Initialize via contract calls on subgraph deployment (e.g., in a block handler at start block). "Protocol tax on payments collected (PPM)" protocolPaymentCut: BigInt! "Withdrawal thawing period for payments escrow (seconds)" escrowThawingPeriod: BigInt! - "Signer revocation thawing period for payments escrow (seconds)" - revokeSignerThawingPeriod: BigInt! } ``` @@ -254,22 +256,6 @@ type DataService @entity { "Tokens kept by the data service" tokensDistributedToDataService: BigInt! - # Parameters - "Max delegation multiplier on service provider stake" - delegationRatio: BigInt! - "Minimum tokens required for a provision" - minProvisionTokens: BigInt! - "Maximum tokens allowed for a provision" - maxProvisionTokens: BigInt! - "Minimum verifier cut (PPM)" - minVerifierCut: BigInt! - "Maximum verifier cut (PPM)" - maxVerifierCut: BigInt! - "Minimum thawing period (seconds)" - minThawingPeriod: BigInt! - "Maximum thawing period (seconds)" - maxThawingPeriod: BigInt! - # Metadata "Block number when entity was created" createdAtBlock: BigInt! @@ -350,12 +336,8 @@ type Provision @entity { lastParametersStagedAt: BigInt! # Fee cuts - "Query fee cut for delegators (PPM)" - queryFeeCut: BigInt! - "Indexing fee cut for delegators (PPM)" - indexingFeeCut: BigInt! - "Indexing reward cut for delegators (PPM)" - indexingRewardCut: BigInt! + "Fee cuts for delegators by payment type" + feeCuts: [ProvisionFeeCut!]! @derivedFrom(field: "provision") # State "Thawing nonce - incremented on slash to invalidate pending thaw requests" @@ -373,13 +355,40 @@ type Provision @entity { } ``` +### ProvisionFeeCut + +Fee cut percentage for a specific payment type on a provision. + +```graphql +type ProvisionFeeCut @entity { + "Concatenation of provision ID and payment type" + id: Bytes! + + # References + "Provision this fee cut belongs to" + provision: Provision! + + # State + "Payment type (maps to PaymentTypes enum: 0 = QueryFee, 1 = IndexingFee, 2 = IndexingReward, ...)" + paymentType: Int! + "Fee cut percentage (PPM)" + feeCut: BigInt! + + # Metadata + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! +} +``` + ### ProvisionThawRequest Pending deprovision request to remove stake from a provision. ```graphql type ProvisionThawRequest @entity { - "Thaw request ID from contract event" + "Thaw request ID (bytes32) emitted by ThawRequestCreated event - globally unique" id: Bytes! # References @@ -397,6 +406,8 @@ type ProvisionThawRequest @entity { thawingUntil: BigInt! "Thawing nonce at time of creation" thawingNonce: BigInt! + "Tokens withdrawn (set on fulfillment, null while pending)" + tokensWithdrawn: BigInt # Status "False if invalidated by slashing" @@ -458,7 +469,7 @@ type DelegationPool @entity { sharesThawing: BigInt! "Total tokens slashed" tokensSlashed: BigInt! - "Total tokens collected by delegators" + "Tokens distributed to this pool from payment collections" tokensCollected: BigInt! # State @@ -571,7 +582,7 @@ Pending undelegation request to remove stake from a delegation. ```graphql type DelegationThawRequest @entity { - "Thaw request ID from contract event" + "Thaw request ID (bytes32) emitted by ThawRequestCreated event - globally unique" id: Bytes! # References @@ -593,6 +604,8 @@ type DelegationThawRequest @entity { thawingUntil: BigInt! "Thawing nonce at time of creation" thawingNonce: BigInt! + "Tokens withdrawn (set on fulfillment, null while pending)" + tokensWithdrawn: BigInt # Status "False if invalidated by slashing" @@ -686,14 +699,10 @@ type Payer @entity { # Relationships "Escrow accounts funded by this payer" escrowAccounts: [EscrowAccount!]! @derivedFrom(field: "payer") - "GraphTally signer authorizations for this payer" - signerAuthorizations: [GraphTallySignerAuthorization!]! @derivedFrom(field: "payer") # Counts "Active escrow accounts" countEscrowAccounts: Int! - "Active signer authorizations" - countSignerAuthorizations: Int! # Tokens "Total tokens in escrow" @@ -717,7 +726,7 @@ type Payer @entity { ### Collector -A contract that facilitates payment collection through GraphPayments (e.g., GraphTallyCollector). +A contract that facilitates payment collection through GraphPayments. ```graphql type Collector @entity { @@ -776,6 +785,8 @@ type EscrowAccount @entity { tokensThawing: BigInt! "Timestamp when thawing completes (0 if not thawing)" thawEndTimestamp: BigInt! + "Total tokens collected from this escrow account" + tokensCollected: BigInt! # Metadata "Block number when entity was created" @@ -789,67 +800,27 @@ type EscrowAccount @entity { } ``` -### GraphTallySigner - -An account authorized to sign RAVs on behalf of payers. - -```graphql -type GraphTallySigner @entity { - "Signer address" - id: Bytes! - - # Relationships - "Authorizations granted to this signer" - authorizations: [GraphTallySignerAuthorization!]! @derivedFrom(field: "signer") - - # Counts - "Active authorizations" - countAuthorizations: Int! - - # Metadata - "Block number when entity was created" - createdAtBlock: BigInt! - "Timestamp when entity was created" - createdAt: BigInt! - "Block number when entity was last updated" - updatedAtBlock: BigInt! - "Timestamp when entity was last updated" - updatedAt: BigInt! -} -``` - -### GraphTallySignerAuthorization - -Authorization for a signer to sign RAVs on behalf of a payer. - -```graphql -type GraphTallySignerAuthorization @entity { - "Concatenation of signer and payer addresses" - id: Bytes! - - # References - "Signer" - signer: GraphTallySigner! - "Payer address" - payer: Payer! - - # State - "Current authorization status" - authorized: Boolean! - "Timestamp when thawing completes (0 if not thawing)" - thawEndTimestamp: BigInt! - - # Metadata - "Block number when entity was created" - createdAtBlock: BigInt! - "Timestamp when entity was created" - createdAt: BigInt! - "Block number when entity was last updated" - updatedAtBlock: BigInt! - "Timestamp when entity was last updated" - updatedAt: BigInt! -} -``` +## Entity Lifecycle + +This section documents when each entity is created during indexing. + +| Entity | Created When | +|--------|--------------| +| `GraphNetwork` | First event handler that accesses it (singleton with fixed ID) | +| `ServiceProvider` | First `HorizonStakeDeposited` event for that address | +| `DataService` | First `ProvisionCreated` event that references it as verifier | +| `Provision` | `ProvisionCreated` event | +| `ProvisionFeeCut` | First `DelegationFeeCutSet` event for that provision + payment type | +| `ProvisionThawRequest` | `ThawRequestCreated` event (when type = Provision) | +| `DelegationPool` | `ProvisionCreated` event (created alongside Provision) | +| `Delegator` | First `TokensDelegated` event for that delegator address | +| `Delegation` | First `TokensDelegated` event for that delegator + service provider + data service | +| `DelegationThawRequest` | `ThawRequestCreated` event (when type = Delegation) | +| `Operator` | First `OperatorSet` event that references it as operator | +| `OperatorAuthorization` | First `OperatorSet` event for that operator + service provider + data service | +| `Payer` | First `Deposit` event for that payer address | +| `Collector` | First `Deposit` event that references it as collector | +| `EscrowAccount` | First `Deposit` event for that payer + collector + service provider | ## Event Handlers @@ -857,7 +828,7 @@ type GraphTallySignerAuthorization @entity { | Event | Handler Action | |-------|----------------| -| `HorizonStakeDeposited` | Update `ServiceProvider.tokensStaked`, `GraphNetwork.tokensStaked` | +| `HorizonStakeDeposited` | Create/update `ServiceProvider`, update `GraphNetwork.tokensStaked` | | `HorizonStakeLocked` | Update `ServiceProvider` | | `HorizonStakeWithdrawn` | Update `ServiceProvider.tokensStaked`, `GraphNetwork.tokensStaked` | | `ProvisionCreated` | Create `Provision`, `DelegationPool`, update `ServiceProvider`, `DataService`, `GraphNetwork` | @@ -874,7 +845,7 @@ type GraphTallySignerAuthorization @entity { | `TokensUndelegated` | Update `Delegation`, `Delegator`, `DelegationPool` thawing fields | | `DelegatedTokensWithdrawn` | Update `Delegation`, `Delegator`, `DelegationPool`, `Provision`, `ServiceProvider`, `DataService`, `GraphNetwork` | | `TokensToDelegationPoolAdded` | Update `DelegationPool.tokens` | -| `DelegationFeeCutSet` | Update fee cut fields on `Provision` | +| `DelegationFeeCutSet` | Create/update `ProvisionFeeCut` for the payment type | | `ThawRequestCreated` | Create `ProvisionThawRequest` or `DelegationThawRequest`, update counts on related entities | | `ThawRequestFulfilled` | Update thaw request's `fulfilled` field, update counts | | `ThawRequestsFulfilled` | Batch update thaw request entities, update counts | @@ -896,22 +867,44 @@ type GraphTallySignerAuthorization @entity { | `Thaw` | Update `EscrowAccount`, `Payer`, `Collector` thawing fields, update `GraphNetwork.tokensThawingFromEscrow` | | `CancelThaw` | Reset `EscrowAccount`, `Payer`, `Collector` thawing fields, update `GraphNetwork.tokensThawingFromEscrow` | | `Withdraw` | Update `EscrowAccount`, `Payer`, `Collector`, `GraphNetwork.tokensEscrowed` | -| `EscrowCollected` | Update `EscrowAccount`, `Payer`, `Collector`, `GraphNetwork.tokensEscrowed` | +| `EscrowCollected` | Update `EscrowAccount`, `Payer`, `Collector` (including `tokensCollected`), update `GraphNetwork.tokensEscrowed` | -### GraphTallyCollector +## Implementation Notes -| Event | Handler Action | -|-------|----------------| -| `SignerAuthorized` | Create/update `GraphTallySigner`, `GraphTallySignerAuthorization` | -| `SignerThawing` | Update `GraphTallySignerAuthorization.thawEndTimestamp` | -| `SignerRevoked` | Update `GraphTallySignerAuthorization.authorized` | -| `SignerThawCanceled` | Reset `GraphTallySignerAuthorization.thawEndTimestamp` | +This section captures implementation patterns and technical details for subgraph developers. -### DataService (ProvisionManager) +### Entity Creation Pattern -| Event | Handler Action | -|-------|----------------| -| `ProvisionTokensRangeSet` | Update `DataService.minProvisionTokens`, `DataService.maxProvisionTokens` | -| `DelegationRatioSet` | Update `DataService.delegationRatio` | -| `VerifierCutRangeSet` | Update `DataService.minVerifierCut`, `DataService.maxVerifierCut` | -| `ThawingPeriodRangeSet` | Update `DataService.minThawingPeriod`, `DataService.maxThawingPeriod` | +Use a `createOrLoad` pattern for all entities. This provides defensive, lazy initialization: + +```typescript +export function createOrLoadServiceProvider(id: Bytes): ServiceProvider { + let entity = ServiceProvider.load(id) + if (entity == null) { + entity = new ServiceProvider(id) + // Initialize all fields with default values + entity.tokensStaked = BigInt.zero() + entity.countProvisions = 0 + // ... etc + entity.save() + } + return entity +} +``` + +Benefits: +- Handlers don't need to know if an entity exists +- Consistent pattern across all entities +- Encapsulates initialization logic in reusable helpers + +### GraphNetwork Singleton + +The `GraphNetwork` entity is a singleton with a fixed ID. Load it at the start of handlers that need protocol-wide state: + +```typescript +let graphNetwork = createOrLoadGraphNetwork() +// Update fields... +graphNetwork.save() +``` + +Immutable protocol parameters (`protocolPaymentCut`, `escrowThawingPeriod`) have no setter events. Initialize them via contract calls within the `createOrLoadGraphNetwork()` helper on first access. From 0a0daf223d57b03902fcb01dd83213f3963225d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Fri, 24 Apr 2026 14:40:11 -0300 Subject: [PATCH 6/8] spec: more specing work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- specs/NETWORK_SUBGRAPH_MIGRATION.md | 276 ++++++++++++++++++ specs/SUBGRAPH_BEST_PRACTICES.md | 421 ++++++++++++++++++++++++++++ 2 files changed, 697 insertions(+) create mode 100644 specs/NETWORK_SUBGRAPH_MIGRATION.md create mode 100644 specs/SUBGRAPH_BEST_PRACTICES.md diff --git a/specs/NETWORK_SUBGRAPH_MIGRATION.md b/specs/NETWORK_SUBGRAPH_MIGRATION.md new file mode 100644 index 0000000..58b0738 --- /dev/null +++ b/specs/NETWORK_SUBGRAPH_MIGRATION.md @@ -0,0 +1,276 @@ +# Network Subgraph Migration + +This document outlines the migration considerations for the Network Subgraph when transitioning from the legacy Graph protocol to Horizon. + +## Overview + +Graph Horizon is an upgrade to The Graph protocol, which has been live for 5 years. The Network Subgraph is designed to track Horizon state via events, but pre-existing participants have state that was never emitted as Horizon events. + +**The problem:** If we only index Horizon events, pre-existing participants will have incorrect or missing state (e.g., a service provider showing 0 stake when they actually have 1M GRT staked from the legacy system). + +## Migration Scope + +### Entities That Do NOT Require Migration + +These entities are new to Horizon - all state is created via Horizon events: + +| Entity | Reason | +|--------|--------| +| `Provision` | Provisions are a new Horizon concept | +| `ProvisionFeeCut` | New concept | +| `ProvisionThawRequest` | New thaw request system | +| `DelegationPool` | Per-provision pools are new | +| `DelegationThawRequest` | New thaw request system | +| `Payer` | PaymentsEscrow is new | +| `Collector` | New concept | +| `EscrowAccount` | New escrow system | + +### State That REQUIRES Migration + +Pre-existing state falls into three categories: **Stake**, **Delegation**, and **Operators**. + +## 1. Stake + +Service providers (indexers) have stake that existed before Horizon. + +| Entity | Field | Notes | +|--------|-------|-------| +| `ServiceProvider` | `tokensStaked` | Legacy stake carried over | +| `ServiceProvider` | `tokensIdle` | Derived: tokensStaked - tokensProvisioned | +| `GraphNetwork` | `tokensStaked` | Protocol-wide total | +| `GraphNetwork` | `countServiceProviders` | Total service providers | + +**Note:** All service providers have pre-existing stake, regardless of which data service they provision to. + +### Contract Calls for Stake + +The `HorizonStaking` contract provides these view functions: + +| Field | Contract Call | Returns | +|-------|---------------|---------| +| `ServiceProvider.tokensStaked` | `getStake(address serviceProvider)` | `uint256` | +| `ServiceProvider.tokensIdle` | `getIdleStake(address serviceProvider)` | `uint256` | + +The `GraphNetwork` aggregate fields don't have direct contract calls. Instead, they are tallied incrementally: + +- **`GraphNetwork.tokensStaked`**: When a service provider is first encountered via any Horizon event, call `getStake()` and add the result to the running total. +- **`GraphNetwork.countServiceProviders`**: Increment when a new service provider entity is created. + +This approach means `GraphNetwork` totals will converge to correct values as service providers interact with Horizon. Providers who never interact with Horizon won't be counted, but this is acceptable since they're not active participants. + +## 2. Delegation + +Legacy delegations were auto-assigned to **Subgraph Service** specifically. + +| Entity | Field | Notes | +|--------|-------|-------| +| `ServiceProvider` | `tokensDelegated` | Total delegated to this SP | +| `ServiceProvider` | `countDelegators` | Number of delegators | +| `Delegator` | `tokensDelegated` | Total tokens this delegator has delegated | +| `Delegator` | `countDelegations` | Number of active delegations | +| `Delegation` | `tokensDelegated` | Amount delegated (Subgraph Service only) | +| `Delegation` | `shares` | Pool shares (Subgraph Service only) | +| `DelegationPool` | `tokens` | Total pool tokens (Subgraph Service only) | +| `DelegationPool` | `shares` | Total pool shares (Subgraph Service only) | +| `DelegationPool` | `countDelegators` | Delegators in pool (Subgraph Service only) | +| `DataService` | `tokensDelegated` | Total delegated (Subgraph Service only) | +| `DataService` | `countDelegators` | Total delegators (Subgraph Service only) | +| `GraphNetwork` | `tokensDelegated` | Protocol-wide total | +| `GraphNetwork` | `countDelegators` | Total delegators | + +**Important:** `Delegation`, `DelegationPool`, and `DataService` delegation fields only have pre-existing state for **Subgraph Service**. Other data services start fresh. + +## 3. Operators + +Legacy operator authorizations were auto-assigned to **Subgraph Service** specifically. + +| Entity | Field | Notes | +|--------|-------|-------| +| `Operator` | `countAuthorizations` | Number of authorizations | +| `OperatorAuthorization` | `allowed` | Authorization status (Subgraph Service only) | +| `DataService` | `countServiceProviders` | SPs with operators (Subgraph Service only) | + +**Important:** `OperatorAuthorization` entities only have pre-existing state for **Subgraph Service**. Other data services start fresh. + +## The Subgraph Service Special Case + +Subgraph Service is a specific data service that inherited all legacy state: +- All legacy delegations → assigned to Subgraph Service +- All legacy operators → assigned to Subgraph Service +- Legacy indexers → become service providers with provisions to Subgraph Service + +This means: +1. `DataService` entity for Subgraph Service address has pre-existing aggregates +2. `DelegationPool` entities for `(serviceProvider, SubgraphService)` have pre-existing state +3. `Delegation` entities for `(delegator, serviceProvider, SubgraphService)` have pre-existing state +4. `OperatorAuthorization` entities for `(operator, serviceProvider, SubgraphService)` have pre-existing state + +The Subgraph Service address needs to be known at indexing time. + +## Migration Approaches + +### Ruled Out: IPFS-Based Seeding + +Loading seed data from IPFS was considered but is **not viable** for two reasons: + +1. **`ipfs.cat` is non-deterministic**: If the file can't be retrieved before timeout, it returns null. This makes the subgraph ineligible for indexing rewards on the decentralized network. + +2. **File Data Sources have entity isolation**: Entities created by File Data Sources are immutable and completely isolated from chain-based entities. Chain-based handlers cannot access or update them, and vice versa. This means seeded entities couldn't be updated by Horizon event handlers. + +### Option A: Index from Protocol Genesis + +Index all events from block 0, including legacy staking/delegation events. State builds up correctly over time. + +**Pros:** +- Pure event-driven, no external dependencies +- Complete historical accuracy +- Deterministic + +**Cons:** +- Complex handlers (legacy + Horizon event formats) +- Long sync time (5+ years of events) +- Need to handle event format changes over protocol history + +### Option B: Bootstrap from Old Subgraph (Grafting) + +Use subgraph grafting to start from the state of the old `graph-network-subgraph` at the Horizon block. + +```yaml +features: + - grafting +graft: + base: QmOldSubgraphId + block: 12345678 # Horizon block +``` + +**Pros:** +- Clean Horizon-only handlers +- Faster sync after bootstrap +- Deterministic + +**Cons:** +- Schema compatibility required with old subgraph +- External dependency on old subgraph deployment +- One-time bootstrap process + +### Option C: Contract Calls (Lazy Initialization) + +Use contract calls in `createOrLoad` helpers to fetch state when entities are first encountered via Horizon events. + +```typescript +function getOrCreateServiceProvider(address: Address): ServiceProvider { + let sp = ServiceProvider.load(address.toHexString()) + if (sp == null) { + sp = new ServiceProvider(address.toHexString()) + sp.tokensStaked = contract.getStake(address) + sp.tokensIdle = contract.getIdleStake(address) + // Update GraphNetwork totals... + } + return sp +} +``` + +**Pros:** +- Ground truth from contracts +- No external dependencies +- No hardcoding required +- Deterministic + +**Cons:** +- Contract calls on every entity creation (slower indexing) +- Providers who never interact with Horizon won't be indexed +- `GraphNetwork` totals only converge as participants interact + +### Option D: Contract Calls (Proactive Seeding) + +Seed all known providers at start block using a block handler, fetching state via contract calls. + +```typescript +const PROVIDERS = ["0xabc...", "0xdef...", ...] + +function handleBlock(block: ethereum.Block): void { + for (let i = 0; i < PROVIDERS.length; i++) { + let addr = Address.fromString(PROVIDERS[i]) + let sp = new ServiceProvider(PROVIDERS[i]) + sp.tokensStaked = contract.getStake(addr) + sp.save() + } +} +``` + +**Pros:** +- Ground truth from contracts at indexing time +- All known providers indexed immediately +- Correct `GraphNetwork` totals from the start +- Deterministic + +**Cons:** +- Requires hardcoded list of provider addresses +- Need to obtain address list from external source (old subgraph, event logs, etc.) +- Block handler with many contract calls could be slow + +### Option E: Hardcoded Block Handler + +Seed all data directly in a block handler with hardcoded values (no contract calls). + +```typescript +function handleBlock(block: ethereum.Block): void { + seedProvider("0xabc...", "1000000000000000000000000") + seedProvider("0xdef...", "500000000000000000000000") + // ... +} +``` + +**Pros:** +- No external dependencies at indexing time +- Fast (no contract calls) +- Deterministic +- Self-contained + +**Cons:** +- Requires hardcoded addresses AND state values +- State is a snapshot that could be stale if indexing starts late +- Large datasets = large WASM binary + +### Option F: Hybrid Approach + +Combine approaches based on entity type: +- New entities (Provision, EscrowAccount, etc.): Pure event-driven +- Pre-existing entities: Contract calls (lazy or proactive) + +**Pros:** +- Optimized per entity type +- Balances correctness and performance + +**Cons:** +- More complex implementation +- Need clear rules per entity + +## Key Decision: Lazy vs Proactive Initialization + +The fundamental trade-off is: + +| Approach | Requires Address List? | Captures Inactive Providers? | +|----------|------------------------|------------------------------| +| Lazy (Option C) | No | No - only indexed when they interact | +| Proactive (Options D, E) | Yes | Yes - all known providers seeded | + +If capturing providers who never interact with Horizon is important, a hardcoded address list is required. This list can be obtained by: +- Querying the old `graph-network-subgraph` +- Parsing historical `HorizonStakeDeposited` events +- Exporting from protocol team records + +## Open Questions + +1. What is the exact Subgraph Service address? +2. What is the Horizon deployment block number? +3. Is capturing inactive providers (who never interact with Horizon) a requirement? +4. If proactive seeding is needed, what's the best source for the address list? + +## Next Steps + +- [ ] Clarify open questions with protocol team +- [ ] Decide on migration approach based on requirements +- [ ] If proactive seeding: obtain and validate address list +- [ ] Document specific implementation for chosen approach +- [ ] Test migration with known pre-existing participants diff --git a/specs/SUBGRAPH_BEST_PRACTICES.md b/specs/SUBGRAPH_BEST_PRACTICES.md new file mode 100644 index 0000000..b511d16 --- /dev/null +++ b/specs/SUBGRAPH_BEST_PRACTICES.md @@ -0,0 +1,421 @@ +# Subgraph Best Practices + +This document outlines best practices for developing subgraphs, compiled from research on high-quality implementations (notably Messari's 176-subgraph repository) and official Graph Protocol documentation. + +## 1. Project Structure & Organization + +### Internal Structure with Future Extraction in Mind + +While we're not adopting a full monorepo structure yet, we organize code internally with clear boundaries to facilitate future extraction (e.g., an analytics subgraph). + +``` +src/ +├── common/ # ← Future extraction candidate +│ ├── constants.ts # BIGINT_ZERO, SECONDS_PER_DAY, etc. +│ ├── numbers.ts # bigIntToBigDecimal, safeDiv, etc. +│ ├── addresses.ts # Address utilities +│ └── ids.ts # ID generation helpers (prefixID, etc.) +│ +├── config/ # ← Multi-network support +│ ├── index.ts # Re-exports active config +│ ├── mainnet.ts # Addresses, start blocks +│ └── arbitrum.ts +│ +├── entities/ # ← Entity-specific logic +│ ├── serviceProvider.ts # getOrCreateServiceProvider(), update helpers +│ ├── delegator.ts +│ ├── provision.ts +│ ├── delegation.ts +│ └── graphNetwork.ts +│ +├── handlers/ # ← Thin event handlers +│ ├── staking.ts # handleStakeDeposited, handleStakeWithdrawn +│ ├── delegation.ts +│ └── escrow.ts +│ +└── mapping.ts # Re-exports all handlers +``` + +### Directory Responsibilities + +| Directory | Responsibility | Extraction Potential | +|-----------|---------------|---------------------| +| `common/` | Pure utilities, no entity imports | High - copy/paste to shared package | +| `config/` | Network-specific addresses/values | High - becomes config package | +| `entities/` | Entity CRUD + business logic | Medium - may diverge between subgraphs | +| `handlers/` | Event → entity function calls | Low - specific to each subgraph | + +### Import Rules + +1. **`common/` imports nothing from other src dirs** - Pure functions only +2. **`entities/` can import `common/` and `config/`** - Not handlers +3. **`handlers/` are thin** - Parse event, call entity functions, done +4. **Config is typed** - Export an interface, each network implements it + +### Multi-Network Configuration + +```typescript +// config/types.ts +export interface NetworkConfig { + network: string + subgraphServiceAddress: Address + horizonStakingAddress: Address + startBlock: i32 +} + +// config/mainnet.ts +export const config: NetworkConfig = { + network: "mainnet", + subgraphServiceAddress: Address.fromString("0x..."), + horizonStakingAddress: Address.fromString("0x..."), + startBlock: 12345678, +} + +// config/index.ts - switched at build time via templating +export { config } from "./mainnet" +``` + +--- + +## 2. Schema Design + +### Type Conventions + +| Type | Used For | Examples | +|------|----------|----------| +| `Bytes!` | Entity IDs, addresses, composite keys | `id`, `serviceProvider`, `dataService` | +| `BigInt!` | Token amounts, timestamps, block numbers, PPM values | `tokensStaked`, `createdAt`, `maxVerifierCut` | +| `Int!` | Counts | `countProvisions`, `countDelegators` | +| `Boolean!` | Status flags | `allowed`, `valid`, `fulfilled` | +| `BigDecimal` | USD amounts, percentage rates (if needed) | `priceUSD`, `apr` | + +### Field Naming Patterns + +| Pattern | Meaning | Examples | +|---------|---------|----------| +| `tokens*` | Token amounts | `tokensStaked`, `tokensDelegated`, `tokensThawing`, `tokensSlashed` | +| `count*` | Entity counts | `countProvisions`, `countDelegators`, `countSlashEvents` | +| `*At` | Timestamps | `createdAt`, `updatedAt`, `thawingUntil`, `lastParametersStagedAt` | +| `*AtBlock` | Block numbers | `createdAtBlock`, `updatedAtBlock` | +| No prefix | Current spot value | `tokens`, `shares`, `allowed` | +| `cumulative*` | Running total from genesis (for analytics) | `cumulativeVolumeUSD` | +| `daily*` / `hourly*` | Snapshot interval aggregate (for analytics) | `dailyActiveUsers` | + +### Entity ID Patterns + +| Entity Type | ID Format | Example | +|-------------|-----------|---------| +| Single address | `address` | `ServiceProvider`, `Delegator`, `Operator` | +| Two-part composite | `addressA.concat(addressB)` | `Provision`, `DelegationPool` | +| Three-part composite | `addressA.concat(addressB).concat(addressC)` | `Delegation`, `EscrowAccount`, `OperatorAuthorization` | +| Event-based | `thawRequestId` (bytes32 from event) | `ProvisionThawRequest`, `DelegationThawRequest` | +| Composite with enum | `provisionId.concat(paymentType)` | `ProvisionFeeCut` | + +### Standard Metadata Fields + +All mutable entities should include consistent metadata: + +```graphql +"Block number when entity was created" +createdAtBlock: BigInt! +"Timestamp when entity was created" +createdAt: BigInt! +"Block number when entity was last updated" +updatedAtBlock: BigInt! +"Timestamp when entity was last updated" +updatedAt: BigInt! +``` + +### Immutable Entities + +Use `@entity(immutable: true)` for entities that log on-chain events and never change after creation: + +```graphql +type Transfer @entity(immutable: true) { + id: Bytes! + from: Bytes! + to: Bytes! + amount: BigInt! + blockNumber: BigInt! + timestamp: BigInt! +} +``` + +**Never** use `immutable: true` for entities with fields that need modification over time. + +### Derived Fields with @derivedFrom + +Use `@derivedFrom` for one-to-many relationships instead of storing arrays directly: + +```graphql +type ServiceProvider @entity { + id: Bytes! + provisions: [Provision!]! @derivedFrom(field: "serviceProvider") +} + +type Provision @entity { + id: Bytes! + serviceProvider: ServiceProvider! +} +``` + +Benefits: +- Significantly improves indexing speed (no array mutations) +- Improves query performance +- Data is computed at query time from the foreign key + +--- + +## 3. Performance Optimization + +### Avoid eth_calls + +Each `eth_call` takes 100ms to several seconds. This is the #1 cause of slow indexing. + +```typescript +// Bad: contract call every event +function handleEvent(event: SomeEvent): void { + let balance = contract.balanceOf(user) // Slow! +} + +// Good: use event data +function handleEvent(event: SomeEvent): void { + let balance = event.params.balance // Data already in event +} + +// Acceptable: one-time call for lazy initialization (migration) +function getOrCreateServiceProvider(address: Address): ServiceProvider { + let sp = ServiceProvider.load(address) + if (!sp) { + sp = new ServiceProvider(address) + sp.tokensStaked = contract.getStake(address) // One-time call + } + return sp +} +``` + +If contract calls are unavoidable: +- Declare calls in manifest for parallel execution +- Cache results in entities to avoid re-fetching + +### Set Correct Start Blocks + +Always use the contract deployment block, not genesis: + +```yaml +dataSources: + - source: + address: "0x..." + startBlock: 12345678 # Horizon deployment block +``` + +### Use Event Handlers Over Call Handlers + +| Handler Type | Speed | Chain Support | +|--------------|-------|---------------| +| Event handlers | Fast | All EVM chains | +| Call handlers | Slow | Limited support | + +Use transaction receipts to access sibling events if needed instead of call handlers. + +### Pruning + +Enable pruning for aggregate state subgraphs: + +```yaml +indexerHints: + prune: auto +``` + +Options: +- `auto` - Retains minimum necessary history (recommended for state subgraphs) +- `` - Custom block retention limit +- `never` - Full history for time-travel queries + +### Batch Entity Updates + +```typescript +// Bad: multiple saves +entity.field1 = value1 +entity.save() +entity.field2 = value2 +entity.save() + +// Good: single save +entity.field1 = value1 +entity.field2 = value2 +entity.save() +``` + +--- + +## 4. Handler Patterns + +### Thin Handlers + +Handlers should be thin - parse event, delegate to entity helpers: + +```typescript +// handlers/staking.ts +export function handleStakeDeposited(event: StakeDeposited): void { + let sp = getOrCreateServiceProvider(event.params.serviceProvider) + let graphNetwork = getOrCreateGraphNetwork() + + // Delegate to entity-specific update functions + updateServiceProviderOnStakeDeposit(sp, event) + updateGraphNetworkOnStakeDeposit(graphNetwork, event) +} + +// entities/serviceProvider.ts +export function updateServiceProviderOnStakeDeposit( + sp: ServiceProvider, + event: StakeDeposited +): void { + sp.tokensStaked = sp.tokensStaked.plus(event.params.tokens) + sp.updatedAt = event.block.timestamp + sp.updatedAtBlock = event.block.number + sp.save() +} +``` + +Benefits: +- Handlers are easy to read (what happens on this event?) +- Entity logic is reusable and testable +- Aligns with project structure (`handlers/` vs `entities/`) + +### getOrCreate Pattern + +Defensive, lazy initialization for all entities: + +```typescript +export function getOrCreateServiceProvider(id: Bytes): ServiceProvider { + let entity = ServiceProvider.load(id) + if (entity == null) { + entity = new ServiceProvider(id) + entity.tokensStaked = BIGINT_ZERO + entity.countProvisions = 0 + // ... initialize all fields with defaults + } + return entity +} +``` + +**Important:** Don't save inside `getOrCreate` - let the caller save after setting event-specific fields. This avoids double saves and keeps initialization separate from updates. + +### Transaction Receipt Pattern + +For extracting sibling event data when needed (apiVersion 0.0.7+): + +```typescript +export function handleSomeEvent(event: SomeEvent): void { + if (!event.receipt) return + + const logs = event.receipt.logs + for (let i = 0; i < logs.length; i++) { + const log = logs.at(i) + if (log.topics.at(0).equals(TARGET_SIGNATURE)) { + // Decode indexed params from topics + // Decode non-indexed params from log.data + } + } +} +``` + +Use when you need data from related events in the same transaction without making contract calls. + +--- + +## 5. Common Utilities + +Utilities in `common/` should be pure functions with no entity imports. + +### constants.ts + +```typescript +import { BigInt, BigDecimal } from "@graphprotocol/graph-ts" + +export const BIGINT_ZERO = BigInt.fromI32(0) +export const BIGINT_ONE = BigInt.fromI32(1) +export const BIGDECIMAL_ZERO = BigDecimal.fromString("0") + +export const SECONDS_PER_DAY = 86400 +export const PPM_DENOMINATOR = BigInt.fromI32(1000000) +``` + +### numbers.ts + +```typescript +import { BigInt, BigDecimal } from "@graphprotocol/graph-ts" +import { BIGDECIMAL_ZERO } from "./constants" + +export function bigIntToBigDecimal(value: BigInt, decimals: i32 = 18): BigDecimal { + return value.toBigDecimal().div( + BigInt.fromI32(10).pow(decimals as u8).toBigDecimal() + ) +} + +export function safeDiv(a: BigDecimal, b: BigDecimal): BigDecimal { + if (b.equals(BIGDECIMAL_ZERO)) { + return BIGDECIMAL_ZERO + } + return a.div(b) +} +``` + +### ids.ts + +```typescript +import { Bytes } from "@graphprotocol/graph-ts" + +export function twoPartId(a: Bytes, b: Bytes): Bytes { + return a.concat(b) +} + +export function threePartId(a: Bytes, b: Bytes, c: Bytes): Bytes { + return a.concat(b).concat(c) +} +``` + +--- + +## 6. Common Pitfalls + +| Don't | Do | +|-------|-----| +| Make uncached repeated contract calls | Cache results in entities | +| Use call handlers | Use event handlers | +| Save inside `getOrCreate` | Let caller save | +| Store arrays directly | Use `@derivedFrom` | +| Use `String` for IDs | Use `Bytes` | +| Hard-code addresses in mappings | Use config files | +| Start from block 0 | Use contract deployment block | + +### Entity Merging Gotcha + +When creating an entity with an existing ID, new values overwrite - including nulls: + +```typescript +// Entity exists with field = "old value" +let entity = new Entity(existingId) +entity.field = null // OVERWRITES with null! +entity.save() + +// Always load first +let entity = Entity.load(id) +if (!entity) { + entity = new Entity(id) +} +entity.field = newValue +entity.save() +``` + +This is why the `getOrCreate` pattern is essential. + +--- + +## References + +- [The Graph Academy - Best Practice Cookbook](https://thegraph.academy/developers/best-practice/) +- [The Graph Docs - AssemblyScript Mappings](https://thegraph.com/docs/en/subgraphs/developing/creating/assemblyscript-mappings/) +- [Messari Subgraphs Repository](https://github.com/messari/subgraphs) +- [Matchstick Unit Testing Framework](https://thegraph.com/docs/en/subgraphs/developing/creating/unit-testing-framework/) From caf9e2ff5ec53c5bc94a1b7992cd1707b6ce7f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Fri, 24 Apr 2026 15:01:26 -0300 Subject: [PATCH 7/8] specs: add first stages implementation plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- plans/IMPLEMENTATION_PLAN.md | 230 ++++++++++++++++++++++++++++ specs/NETWORK_SUBGRAPH_MIGRATION.md | 21 ++- 2 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 plans/IMPLEMENTATION_PLAN.md diff --git a/plans/IMPLEMENTATION_PLAN.md b/plans/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..822c476 --- /dev/null +++ b/plans/IMPLEMENTATION_PLAN.md @@ -0,0 +1,230 @@ +# Network Subgraph Implementation Plan + +Incremental implementation with validation checkpoints between stages. + +## High-Level Stages + +| Stage | Focus | Entities | Validation | +|-------|-------|----------|------------| +| 1 | Boilerplate | - | Project builds, deploys empty subgraph | +| 2 | Service Provider Stake | `GraphNetwork`, `ServiceProvider` | Query SPs, verify stake totals | +| 3 | Provisions | `Provision`, `DataService` | Query provisions, verify tokensProvisioned | +| 4 | Delegation | `Delegator`, `Delegation`, `DelegationPool` | Query delegations, verify pool math | +| 5 | Thaw Requests | `ProvisionThawRequest`, `DelegationThawRequest` | Query pending thaws, verify lifecycle | +| 6 | Operators | `Operator`, `OperatorAuthorization` | Query authorizations | +| 7 | Payments & Escrow | `Payer`, `Collector`, `EscrowAccount` | Query escrow balances | +| 8 | Slashing & Fees | Slashing fields, `ProvisionFeeCut` | Verify slash accounting | + +--- + +## Stage 1: Boilerplate + +### Goal +Project structure, build pipeline, empty deployable subgraph. + +### Deliverables + +**1. Project structure:** +``` +network-subgraph/ +├── src/ +│ ├── common/ +│ │ ├── constants.ts +│ │ ├── numbers.ts +│ │ └── ids.ts +│ ├── config/ +│ │ ├── index.ts +│ │ └── mainnet.ts +│ ├── entities/ +│ │ └── (empty for now) +│ └── handlers/ +│ └── (empty for now) +├── abis/ +│ └── HorizonStaking.json +├── schema.graphql +├── subgraph.yaml +├── package.json +└── tsconfig.json +``` + +**2. Minimal schema** (just enough to deploy): +```graphql +type GraphNetwork @entity { + id: Bytes! +} +``` + +**3. Config setup:** +```typescript +// config/mainnet.ts +export const config = { + network: "mainnet", + horizonStakingAddress: "0x...", + startBlock: 12345678, +} +``` + +**4. Common utilities:** +- `constants.ts`: BIGINT_ZERO, BIGINT_ONE +- `numbers.ts`: bigIntToBigDecimal, safeDiv (if needed) +- `ids.ts`: twoPartId, threePartId + +**5. Manifest with placeholder handler:** +```yaml +specVersion: 1.0.0 +indexerHints: + prune: auto +dataSources: + - kind: ethereum + name: HorizonStaking + source: + address: "0x..." + abi: HorizonStaking + startBlock: 12345678 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - GraphNetwork + abis: + - name: HorizonStaking + file: ./abis/HorizonStaking.json + eventHandlers: + - event: HorizonStakeDeposited(indexed address,uint256) + handler: handleHorizonStakeDeposited +``` + +### Validation Checkpoint +- [ ] `graph codegen` succeeds +- [ ] `graph build` succeeds +- [ ] Deploy to local graph-node or hosted service +- [ ] GraphQL playground loads (empty data is fine) + +--- + +## Stage 2: Service Provider Stake + +### Goal +Service providers with stake, including migration of ~180 existing SPs. + +### Deliverables + +**1. Schema additions:** +```graphql +type GraphNetwork @entity { + id: Bytes! + countServiceProviders: Int! + tokensStaked: BigInt! +} + +type ServiceProvider @entity { + id: Bytes! + tokensStaked: BigInt! + tokensProvisioned: BigInt! + tokensIdle: BigInt! + createdAtBlock: BigInt! + createdAt: BigInt! + updatedAtBlock: BigInt! + updatedAt: BigInt! +} +``` + +**2. Entity helpers:** +``` +src/entities/ +├── graphNetwork.ts # getOrCreateGraphNetwork() +└── serviceProvider.ts # getOrCreateServiceProvider() +``` + +**3. Migration handler:** +``` +src/handlers/migration.ts # handleStartBlock() +src/config/serviceProviders.ts # List of 180 addresses +``` + +**4. Event handlers:** +``` +src/handlers/staking.ts +├── handleHorizonStakeDeposited() +└── handleHorizonStakeWithdrawn() +``` + +**5. Manifest updates:** +- Add block handler with `filter: kind: once` +- Add HorizonStakeWithdrawn event handler + +### Handler Logic Summary + +**handleStartBlock:** +1. Create GraphNetwork singleton +2. Loop through 180 SP addresses +3. For each: call `getStake()`, `getIdleStake()`, create entity +4. Tally totals on GraphNetwork + +**handleHorizonStakeDeposited:** +1. getOrCreateServiceProvider +2. Add tokens to `tokensStaked` +3. Recalculate `tokensIdle` +4. Update GraphNetwork totals +5. Update metadata (updatedAt, updatedAtBlock) + +**handleHorizonStakeWithdrawn:** +1. Load ServiceProvider +2. Subtract tokens from `tokensStaked` +3. Recalculate `tokensIdle` +4. Update GraphNetwork totals +5. Update metadata + +### Validation Checkpoint + +**Queries to run:** +```graphql +# Check GraphNetwork totals +{ + graphNetwork(id: "0x01") { + countServiceProviders + tokensStaked + } +} + +# Check individual SP +{ + serviceProvider(id: "0x...known-sp-address...") { + tokensStaked + tokensIdle + } +} + +# List all SPs +{ + serviceProviders(first: 10, orderBy: tokensStaked, orderDirection: desc) { + id + tokensStaked + } +} +``` + +**Validation checks:** +- [ ] `countServiceProviders` = 180 (or current count) +- [ ] `tokensStaked` on GraphNetwork = sum of all SP stakes +- [ ] Known SP addresses have expected stake values (compare to contract) +- [ ] After a new stake event: values update correctly + +--- + +## Stages 3-8: To Be Detailed Later + +Will detail these after Stage 2 is validated. High-level scope: + +**Stage 3 - Provisions:** ProvisionCreated, ProvisionIncreased, ProvisionParametersStaged/Set, TokensDeprovisioned. Links SP to DataService. + +**Stage 4 - Delegation:** Migration of legacy delegations to Subgraph Service. TokensDelegated, TokensUndelegated, DelegatedTokensWithdrawn. + +**Stage 5 - Thaw Requests:** ThawRequestCreated, ThawRequestFulfilled, ThawRequestsFulfilled. Both provision and delegation types. + +**Stage 6 - Operators:** OperatorSet event. Migration of legacy operators to Subgraph Service. + +**Stage 7 - Payments & Escrow:** GraphPayments and PaymentsEscrow events. New entities only (no migration). + +**Stage 8 - Slashing & Fees:** ProvisionSlashed, DelegationSlashed, DelegationFeeCutSet. Complete remaining fields. diff --git a/specs/NETWORK_SUBGRAPH_MIGRATION.md b/specs/NETWORK_SUBGRAPH_MIGRATION.md index 58b0738..64c5e24 100644 --- a/specs/NETWORK_SUBGRAPH_MIGRATION.md +++ b/specs/NETWORK_SUBGRAPH_MIGRATION.md @@ -51,12 +51,21 @@ The `HorizonStaking` contract provides these view functions: | `ServiceProvider.tokensStaked` | `getStake(address serviceProvider)` | `uint256` | | `ServiceProvider.tokensIdle` | `getIdleStake(address serviceProvider)` | `uint256` | -The `GraphNetwork` aggregate fields don't have direct contract calls. Instead, they are tallied incrementally: +### Migration Approach: Proactive Seeding with Contract Calls -- **`GraphNetwork.tokensStaked`**: When a service provider is first encountered via any Horizon event, call `getStake()` and add the result to the running total. -- **`GraphNetwork.countServiceProviders`**: Increment when a new service provider entity is created. +There are ~180 service providers. This is small enough to seed proactively at the start block. -This approach means `GraphNetwork` totals will converge to correct values as service providers interact with Horizon. Providers who never interact with Horizon won't be counted, but this is acceptable since they're not active participants. +**Approach:** +1. Hardcode the list of 180 service provider addresses in the subgraph +2. Use a block handler (triggered once at start block) to seed all entities +3. Fetch `tokensStaked` and `tokensIdle` via contract calls for fresh values +4. Tally `GraphNetwork` totals during seeding + +**Benefits:** +- Correct `GraphNetwork.tokensStaked` and `countServiceProviders` from block 1 +- Fresh values from contract state (not stale snapshot) +- Deterministic (same addresses indexed every time) +- 180 contract calls is negligible overhead (one-time) ## 2. Delegation @@ -264,8 +273,8 @@ If capturing providers who never interact with Horizon is important, a hardcoded 1. What is the exact Subgraph Service address? 2. What is the Horizon deployment block number? -3. Is capturing inactive providers (who never interact with Horizon) a requirement? -4. If proactive seeding is needed, what's the best source for the address list? +3. ~~Is capturing inactive providers (who never interact with Horizon) a requirement?~~ **Decided:** Yes, proactive seeding for all ~180 service providers. +4. ~~If proactive seeding is needed, what's the best source for the address list?~~ **Decided:** Query old subgraph or protocol team records. ## Next Steps From d6ea8a5d1f04062833a2b8218f9fe3d5649007a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Fri, 24 Apr 2026 15:24:41 -0300 Subject: [PATCH 8/8] chore: repo cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- .claude/settings.local.json | 9 ++++++++- .gitignore | 2 ++ .../packages/scripts}/allocation-stats.json | 0 .../packages/scripts}/allocation-stats.sh | 0 .../packages/scripts}/upgrade-indexer/.gitignore | 0 .../packages/scripts}/upgrade-indexer/README.md | 0 .../scripts}/upgrade-indexer/allos-snapshot-before.json | 0 .../packages/scripts}/upgrade-indexer/approve.sh | 0 .../scripts}/upgrade-indexer/close-duplicates.sh | 0 .../packages/scripts}/upgrade-indexer/close-inactive.sh | 0 .../packages/scripts}/upgrade-indexer/compare.sh | 0 .../packages/scripts}/upgrade-indexer/create-lost.sh | 0 .../packages/scripts}/upgrade-indexer/execute.sh | 0 .../packages/scripts}/upgrade-indexer/fetch.sh | 0 .../packages/scripts}/upgrade-indexer/lost-allowlist.txt | 0 .../packages/scripts}/upgrade-indexer/migrate.sh | 0 .../packages/scripts}/upgrade-indexer/queue.sh | 0 .../scripts}/upgrade-indexer/recreate-skipped.sh | 0 .../packages/scripts}/upgrade-indexer/run.sh | 0 .../packages/scripts}/upgrade-indexer/setup.sh | 0 .../scripts}/upgrade-indexer/skipped-inactive.log.bkp | 0 .../packages/scripts}/upgrade-indexer/status.sh | 0 22 files changed, 10 insertions(+), 1 deletion(-) rename {scripts => typescript/packages/scripts}/allocation-stats.json (100%) rename {scripts => typescript/packages/scripts}/allocation-stats.sh (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/.gitignore (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/README.md (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/allos-snapshot-before.json (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/approve.sh (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/close-duplicates.sh (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/close-inactive.sh (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/compare.sh (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/create-lost.sh (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/execute.sh (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/fetch.sh (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/lost-allowlist.txt (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/migrate.sh (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/queue.sh (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/recreate-skipped.sh (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/run.sh (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/setup.sh (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/skipped-inactive.log.bkp (100%) rename {scripts => typescript/packages/scripts}/upgrade-indexer/status.sh (100%) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 19774ae..7520ed2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,14 @@ "permissions": { "allow": [ "Bash(grep:*)", - "Bash(find:*)" + "Bash(find:*)", + "WebFetch(domain:api.github.com)", + "WebFetch(domain:raw.githubusercontent.com)", + "WebFetch(domain:github.com)", + "WebSearch", + "Bash(git clone:*)", + "WebFetch(domain:thegraph.academy)", + "WebFetch(domain:thegraph.com)" ] } } diff --git a/.gitignore b/.gitignore index 7610089..8f25260 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,5 @@ dist # End of https://www.toptal.com/developers/gitignore/api/node +# macOS crap +.DS_Store \ No newline at end of file diff --git a/scripts/allocation-stats.json b/typescript/packages/scripts/allocation-stats.json similarity index 100% rename from scripts/allocation-stats.json rename to typescript/packages/scripts/allocation-stats.json diff --git a/scripts/allocation-stats.sh b/typescript/packages/scripts/allocation-stats.sh similarity index 100% rename from scripts/allocation-stats.sh rename to typescript/packages/scripts/allocation-stats.sh diff --git a/scripts/upgrade-indexer/.gitignore b/typescript/packages/scripts/upgrade-indexer/.gitignore similarity index 100% rename from scripts/upgrade-indexer/.gitignore rename to typescript/packages/scripts/upgrade-indexer/.gitignore diff --git a/scripts/upgrade-indexer/README.md b/typescript/packages/scripts/upgrade-indexer/README.md similarity index 100% rename from scripts/upgrade-indexer/README.md rename to typescript/packages/scripts/upgrade-indexer/README.md diff --git a/scripts/upgrade-indexer/allos-snapshot-before.json b/typescript/packages/scripts/upgrade-indexer/allos-snapshot-before.json similarity index 100% rename from scripts/upgrade-indexer/allos-snapshot-before.json rename to typescript/packages/scripts/upgrade-indexer/allos-snapshot-before.json diff --git a/scripts/upgrade-indexer/approve.sh b/typescript/packages/scripts/upgrade-indexer/approve.sh similarity index 100% rename from scripts/upgrade-indexer/approve.sh rename to typescript/packages/scripts/upgrade-indexer/approve.sh diff --git a/scripts/upgrade-indexer/close-duplicates.sh b/typescript/packages/scripts/upgrade-indexer/close-duplicates.sh similarity index 100% rename from scripts/upgrade-indexer/close-duplicates.sh rename to typescript/packages/scripts/upgrade-indexer/close-duplicates.sh diff --git a/scripts/upgrade-indexer/close-inactive.sh b/typescript/packages/scripts/upgrade-indexer/close-inactive.sh similarity index 100% rename from scripts/upgrade-indexer/close-inactive.sh rename to typescript/packages/scripts/upgrade-indexer/close-inactive.sh diff --git a/scripts/upgrade-indexer/compare.sh b/typescript/packages/scripts/upgrade-indexer/compare.sh similarity index 100% rename from scripts/upgrade-indexer/compare.sh rename to typescript/packages/scripts/upgrade-indexer/compare.sh diff --git a/scripts/upgrade-indexer/create-lost.sh b/typescript/packages/scripts/upgrade-indexer/create-lost.sh similarity index 100% rename from scripts/upgrade-indexer/create-lost.sh rename to typescript/packages/scripts/upgrade-indexer/create-lost.sh diff --git a/scripts/upgrade-indexer/execute.sh b/typescript/packages/scripts/upgrade-indexer/execute.sh similarity index 100% rename from scripts/upgrade-indexer/execute.sh rename to typescript/packages/scripts/upgrade-indexer/execute.sh diff --git a/scripts/upgrade-indexer/fetch.sh b/typescript/packages/scripts/upgrade-indexer/fetch.sh similarity index 100% rename from scripts/upgrade-indexer/fetch.sh rename to typescript/packages/scripts/upgrade-indexer/fetch.sh diff --git a/scripts/upgrade-indexer/lost-allowlist.txt b/typescript/packages/scripts/upgrade-indexer/lost-allowlist.txt similarity index 100% rename from scripts/upgrade-indexer/lost-allowlist.txt rename to typescript/packages/scripts/upgrade-indexer/lost-allowlist.txt diff --git a/scripts/upgrade-indexer/migrate.sh b/typescript/packages/scripts/upgrade-indexer/migrate.sh similarity index 100% rename from scripts/upgrade-indexer/migrate.sh rename to typescript/packages/scripts/upgrade-indexer/migrate.sh diff --git a/scripts/upgrade-indexer/queue.sh b/typescript/packages/scripts/upgrade-indexer/queue.sh similarity index 100% rename from scripts/upgrade-indexer/queue.sh rename to typescript/packages/scripts/upgrade-indexer/queue.sh diff --git a/scripts/upgrade-indexer/recreate-skipped.sh b/typescript/packages/scripts/upgrade-indexer/recreate-skipped.sh similarity index 100% rename from scripts/upgrade-indexer/recreate-skipped.sh rename to typescript/packages/scripts/upgrade-indexer/recreate-skipped.sh diff --git a/scripts/upgrade-indexer/run.sh b/typescript/packages/scripts/upgrade-indexer/run.sh similarity index 100% rename from scripts/upgrade-indexer/run.sh rename to typescript/packages/scripts/upgrade-indexer/run.sh diff --git a/scripts/upgrade-indexer/setup.sh b/typescript/packages/scripts/upgrade-indexer/setup.sh similarity index 100% rename from scripts/upgrade-indexer/setup.sh rename to typescript/packages/scripts/upgrade-indexer/setup.sh diff --git a/scripts/upgrade-indexer/skipped-inactive.log.bkp b/typescript/packages/scripts/upgrade-indexer/skipped-inactive.log.bkp similarity index 100% rename from scripts/upgrade-indexer/skipped-inactive.log.bkp rename to typescript/packages/scripts/upgrade-indexer/skipped-inactive.log.bkp diff --git a/scripts/upgrade-indexer/status.sh b/typescript/packages/scripts/upgrade-indexer/status.sh similarity index 100% rename from scripts/upgrade-indexer/status.sh rename to typescript/packages/scripts/upgrade-indexer/status.sh