diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml index 931fd4811..a3d53c24e 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -10,11 +10,11 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v2 - - name: Set up Node.js v20 - uses: actions/setup-node@v2.1.5 + uses: actions/checkout@v4 + - name: Set up Node.js v24 + uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 - name: Build and Format run: yarn - name: Check Formatting diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7827622d..bf0dcf756 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: build: strategy: matrix: - node-version: [20, 22] + node-version: [22, 24] system: - os: ubuntu-22.04 runs-on: ${{ matrix.system.os }} @@ -32,13 +32,13 @@ jobs: --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: update OS run: | sudo apt-get update sudo apt install -y --no-install-recommends gcc g++ make build-essential - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5bc9669f9..59b45f7ea 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,11 +35,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/indexer-agent-image.yml b/.github/workflows/indexer-agent-image.yml index aee116c5a..03dc9add9 100644 --- a/.github/workflows/indexer-agent-image.yml +++ b/.github/workflows/indexer-agent-image.yml @@ -17,34 +17,37 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Docker meta id: docker_meta - uses: crazy-max/ghaction-docker-meta@v1 + uses: docker/metadata-action@v5 with: images: ghcr.io/graphprotocol/indexer-agent - tag-sha: true + tags: | + type=sha + type=ref,event=branch + type=ref,event=tag - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{github.repository_owner}} password: ${{secrets.GITHUB_TOKEN}} - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.11' - - name: Set up Node.js v20 - uses: actions/setup-node@v2.1.5 + - name: Set up Node.js v24 + uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 - name: Build and push Indexer Agent image id: docker_build - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v6 with: context: . file: Dockerfile.indexer-agent diff --git a/.github/workflows/indexer-cli-image.yml b/.github/workflows/indexer-cli-image.yml index c3b36fb83..979609d59 100644 --- a/.github/workflows/indexer-cli-image.yml +++ b/.github/workflows/indexer-cli-image.yml @@ -17,34 +17,37 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Docker meta id: docker_meta - uses: crazy-max/ghaction-docker-meta@v1 + uses: docker/metadata-action@v5 with: images: ghcr.io/graphprotocol/indexer-cli - tag-sha: true + tags: | + type=sha + type=ref,event=branch + type=ref,event=tag - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{github.repository_owner}} password: ${{secrets.GITHUB_TOKEN}} - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.11' - - name: Set up Node.js v20 - uses: actions/setup-node@v2.1.5 + - name: Set up Node.js v24 + uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 - name: Build and push Indexer CLI image id: docker_build - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v6 with: context: . file: Dockerfile.indexer-cli diff --git a/.gitignore b/.gitignore index c80f8ddb5..2e4624642 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,4 @@ tap-contracts.json config/config.yaml commit-changes.sh +**/.claude diff --git a/packages/indexer-agent/src/__tests__/agent.ts b/packages/indexer-agent/src/__tests__/agent.ts index 700e15f98..e1f764240 100644 --- a/packages/indexer-agent/src/__tests__/agent.ts +++ b/packages/indexer-agent/src/__tests__/agent.ts @@ -1,7 +1,4 @@ -import { - convertSubgraphBasedRulesToDeploymentBased, - consolidateAllocationDecisions, -} from '../agent' +import { convertSubgraphBasedRulesToDeploymentBased } from '../agent' import { INDEXING_RULE_GLOBAL, IndexingDecisionBasis, @@ -169,42 +166,3 @@ describe('Agent convenience function tests', () => { ).toEqual(inputRules) }) }) - -describe('consolidateAllocationDecisions function', () => { - it('produces a set with unique deployment ids', () => { - const a = new SubgraphDeploymentID( - 'QmXZiV6S13ha6QXq4dmaM3TB4CHcDxBMvGexSNu9Kc28EH', - ) - const b = new SubgraphDeploymentID( - 'QmRKs2ZfuwvmZA3QAWmCqrGUjV9pxtBUDP3wuc6iVGnjA2', - ) - const c = new SubgraphDeploymentID( - 'QmULAfA3eS5yojxeSR2KmbyuiwCGYPjymsFcpa6uYsu6CJ', - ) - - const allocationDecisions = { - 'eip155:0': [ - { deployment: a, toAllocate: false }, - { deployment: b, toAllocate: true }, - ], - 'eip155:1': [ - { deployment: b, toAllocate: true }, - { deployment: c, toAllocate: false }, - ], - 'eip155:2': [ - { deployment: c, toAllocate: true }, - { deployment: a, toAllocate: false }, - ], - } - - const expected = new Set([c, b]) - - const result = consolidateAllocationDecisions(allocationDecisions) - - expect(result).toStrictEqual(expected) - expect(result).toHaveProperty('size', 2) - expect(result).toContain(c) - expect(result).toContain(b) - expect(result).not.toContain(a) - }) -}) diff --git a/packages/indexer-agent/src/__tests__/indexer.ts b/packages/indexer-agent/src/__tests__/indexer.ts index c6b1ce463..7e7bcc6bf 100644 --- a/packages/indexer-agent/src/__tests__/indexer.ts +++ b/packages/indexer-agent/src/__tests__/indexer.ts @@ -18,7 +18,6 @@ import { specification, QueryFeeModels, defineQueryFeeModels, - MultiNetworks, loadTestYamlConfig, } from '@graphprotocol/indexer-common' import { Sequelize } from 'sequelize' @@ -153,11 +152,6 @@ const setup = async () => { metrics, ) - const multiNetworks = new MultiNetworks( - [network], - (n: Network) => n.specification.networkIdentifier, - ) - indexerManagementClient = await createIndexerManagementClient({ models, graphNode, @@ -168,7 +162,7 @@ const setup = async () => { parallelAllocations: 1, }, }, - multiNetworks, + network, }) operator = new Operator(logger, indexerManagementClient, networkSpecification) diff --git a/packages/indexer-agent/src/agent.ts b/packages/indexer-agent/src/agent.ts index f9f93d066..7aee596e5 100644 --- a/packages/indexer-agent/src/agent.ts +++ b/packages/indexer-agent/src/agent.ts @@ -29,8 +29,6 @@ import { GraphNode, Operator, validateProviderNetworkIdentifier, - MultiNetworks, - NetworkMapped, DeploymentManagementMode, SubgraphStatus, sequentialTimerMap, @@ -40,14 +38,7 @@ import { import PQueue from 'p-queue' import pMap from 'p-map' import pFilter from 'p-filter' -import zip from 'lodash.zip' -import { AgentConfigs, NetworkAndOperator } from './types' - -type ActionReconciliationContext = [ - AllocationDecision[], - number, - HorizonTransitionValue, -] +import { AgentConfigs } from './types' const deploymentInList = ( list: SubgraphDeploymentID[], @@ -128,65 +119,12 @@ export const convertSubgraphBasedRulesToDeploymentBased = ( return rules } -// Extracts the network identifier from a pair of matching Network and Operator objects. -function networkAndOperatorIdentity({ - network, - operator, -}: NetworkAndOperator): string { - const networkId = network.specification.networkIdentifier - const operatorId = operator.specification.networkIdentifier - if (networkId !== operatorId) { - throw new Error( - `Network and Operator pairs have different network identifiers: ${networkId} != ${operatorId}`, - ) - } - return networkId -} - -// Helper function to produce a `MultiNetworks` while validating its -// inputs. -function createMultiNetworks( - networks: Network[], - operators: Operator[], -): MultiNetworks { - // Validate that Networks and Operator arrays have even lengths and - // contain unique, matching network identifiers. - const visited = new Set() - const validInputs = - networks.length === operators.length && - networks.every((network, index) => { - const sameIdentifier = - network.specification.networkIdentifier === - operators[index].specification.networkIdentifier - if (!sameIdentifier) { - return false - } - if (visited.has(network.specification.networkIdentifier)) { - return false - } - visited.add(network.specification.networkIdentifier) - return true - }) - - if (!validInputs) { - throw new Error( - 'Invalid Networks and Operator pairs used in Agent initialization', - ) - } - // Note on undefineds: `lodash.zip` can return `undefined` if array lengths are - // uneven, but we have just checked that. - const networksAndOperators = zip(networks, operators).map(pair => { - const [network, operator] = pair - return { network: network!, operator: operator! } - }) - return new MultiNetworks(networksAndOperators, networkAndOperatorIdentity) -} - export class Agent { logger: Logger metrics: Metrics graphNode: GraphNode - multiNetworks: MultiNetworks + network: Network + operator: Operator indexerManagement: IndexerManagementClient offchainSubgraphs: SubgraphDeploymentID[] deploymentManagement: DeploymentManagementMode @@ -197,10 +135,8 @@ export class Agent { this.metrics = configs.metrics this.graphNode = configs.graphNode this.indexerManagement = configs.indexerManagement - this.multiNetworks = createMultiNetworks( - configs.networks, - configs.operators, - ) + this.network = configs.network + this.operator = configs.operator this.offchainSubgraphs = configs.offchainSubgraphs this.deploymentManagement = configs.deploymentManagement this.pollingInterval = configs.pollingInterval @@ -226,24 +162,20 @@ export class Agent { // * Ensure NetworkSubgraph is indexing // * Register the Indexer in the Network // -------------------------------------------------------------------------------- - await this.multiNetworks.map( - async ({ network, operator }: NetworkAndOperator) => { - try { - await operator.ensureGlobalIndexingRule() - await this.ensureAllSubgraphsIndexing(network) - await network.provision() - await network.register() - } catch (err) { - this.logger.critical( - `Failed to prepare indexer for ${network.specification.networkIdentifier}`, - { - error: err.message, - }, - ) - process.exit(1) - } - }, - ) + try { + await this.operator.ensureGlobalIndexingRule() + await this.ensureAllSubgraphsIndexing(this.network) + await this.network.provision() + await this.network.register() + } catch (err) { + this.logger.critical( + `Failed to prepare indexer for ${this.network.specification.networkIdentifier}`, + { + error: err.message, + }, + ) + process.exit(1) + } this.reconciliationLoop() return this @@ -253,68 +185,65 @@ export class Agent { const requestIntervalSmall = this.pollingInterval const requestIntervalLarge = this.pollingInterval * 5 const logger = this.logger.child({ component: 'ReconciliationLoop' }) - const currentEpochNumber: Eventual> = - sequentialTimerMap( - { logger, milliseconds: requestIntervalLarge }, - async () => - await this.multiNetworks.map(({ network }) => { - logger.trace('Fetching current epoch number', { - protocolNetwork: network.specification.networkIdentifier, - }) - return network.networkMonitor.currentEpochNumber() - }), - { - onError: error => - logger.warn(`Failed to fetch current epoch`, { error }), - }, - ) + const network = this.network + const operator = this.operator - const maxAllocationDuration: Eventual< - NetworkMapped - > = sequentialTimerMap( + const currentEpochNumber: Eventual = sequentialTimerMap( { logger, milliseconds: requestIntervalLarge }, - () => - this.multiNetworks.map(({ network }) => { - logger.trace('Fetching max allocation duration', { - protocolNetwork: network.specification.networkIdentifier, - }) - return network.networkMonitor.maxAllocationDuration() - }), + async () => { + logger.trace('Fetching current epoch number', { + protocolNetwork: network.specification.networkIdentifier, + }) + return network.networkMonitor.currentEpochNumber() + }, { onError: error => - logger.warn(`Failed to fetch max allocation duration`, { error }), + logger.warn(`Failed to fetch current epoch`, { error }), }, ) - const indexingRules: Eventual> = + const maxAllocationDuration: Eventual = + sequentialTimerMap( + { logger, milliseconds: requestIntervalLarge }, + () => { + logger.trace('Fetching max allocation duration', { + protocolNetwork: network.specification.networkIdentifier, + }) + return network.networkMonitor.maxAllocationDuration() + }, + { + onError: error => + logger.warn(`Failed to fetch max allocation duration`, { error }), + }, + ) + + const indexingRules: Eventual = sequentialTimerMap( { logger, milliseconds: requestIntervalSmall }, async () => { - return this.multiNetworks.map(async ({ network, operator }) => { - logger.trace('Fetching indexing rules', { - protocolNetwork: network.specification.networkIdentifier, - }) - let rules = await operator.indexingRules(true) - const subgraphRuleIds = rules - .filter( - rule => rule.identifierType == SubgraphIdentifierType.SUBGRAPH, - ) - .map(rule => rule.identifier!) - const subgraphsMatchingRules = - await network.networkMonitor.subgraphs(subgraphRuleIds) - if (subgraphsMatchingRules.length >= 1) { - const epochLength = - await network.contracts.EpochManager.epochLength() - const blockPeriod = 15 - const bufferPeriod = Number(epochLength) * blockPeriod * 100 // 100 epochs - rules = convertSubgraphBasedRulesToDeploymentBased( - rules, - subgraphsMatchingRules, - bufferPeriod, - ) - } - return rules + logger.trace('Fetching indexing rules', { + protocolNetwork: network.specification.networkIdentifier, }) + let rules = await operator.indexingRules(true) + const subgraphRuleIds = rules + .filter( + rule => rule.identifierType == SubgraphIdentifierType.SUBGRAPH, + ) + .map(rule => rule.identifier!) + const subgraphsMatchingRules = + await network.networkMonitor.subgraphs(subgraphRuleIds) + if (subgraphsMatchingRules.length >= 1) { + const epochLength = + await network.contracts.EpochManager.epochLength() + const blockPeriod = 15 + const bufferPeriod = Number(epochLength) * blockPeriod * 100 // 100 epochs + rules = convertSubgraphBasedRulesToDeploymentBased( + rules, + subgraphsMatchingRules, + bufferPeriod, + ) + } + return rules }, { onError: error => @@ -351,16 +280,15 @@ export class Agent { }, ) - const networkDeployments: Eventual> = + const networkDeployments: Eventual = sequentialTimerMap( { logger, milliseconds: requestIntervalSmall }, - async () => - await this.multiNetworks.map(({ network }) => { - logger.trace('Fetching network deployments', { - protocolNetwork: network.specification.networkIdentifier, - }) - return network.networkMonitor.subgraphDeployments() - }), + async () => { + logger.trace('Fetching network deployments', { + protocolNetwork: network.specification.networkIdentifier, + }) + return network.networkMonitor.subgraphDeployments() + }, { onError: error => logger.warn( @@ -370,52 +298,40 @@ export class Agent { }, ) - const networkDeploymentAllocationDecisions: Eventual< - NetworkMapped - > = join({ - networkDeployments, - indexingRules, - }).tryMap( - async ({ indexingRules, networkDeployments }) => { - return this.multiNetworks.mapNetworkMapped( - this.multiNetworks.zip(indexingRules, networkDeployments), - async ( - { network }: NetworkAndOperator, - [indexingRules, networkDeployments]: [ - IndexingRuleAttributes[], - SubgraphDeployment[], - ], - ) => { - // Skip evaluation entirely for networks in manual allocation mode - if ( - network.specification.indexerOptions.allocationManagementMode === - AllocationManagementMode.MANUAL - ) { - logger.trace( - `Skipping deployment evaluation since AllocationManagementMode = 'manual'`, - { - protocolNetwork: network.specification.networkIdentifier, - }, - ) - return [] - } + const networkDeploymentAllocationDecisions: Eventual = + join({ + networkDeployments, + indexingRules, + }).tryMap( + async ({ indexingRules, networkDeployments }) => { + // Skip evaluation entirely for networks in manual allocation mode + if ( + network.specification.indexerOptions.allocationManagementMode === + AllocationManagementMode.MANUAL + ) { + logger.trace( + `Skipping deployment evaluation since AllocationManagementMode = 'manual'`, + { + protocolNetwork: network.specification.networkIdentifier, + }, + ) + return [] + } - // Identify subgraph deployments on the network that are worth picking up; - // these may overlap with the ones we're already indexing - logger.trace('Evaluating which deployments are worth allocating to') - return indexingRules.length === 0 - ? [] - : evaluateDeployments(logger, networkDeployments, indexingRules) - }, - ) - }, - { - onError: error => - logger.warn(`Failed to evaluate deployments, trying again later`, { - error, - }), - }, - ) + // Identify subgraph deployments on the network that are worth picking up; + // these may overlap with the ones we're already indexing + logger.trace('Evaluating which deployments are worth allocating to') + return indexingRules.length === 0 + ? [] + : evaluateDeployments(logger, networkDeployments, indexingRules) + }, + { + onError: error => + logger.warn(`Failed to evaluate deployments, trying again later`, { + error, + }), + }, + ) // let targetDeployments be an union of targetAllocations // and offchain subgraphs. @@ -425,12 +341,14 @@ export class Agent { }).tryMap( async ({ indexingRules, networkDeploymentAllocationDecisions }) => { logger.trace('Resolving target deployments') - const targetDeploymentIDs: Set = - consolidateAllocationDecisions(networkDeploymentAllocationDecisions) + const targetDeploymentIDs: Set = new Set( + networkDeploymentAllocationDecisions + .filter(decision => decision.toAllocate === true) + .map(decision => decision.deployment), + ) // Add offchain subgraphs to the deployment list from rules - Object.values(indexingRules) - .flat() + indexingRules .filter( rule => rule?.decisionBasis === IndexingDecisionBasis.OFFCHAIN, ) @@ -451,23 +369,21 @@ export class Agent { }, ) - const activeAllocations: Eventual> = - sequentialTimerMap( - { logger, milliseconds: requestIntervalSmall }, - () => - this.multiNetworks.map(({ network }) => { - logger.trace('Fetching active allocations', { - protocolNetwork: network.specification.networkIdentifier, - }) - return network.networkMonitor.allocations(AllocationStatus.ACTIVE) - }), - { - onError: () => - logger.warn( - `Failed to obtain active allocations, trying again later`, - ), - }, - ) + const activeAllocations: Eventual = sequentialTimerMap( + { logger, milliseconds: requestIntervalSmall }, + () => { + logger.trace('Fetching active allocations', { + protocolNetwork: network.specification.networkIdentifier, + }) + return network.networkMonitor.allocations(AllocationStatus.ACTIVE) + }, + { + onError: () => + logger.warn( + `Failed to obtain active allocations, trying again later`, + ), + }, + ) // `activeAllocations` is used to trigger this Eventual, but not really needed // inside. @@ -477,20 +393,14 @@ export class Agent { }).tryMap( // eslint-disable-next-line @typescript-eslint/no-unused-vars async ({ activeAllocations: _, currentEpochNumber }) => { - const allocationsByNetwork = await this.multiNetworks.mapNetworkMapped( + logger.trace('Fetching recently closed allocations', { + protocolNetwork: network.specification.networkIdentifier, currentEpochNumber, - async ({ network }, epochNumber): Promise => { - logger.trace('Fetching recently closed allocations', { - protocolNetwork: network.specification.networkIdentifier, - currentEpochNumber, - }) - return network.networkMonitor.recentlyClosedAllocations( - epochNumber, - 1, - ) - }, + }) + return network.networkMonitor.recentlyClosedAllocations( + currentEpochNumber, + 1, ) - return Object.values(allocationsByNetwork).flat() }, { onError: () => @@ -500,26 +410,21 @@ export class Agent { }, ) - const disputableAllocations: Eventual> = join({ + const disputableAllocations: Eventual = join({ currentEpochNumber, activeDeployments, }).tryMap( - async ({ currentEpochNumber, activeDeployments }) => - this.multiNetworks.mapNetworkMapped( + async ({ currentEpochNumber, activeDeployments }) => { + logger.trace('Fetching disputable allocations', { + protocolNetwork: network.specification.networkIdentifier, currentEpochNumber, - ({ network }: NetworkAndOperator, currentEpochNumber: number) => { - logger.trace('Fetching disputable allocations', { - protocolNetwork: network.specification.networkIdentifier, - currentEpochNumber, - }) - return network.networkMonitor.disputableAllocations( - currentEpochNumber, - activeDeployments, - 0, - ) - }, - ), - + }) + return network.networkMonitor.disputableAllocations( + currentEpochNumber, + activeDeployments, + 0, + ) + }, { onError: () => logger.warn( @@ -554,31 +459,16 @@ export class Agent { }) try { - const disputableEpochs = await this.multiNetworks.mapNetworkMapped( - currentEpochNumber, - async ( - { network }: NetworkAndOperator, - currentEpochNumber: number, - ) => - currentEpochNumber - - (network.specification.indexerOptions - .poiDisputableEpochs as number), - ) + const disputableEpoch = + currentEpochNumber - + (network.specification.indexerOptions.poiDisputableEpochs as number) // Find disputable allocations - await this.multiNetworks.mapNetworkMapped( - this.multiNetworks.zip(disputableEpochs, disputableAllocations), - async ( - { network, operator }: NetworkAndOperator, - [disputableEpoch, disputableAllocations]: [number, Allocation[]], - ): Promise => { - await this.identifyPotentialDisputes( - disputableAllocations, - disputableEpoch, - operator, - network, - ) - }, + await this.identifyPotentialDisputes( + disputableAllocations, + disputableEpoch, + operator, + network, ) } catch (err) { logger.warn(`Failed POI dispute monitoring`, { err }) @@ -586,7 +476,7 @@ export class Agent { const eligibleAllocations: Allocation[] = [ ...recentlyClosedAllocations, - ...Object.values(activeAllocations).flat(), + ...activeAllocations, ] // Reconcile deployments @@ -783,8 +673,6 @@ export class Agent { }) } - // This function assumes that allocations and deployments passed to it have already - // been retrieved from multiple networks. async reconcileDeployments( activeDeployments: SubgraphDeploymentID[], targetDeployments: SubgraphDeploymentID[], @@ -796,15 +684,13 @@ export class Agent { // Ensure the network subgraph deployment is _always_ indexed // ---------------------------------------------------------------------------------------- let indexingNetworkSubgraph = false - await this.multiNetworks.map(async ({ network }) => { - if (network.networkSubgraph.deployment) { - const networkDeploymentID = network.networkSubgraph.deployment.id - if (!deploymentInList(targetDeployments, networkDeploymentID)) { - targetDeployments.push(networkDeploymentID) - indexingNetworkSubgraph = true - } + if (this.network.networkSubgraph.deployment) { + const networkDeploymentID = this.network.networkSubgraph.deployment.id + if (!deploymentInList(targetDeployments, networkDeploymentID)) { + targetDeployments.push(networkDeploymentID) + indexingNetworkSubgraph = true } - }) + } // ---------------------------------------------------------------------------------------- // Inspect Deployments and Networks @@ -924,17 +810,9 @@ export class Agent { expiredAllocations, async (allocation: Allocation) => { try { - if (allocation.isLegacy) { - const onChainAllocation = - await network.contracts.LegacyStaking.getAllocation(allocation.id) - return onChainAllocation.closedAtEpoch == 0n - } else { - const onChainAllocation = - await network.contracts.SubgraphService.getAllocation( - allocation.id, - ) - return onChainAllocation.closedAt == 0n - } + const onChainAllocation = + await network.contracts.SubgraphService.getAllocation(allocation.id) + return onChainAllocation.closedAt == 0n } catch (err) { this.logger.warn( `Failed to cross-check allocation state with contracts; assuming it needs to be closed`, @@ -1051,115 +929,92 @@ export class Agent { } async reconcileActions( - networkDeploymentAllocationDecisions: NetworkMapped, - epoch: NetworkMapped, - maxAllocationDuration: NetworkMapped, + allocationDecisions: AllocationDecision[], + epoch: number, + maxAllocationDuration: HorizonTransitionValue, ): Promise { + const network = this.network + const operator = this.operator + // -------------------------------------------------------------------------------- // Filter out networks set to `manual` allocation management mode, and ensure the // Network Subgraph is NEVER allocated towards // -------------------------------------------------------------------------------- - const validatedAllocationDecisions = - await this.multiNetworks.mapNetworkMapped( - networkDeploymentAllocationDecisions, - async ( - { network }: NetworkAndOperator, - allocationDecisions: AllocationDecision[], - ) => { - if ( - network.specification.indexerOptions.allocationManagementMode === - AllocationManagementMode.MANUAL - ) { - this.logger.trace( - `Skipping allocation reconciliation since AllocationManagementMode = 'manual'`, - { - protocolNetwork: network.specification.networkIdentifier, - targetDeployments: allocationDecisions - .filter(decision => decision.toAllocate) - .map(decision => decision.deployment.ipfsHash), - }, - ) - return [] as AllocationDecision[] - } - const networkSubgraphDeployment = network.networkSubgraph.deployment - if ( - networkSubgraphDeployment && - !network.specification.indexerOptions.allocateOnNetworkSubgraph - ) { - const networkSubgraphIndex = allocationDecisions.findIndex( - decision => - decision.deployment.bytes32 == - networkSubgraphDeployment.id.bytes32, - ) - if (networkSubgraphIndex >= 0) { - allocationDecisions[networkSubgraphIndex].toAllocate = false - } - } - return allocationDecisions + if ( + network.specification.indexerOptions.allocationManagementMode === + AllocationManagementMode.MANUAL + ) { + this.logger.trace( + `Skipping allocation reconciliation since AllocationManagementMode = 'manual'`, + { + protocolNetwork: network.specification.networkIdentifier, + targetDeployments: allocationDecisions + .filter(decision => decision.toAllocate) + .map(decision => decision.deployment.ipfsHash), }, ) + return + } + + const networkSubgraphDeployment = network.networkSubgraph.deployment + if ( + networkSubgraphDeployment && + !network.specification.indexerOptions.allocateOnNetworkSubgraph + ) { + const networkSubgraphIndex = allocationDecisions.findIndex( + decision => + decision.deployment.bytes32 == networkSubgraphDeployment.id.bytes32, + ) + if (networkSubgraphIndex >= 0) { + allocationDecisions[networkSubgraphIndex].toAllocate = false + } + } //---------------------------------------------------------------------------------------- - // For every network, loop through all deployments and queue allocation actions if needed + // Loop through all deployments and queue allocation actions if needed //---------------------------------------------------------------------------------------- - await this.multiNetworks.mapNetworkMapped( - this.multiNetworks.zip3( - validatedAllocationDecisions, - epoch, - maxAllocationDuration, - ), - async ( - { network, operator }: NetworkAndOperator, - [ - allocationDecisions, - epoch, - maxAllocationDuration, - ]: ActionReconciliationContext, - ) => { - // Do nothing if there are already approved actions in the queue awaiting execution - const approvedActions = await operator.fetchActions({ - status: ActionStatus.APPROVED, - protocolNetwork: network.specification.networkIdentifier, - }) - if (approvedActions.length > 0) { - this.logger.info( - `There are ${approvedActions.length} approved actions awaiting execution, will reconcile with the network once they are executed`, - { protocolNetwork: network.specification.networkIdentifier }, - ) - return - } + // Do nothing if there are already approved actions in the queue awaiting execution + const approvedActions = await operator.fetchActions({ + status: ActionStatus.APPROVED, + protocolNetwork: network.specification.networkIdentifier, + }) + if (approvedActions.length > 0) { + this.logger.info( + `There are ${approvedActions.length} approved actions awaiting execution, will reconcile with the network once they are executed`, + { protocolNetwork: network.specification.networkIdentifier }, + ) + return + } - // Accuracy check: re-fetch allocations to ensure that we have a fresh state since the - // start of the reconciliation loop. This means we don't use the allocations coming from - // the Eventual input. - const activeAllocations: Allocation[] = - await network.networkMonitor.allocations(AllocationStatus.ACTIVE) + // Accuracy check: re-fetch allocations to ensure that we have a fresh state since the + // start of the reconciliation loop. This means we don't use the allocations coming from + // the Eventual input. + const activeAllocations: Allocation[] = + await network.networkMonitor.allocations(AllocationStatus.ACTIVE) - this.logger.trace(`Reconcile allocation actions`, { - protocolNetwork: network.specification.networkIdentifier, - epoch, - maxAllocationDuration, - targetDeployments: allocationDecisions - .filter(decision => decision.toAllocate) - .map(decision => decision.deployment.ipfsHash), - activeAllocations: activeAllocations.map(allocation => ({ - id: allocation.id, - deployment: allocation.subgraphDeployment.id.ipfsHash, - createdAtEpoch: allocation.createdAtEpoch, - })), - }) + this.logger.trace(`Reconcile allocation actions`, { + protocolNetwork: network.specification.networkIdentifier, + epoch, + maxAllocationDuration, + targetDeployments: allocationDecisions + .filter(decision => decision.toAllocate) + .map(decision => decision.deployment.ipfsHash), + activeAllocations: activeAllocations.map(allocation => ({ + id: allocation.id, + deployment: allocation.subgraphDeployment.id.ipfsHash, + createdAtEpoch: allocation.createdAtEpoch, + })), + }) - return pMap(allocationDecisions, async decision => - this.reconcileDeploymentAllocationAction( - decision, - activeAllocations, - epoch, - maxAllocationDuration, - network, - operator, - ), - ) - }, + await pMap(allocationDecisions, async decision => + this.reconcileDeploymentAllocationAction( + decision, + activeAllocations, + epoch, + maxAllocationDuration, + network, + operator, + ), ) } @@ -1216,18 +1071,3 @@ export class Agent { } } } - -export interface AllocationDecisionInterface { - toAllocate: boolean - deployment: SubgraphDeploymentID -} -export function consolidateAllocationDecisions( - allocationDecisions: Record, -): Set { - return new Set( - Object.values(allocationDecisions) - .flat() - .filter(decision => decision.toAllocate === true) - .map(decision => decision.deployment), - ) -} diff --git a/packages/indexer-agent/src/commands/start.ts b/packages/indexer-agent/src/commands/start.ts index 58e0fea8f..52fd71584 100644 --- a/packages/indexer-agent/src/commands/start.ts +++ b/packages/indexer-agent/src/commands/start.ts @@ -19,7 +19,6 @@ import { GraphNode, indexerError, IndexerErrorCode, - MultiNetworks, Network, Operator, registerIndexerErrorMetrics, @@ -622,10 +621,8 @@ export async function run( // -------------------------------------------------------------------------------- // * Indexer Management (GraphQL) Server // -------------------------------------------------------------------------------- - const multiNetworks = new MultiNetworks( - networks, - (n: Network) => n.specification.networkIdentifier, - ) + // Single network mode: use the first (and only) network + const network = networks[0] const indexerManagementClient = await createIndexerManagementClient({ models: managementModels, @@ -638,7 +635,7 @@ export async function run( parallelAllocations: 1, }, }, - multiNetworks, + network, }) // -------------------------------------------------------------------------------- @@ -661,9 +658,8 @@ export async function run( await createSyncingServer({ logger, - networkSubgraphs: await multiNetworks.map( - async network => network.networkSubgraph, - ), + networkSubgraph: network.networkSubgraph, + networkIdentifier: network.specification.networkIdentifier, port: argv.syncingPort, }) logger.info(`Successfully launched syncing server`) @@ -671,10 +667,10 @@ export async function run( // -------------------------------------------------------------------------------- // * Operator // -------------------------------------------------------------------------------- - const operators: Operator[] = await pMap( - networkSpecifications, - async (spec: NetworkSpecification) => - new Operator(logger, indexerManagementClient, spec), + const operator = new Operator( + logger, + indexerManagementClient, + network.specification, ) // -------------------------------------------------------------------------------- @@ -684,9 +680,9 @@ export async function run( logger, metrics, graphNode, - operators, + operator, indexerManagement: indexerManagementClient, - networks, + network, deploymentManagement: argv.deploymentManagement, offchainSubgraphs: argv.offchainSubgraphs.map( (s: string) => new SubgraphDeploymentID(s), diff --git a/packages/indexer-agent/src/syncing-server.ts b/packages/indexer-agent/src/syncing-server.ts index 2c08da48b..37719a9e9 100644 --- a/packages/indexer-agent/src/syncing-server.ts +++ b/packages/indexer-agent/src/syncing-server.ts @@ -5,21 +5,19 @@ import bodyParser from 'body-parser' import morgan from 'morgan' import { Logger } from '@graphprotocol/common-ts' import { parse } from 'graphql' -import { - NetworkMapped, - SubgraphClient, - resolveChainId, -} from '@graphprotocol/indexer-common' +import { SubgraphClient, resolveChainId } from '@graphprotocol/indexer-common' export interface CreateSyncingServerOptions { logger: Logger - networkSubgraphs: NetworkMapped + networkSubgraph: SubgraphClient + networkIdentifier: string port: number } export const createSyncingServer = async ({ logger, - networkSubgraphs, + networkSubgraph, + networkIdentifier: configuredNetworkIdentifier, port, }: CreateSyncingServerOptions): Promise => { logger = logger.child({ component: 'SyncingServer' }) @@ -64,8 +62,7 @@ export const createSyncingServer = async ({ .send(`Unknown network identifier: '${unvalidatedNetworkIdentifier}'`) } - const networkSubgraph = networkSubgraphs[networkIdentifier] - if (!networkSubgraph) { + if (networkIdentifier !== configuredNetworkIdentifier) { return res .status(404) .send( diff --git a/packages/indexer-agent/src/types.ts b/packages/indexer-agent/src/types.ts index a93c50997..98f851e60 100644 --- a/packages/indexer-agent/src/types.ts +++ b/packages/indexer-agent/src/types.ts @@ -7,20 +7,13 @@ import { Operator, } from '@graphprotocol/indexer-common' -// Represents a pair of Network and Operator instances belonging to the same protocol -// network. Used when mapping over multiple protocol networks. -export type NetworkAndOperator = { - network: Network - operator: Operator -} - export interface AgentConfigs { logger: Logger metrics: Metrics graphNode: GraphNode - operators: Operator[] + operator: Operator indexerManagement: IndexerManagementClient - networks: Network[] + network: Network deploymentManagement: DeploymentManagementMode offchainSubgraphs: SubgraphDeploymentID[] pollingInterval: number diff --git a/packages/indexer-cli/src/__tests__/cli.test.ts b/packages/indexer-cli/src/__tests__/cli.test.ts index 40fcda30c..d1b0beee5 100644 --- a/packages/indexer-cli/src/__tests__/cli.test.ts +++ b/packages/indexer-cli/src/__tests__/cli.test.ts @@ -1,10 +1,10 @@ -import { cliTest, setupMultiNetworks, teardown } from './util' +import { cliTest, setupNetwork, teardown } from './util' import path from 'path' const baseDir = path.join(__dirname) describe('Indexer cli tests', () => { - beforeEach(setupMultiNetworks) + beforeEach(setupNetwork) afterEach(teardown) describe('General', () => { diff --git a/packages/indexer-cli/src/__tests__/indexer/actions.test.ts b/packages/indexer-cli/src/__tests__/indexer/actions.test.ts index 6d432b3d4..5ceba9eba 100644 --- a/packages/indexer-cli/src/__tests__/indexer/actions.test.ts +++ b/packages/indexer-cli/src/__tests__/indexer/actions.test.ts @@ -2,7 +2,7 @@ import { cliTest, deleteFromAllTables, seedActions, - setupMultiNetworks, + setupNetwork, teardown, } from '../util' import path from 'path' @@ -10,7 +10,7 @@ import path from 'path' const baseDir = path.join(__dirname, '..') describe('Indexer actions tests', () => { describe('With indexer management server', () => { - beforeAll(setupMultiNetworks) + beforeAll(setupNetwork) afterAll(teardown) beforeEach(seedActions) afterEach(deleteFromAllTables) diff --git a/packages/indexer-cli/src/__tests__/indexer/cost.test.ts b/packages/indexer-cli/src/__tests__/indexer/cost.test.ts index f5f5e6443..f6678e1e6 100644 --- a/packages/indexer-cli/src/__tests__/indexer/cost.test.ts +++ b/packages/indexer-cli/src/__tests__/indexer/cost.test.ts @@ -4,7 +4,7 @@ import { connect, deleteFromAllTables, seedCostModels, - setupSingleNetwork, + setupNetwork, } from '../util' import path from 'path' @@ -12,7 +12,7 @@ const baseDir = path.join(__dirname, '..') describe('Indexer cost tests', () => { describe('With indexer management server', () => { - beforeAll(setupSingleNetwork) + beforeAll(setupNetwork) afterAll(teardown) beforeEach(seedCostModels) afterEach(deleteFromAllTables) diff --git a/packages/indexer-cli/src/__tests__/indexer/rules.test.ts b/packages/indexer-cli/src/__tests__/indexer/rules.test.ts index 67474c0a6..373e7a4ae 100644 --- a/packages/indexer-cli/src/__tests__/indexer/rules.test.ts +++ b/packages/indexer-cli/src/__tests__/indexer/rules.test.ts @@ -4,7 +4,7 @@ import { teardown, deleteFromAllTables, seedIndexingRules, - setupMultiNetworks, + setupNetwork, } from '../util' import path from 'path' @@ -12,7 +12,7 @@ const baseDir = path.join(__dirname, '..') describe('Indexer rules tests', () => { describe('With indexer management server', () => { - beforeAll(setupMultiNetworks) + beforeAll(setupNetwork) afterAll(teardown) beforeEach(seedIndexingRules) afterEach(deleteFromAllTables) @@ -58,9 +58,9 @@ describe('Indexer rules tests', () => { }, ) cliTest( - 'Indexer rules start - no network', + 'Indexer rules start - no identifier', ['indexer', 'rules', 'start'], - 'references/indexer-rules-no-network', + 'references/indexer-rules-no-identifier', { expectedExitCode: 1, cwd: baseDir, @@ -125,9 +125,9 @@ describe('Indexer rules tests', () => { }, ) cliTest( - 'Indexer rules prepare - no network', + 'Indexer rules prepare - no identifier', ['indexer', 'rules', 'prepare'], - 'references/indexer-rules-no-network', + 'references/indexer-rules-no-identifier', { expectedExitCode: 1, cwd: baseDir, @@ -175,9 +175,9 @@ describe('Indexer rules tests', () => { }, ) cliTest( - 'Indexer rules stop - no network', + 'Indexer rules stop - no identifier', ['indexer', 'rules', 'stop'], - 'references/indexer-rules-no-network', + 'references/indexer-rules-no-identifier', { expectedExitCode: 1, cwd: baseDir, @@ -225,9 +225,9 @@ describe('Indexer rules tests', () => { }, ) cliTest( - 'Indexer rules maybe - no network', + 'Indexer rules maybe - no identifier', ['indexer', 'rules', 'maybe'], - 'references/indexer-rules-no-network', + 'references/indexer-rules-no-identifier', { expectedExitCode: 1, cwd: baseDir, @@ -275,9 +275,9 @@ describe('Indexer rules tests', () => { }, ) cliTest( - 'Indexer rules clear - no network', + 'Indexer rules clear - no identifier', ['indexer', 'rules', 'clear'], - 'references/indexer-rules-no-network', + 'references/indexer-rules-no-identifier', { expectedExitCode: 1, cwd: baseDir, @@ -342,9 +342,9 @@ describe('Indexer rules tests', () => { }, ) cliTest( - 'Indexer rules delete - no network', + 'Indexer rules delete - no identifier', ['indexer', 'rules', 'delete'], - 'references/indexer-rules-no-network', + 'references/indexer-rules-no-identifier', { expectedExitCode: 1, cwd: baseDir, @@ -520,7 +520,7 @@ describe('Indexer rules tests', () => { cliTest( 'Indexer rules set - no args', ['indexer', 'rules', 'set'], - 'references/indexer-rules-no-network', + 'references/indexer-rules-no-identifier', { expectedExitCode: 1, cwd: baseDir, diff --git a/packages/indexer-cli/src/__tests__/references/indexer-rules-no-network.stderr b/packages/indexer-cli/src/__tests__/references/indexer-rules-no-network.stderr deleted file mode 100644 index 0c57fdb91..000000000 --- a/packages/indexer-cli/src/__tests__/references/indexer-rules-no-network.stderr +++ /dev/null @@ -1 +0,0 @@ -Error: The option '--network' is required diff --git a/packages/indexer-cli/src/__tests__/util.ts b/packages/indexer-cli/src/__tests__/util.ts index 41df97e13..0639cff56 100644 --- a/packages/indexer-cli/src/__tests__/util.ts +++ b/packages/indexer-cli/src/__tests__/util.ts @@ -18,7 +18,6 @@ import { IndexerManagementModels, IndexingDecisionBasis, loadTestYamlConfig, - MultiNetworks, Network, QueryFeeModels, specification, @@ -33,7 +32,6 @@ import { parseGRT, SubgraphDeploymentID, } from '@graphprotocol/common-ts' -import cloneDeep from 'lodash.clonedeep' const INDEXER_SAVE_CLI_TEST_OUTPUT: boolean = !!process.env.INDEXER_SAVE_CLI_TEST_OUTPUT && @@ -71,15 +69,7 @@ function stripAnsi(str: string): string { return str.replace(new RegExp(pattern, 'g'), '') } -export const setupMultiNetworks = async () => { - return await setup(true) -} - -export const setupSingleNetwork = async () => { - return await setup(false) -} - -export const setup = async (multiNetworksEnabled: boolean) => { +export const setupNetwork = async () => { logger = createLogger({ name: 'Setup', async: false, @@ -113,16 +103,6 @@ export const setup = async (multiNetworksEnabled: boolean) => { metrics, ) - const fakeMainnetNetwork = cloneDeep(network) as Network - fakeMainnetNetwork.specification.networkIdentifier = 'eip155:1' - - const multiNetworks = multiNetworksEnabled - ? new MultiNetworks( - [network, fakeMainnetNetwork], - (n: Network) => n.specification.networkIdentifier, - ) - : new MultiNetworks([network], (n: Network) => n.specification.networkIdentifier) - const defaults: IndexerManagementDefaults = { globalIndexingRule: { allocationAmount: parseGRT('100'), @@ -137,7 +117,7 @@ export const setup = async (multiNetworksEnabled: boolean) => { graphNode, logger, defaults, - multiNetworks, + network, }) server = await createIndexerManagementServer({ diff --git a/packages/indexer-cli/src/actions.ts b/packages/indexer-cli/src/actions.ts index 74f464929..5270f7ebc 100644 --- a/packages/indexer-cli/src/actions.ts +++ b/packages/indexer-cli/src/actions.ts @@ -35,7 +35,7 @@ export async function buildActionInput( reason: string, status: ActionStatus, priority: number, - protocolNetwork: string, + protocolNetwork?: string, ): Promise { await validateActionInput(type, actionParams) diff --git a/packages/indexer-cli/src/command-helpers.ts b/packages/indexer-cli/src/command-helpers.ts index 212a9b7c8..b1fa236b2 100644 --- a/packages/indexer-cli/src/command-helpers.ts +++ b/packages/indexer-cli/src/command-helpers.ts @@ -230,22 +230,15 @@ export function suggestCommands( } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function extractProtocolNetworkOption( - options: { - [key: string]: any - }, - required = false, -): string | undefined { +export function extractProtocolNetworkOption(options: { + [key: string]: any +}): string | undefined { const { n, network } = options // Tries to extract the --network option from Gluegun options. - // Throws if required is set to true and the option is not found. + // Returns undefined if not provided (single network mode will use context network). if (!n && !network) { - if (required) { - throw new Error("The option '--network' is required") - } else { - return undefined - } + return undefined } // Check for invalid usage @@ -264,13 +257,14 @@ export function extractProtocolNetworkOption( } } -// Same as `extractProtocolNetworkOption`, but always require the --network option to be set -export function requireProtocolNetworkOption(options: { [key: string]: any }): string { - const protocolNetwork = extractProtocolNetworkOption(options, true) - if (!protocolNetwork) { - throw new Error("The option '--network' is required") - } - return protocolNetwork +/** + * @deprecated Use extractProtocolNetworkOption instead. Network is no longer required + * as the system operates in single-network mode. + */ +export function requireProtocolNetworkOption(options: { + [key: string]: any +}): string | undefined { + return extractProtocolNetworkOption(options) } export function wrapCell(value: unknown, wrapWidth: number): string { diff --git a/packages/indexer-cli/src/commands/indexer/allocations/close.ts b/packages/indexer-cli/src/commands/indexer/allocations/close.ts index 3e4c033c4..d9b114f01 100644 --- a/packages/indexer-cli/src/commands/indexer/allocations/close.ts +++ b/packages/indexer-cli/src/commands/indexer/allocations/close.ts @@ -83,7 +83,7 @@ module.exports = { spinner.text = `Closing allocation '${id}` try { - const protocolNetwork = extractProtocolNetworkOption(parameters.options, true) + const protocolNetwork = extractProtocolNetworkOption(parameters.options) if (!protocolNetwork) { throw new Error( diff --git a/packages/indexer-cli/src/commands/indexer/allocations/collect.ts b/packages/indexer-cli/src/commands/indexer/allocations/collect.ts index d0b293b17..331a5ce0f 100644 --- a/packages/indexer-cli/src/commands/indexer/allocations/collect.ts +++ b/packages/indexer-cli/src/commands/indexer/allocations/collect.ts @@ -52,7 +52,7 @@ module.exports = { spinner.text = `Collecting receipts for allocation '${id}` try { - const protocolNetwork = extractProtocolNetworkOption(parameters.options, true) + const protocolNetwork = extractProtocolNetworkOption(parameters.options) if (!protocolNetwork) { throw new Error( diff --git a/packages/indexer-cli/src/commands/indexer/allocations/create.ts b/packages/indexer-cli/src/commands/indexer/allocations/create.ts index f635e4d34..9047ec813 100644 --- a/packages/indexer-cli/src/commands/indexer/allocations/create.ts +++ b/packages/indexer-cli/src/commands/indexer/allocations/create.ts @@ -52,7 +52,7 @@ module.exports = { const [deploymentID, amount, indexNode] = parameters.array || [] try { - const protocolNetwork = extractProtocolNetworkOption(parameters.options, true) + const protocolNetwork = extractProtocolNetworkOption(parameters.options) if (!deploymentID || !amount || !protocolNetwork) { throw new Error( diff --git a/packages/indexer-cli/src/commands/indexer/allocations/get.ts b/packages/indexer-cli/src/commands/indexer/allocations/get.ts index 5be6a6f70..4d1cf0e43 100644 --- a/packages/indexer-cli/src/commands/indexer/allocations/get.ts +++ b/packages/indexer-cli/src/commands/indexer/allocations/get.ts @@ -46,7 +46,7 @@ module.exports = { } try { - const protocolNetwork = extractProtocolNetworkOption(parameters.options, true) + const protocolNetwork = extractProtocolNetworkOption(parameters.options) if (!['json', 'yaml', 'table'].includes(outputFormat)) { throw Error( diff --git a/packages/indexer-cli/src/commands/indexer/allocations/reallocate.ts b/packages/indexer-cli/src/commands/indexer/allocations/reallocate.ts index 95923c4ac..7d6f68e18 100644 --- a/packages/indexer-cli/src/commands/indexer/allocations/reallocate.ts +++ b/packages/indexer-cli/src/commands/indexer/allocations/reallocate.ts @@ -75,7 +75,7 @@ module.exports = { } try { - const protocolNetwork = extractProtocolNetworkOption(parameters.options, true) + const protocolNetwork = extractProtocolNetworkOption(parameters.options) if (!protocolNetwork) { throw new Error( diff --git a/packages/indexer-cli/src/commands/indexer/provision/add.ts b/packages/indexer-cli/src/commands/indexer/provision/add.ts index ce4be5257..c0fe0a710 100644 --- a/packages/indexer-cli/src/commands/indexer/provision/add.ts +++ b/packages/indexer-cli/src/commands/indexer/provision/add.ts @@ -42,7 +42,7 @@ module.exports = { throw new Error('Must provide an amount to add to the provision') } - const protocolNetwork = extractProtocolNetworkOption(parameters.options, true) + const protocolNetwork = extractProtocolNetworkOption(parameters.options) if (!['json', 'yaml', 'table'].includes(outputFormat)) { throw Error( diff --git a/packages/indexer-cli/src/commands/indexer/provision/get.ts b/packages/indexer-cli/src/commands/indexer/provision/get.ts index 31e9dad99..c8727df5c 100644 --- a/packages/indexer-cli/src/commands/indexer/provision/get.ts +++ b/packages/indexer-cli/src/commands/indexer/provision/get.ts @@ -37,7 +37,7 @@ module.exports = { } try { - const protocolNetwork = extractProtocolNetworkOption(parameters.options, true) + const protocolNetwork = extractProtocolNetworkOption(parameters.options) if (!['json', 'yaml', 'table'].includes(outputFormat)) { throw Error( diff --git a/packages/indexer-cli/src/commands/indexer/provision/list-thaw.ts b/packages/indexer-cli/src/commands/indexer/provision/list-thaw.ts index 167c2844a..eecaef782 100644 --- a/packages/indexer-cli/src/commands/indexer/provision/list-thaw.ts +++ b/packages/indexer-cli/src/commands/indexer/provision/list-thaw.ts @@ -36,7 +36,7 @@ module.exports = { } try { - const protocolNetwork = extractProtocolNetworkOption(parameters.options, true) + const protocolNetwork = extractProtocolNetworkOption(parameters.options) if (!['json', 'yaml', 'table'].includes(outputFormat)) { throw Error( diff --git a/packages/indexer-cli/src/commands/indexer/provision/remove.ts b/packages/indexer-cli/src/commands/indexer/provision/remove.ts index c585ef5aa..40352cdc5 100644 --- a/packages/indexer-cli/src/commands/indexer/provision/remove.ts +++ b/packages/indexer-cli/src/commands/indexer/provision/remove.ts @@ -38,7 +38,7 @@ module.exports = { } try { - const protocolNetwork = extractProtocolNetworkOption(parameters.options, true) + const protocolNetwork = extractProtocolNetworkOption(parameters.options) if (!['json', 'yaml', 'table'].includes(outputFormat)) { throw Error( diff --git a/packages/indexer-cli/src/commands/indexer/provision/thaw.ts b/packages/indexer-cli/src/commands/indexer/provision/thaw.ts index 2e4dda78a..fc6dc433e 100644 --- a/packages/indexer-cli/src/commands/indexer/provision/thaw.ts +++ b/packages/indexer-cli/src/commands/indexer/provision/thaw.ts @@ -42,7 +42,7 @@ module.exports = { throw new Error('Must provide an amount to thaw from the provision') } - const protocolNetwork = extractProtocolNetworkOption(parameters.options, true) + const protocolNetwork = extractProtocolNetworkOption(parameters.options) if (!['json', 'yaml', 'table'].includes(outputFormat)) { throw Error( diff --git a/packages/indexer-common/src/actions.ts b/packages/indexer-common/src/actions.ts index 68769dc9b..c67f10ec0 100644 --- a/packages/indexer-common/src/actions.ts +++ b/packages/indexer-common/src/actions.ts @@ -53,7 +53,12 @@ export interface ActionInput { reason: string status: ActionStatus priority: number | undefined - protocolNetwork: string + /** + * @deprecated LEGACY DEBT: Part of composite primary key for multi-network support. + * Multi-network has been removed but this field remains in the DB schema. + * Optional in API - resolvers will use the context's single network if not provided. + */ + protocolNetwork?: string isLegacy: boolean } diff --git a/packages/indexer-common/src/allocations/keys.ts b/packages/indexer-common/src/allocations/keys.ts index e3dcc7d1b..d00b8e82e 100644 --- a/packages/indexer-common/src/allocations/keys.ts +++ b/packages/indexer-common/src/allocations/keys.ts @@ -90,19 +90,6 @@ export const uniqueAllocationID = ( throw new Error(`Exhausted limit of 100 parallel allocations`) } -export const legacyAllocationIdProof = ( - signer: Signer, - indexerAddress: string, - allocationId: string, -): Promise => { - const messageHash = solidityPackedKeccak256( - ['address', 'address'], - [indexerAddress, allocationId], - ) - const messageHashBytes = getBytes(messageHash) - return signer.signMessage(messageHashBytes) -} - export const EIP712_ALLOCATION_ID_PROOF_TYPES = { AllocationIdProof: [ { name: 'indexer', type: 'address' }, diff --git a/packages/indexer-common/src/index.ts b/packages/indexer-common/src/index.ts index ab3eedd97..e22722403 100644 --- a/packages/indexer-common/src/index.ts +++ b/packages/indexer-common/src/index.ts @@ -15,5 +15,4 @@ export * from './types' export * from './utils' export * from './parsers' export * as specification from './network-specification' -export * from './multi-networks' export * from './sequential-timer' diff --git a/packages/indexer-common/src/indexer-management/__tests__/resolvers/actions.test.ts b/packages/indexer-common/src/indexer-management/__tests__/resolvers/actions.test.ts index ae09ce91c..3ba5be594 100644 --- a/packages/indexer-common/src/indexer-management/__tests__/resolvers/actions.test.ts +++ b/packages/indexer-common/src/indexer-management/__tests__/resolvers/actions.test.ts @@ -711,7 +711,8 @@ describe.skip('Actions', () => { priority: 0, // When writing directly to the database, `protocolNetwork` must be in the CAIP2-ID format. protocolNetwork: 'eip155:421614', - } as ActionInput + isLegacy: false, + } const proposedAction = { status: ActionStatus.QUEUED, @@ -722,7 +723,8 @@ describe.skip('Actions', () => { reason: 'indexingRule', priority: 0, protocolNetwork: 'arbitrum-sepolia', - } as ActionInput + isLegacy: false, + } await managementModels.Action.create(failedAction, { validate: true, @@ -766,7 +768,8 @@ describe.skip('Actions', () => { priority: 0, // When writing directly to the database, `protocolNetwork` must be in the CAIP2-ID format. protocolNetwork: 'eip155:421614', - } as ActionInput + isLegacy: false, + } const proposedAction = { status: ActionStatus.QUEUED, @@ -777,7 +780,8 @@ describe.skip('Actions', () => { reason: 'indexingRule', priority: 0, protocolNetwork: 'arbitrum-sepolia', - } as ActionInput + isLegacy: false, + } await managementModels.Action.create(successfulAction, { validate: true, @@ -819,7 +823,8 @@ describe.skip('Actions', () => { priority: 0, // When writing directly to the database, `protocolNetwork` must be in the CAIP2-ID format. protocolNetwork: 'eip155:421614', - } as ActionInput + isLegacy: false, + } const queuedAllocateAction = { status: ActionStatus.QUEUED, @@ -831,7 +836,8 @@ describe.skip('Actions', () => { reason: 'indexingRule', priority: 0, protocolNetwork: 'arbitrum-sepolia', - } as ActionInput + isLegacy: false, + } await managementModels.Action.create(queuedUnallocateAction, { validate: true, diff --git a/packages/indexer-common/src/indexer-management/__tests__/util.ts b/packages/indexer-common/src/indexer-management/__tests__/util.ts index 83e6ee409..0fa6b98b0 100644 --- a/packages/indexer-common/src/indexer-management/__tests__/util.ts +++ b/packages/indexer-common/src/indexer-management/__tests__/util.ts @@ -9,7 +9,6 @@ import { IndexerManagementClient, IndexerManagementDefaults, loadTestYamlConfig, - MultiNetworks, Network, specification, } from '@graphprotocol/indexer-common' @@ -68,17 +67,12 @@ export const createTestManagementClient = async ( network.specification.networkIdentifier = networkIdentifierOverride } - const multiNetworks = new MultiNetworks( - [network], - (n: Network) => n.specification.networkIdentifier, - ) - return await createIndexerManagementClient({ models: managementModels, graphNode, logger, defaults, - multiNetworks, + network, }) } diff --git a/packages/indexer-common/src/indexer-management/actions.ts b/packages/indexer-common/src/indexer-management/actions.ts index dc3df35c7..157932940 100644 --- a/packages/indexer-common/src/indexer-management/actions.ts +++ b/packages/indexer-common/src/indexer-management/actions.ts @@ -13,8 +13,6 @@ import { IndexerErrorCode, IndexerManagementModels, isActionFailure, - MultiNetworks, - NetworkMapped, Network, OrderDirection, GraphNode, @@ -23,37 +21,34 @@ import { import { Order, Transaction } from 'sequelize' import { Eventual, join, Logger } from '@graphprotocol/common-ts' -import groupBy from 'lodash.groupby' export class ActionManager { - declare multiNetworks: MultiNetworks + declare network: Network declare logger: Logger declare models: IndexerManagementModels - declare allocationManagers: NetworkMapped + declare allocationManager: AllocationManager executeBatchActionsPromise: Promise | undefined static async create( - multiNetworks: MultiNetworks, + network: Network, logger: Logger, models: IndexerManagementModels, graphNode: GraphNode, ): Promise { const actionManager = new ActionManager() - actionManager.multiNetworks = multiNetworks + actionManager.network = network actionManager.logger = logger.child({ component: 'ActionManager' }) actionManager.models = models - actionManager.allocationManagers = await multiNetworks.map(async (network) => { - return new AllocationManager( - logger.child({ - component: 'AllocationManager', - protocolNetwork: network.specification.networkIdentifier, - }), - models, - graphNode, - network, - ) - }) + actionManager.allocationManager = new AllocationManager( + logger.child({ + component: 'AllocationManager', + protocolNetwork: network.specification.networkIdentifier, + }), + models, + graphNode, + network, + ) logger.info('Begin monitoring the queue for approved actions to execute') await actionManager.monitorQueue() @@ -61,11 +56,7 @@ export class ActionManager { return actionManager } - private async batchReady( - approvedActions: Action[], - network: Network, - logger: Logger, - ): Promise { + private async batchReady(approvedActions: Action[], logger: Logger): Promise { logger.info('Batch ready?', { approvedActions, }) @@ -75,6 +66,8 @@ export class ActionManager { return false } + const network = this.network + // In auto management mode the worker will execute the batch if: // 1) Number of approved actions >= minimum batch size // or 2) Oldest affected allocation will expiring after the current epoch @@ -125,6 +118,9 @@ export class ActionManager { async monitorQueue(): Promise { const logger = this.logger.child({ component: 'QueueMonitor' }) + const network = this.network + const protocolNetwork = network.specification.networkIdentifier + const approvedActions: Eventual = sequentialTimerMap( { logger, @@ -136,6 +132,7 @@ export class ActionManager { try { actions = await ActionManager.fetchActions(this.models, null, { status: [ActionStatus.APPROVED, ActionStatus.DEPLOYING], + protocolNetwork, }) logger.trace(`Fetched ${actions.length} approved actions`) } catch (err) { @@ -153,62 +150,53 @@ export class ActionManager { join({ approvedActions }).pipe(async ({ approvedActions }) => { logger.debug('Approved actions found, evaluating batch') - const approvedActionsByNetwork: NetworkMapped = groupBy( - approvedActions, - (action: Action) => action.protocolNetwork, - ) - await this.multiNetworks.mapNetworkMapped( - approvedActionsByNetwork, - async (network: Network, approvedActions: Action[]) => { - const networkLogger = logger.child({ - protocolNetwork: network.specification.networkIdentifier, - indexer: network.specification.indexerOptions.address, - operator: network.transactionManager.wallet.address, + const networkLogger = logger.child({ + protocolNetwork: network.specification.networkIdentifier, + indexer: network.specification.indexerOptions.address, + operator: network.transactionManager.wallet.address, + }) + + if (await this.batchReady(approvedActions, networkLogger)) { + const paused = await network.paused.value() + const isOperator = await network.isOperator.value() + networkLogger.debug('Batch ready, preparing to execute', { + paused, + isOperator, + protocolNetwork: network.specification.networkIdentifier, + }) + // Do nothing else if the network is paused + if (paused) { + networkLogger.info( + `The network is currently paused, not doing anything until it resumes`, + ) + return + } + + // Do nothing if we're not authorized as an operator for the indexer + if (!isOperator) { + networkLogger.error(`Not authorized as an operator for the indexer`, { + err: indexerError(IndexerErrorCode.IE034), }) + return + } - if (await this.batchReady(approvedActions, network, networkLogger)) { - const paused = await network.paused.value() - const isOperator = await network.isOperator.value() - networkLogger.debug('Batch ready, preparing to execute', { - paused, - isOperator, - protocolNetwork: network.specification.networkIdentifier, - }) - // Do nothing else if the network is paused - if (paused) { - networkLogger.info( - `The network is currently paused, not doing anything until it resumes`, - ) - return - } - - // Do nothing if we're not authorized as an operator for the indexer - if (!isOperator) { - networkLogger.error(`Not authorized as an operator for the indexer`, { - err: indexerError(IndexerErrorCode.IE034), - }) - return - } - - networkLogger.info('Executing batch of approved actions', { - actions: approvedActions, - note: 'If actions were approved very recently they may be missing from this batch', - }) + networkLogger.info('Executing batch of approved actions', { + actions: approvedActions, + note: 'If actions were approved very recently they may be missing from this batch', + }) - try { - const attemptedActions = await this.executeApprovedActions(network) - networkLogger.trace('Attempted to execute all approved actions', { - actions: attemptedActions, - }) - } catch (error) { - networkLogger.error('Failed to execute batch of approved actions', { - error, - }) - } - } - }, - ) + try { + const attemptedActions = await this.executeApprovedActions() + networkLogger.trace('Attempted to execute all approved actions', { + actions: attemptedActions, + }) + } catch (error) { + networkLogger.error('Failed to execute batch of approved actions', { + error, + }) + } + } }) } @@ -275,7 +263,7 @@ export class ActionManager { } // a promise guard to ensure that only one batch of actions is executed at a time - async executeApprovedActions(network: Network): Promise { + async executeApprovedActions(): Promise { if (this.executeBatchActionsPromise) { this.logger.warn('Previous batch action execution is still in progress') return this.executeBatchActionsPromise @@ -283,7 +271,7 @@ export class ActionManager { let updatedActions: Action[] = [] try { - this.executeBatchActionsPromise = this.executeApprovedActionsInner(network) + this.executeBatchActionsPromise = this.executeApprovedActionsInner() updatedActions = await this.executeBatchActionsPromise } catch (error) { this.logger.error(`Failed to execute batch of approved actions -> ${error}`) @@ -293,9 +281,9 @@ export class ActionManager { return updatedActions } - async executeApprovedActionsInner(network: Network): Promise { + async executeApprovedActionsInner(): Promise { let updatedActions: Action[] = [] - const protocolNetwork = network.specification.networkIdentifier + const protocolNetwork = this.network.specification.networkIdentifier const logger = this.logger.child({ function: 'executeApprovedActions', protocolNetwork, @@ -371,9 +359,6 @@ export class ActionManager { startTimeMs: Date.now() - batchStartTime, }) - const allocationManager = - this.allocationManagers[network.specification.networkIdentifier] - let results: AllocationResult[] try { // TODO: we should lift the batch execution (graph-node, then contracts) up to here so we can @@ -394,92 +379,68 @@ export class ActionManager { ) } // This will return all results if successful, if failed it will return the failed actions - results = await allocationManager.executeBatch( + results = await this.allocationManager.executeBatch( prioritizedActions, onFinishedDeploying, ) - logger.debug('Completed batch action execution', { - results, - endTimeMs: Date.now() - batchStartTime, - }) } catch (error) { - // Release the actions from the PENDING state. This means they will be retried again on the next batch execution. - logger.error( - `Error raised during executeBatch, releasing ${prioritizedActions.length} actions from PENDING state. \ - These will be attempted again on the next batch.`, - error, - ) + logger.error('Failed to execute batch of approved actions', { error }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await this.models.Action.sequelize!.transaction( { isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE }, async (transaction) => { - return await this.markActions( - prioritizedActions, - transaction, - ActionStatus.APPROVED, - ) + await this.markActions(prioritizedActions, transaction, ActionStatus.APPROVED) }, ) return [] } - - // Happy path: execution went well (success or failure but no exceptions). Update the actions with the results. - updatedActions = await this.models.Action.sequelize!.transaction( + logger.debug('Finished executing batch of approved actions', { + results, + elapsedMs: Date.now() - batchStartTime, + }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await this.models.Action.sequelize!.transaction( { isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE }, async (transaction) => { - return await this.updateActionStatusesWithResults(results, transaction) + updatedActions = await this.updateActionStatusesWithResults( + results, + transaction, + ) }, ) - - logger.debug('Updated action statuses', { - updatedActions, - updatedTimeMs: Date.now() - batchStartTime, - }) } catch (error) { - logger.error(`Failed to execute batch tx on staking contract: ${error}`) - throw indexerError(IndexerErrorCode.IE072, error) + logger.error('Failed to execute batch of approved actions', { error }) } - - logger.debug('End executing approved actions') return updatedActions } - public static async fetchActions( + static async fetchActions( models: IndexerManagementModels, - transaction: Transaction | null, - filter: ActionFilter, - orderBy?: ActionParams, - orderDirection?: OrderDirection, + orderBy: ActionParams | null, + filter?: ActionFilter, first?: number, + orderDirection?: OrderDirection, ): Promise { - const orderObject: Order = orderBy + const order: Order | undefined = orderBy ? [[orderBy.toString(), orderDirection ?? 'desc']] - : [['id', 'desc']] - + : undefined + const whereClause = filter ? actionFilterToWhereOptions(filter) : undefined + const limit = first ?? undefined return await models.Action.findAll({ - transaction, - where: actionFilterToWhereOptions(filter), - order: orderObject, - limit: first, + order, + where: whereClause, + limit, }) } - public static async updateActions( + static async updateActions( models: IndexerManagementModels, action: ActionUpdateInput, filter: ActionFilter, ): Promise<[number, Action[]]> { - if (Object.keys(filter).length === 0) { - throw Error( - 'Cannot bulk update actions without a filter, please provide a least 1 filter value', - ) - } - return await models.Action.update( - { ...action }, - { - where: actionFilterToWhereOptions(filter), - returning: true, - validate: true, - }, - ) + return await models.Action.update(action, { + where: actionFilterToWhereOptions(filter), + returning: true, + }) } } diff --git a/packages/indexer-common/src/indexer-management/allocations.ts b/packages/indexer-common/src/indexer-management/allocations.ts index 8170a2855..8f8621d32 100644 --- a/packages/indexer-common/src/indexer-management/allocations.ts +++ b/packages/indexer-common/src/indexer-management/allocations.ts @@ -10,7 +10,6 @@ import { ActionFailure, ActionType, Allocation, - legacyAllocationIdProof, AllocationResult, AllocationStatus, CloseAllocationResult, @@ -52,7 +51,6 @@ import { import { BigNumberish, BytesLike, - ContractTransaction, hexlify, Result, TransactionReceipt, @@ -720,29 +718,19 @@ export class AllocationManager { // Double-check whether the allocationID already exists on chain, to // avoid unnecessary transactions. - let allocationExistsSubgraphService = false - let allocationExistsStaking = false - const isHorizon = await this.network.isHorizon.value() - if (isHorizon) { - const allocation = - await this.network.contracts.SubgraphService.getAllocation(allocationId) - const legacyAllocation = - await this.network.contracts.SubgraphService.getLegacyAllocation(allocationId) - allocationExistsSubgraphService = allocation.createdAt !== 0n - allocationExistsStaking = legacyAllocation.indexer !== ZeroAddress - } else { - const state = - await this.network.contracts.LegacyStaking.getAllocationState(allocationId) - allocationExistsStaking = state !== 0n - } - - if (allocationExistsSubgraphService || allocationExistsStaking) { + const existingAllocation = + await this.network.contracts.SubgraphService.getAllocation(allocationId) + const existingLegacyAllocation = + await this.network.contracts.SubgraphService.getLegacyAllocation(allocationId) + const allocationExistsSubgraphService = existingAllocation.createdAt !== 0n + const allocationExistsLegacy = existingLegacyAllocation.indexer !== ZeroAddress + + if (allocationExistsSubgraphService || allocationExistsLegacy) { logger.debug(`Skipping allocation as it already exists onchain`, { indexer: this.network.specification.indexerOptions.address, allocation: allocationId, - isHorizon, allocationExistsSubgraphService, - allocationExistsStaking, + allocationExistsLegacy, }) throw indexerError( IndexerErrorCode.IE066, @@ -756,23 +744,16 @@ export class AllocationManager { indexerAddress: this.network.specification.indexerOptions.address, }) - const proof = isHorizon - ? await horizonAllocationIdProof( - allocationSigner, - Number(this.network.specification.networkIdentifier.split(':')[1]), - this.network.specification.indexerOptions.address, - allocationId, - this.network.contracts.SubgraphService.target.toString(), - ) - : await legacyAllocationIdProof( - allocationSigner, - this.network.specification.indexerOptions.address, - allocationId, - ) + const proof = await horizonAllocationIdProof( + allocationSigner, + Number(this.network.specification.networkIdentifier.split(':')[1]), + this.network.specification.indexerOptions.address, + allocationId, + this.network.contracts.SubgraphService.target.toString(), + ) logger.debug('Successfully generated allocation ID proof', { allocationIDProof: proof, - isLegacy: !isHorizon, }) return { @@ -792,39 +773,24 @@ export class AllocationManager { receipt: TransactionReceipt | 'paused' | 'unauthorized', ): Promise { const logger = this.logger.child({ action: actionID }) - const subgraphDeployment = new SubgraphDeploymentID(deployment) - const isLegacy = - (receipt as TransactionReceipt).to === this.network.contracts.HorizonStaking.target - logger.info(`Confirming allocation creation transaction`, { - isLegacy, - }) + logger.info(`Confirming allocation creation transaction`) if (receipt === 'paused' || receipt === 'unauthorized') { throw indexerError( IndexerErrorCode.IE062, - `Allocation not created. ${ receipt === 'paused' ? 'Network paused' : 'Operator not authorized' }`, ) } - const createAllocationEventLogs = isLegacy - ? this.network.transactionManager.findEvent( - 'AllocationCreated', - this.network.contracts.LegacyStaking.interface, - 'subgraphDeploymentID', - subgraphDeployment.bytes32, - receipt, - this.logger, - ) - : this.network.transactionManager.findEvent( - 'AllocationCreated', - this.network.contracts.SubgraphService.interface, - 'indexer', - this.network.specification.indexerOptions.address, - receipt, - logger, - ) + const createAllocationEventLogs = this.network.transactionManager.findEvent( + 'AllocationCreated', + this.network.contracts.SubgraphService.interface, + 'indexer', + this.network.specification.indexerOptions.address, + receipt, + logger, + ) if (!createAllocationEventLogs) { throw indexerError(IndexerErrorCode.IE014, `Allocation was never mined`) @@ -832,14 +798,9 @@ export class AllocationManager { logger.info(`Successfully allocated to subgraph deployment`, { amountGRT: formatGRT(createAllocationEventLogs.tokens), - allocation: isLegacy - ? createAllocationEventLogs.allocationID - : createAllocationEventLogs.allocationId, + allocation: createAllocationEventLogs.allocationId, deployment: createAllocationEventLogs.subgraphDeploymentID, - epoch: isLegacy - ? createAllocationEventLogs.epoch.toString() - : createAllocationEventLogs.currentEpoch.toString(), - isLegacy, + epoch: createAllocationEventLogs.currentEpoch.toString(), }) const subgraphDeploymentID = new SubgraphDeploymentID(deployment) @@ -864,9 +825,7 @@ export class AllocationManager { type: 'allocate', transactionID: receipt.hash, deployment: deployment, - allocation: isLegacy - ? createAllocationEventLogs.allocationID - : createAllocationEventLogs.allocationId, + allocation: createAllocationEventLogs.allocationId, allocatedTokens: amount, protocolNetwork: this.network.specification.networkIdentifier, } @@ -880,7 +839,6 @@ export class AllocationManager { actionID: number, protocolNetwork: string, ): Promise { - const isHorizon = await this.network.isHorizon.value() const params = await this.prepareAllocateParams(logger, context, deployment, amount) logger.debug(`Populating allocation creation transaction`, { indexer: params.indexer, @@ -888,41 +846,26 @@ export class AllocationManager { amount: formatGRT(params.tokens), allocation: params.allocationID, proof: params.proof, - isLegacy: !isHorizon, }) - let populatedTransaction: ContractTransaction - if (isHorizon) { - // Fail automatically if the indexer is not registered - // This provides a better ux during the transition period - const registrationData = await this.network.contracts.SubgraphService.indexers( + // Fail automatically if the indexer is not registered + const registrationData = await this.network.contracts.SubgraphService.indexers( + params.indexer, + ) + if (registrationData.url.length === 0) { + throw indexerError(IndexerErrorCode.IE086) + } + const encodedData = encodeStartServiceData( + params.subgraphDeploymentID.toString(), + BigInt(params.tokens), + params.allocationID, + params.proof.toString(), + ) + const populatedTransaction = + await this.network.contracts.SubgraphService.startService.populateTransaction( params.indexer, + encodedData, ) - if (registrationData.url.length === 0) { - throw indexerError(IndexerErrorCode.IE086) - } - const encodedData = encodeStartServiceData( - params.subgraphDeploymentID.toString(), - BigInt(params.tokens), - params.allocationID, - params.proof.toString(), - ) - populatedTransaction = - await this.network.contracts.SubgraphService.startService.populateTransaction( - params.indexer, - encodedData, - ) - } else { - populatedTransaction = - await this.network.contracts.LegacyStaking.allocateFrom.populateTransaction( - params.indexer, - params.subgraphDeploymentID, - params.tokens, - params.allocationID, - params.metadata, - params.proof, - ) - } return { protocolNetwork, actionID, @@ -959,19 +902,10 @@ export class AllocationManager { // Double-check whether the allocation is still active on chain, to // avoid unnecessary transactions. - if (allocation.isLegacy) { - const state = await this.network.contracts.HorizonStaking.getAllocationState( - allocation.id, - ) - if (state !== 1n) { - throw indexerError(IndexerErrorCode.IE065) - } - } else { - const allocation = - await this.network.contracts.SubgraphService.getAllocation(allocationID) - if (allocation.closedAt !== 0n) { - throw indexerError(IndexerErrorCode.IE065) - } + const onChainAllocation = + await this.network.contracts.SubgraphService.getAllocation(allocationID) + if (onChainAllocation.closedAt !== 0n) { + throw indexerError(IndexerErrorCode.IE065) } return { @@ -990,13 +924,8 @@ export class AllocationManager { receipt: TransactionReceipt | 'paused' | 'unauthorized', ): Promise { const logger = this.logger.child({ action: actionID }) - const isLegacy = - (receipt as TransactionReceipt).to === this.network.contracts.HorizonStaking.target - const isHorizon = await this.network.isHorizon.value() - logger.info(`Confirming unallocate transaction`, { - isLegacy, - }) + logger.info(`Confirming unallocate transaction`) if (receipt === 'paused' || receipt === 'unauthorized') { throw indexerError( @@ -1005,23 +934,14 @@ export class AllocationManager { ) } - const closeAllocationEventLogs = isLegacy - ? this.network.transactionManager.findEvent( - 'AllocationClosed', - this.network.contracts.LegacyStaking.interface, - 'allocationID', - allocationID, - receipt, - this.logger, - ) - : this.network.transactionManager.findEvent( - 'AllocationClosed', - this.network.contracts.SubgraphService.interface, - 'allocationId', - allocationID, - receipt, - this.logger, - ) + const closeAllocationEventLogs = this.network.transactionManager.findEvent( + 'AllocationClosed', + this.network.contracts.SubgraphService.interface, + 'allocationId', + allocationID, + receipt, + this.logger, + ) if (!closeAllocationEventLogs) { throw indexerError( @@ -1030,38 +950,23 @@ export class AllocationManager { ) } - const rewardsEventLogs = isLegacy - ? this.network.transactionManager.findEvent( - isHorizon ? 'HorizonRewardsAssigned' : 'RewardsAssigned', - this.network.contracts.RewardsManager.interface, - 'allocationID', - allocationID, - receipt, - this.logger, - ) - : this.network.transactionManager.findEvent( - 'IndexingRewardsCollected', - this.network.contracts.SubgraphService.interface, - 'allocationId', - allocationID, - receipt, - this.logger, - ) + const rewardsEventLogs = this.network.transactionManager.findEvent( + 'IndexingRewardsCollected', + this.network.contracts.SubgraphService.interface, + 'allocationId', + allocationID, + receipt, + this.logger, + ) - const rewardsAssigned = rewardsEventLogs - ? isLegacy - ? rewardsEventLogs.amount - : rewardsEventLogs.tokensIndexerRewards - : 0 + const rewardsAssigned = rewardsEventLogs ? rewardsEventLogs.tokensIndexerRewards : 0 if (rewardsAssigned == 0) { logger.warn('No rewards were distributed upon closing the allocation') } const subgraphDeploymentID = new SubgraphDeploymentID( - isLegacy - ? closeAllocationEventLogs.subgraphDeploymentID - : closeAllocationEventLogs.subgraphDeploymentId, + closeAllocationEventLogs.subgraphDeploymentId, ) logger.info(`Successfully closed allocation`, { @@ -1111,56 +1016,38 @@ export class AllocationManager { poiData: params.poi, }) - if (params.isLegacy) { - const tx = - await this.network.contracts.HorizonStaking.closeAllocation.populateTransaction( - params.allocationID, - params.poi.poi, - ) - return { - protocolNetwork: params.protocolNetwork, - actionID: params.actionID, - ...tx, - } - } else { - // Horizon: Need to multicall collect and stopService - - // collect - const collectIndexingRewardsData = encodeCollectIndexingRewardsData( - params.allocationID, - params.poi.poi, - encodePOIMetadata( - params.poi.blockNumber, - params.poi.publicPOI, - params.poi.indexingStatus, - 0, - 0, - ), - ) - const collectCallData = - this.network.contracts.SubgraphService.interface.encodeFunctionData('collect', [ - params.indexer, - PaymentTypes.IndexingRewards, - collectIndexingRewardsData, - ]) - - // stopService - const stopServiceCallData = - this.network.contracts.SubgraphService.interface.encodeFunctionData( - 'stopService', - [params.indexer, encodeStopServiceData(params.allocationID)], - ) + // Multicall collect and stopService + const collectIndexingRewardsData = encodeCollectIndexingRewardsData( + params.allocationID, + params.poi.poi, + encodePOIMetadata( + params.poi.blockNumber, + params.poi.publicPOI, + params.poi.indexingStatus, + 0, + 0, + ), + ) + const collectCallData = + this.network.contracts.SubgraphService.interface.encodeFunctionData('collect', [ + params.indexer, + PaymentTypes.IndexingRewards, + collectIndexingRewardsData, + ]) - const tx = - await this.network.contracts.SubgraphService.multicall.populateTransaction([ - collectCallData, - stopServiceCallData, - ]) - return { - protocolNetwork: params.protocolNetwork, - actionID: params.actionID, - ...tx, - } + const stopServiceCallData = + this.network.contracts.SubgraphService.interface.encodeFunctionData('stopService', [ + params.indexer, + encodeStopServiceData(params.allocationID), + ]) + + const tx = await this.network.contracts.SubgraphService.multicall.populateTransaction( + [collectCallData, stopServiceCallData], + ) + return { + protocolNetwork: params.protocolNetwork, + actionID: params.actionID, + ...tx, } } @@ -1247,24 +1134,11 @@ export class AllocationManager { // Double-check whether the allocation is still active on chain, to // avoid unnecessary transactions. - if (allocation.isLegacy) { - const state = await this.network.contracts.HorizonStaking.getAllocationState( - allocation.id, - ) - if (state !== 1n) { - logger.warn(`Allocation has already been closed`) - throw indexerError( - IndexerErrorCode.IE065, - `Legacy allocation has already been closed`, - ) - } - } else { - const allocationData = - await this.network.contracts.SubgraphService.getAllocation(allocationID) - if (allocationData.closedAt !== 0n) { - logger.warn(`Allocation has already been closed`) - throw indexerError(IndexerErrorCode.IE065, `Allocation has already been closed`) - } + const onChainAllocation = + await this.network.contracts.SubgraphService.getAllocation(allocationID) + if (onChainAllocation.closedAt !== 0n) { + logger.warn(`Allocation has already been closed`) + throw indexerError(IndexerErrorCode.IE065, `Allocation has already been closed`) } if (amount < 0n) { @@ -1296,56 +1170,32 @@ export class AllocationManager { // Double-check whether the allocationID already exists on chain, to // avoid unnecessary transactions. - const isHorizon = await this.network.isHorizon.value() - if (isHorizon) { - const allocationData = - await this.network.contracts.SubgraphService.getAllocation(newAllocationId) - if (allocationData.createdAt !== 0n) { - logger.warn(`Skipping allocation as it already exists onchain`, { - indexer: this.network.specification.indexerOptions.address, - allocation: newAllocationId, - allocationData, - isHorizon, - }) - throw indexerError(IndexerErrorCode.IE066, 'AllocationID already exists') - } - } else { - const newAllocationState = - await this.network.contracts.HorizonStaking.getAllocationState(newAllocationId) - if (newAllocationState !== 0n) { - logger.warn(`Skipping allocation as it already exists onchain (legacy)`, { - indexer: this.network.specification.indexerOptions.address, - allocation: newAllocationId, - newAllocationState, - isHorizon, - }) - throw indexerError(IndexerErrorCode.IE066, 'Legacy AllocationID already exists') - } + const allocationData = + await this.network.contracts.SubgraphService.getAllocation(newAllocationId) + if (allocationData.createdAt !== 0n) { + logger.warn(`Skipping allocation as it already exists onchain`, { + indexer: this.network.specification.indexerOptions.address, + allocation: newAllocationId, + allocationData, + }) + throw indexerError(IndexerErrorCode.IE066, 'AllocationID already exists') } logger.debug('Generating new allocation ID proof', { newAllocationSigner: allocationSigner, newAllocationID: newAllocationId, indexerAddress: this.network.specification.indexerOptions.address, - isHorizon, }) - const proof = isHorizon - ? await horizonAllocationIdProof( - allocationSigner, - Number(this.network.specification.networkIdentifier.split(':')[1]), - this.network.specification.indexerOptions.address, - newAllocationId, - this.network.contracts.SubgraphService.target.toString(), - ) - : await legacyAllocationIdProof( - allocationSigner, - this.network.specification.indexerOptions.address, - newAllocationId, - ) + const proof = await horizonAllocationIdProof( + allocationSigner, + Number(this.network.specification.networkIdentifier.split(':')[1]), + this.network.specification.indexerOptions.address, + newAllocationId, + this.network.contracts.SubgraphService.target.toString(), + ) logger.debug('Successfully generated allocation ID proof', { allocationIDProof: proof, - isHorizon, }) logger.info(`Prepared close and allocate multicall transaction`, { @@ -1381,114 +1231,57 @@ export class AllocationManager { receipts: TransactionReceipt[], ): Promise { const logger = this.logger.child({ action: actionID }) - const isHorizon = await this.network.isHorizon.value() logger.info(`Confirming reallocate transaction`, { allocationID, - isHorizon, receiptCount: receipts.length, }) - // We need to find the close and create allocation events and obtain the subgraph deployment ID and rewards assigned - // We could have one receipt per contract (Horizon Staking and Subgraph Service) so we need to check both targets + // Find close and create allocation events from SubgraphService contract let closeAllocationEventLogs: Result | undefined let createAllocationEventLogs: Result | undefined let subgraphDeploymentID: SubgraphDeploymentID | undefined let rewardsAssigned = 0n + for (const receipt of receipts) { - // Staking contract - if (receipt.to === this.network.contracts.HorizonStaking.target) { - const stakingCloseAllocationEventLogs = this.network.transactionManager.findEvent( - 'AllocationClosed', - this.network.contracts.LegacyStaking.interface, - 'allocationID', + const closeEventLogs = this.network.transactionManager.findEvent( + 'AllocationClosed', + this.network.contracts.SubgraphService.interface, + 'allocationId', + allocationID, + receipt, + this.logger, + ) + if (closeEventLogs !== undefined) { + closeAllocationEventLogs = closeEventLogs + + const rewardsEventLogs = this.network.transactionManager.findEvent( + 'IndexingRewardsCollected', + this.network.contracts.SubgraphService.interface, + 'allocationId', allocationID, receipt, this.logger, ) - - if (stakingCloseAllocationEventLogs !== undefined) { - closeAllocationEventLogs = stakingCloseAllocationEventLogs - - const rewardsEventLogs = this.network.transactionManager.findEvent( - isHorizon ? 'HorizonRewardsAssigned' : 'RewardsAssigned', - this.network.contracts.RewardsManager.interface, - 'allocationID', - allocationID, - receipt, - this.logger, - ) - if (rewardsEventLogs !== undefined) { - rewardsAssigned = rewardsEventLogs.amount - } - - subgraphDeploymentID = new SubgraphDeploymentID( - stakingCloseAllocationEventLogs.subgraphDeploymentID, - ) - - // not possible to create legacy allocation if what was closed was not a legacy allocation - const stakingCreateAllocationEventLogs = - this.network.transactionManager.findEvent( - 'AllocationCreated', - this.network.contracts.LegacyStaking.interface, - 'subgraphDeploymentID', - stakingCloseAllocationEventLogs.subgraphDeploymentID, - receipt, - this.logger, - ) - - if (stakingCreateAllocationEventLogs !== undefined) { - createAllocationEventLogs = stakingCreateAllocationEventLogs - } + if (rewardsEventLogs !== undefined) { + rewardsAssigned = rewardsEventLogs.tokensIndexerRewards } - } - // Subgraph Service contract - else { - // Subgraph Service contract - // Possible transactions to handle: - // - collect + stopService for a new allocation - // - startService for a new allocation - const subgraphServiceCloseAllocationEventLogs = - this.network.transactionManager.findEvent( - 'AllocationClosed', - this.network.contracts.SubgraphService.interface, - 'allocationId', - allocationID, - receipt, - this.logger, - ) - if (subgraphServiceCloseAllocationEventLogs !== undefined) { - closeAllocationEventLogs = subgraphServiceCloseAllocationEventLogs - - const rewardsEventLogs = this.network.transactionManager.findEvent( - 'IndexingRewardsCollected', - this.network.contracts.SubgraphService.interface, - 'allocationId', - allocationID, - receipt, - this.logger, - ) - if (rewardsEventLogs !== undefined) { - rewardsAssigned = rewardsEventLogs.tokensIndexerRewards - } - subgraphDeploymentID = new SubgraphDeploymentID( - subgraphServiceCloseAllocationEventLogs.subgraphDeploymentId, - ) - } + subgraphDeploymentID = new SubgraphDeploymentID( + closeEventLogs.subgraphDeploymentId, + ) + } - const subgraphServiceCreateAllocationEventLogs = - this.network.transactionManager.findEvent( - 'AllocationCreated', - this.network.contracts.SubgraphService.interface, - 'indexer', - this.network.specification.indexerOptions.address, - receipt, - logger, - ) - if (subgraphServiceCreateAllocationEventLogs !== undefined) { - createAllocationEventLogs = subgraphServiceCreateAllocationEventLogs - } + const createEventLogs = this.network.transactionManager.findEvent( + 'AllocationCreated', + this.network.contracts.SubgraphService.interface, + 'indexer', + this.network.specification.indexerOptions.address, + receipt, + logger, + ) + if (createEventLogs !== undefined) { + createAllocationEventLogs = createEventLogs } } @@ -1607,166 +1400,61 @@ export class AllocationManager { metadata: params.metadata, proof: params.proof, }) - const isHorizon = await this.network.isHorizon.value() const txs: TransactionRequest[] = [] - // -- close allocation - if (params.closingAllocationIsLegacy) { - txs.push( - await this.network.contracts.LegacyStaking.closeAllocation.populateTransaction( - params.closingAllocationID, - params.poi.poi, - ), - ) - - // If we are in Horizon and the indexer already has a provision - // automatically add the allocated stake to the Subgraph Service provision - // this prevents the indexer from having to do it manually, which is likely to cause downtime - if (isHorizon) { - const provision = await this.network.contracts.HorizonStaking.getProvision( - this.network.specification.indexerOptions.address, - this.network.contracts.SubgraphService.target, - ) - if (provision.createdAt > 0n) { - logger.info( - 'Automatically provisioning the legacy allocation unallocated stake to the Subgraph Service', - { - indexer: this.network.specification.indexerOptions.address, - subgraphDeploymentID: params.subgraphDeploymentID.toString(), - legacyAllocationID: params.closingAllocationID, - newAllocationID: params.newAllocationID, - tokens: formatGRT(params.tokens), - }, - ) + // -- close allocation: multicall collect and stopService + const collectIndexingRewardsData = encodeCollectIndexingRewardsData( + params.closingAllocationID, + params.poi.poi, + encodePOIMetadata( + params.poi.blockNumber, + params.poi.publicPOI, + params.poi.indexingStatus, + 0, + 0, + ), + ) + const collectCallData = + this.network.contracts.SubgraphService.interface.encodeFunctionData('collect', [ + params.indexer, + PaymentTypes.IndexingRewards, + collectIndexingRewardsData, + ]) - try { - // Calculate how much of the allocated amount "corresponds" to the indexer's own stake - // Note that this calculation is imperfect by design, the real calculation is too complicated to be done here - // but also the network state can change between the calculation and the moment the transaction is executed - // In those cases it's possible that the transaction will fail and the reallocation will be rejected, requiring - // manual intervention from the indexer. - const ownStake = - await this.network.contracts.HorizonStaking.getProviderTokensAvailable( - params.indexer, - this.network.contracts.SubgraphService.target, - ) - const delegatedStake = - await this.network.contracts.HorizonStaking.getDelegatedTokensAvailable( - params.indexer, - this.network.contracts.SubgraphService.target, - ) - const totalStake = ownStake + delegatedStake - const stakeRatio = ownStake / totalStake - const tokensToAdd = BigInt(params.tokens) * stakeRatio - logger.info('Automatic provisioning amount calculated', { - indexer: params.indexer, - ownStake: formatGRT(ownStake), - delegatedStake: formatGRT(delegatedStake), - stakeRatio: stakeRatio.toString(), - tokensToAdd: formatGRT(tokensToAdd), - }) - if (tokensToAdd > 0n) { - txs.push( - await this.network.contracts.HorizonStaking.addToProvision.populateTransaction( - params.indexer, - this.network.contracts.SubgraphService.target, - tokensToAdd, - ), - ) - } - } catch (error) { - logger.error( - 'Error while automatically provisioning the legacy allocation unallocated stake to the Subgraph Service', - { - error: error, - }, - ) - } - } else { - logger.info( - 'Skipping automatic provisioning after legacy allocation closure, could not find indexer provision', - { - indexer: this.network.specification.indexerOptions.address, - subgraphDeploymentID: params.subgraphDeploymentID.toString(), - legacyAllocationID: params.closingAllocationID, - newAllocationID: params.newAllocationID, - tokens: formatGRT(params.tokens), - }, - ) - } - } - } else { - // Horizon: Need to multicall collect and stopService - - // collect - const collectIndexingRewardsData = encodeCollectIndexingRewardsData( - params.closingAllocationID, - params.poi.poi, - encodePOIMetadata( - params.poi.blockNumber, - params.poi.publicPOI, - params.poi.indexingStatus, - 0, - 0, - ), - ) - const collectCallData = - this.network.contracts.SubgraphService.interface.encodeFunctionData('collect', [ - params.indexer, - PaymentTypes.IndexingRewards, - collectIndexingRewardsData, - ]) - - // stopService - const stopServiceCallData = - this.network.contracts.SubgraphService.interface.encodeFunctionData( - 'stopService', - [params.indexer, encodeStopServiceData(params.closingAllocationID)], - ) + const stopServiceCallData = + this.network.contracts.SubgraphService.interface.encodeFunctionData('stopService', [ + params.indexer, + encodeStopServiceData(params.closingAllocationID), + ]) + + txs.push( + await this.network.contracts.SubgraphService.multicall.populateTransaction([ + collectCallData, + stopServiceCallData, + ]), + ) - txs.push( - await this.network.contracts.SubgraphService.multicall.populateTransaction([ - collectCallData, - stopServiceCallData, - ]), - ) + // -- create new allocation + // Fail automatically if the indexer is not registered + const registrationData = await this.network.contracts.SubgraphService.indexers( + params.indexer, + ) + if (registrationData.url.length === 0) { + throw indexerError(IndexerErrorCode.IE086) } - // -- create new allocation - if (isHorizon) { - // Fail automatically if the indexer is not registered - // This provides a better ux during the transition period - const registrationData = await this.network.contracts.SubgraphService.indexers( + const encodedData = encodeStartServiceData( + params.subgraphDeploymentID.toString(), + BigInt(params.tokens), + params.newAllocationID, + params.proof.toString(), + ) + txs.push( + await this.network.contracts.SubgraphService.startService.populateTransaction( params.indexer, - ) - if (registrationData.url.length === 0) { - throw indexerError(IndexerErrorCode.IE086) - } - - const encodedData = encodeStartServiceData( - params.subgraphDeploymentID.toString(), - BigInt(params.tokens), - params.newAllocationID, - params.proof.toString(), - ) - txs.push( - await this.network.contracts.SubgraphService.startService.populateTransaction( - params.indexer, - encodedData, - ), - ) - } else { - txs.push( - await this.network.contracts.LegacyStaking.allocateFrom.populateTransaction( - params.indexer, - params.subgraphDeploymentID, - params.tokens, - params.newAllocationID, - params.metadata, - params.proof, - ), - ) - } + encodedData, + ), + ) return txs.map((tx) => ({ actionID: params.actionID, @@ -1865,27 +1553,20 @@ export class AllocationManager { const logger = this.logger.child({ function: 'validateActionBatch' }) logger.debug(`Validating action batch`, { size: batch.length }) - // Validate stake feasibility - we need to analyse stake depending on the action type + // Validate stake feasibility const indexerFreeStake = await this.network.networkMonitor.freeStake() const actionsBatchStakeUsageSummaries = await pMap(batch, async (action: Action) => this.stakeUsageSummary(action), ) - const batchDeltaLegacy = actionsBatchStakeUsageSummaries - .filter((summary: ActionStakeUsageSummary) => summary.action.isLegacy) - .map((summary: ActionStakeUsageSummary) => summary.balance) - .reduce((a: bigint, b: bigint) => a + b, 0n) const batchDelta = actionsBatchStakeUsageSummaries - .filter((summary: ActionStakeUsageSummary) => !summary.action.isLegacy) .map((summary: ActionStakeUsageSummary) => summary.balance) .reduce((a: bigint, b: bigint) => a + b, 0n) const indexerNewBalance = indexerFreeStake.horizon - batchDelta - const indexerNewBalanceLegacy = indexerFreeStake.legacy - batchDeltaLegacy logger.trace('Action batch stake usage summary', { - indexerFreeStake: indexerFreeStake.toString(), - indexerFreeStakeLegacy: indexerFreeStake.legacy.toString(), + indexerFreeStake: indexerFreeStake.horizon.toString(), actionsBatchStakeUsageSummaries: actionsBatchStakeUsageSummaries.map((summary) => { return { action: summary.action, @@ -1896,22 +1577,17 @@ export class AllocationManager { } }), batchDelta: batchDelta.toString(), - batchDeltaLegacy: batchDeltaLegacy.toString(), indexerNewBalance: indexerNewBalance.toString(), - indexerNewBalanceLegacy: indexerNewBalanceLegacy.toString(), }) - if (indexerNewBalance < 0n || indexerNewBalanceLegacy < 0n) { - { - throw indexerError( - IndexerErrorCode.IE013, - `Unfeasible action batch: Approved action batch GRT balance is ` + - `${formatGRT(batchDelta)} for horizon actions and ` + - `${formatGRT(batchDeltaLegacy)} for legacy actions ` + - `but available horizon stake equals ${formatGRT(indexerFreeStake.horizon)} ` + - `and legacy stake equals ${formatGRT(indexerFreeStake.legacy)}.`, - ) - } + if (indexerNewBalance < 0n) { + throw indexerError( + IndexerErrorCode.IE013, + `Unfeasible action batch: Approved action batch GRT balance is ` + + `${formatGRT(batchDelta)} but available stake equals ${formatGRT( + indexerFreeStake.horizon, + )}.`, + ) } /* Return actions sorted by GRT balance (ascending). diff --git a/packages/indexer-common/src/indexer-management/client.ts b/packages/indexer-common/src/indexer-management/client.ts index 27cd68ce9..671c90363 100644 --- a/packages/indexer-common/src/indexer-management/client.ts +++ b/packages/indexer-common/src/indexer-management/client.ts @@ -14,12 +14,7 @@ import poiDisputeResolvers from './resolvers/poi-disputes' import statusResolvers from './resolvers/indexer-status' import provisionResolvers from './resolvers/provisions' import { GraphNode } from '../graph-node' -import { - ActionManager, - MultiNetworks, - Network, - RulesManager, -} from '@graphprotocol/indexer-common' +import { ActionManager, Network, RulesManager } from '@graphprotocol/indexer-common' export interface IndexerManagementResolverContext { models: IndexerManagementModels @@ -28,7 +23,7 @@ export interface IndexerManagementResolverContext { defaults: IndexerManagementDefaults actionManager: ActionManager | undefined rulesManager: RulesManager | undefined - multiNetworks: MultiNetworks | undefined + network: Network | undefined } const SCHEMA_SDL = gql` @@ -157,7 +152,7 @@ const SCHEMA_SDL = gql` source: String! reason: String! priority: Int! - protocolNetwork: String! + protocolNetwork: String isLegacy: Boolean! } @@ -226,7 +221,7 @@ const SCHEMA_SDL = gql` input POIDisputeIdentifier { allocationID: String! - protocolNetwork: String! + protocolNetwork: String } type POIDispute { @@ -260,7 +255,7 @@ const SCHEMA_SDL = gql` previousEpochStartBlockNumber: Int! previousEpochReferenceProof: String status: String! - protocolNetwork: String! + protocolNetwork: String } type IndexingRule { @@ -298,12 +293,12 @@ const SCHEMA_SDL = gql` decisionBasis: IndexingDecisionBasis requireSupported: Boolean safety: Boolean - protocolNetwork: String! + protocolNetwork: String } input IndexingRuleIdentifier { identifier: String! - protocolNetwork: String! + protocolNetwork: String } type GeoLocation { @@ -445,9 +440,9 @@ const SCHEMA_SDL = gql` merged: Boolean! = false ): IndexingRule indexingRules(merged: Boolean! = false, protocolNetwork: String): [IndexingRule!]! - indexerRegistration(protocolNetwork: String!): [IndexerRegistration]! + indexerRegistration(protocolNetwork: String): [IndexerRegistration]! indexerDeployments: [IndexerDeployment]! - indexerAllocations(protocolNetwork: String!): [IndexerAllocation]! + indexerAllocations(protocolNetwork: String): [IndexerAllocation]! indexerEndpoints(protocolNetwork: String): [IndexerEndpoints!]! costModels(deployments: [String!]): [CostModel!]! @@ -471,8 +466,8 @@ const SCHEMA_SDL = gql` first: Int ): [Action]! - provisions(protocolNetwork: String!): [Provision!]! - thawRequests(protocolNetwork: String!): [ThawRequest!]! + provisions(protocolNetwork: String): [Provision!]! + thawRequests(protocolNetwork: String): [ThawRequest!]! } type Mutation { @@ -490,7 +485,7 @@ const SCHEMA_SDL = gql` deployment: String! amount: String! indexNode: String - protocolNetwork: String! + protocolNetwork: String ): CreateAllocationResult! closeAllocation( allocation: String! @@ -498,7 +493,7 @@ const SCHEMA_SDL = gql` blockNumber: Int publicPOI: String force: Boolean - protocolNetwork: String! + protocolNetwork: String ): CloseAllocationResult! reallocateAllocation( allocation: String! @@ -507,9 +502,9 @@ const SCHEMA_SDL = gql` publicPOI: String amount: String! force: Boolean - protocolNetwork: String! + protocolNetwork: String ): ReallocateAllocationResult! - submitCollectReceiptsJob(allocation: String!, protocolNetwork: String!): Boolean! + submitCollectReceiptsJob(allocation: String!, protocolNetwork: String): Boolean! updateAction(action: ActionInput!): Action! updateActions(filter: ActionFilter!, action: ActionUpdateInput!): [Action]! @@ -519,9 +514,9 @@ const SCHEMA_SDL = gql` approveActions(actionIDs: [String!]!): [Action]! executeApprovedActions: [ActionResult!]! - addToProvision(protocolNetwork: String!, amount: String!): AddToProvisionResult! - thawFromProvision(protocolNetwork: String!, amount: String!): ThawFromProvisionResult! - removeFromProvision(protocolNetwork: String!): RemoveFromProvisionResult! + addToProvision(protocolNetwork: String, amount: String!): AddToProvisionResult! + thawFromProvision(protocolNetwork: String, amount: String!): ThawFromProvisionResult! + removeFromProvision(protocolNetwork: String): RemoveFromProvisionResult! } ` @@ -536,7 +531,7 @@ export interface IndexerManagementClientOptions { logger: Logger models: IndexerManagementModels graphNode: GraphNode - multiNetworks: MultiNetworks | undefined + network: Network | undefined defaults: IndexerManagementDefaults } @@ -552,12 +547,10 @@ export class IndexerManagementClient extends Client { } } -// TODO:L2: Put the IndexerManagementClient creation inside the Agent, and receive -// MultiNetworks from it export const createIndexerManagementClient = async ( options: IndexerManagementClientOptions, ): Promise => { - const { models, graphNode, logger, defaults, multiNetworks } = options + const { models, graphNode, logger, defaults, network } = options const schema = buildSchema(print(SCHEMA_SDL)) const resolvers = { ...indexingRuleResolvers, @@ -569,12 +562,12 @@ export const createIndexerManagementClient = async ( ...provisionResolvers, } - const actionManager = multiNetworks - ? await ActionManager.create(multiNetworks, logger, models, graphNode) + const actionManager = network + ? await ActionManager.create(network, logger, models, graphNode) : undefined - const rulesManager = multiNetworks - ? await RulesManager.create(multiNetworks, logger, models) + const rulesManager = network + ? await RulesManager.create(network, logger, models) : undefined const context: IndexerManagementResolverContext = { @@ -582,7 +575,7 @@ export const createIndexerManagementClient = async ( graphNode, defaults, logger: logger.child({ component: 'IndexerManagementClient' }), - multiNetworks, + network, actionManager, rulesManager, } diff --git a/packages/indexer-common/src/indexer-management/models/action.ts b/packages/indexer-common/src/indexer-management/models/action.ts index e40f37b99..cc77862d1 100644 --- a/packages/indexer-common/src/indexer-management/models/action.ts +++ b/packages/indexer-common/src/indexer-management/models/action.ts @@ -35,6 +35,7 @@ export class Action extends Model< declare createdAt: CreationOptional declare updatedAt: CreationOptional + /** @deprecated LEGACY DEBT: Multi-network removed. See protocolNetwork field comment in defineActionModels. */ declare protocolNetwork: string declare isLegacy: boolean @@ -150,6 +151,10 @@ export const defineActionModels = (sequelize: Sequelize): ActionModels => { allowNull: true, defaultValue: null, }, + // LEGACY DEBT: Part of composite primary key for multi-network support. + // Multi-network has been removed but this field remains in the DB schema. + // Always populated with the single configured network. Can be removed in a future + // migration that changes the primary key structure. protocolNetwork: { type: DataTypes.STRING(50), primaryKey: true, diff --git a/packages/indexer-common/src/indexer-management/models/indexing-rule.ts b/packages/indexer-common/src/indexer-management/models/indexing-rule.ts index 594191537..99ebb06f8 100644 --- a/packages/indexer-common/src/indexer-management/models/indexing-rule.ts +++ b/packages/indexer-common/src/indexer-management/models/indexing-rule.ts @@ -30,14 +30,29 @@ export interface IndexingRuleAttributes { decisionBasis: IndexingDecisionBasis requireSupported: boolean safety: boolean + /** + * @deprecated LEGACY DEBT: Part of composite primary key for multi-network support. + * Multi-network has been removed but this field remains in the DB schema. + * Always populated with the single configured network. Can be removed in a future + * migration that changes the primary key structure. + */ protocolNetwork: string } -// Unambiguously identify a Indexing Rule in the Database. -// This type should match the IndexingRules primary key columns. +/** + * Unambiguously identify an Indexing Rule in the Database. + * This type should match the IndexingRules primary key columns. + */ export interface IndexingRuleIdentifier { identifier: string - protocolNetwork: string + /** + * @deprecated LEGACY DEBT: Part of composite primary key for multi-network support. + * Multi-network has been removed but this field remains in the DB schema. + * Always populated with the single configured network. Can be removed in a future + * migration that changes the primary key structure. + * Optional in API - resolvers will use the context's single network if not provided. + */ + protocolNetwork?: string } export interface IndexingRuleCreationAttributes @@ -82,6 +97,7 @@ export class IndexingRule declare decisionBasis: IndexingDecisionBasis declare requireSupported: boolean declare safety: boolean + /** @deprecated LEGACY DEBT: Multi-network removed. See IndexingRuleAttributes.protocolNetwork */ declare protocolNetwork: string declare createdAt: Date @@ -259,6 +275,10 @@ export const defineIndexingRuleModels = (sequelize: Sequelize): IndexingRuleMode allowNull: false, defaultValue: true, }, + // LEGACY DEBT: Part of composite primary key for multi-network support. + // Multi-network has been removed but this field remains in the DB schema. + // Always populated with the single configured network. Can be removed in a future + // migration that changes the primary key structure. protocolNetwork: { type: DataTypes.STRING, primaryKey: true, diff --git a/packages/indexer-common/src/indexer-management/models/poi-dispute.ts b/packages/indexer-common/src/indexer-management/models/poi-dispute.ts index a4cd1e862..ccfb0a7b8 100644 --- a/packages/indexer-common/src/indexer-management/models/poi-dispute.ts +++ b/packages/indexer-common/src/indexer-management/models/poi-dispute.ts @@ -18,14 +18,29 @@ export interface POIDisputeAttributes { previousEpochStartBlockHash: string previousEpochStartBlockNumber: number status: string + /** + * @deprecated LEGACY DEBT: Part of composite primary key for multi-network support. + * Multi-network has been removed but this field remains in the DB schema. + * Always populated with the single configured network. Can be removed in a future + * migration that changes the primary key structure. + */ protocolNetwork: string } -// Unambiguously identify a POI Dispute in the Database. -// This type should match the POIDispute primary key columns. +/** + * Unambiguously identify a POI Dispute in the Database. + * This type should match the POIDispute primary key columns. + */ export interface POIDisputeIdentifier { allocationID: string - protocolNetwork: string + /** + * @deprecated LEGACY DEBT: Part of composite primary key for multi-network support. + * Multi-network has been removed but this field remains in the DB schema. + * Always populated with the single configured network. Can be removed in a future + * migration that changes the primary key structure. + * Optional in API - resolvers will use the context's single network if not provided. + */ + protocolNetwork?: string } export interface POIDisputeCreationAttributes @@ -64,6 +79,7 @@ export class POIDispute declare previousEpochStartBlockHash: string declare previousEpochStartBlockNumber: number declare status: string + /** @deprecated LEGACY DEBT: Multi-network removed. See POIDisputeAttributes.protocolNetwork */ declare protocolNetwork: string declare createdAt: Date @@ -255,6 +271,10 @@ export const definePOIDisputeModels = (sequelize: Sequelize): POIDisputeModels = type: DataTypes.STRING, allowNull: false, }, + // LEGACY DEBT: Part of composite primary key for multi-network support. + // Multi-network has been removed but this field remains in the DB schema. + // Always populated with the single configured network. Can be removed in a future + // migration that changes the primary key structure. protocolNetwork: { type: DataTypes.STRING, primaryKey: true, diff --git a/packages/indexer-common/src/indexer-management/monitor.ts b/packages/indexer-common/src/indexer-management/monitor.ts index eb39634d5..18fa80336 100644 --- a/packages/indexer-common/src/indexer-management/monitor.ts +++ b/packages/indexer-common/src/indexer-management/monitor.ts @@ -71,37 +71,24 @@ export class NetworkMonitor { // To simplify the agent logic, this function converts horizon allocation values, returning epoch values // regardless of the allocation type. async maxAllocationDuration(): Promise { - const isHorizon = await this.isHorizon() - - if (isHorizon) { - // TODO HORIZON: this assumes a block time of 12 seconds which is true for current protocol chain but not always - const BLOCK_IN_SECONDS = 12n - const epochLengthInBlocks = await this.contracts.EpochManager.epochLength() - const epochLengthInSeconds = Number(epochLengthInBlocks * BLOCK_IN_SECONDS) - - // When converting to epochs we give it a bit of leeway since missing the allocation expiration in horizon - // incurs in a severe penalty (missing out on indexing rewards) - const horizonDurationInSeconds = Number( - await this.contracts.SubgraphService.maxPOIStaleness(), - ) - const horizonDurationInEpochs = Math.max( - 1, - Math.floor(horizonDurationInSeconds / epochLengthInSeconds) - 1, - ) + // TODO HORIZON: this assumes a block time of 12 seconds which is true for current protocol chain but not always + const BLOCK_IN_SECONDS = 12n + const epochLengthInBlocks = await this.contracts.EpochManager.epochLength() + const epochLengthInSeconds = Number(epochLengthInBlocks * BLOCK_IN_SECONDS) + + // When converting to epochs we give it a bit of leeway since missing the allocation expiration in horizon + // incurs in a severe penalty (missing out on indexing rewards) + const horizonDurationInSeconds = Number( + await this.contracts.SubgraphService.maxPOIStaleness(), + ) + const horizonDurationInEpochs = Math.max( + 1, + Math.floor(horizonDurationInSeconds / epochLengthInSeconds) - 1, + ) - return { - // Hardcoded to the latest known value. This is required to check for legacy allo expiration during the transition period. - // - Arbitrum One: 28 - // - Arbitrum Sepolia: 8 - // - Local Network: 4 - legacy: 28, - horizon: horizonDurationInEpochs, - } - } else { - return { - legacy: Number(await this.contracts.LegacyStaking.maxAllocationEpochs()), - horizon: 0, - } + return { + legacy: 0, + horizon: horizonDurationInEpochs, } } @@ -109,42 +96,27 @@ export class NetworkMonitor { * Returns the amount of free stake for the indexer. * * The free stake is the amount of tokens that the indexer can use to stake in - * new allocations. - * - * Horizon: It's calculated as the difference between the tokens - * available in the provision and the tokens already locked allocations. - * - * Legacy: It's given by the indexer's stake capacity. + * new allocations. It's calculated as the difference between the tokens + * available in the provision and the tokens already locked in allocations. * * @returns The amount of free stake for the indexer. */ async freeStake(): Promise> { - const isHorizon = await this.isHorizon() - - if (isHorizon) { - const address = this.indexerOptions.address - const dataService = this.contracts.SubgraphService.target.toString() - const delegationRatio = await this.contracts.SubgraphService.getDelegationRatio() - const tokensAvailable = await this.contracts.HorizonStaking.getTokensAvailable( - address, - dataService, - delegationRatio, - ) - const lockedStake = - await this.contracts.SubgraphService.allocationProvisionTracker(address) - const freeStake = tokensAvailable > lockedStake ? tokensAvailable - lockedStake : 0n + const address = this.indexerOptions.address + const dataService = this.contracts.SubgraphService.target.toString() + const delegationRatio = await this.contracts.SubgraphService.getDelegationRatio() + const tokensAvailable = await this.contracts.HorizonStaking.getTokensAvailable( + address, + dataService, + delegationRatio, + ) + const lockedStake = + await this.contracts.SubgraphService.allocationProvisionTracker(address) + const freeStake = tokensAvailable > lockedStake ? tokensAvailable - lockedStake : 0n - return { - legacy: 0n, // In horizon new legacy allocations cannot be created so we return 0 - horizon: freeStake, - } - } else { - return { - legacy: await this.contracts.LegacyStaking.getIndexerCapacity( - this.indexerOptions.address, - ), - horizon: 0n, - } + return { + legacy: 0n, + horizon: freeStake, } } @@ -1394,18 +1366,11 @@ Please submit an issue at https://github.com/graphprotocol/block-oracle/issues/n } private async isOperator(operatorAddress: string, indexerAddress: string) { - if (await this.isHorizon()) { - return await this.contracts.HorizonStaking.isAuthorized( - indexerAddress, - this.contracts.SubgraphService.target, - operatorAddress, - ) - } else { - return await this.contracts.LegacyStaking.isOperator( - operatorAddress, - indexerAddress, - ) - } + return await this.contracts.HorizonStaking.isAuthorized( + indexerAddress, + this.contracts.SubgraphService.target, + operatorAddress, + ) } // Returns a tuple of [POI, blockNumber] diff --git a/packages/indexer-common/src/indexer-management/resolvers/actions.ts b/packages/indexer-common/src/indexer-management/resolvers/actions.ts index 308e57c3d..3851aacf9 100644 --- a/packages/indexer-common/src/indexer-management/resolvers/actions.ts +++ b/packages/indexer-common/src/indexer-management/resolvers/actions.ts @@ -11,16 +11,15 @@ import { ActionType, ActionUpdateInput, IndexerManagementModels, - Network, - NetworkMapped, OrderDirection, validateActionInputs, - validateNetworkIdentifier, } from '@graphprotocol/indexer-common' import { literal, Op, Transaction } from 'sequelize' import { ActionManager } from '../actions' -import groupBy from 'lodash.groupby' -import { extractNetwork } from './utils' +import { getProtocolNetwork } from './utils' + +/** ActionInput with protocolNetwork guaranteed to be set (after normalization) */ +type NormalizedActionInput = ActionInput & { protocolNetwork: string } // Perform insert, update, or no-op depending on existing queue data // INSERT - No item in the queue yet targeting this deploymentID @@ -29,7 +28,7 @@ import { extractNetwork } from './utils' // TODO: Use pending status for actions in process of execution, detect here and if duplicate pending found, NOOP async function executeQueueOperation( logger: Logger, - action: ActionInput, + action: NormalizedActionInput, actionsAwaitingExecution: Action[], recentlyAttemptedActions: Action[], models: IndexerManagementModels, @@ -142,64 +141,51 @@ export default { }) return await ActionManager.fetchActions( models, - null, - filter, orderBy, - orderDirection, + filter, first, + orderDirection, ) }, queueActions: async ( { actions }: { actions: ActionInput[] }, - { actionManager, logger, multiNetworks, models }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { actionManager, logger, network, models } = context logger.debug(`Execute 'queueActions' mutation`, { actions, }) - if (!actionManager || !multiNetworks) { + if (!actionManager || !network) { throw Error('IndexerManagementClient must be in `network` mode to modify actions') } - // Sanitize protocol network identifier + // Normalize protocol network identifier - use context network if not provided actions.forEach((action) => { - try { - action.protocolNetwork = validateNetworkIdentifier(action.protocolNetwork) - } catch (e) { - throw Error(`Invalid value for the field 'protocolNetwork'. ${e}`) - } + action.protocolNetwork = getProtocolNetwork(context, action.protocolNetwork) }) // Set proper value for isLegacy - any new actions in horizon are not legacy await Promise.all( actions.map(async (action) => { - const network = extractNetwork(action.protocolNetwork, multiNetworks) action.isLegacy = !(await network.isHorizon.value()) }), ) - // Let Network Monitors validate actions based on their protocol networks - await multiNetworks.mapNetworkMapped( - groupBy(actions, (action) => action.protocolNetwork), - (network: Network, actions: ActionInput[]) => - validateActionInputs(actions, network.networkMonitor, logger), - ) + // Validate actions using the network monitor + await validateActionInputs(actions, network.networkMonitor, logger) let results: ActionResult[] = [] // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await models.Action.sequelize!.transaction(async (transaction) => { - const alreadyQueuedActions = await ActionManager.fetchActions(models, transaction, { + const alreadyQueuedActions = await ActionManager.fetchActions(models, null, { status: ActionStatus.QUEUED, }) - const alreadyApprovedActions = await ActionManager.fetchActions( - models, - transaction, - { - status: ActionStatus.APPROVED, - }, - ) + const alreadyApprovedActions = await ActionManager.fetchActions(models, null, { + status: ActionStatus.APPROVED, + }) const actionsAwaitingExecution = alreadyQueuedActions.concat(alreadyApprovedActions) // Fetch recently attempted actions @@ -207,23 +193,15 @@ export default { [Op.gte]: literal("NOW() - INTERVAL '15m'"), } - const recentlyFailedActions = await ActionManager.fetchActions( - models, - transaction, - { - status: ActionStatus.FAILED, - updatedAt: last15Minutes, - }, - ) + const recentlyFailedActions = await ActionManager.fetchActions(models, null, { + status: ActionStatus.FAILED, + updatedAt: last15Minutes, + }) - const recentlySuccessfulActions = await ActionManager.fetchActions( - models, - transaction, - { - status: ActionStatus.SUCCESS, - updatedAt: last15Minutes, - }, - ) + const recentlySuccessfulActions = await ActionManager.fetchActions(models, null, { + status: ActionStatus.SUCCESS, + updatedAt: last15Minutes, + }) logger.trace('Recently attempted actions', { recentlySuccessfulActions, @@ -234,7 +212,8 @@ export default { recentlySuccessfulActions, ) - for (const action of actions) { + // Cast to NormalizedActionInput since we've already normalized protocolNetwork + for (const action of actions as NormalizedActionInput[]) { const result = await executeQueueOperation( logger, action, @@ -369,28 +348,18 @@ export default { if (!actionManager) { throw Error('IndexerManagementClient must be in `network` mode to modify actions') } - const result: NetworkMapped = await actionManager.multiNetworks.map( - async (network: Network) => { - logger.debug(`Execute 'executeApprovedActions' mutation`, { - protocolNetwork: network.specification.networkIdentifier, - }) - try { - return await actionManager.executeApprovedActions(network) - } catch (error) { - logger.error('Failed to execute approved actions for network', { - protocolNetwork: network.specification.networkIdentifier, - error, - }) - return [] - } - }, - ) - return Object.values(result).flat() + logger.debug(`Execute 'executeApprovedActions' mutation`) + try { + return await actionManager.executeApprovedActions() + } catch (error) { + logger.error('Failed to execute approved actions', { error }) + return [] + } }, } // Helper function to assess equality among a enqueued and a proposed actions -function compareActions(enqueued: Action, proposed: ActionInput): boolean { +function compareActions(enqueued: Action, proposed: NormalizedActionInput): boolean { // actions are not the same if they target different protocol networks if (enqueued.protocolNetwork !== proposed.protocolNetwork) { return false diff --git a/packages/indexer-common/src/indexer-management/resolvers/allocations.ts b/packages/indexer-common/src/indexer-management/resolvers/allocations.ts index 2f7803c62..a2faf0974 100644 --- a/packages/indexer-common/src/indexer-management/resolvers/allocations.ts +++ b/packages/indexer-common/src/indexer-management/resolvers/allocations.ts @@ -3,7 +3,7 @@ import pMap from 'p-map' import gql from 'graphql-tag' -import { ethers, hexlify, ZeroAddress } from 'ethers' +import { ethers, ZeroAddress } from 'ethers' import { Address, @@ -26,7 +26,6 @@ import { IndexerManagementResolverContext, IndexingDecisionBasis, IndexingRuleAttributes, - legacyAllocationIdProof, Network, POIData, ReallocateAllocationResult, @@ -41,7 +40,7 @@ import { encodeStopServiceData, PaymentTypes, } from '@graphprotocol/toolshed' -import { extractNetwork } from './utils' +import { getNetwork, getProtocolNetwork } from './utils' import { tryParseCustomError } from '../../utils' import { GraphNode } from '../../graph-node' @@ -318,163 +317,7 @@ async function queryAllocations( ) } -async function createLegacyAllocation( - network: Network, - graphNode: GraphNode, - allocationAmount: bigint, - logger: Logger, - subgraphDeployment: SubgraphDeploymentID, - currentEpoch: bigint, - activeAllocations: Allocation[], - protocolNetwork: string, -): Promise<{ txHash: string; allocationId: Address }> { - const contracts = network.contracts - const transactionManager = network.transactionManager - const address = network.specification.indexerOptions.address - - // Identify how many GRT the indexer has staked - const freeStake = await contracts.LegacyStaking.getIndexerCapacity(address) - - // If there isn't enough left for allocating, abort - if (freeStake < allocationAmount) { - logger.error( - `Legacy allocation of ${formatGRT( - allocationAmount, - )} GRT cancelled: indexer only has a free stake amount of ${formatGRT( - freeStake, - )} GRT`, - ) - throw indexerError( - IndexerErrorCode.IE013, - `Legacy allocation of ${formatGRT( - allocationAmount, - )} GRT cancelled: indexer only has a free stake amount of ${formatGRT( - freeStake, - )} GRT`, - ) - } - - // Ensure subgraph is deployed before allocating - await graphNode.ensure( - `indexer-agent/${subgraphDeployment.ipfsHash.slice(-10)}`, - subgraphDeployment, - ) - - logger.debug('Obtain a unique legacy Allocation ID') - - // Obtain a unique allocation ID - const recentlyClosedAllocations = - await network.networkMonitor.recentlyClosedAllocations(Number(currentEpoch), 2) - const activeAndRecentlyClosedAllocations: Allocation[] = [ - ...recentlyClosedAllocations, - ...activeAllocations, - ] - const { allocationSigner, allocationId } = uniqueAllocationID( - transactionManager.wallet.mnemonic!.phrase, - Number(currentEpoch), - subgraphDeployment, - activeAndRecentlyClosedAllocations.map((allocation) => allocation.id), - ) - - // Double-check whether the allocationID already exists on chain, to - // avoid unnecessary transactions. - // Note: We're checking the allocation state here, which is defined as - // - // enum AllocationState { Null, Active, Closed, Finalized } - // - // in the contracts. - const state = await contracts.LegacyStaking.getAllocationState(allocationId) - if (state !== 0n) { - logger.debug(`Skipping legacy allocation as it already exists onchain`, { - indexer: address, - allocation: allocationId, - }) - throw indexerError( - IndexerErrorCode.IE066, - `Legacy allocation '${allocationId}' already exists onchain`, - ) - } - - logger.debug('Generating new legacy allocation ID proof', { - newAllocationSigner: allocationSigner, - newAllocationID: allocationId, - indexerAddress: address, - }) - - const proof = await legacyAllocationIdProof(allocationSigner, address, allocationId) - - logger.debug('Successfully generated legacy allocation ID proof', { - allocationIDProof: proof, - }) - - logger.debug(`Sending legacy allocateFrom transaction`, { - indexer: address, - subgraphDeployment: subgraphDeployment.ipfsHash, - amount: formatGRT(allocationAmount), - allocation: allocationId, - proof, - protocolNetwork, - }) - - const receipt = await transactionManager.executeTransaction( - async () => - contracts.LegacyStaking.allocateFrom.estimateGas( - address, - subgraphDeployment.bytes32, - allocationAmount, - allocationId, - hexlify(new Uint8Array(32).fill(0)), - proof, - ), - async (gasLimit) => - contracts.LegacyStaking.allocateFrom( - address, - subgraphDeployment.bytes32, - allocationAmount, - allocationId, - hexlify(new Uint8Array(32).fill(0)), - proof, - { gasLimit }, - ), - logger.child({ action: 'allocate' }), - ) - - if (receipt === 'paused' || receipt === 'unauthorized') { - throw indexerError( - IndexerErrorCode.IE062, - `Legacy allocation not created. ${ - receipt === 'paused' ? 'Network paused' : 'Operator not authorized' - }`, - ) - } - - const createAllocationEventLogs = network.transactionManager.findEvent( - 'AllocationCreated', - network.contracts.LegacyStaking.interface, - 'subgraphDeploymentID', - subgraphDeployment.toString(), - receipt, - logger, - ) - - if (!createAllocationEventLogs) { - throw indexerError( - IndexerErrorCode.IE014, - `Legacy allocation create transaction was never mined`, - ) - } - - logger.info(`Successfully legacy allocated to subgraph deployment`, { - amountGRT: formatGRT(createAllocationEventLogs.tokens), - allocation: createAllocationEventLogs.allocationID, - epoch: createAllocationEventLogs.epoch.toString(), - transaction: receipt.hash, - }) - - return { txHash: receipt.hash, allocationId: createAllocationEventLogs.allocationID } -} - -async function createHorizonAllocation( +async function createAllocation( network: Network, graphNode: GraphNode, allocationAmount: bigint, @@ -629,95 +472,7 @@ async function createHorizonAllocation( return { txHash: receipt.hash, allocationId } } -async function closeLegacyAllocation( - allocation: Allocation, - poi: string, - network: Network, - logger: Logger, -): Promise<{ txHash: string; rewardsAssigned: bigint }> { - const contracts = network.contracts - const transactionManager = network.transactionManager - const isHorizon = await network.isHorizon.value() - - // Double-check whether the allocation is still active on chain, to - // avoid unnecessary transactions. - // Note: We're checking the allocation state here, which is defined as - // - // enum AllocationState { Null, Active, Closed, Finalized } - // - // in the contracts. - const state = await contracts.LegacyStaking.getAllocationState(allocation.id) - if (state !== 1n) { - throw indexerError( - IndexerErrorCode.IE065, - 'Legacy allocation has already been closed', - ) - } - - logger.debug('Sending legacy closeAllocation transaction') - const receipt = await transactionManager.executeTransaction( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - () => contracts.LegacyStaking.closeAllocation.estimateGas(allocation.id, poi!), - (gasLimit) => - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - contracts.LegacyStaking.closeAllocation(allocation.id, poi!, { - gasLimit, - }), - logger, - ) - - if (receipt === 'paused' || receipt === 'unauthorized') { - throw indexerError( - IndexerErrorCode.IE062, - `Legacy allocation '${allocation.id}' could not be closed: ${receipt}`, - ) - } - - const closeAllocationEventLogs = transactionManager.findEvent( - 'AllocationClosed', - contracts.LegacyStaking.interface, - 'allocationID', - allocation.id, - receipt, - logger, - ) - - if (!closeAllocationEventLogs) { - throw indexerError( - IndexerErrorCode.IE015, - `Legacy allocation close transaction was never successfully mined`, - ) - } - - const rewardsEventLogs = transactionManager.findEvent( - isHorizon ? 'HorizonRewardsAssigned' : 'RewardsAssigned', - contracts.RewardsManager.interface, - 'allocationID', - allocation.id, - receipt, - logger, - ) - - const rewardsAssigned = rewardsEventLogs ? rewardsEventLogs.amount : 0 - if (rewardsAssigned == 0) { - logger.warn('No rewards were distributed upon closing the legacy allocation') - } - - logger.info(`Successfully closed legacy allocation`, { - deployment: closeAllocationEventLogs.subgraphDeploymentID, - allocation: closeAllocationEventLogs.allocationID, - indexer: closeAllocationEventLogs.indexer, - amountGRT: formatGRT(closeAllocationEventLogs.tokens), - poi: closeAllocationEventLogs.poi, - epoch: closeAllocationEventLogs.epoch.toString(), - transaction: receipt.hash, - indexingRewards: rewardsAssigned, - }) - - return { txHash: receipt.hash, rewardsAssigned } -} - -async function closeHorizonAllocation( +async function closeAllocation( allocation: Allocation, poiData: POIData, network: Network, @@ -841,226 +596,7 @@ async function closeHorizonAllocation( return { txHash: receipt.hash, rewardsAssigned } } -// isHorizon: false -async function reallocateLegacyAllocation( - allocation: Allocation, - allocationAmount: bigint, - activeAllocations: Allocation[], - poi: string, - network: Network, - logger: Logger, -): Promise<{ txHash: string; rewardsAssigned: bigint; newAllocationId: Address }> { - const contracts = network.contracts - const transactionManager = network.transactionManager - const address = network.specification.indexerOptions.address - const currentEpoch = await contracts.EpochManager.currentEpoch() - const isHorizon = await network.isHorizon.value() - - // Double-check whether the allocation is still active on chain, to - // avoid unnecessary transactions. - // Note: We're checking the allocation state here, which is defined as - // - // enum AllocationState { Null, Active, Closed, Finalized } - // - // in the contracts. - const state = await contracts.LegacyStaking.getAllocationState(allocation.id) - if (state !== 1n) { - logger.warn(`Legacy allocation has already been closed`) - throw indexerError( - IndexerErrorCode.IE065, - `Legacy allocation has already been closed`, - ) - } - - if (allocationAmount < 0n) { - logger.warn('Cannot legacy reallocate a negative amount of GRT', { - amount: allocationAmount.toString(), - }) - throw indexerError( - IndexerErrorCode.IE061, - 'Cannot legacy reallocate a negative amount of GRT', - ) - } - - logger.info(`Legacy reallocate to subgraph deployment`, { - existingAllocationAmount: formatGRT(allocation.allocatedTokens), - newAllocationAmount: formatGRT(allocationAmount), - epoch: currentEpoch.toString(), - }) - - // Identify how many GRT the indexer has staked - const freeStake = (await network.networkMonitor.freeStake()).legacy - - // When reallocating, we will first close the old allocation and free up the GRT in that allocation - // This GRT will be available in addition to freeStake for the new allocation - const postCloseFreeStake = freeStake + allocation.allocatedTokens - - // If there isn't enough left for allocating, abort - if (postCloseFreeStake < allocationAmount) { - throw indexerError( - IndexerErrorCode.IE013, - `Unable to legacy allocate ${formatGRT( - allocationAmount, - )} GRT: indexer only has a free stake amount of ${formatGRT( - freeStake, - )} GRT, plus ${formatGRT( - allocation.allocatedTokens, - )} GRT from the existing allocation`, - ) - } - - logger.debug('Generating a new unique legacy Allocation ID') - const recentlyClosedAllocations = - await network.networkMonitor.recentlyClosedAllocations(Number(currentEpoch), 2) - const activeAndRecentlyClosedAllocations: Allocation[] = [ - ...recentlyClosedAllocations, - ...activeAllocations, - ] - const { allocationSigner, allocationId: newAllocationId } = uniqueAllocationID( - transactionManager.wallet.mnemonic!.phrase, - Number(currentEpoch), - allocation.subgraphDeployment.id, - activeAndRecentlyClosedAllocations.map((allocation) => allocation.id), - ) - - logger.debug('New unique legacy Allocation ID generated', { - newAllocationID: newAllocationId, - newAllocationSigner: allocationSigner, - }) - - // Double-check whether the allocationID already exists on chain, to - // avoid unnecessary transactions. - // Note: We're checking the allocation state here, which is defined as - // - // enum AllocationState { Null, Active, Closed, Finalized } - // - // in the contracts. - const newAllocationState = - await contracts.LegacyStaking.getAllocationState(newAllocationId) - if (newAllocationState !== 0n) { - logger.warn(`Skipping legacy Allocation as it already exists onchain`, { - indexer: address, - allocation: newAllocationId, - newAllocationState, - }) - throw indexerError(IndexerErrorCode.IE066, 'AllocationID already exists') - } - - logger.debug('Generating new legacy allocation ID proof', { - newAllocationSigner: allocationSigner, - newAllocationID: newAllocationId, - indexerAddress: address, - }) - const proof = await legacyAllocationIdProof(allocationSigner, address, newAllocationId) - logger.debug('Successfully generated legacy allocation ID proof', { - allocationIDProof: proof, - }) - - logger.info(`Sending legacy close and legacy allocate multicall transaction`, { - indexer: address, - amount: formatGRT(allocationAmount), - oldAllocation: allocation.id, - newAllocation: newAllocationId, - newAllocationAmount: formatGRT(allocationAmount), - deployment: allocation.subgraphDeployment.id.toString(), - poi: poi, - proof, - epoch: currentEpoch.toString(), - }) - - const callData = [ - await contracts.LegacyStaking.closeAllocation.populateTransaction(allocation.id, poi), - await contracts.LegacyStaking.allocateFrom.populateTransaction( - address, - allocation.subgraphDeployment.id.bytes32, - allocationAmount, - newAllocationId, - hexlify(new Uint8Array(32).fill(0)), // metadata - proof, - ), - ].map((tx) => tx.data as string) - - const receipt = await transactionManager.executeTransaction( - async () => contracts.LegacyStaking.multicall.estimateGas(callData), - async (gasLimit) => contracts.LegacyStaking.multicall(callData, { gasLimit }), - logger.child({ - function: 'closeAndAllocate', - }), - ) - - if (receipt === 'paused' || receipt === 'unauthorized') { - throw indexerError( - IndexerErrorCode.IE062, - `Legacy allocation '${newAllocationId}' could not be closed: ${receipt}`, - ) - } - - const createAllocationEventLogs = transactionManager.findEvent( - 'AllocationCreated', - contracts.LegacyStaking.interface, - 'subgraphDeploymentID', - allocation.subgraphDeployment.id.toString(), - receipt, - logger, - ) - - if (!createAllocationEventLogs) { - throw indexerError(IndexerErrorCode.IE014, `Legacy allocation was never mined`) - } - - const closeAllocationEventLogs = transactionManager.findEvent( - 'AllocationClosed', - contracts.LegacyStaking.interface, - 'allocationID', - allocation.id, - receipt, - logger, - ) - - if (!closeAllocationEventLogs) { - throw indexerError( - IndexerErrorCode.IE015, - `Legacy allocation close transaction was never successfully mined`, - ) - } - - const rewardsEventLogs = transactionManager.findEvent( - isHorizon ? 'HorizonRewardsAssigned' : 'RewardsAssigned', - contracts.RewardsManager.interface, - 'allocationID', - allocation.id, - receipt, - logger, - ) - - const rewardsAssigned = rewardsEventLogs ? rewardsEventLogs.amount : 0 - if (rewardsAssigned == 0) { - logger.warn('No rewards were distributed upon closing the legacy allocation') - } - - logger.info(`Successfully reallocated legacy allocation`, { - deployment: createAllocationEventLogs.subgraphDeploymentID, - closedAllocation: closeAllocationEventLogs.allocationID, - closedAllocationStakeGRT: formatGRT(closeAllocationEventLogs.tokens), - closedAllocationPOI: closeAllocationEventLogs.poi, - closedAllocationEpoch: closeAllocationEventLogs.epoch.toString(), - indexingRewardsCollected: rewardsAssigned, - createdAllocation: createAllocationEventLogs.allocationID, - createdAllocationStakeGRT: formatGRT(createAllocationEventLogs.tokens), - indexer: createAllocationEventLogs.indexer, - epoch: createAllocationEventLogs.epoch.toString(), - transaction: receipt.hash, - }) - - logger.info('Identifying receipts worth collecting', { - allocation: closeAllocationEventLogs.allocationID, - }) - - return { txHash: receipt.hash, rewardsAssigned, newAllocationId } -} - -// isHorizon: true and allocation: not legacy -async function reallocateHorizonAllocation( +async function reallocateAllocation( allocation: Allocation, allocationAmount: bigint, activeAllocations: Allocation[], @@ -1312,139 +848,82 @@ async function reallocateHorizonAllocation( return { txHash: receipt.hash, rewardsAssigned, newAllocationId } } -// isHorizon: true and allocation: legacy -async function migrateLegacyAllocationToHorizon( - allocation: Allocation, - allocationAmount: bigint, - activeAllocations: Allocation[], - poi: string, - network: Network, - graphNode: GraphNode, - logger: Logger, -): Promise<{ txHash: string; rewardsAssigned: bigint; newAllocationId: Address }> { - const contracts = network.contracts - const currentEpoch = await contracts.EpochManager.currentEpoch() - - // We want to make sure that we close the legacy allocation even if reallocating to horizon would fail - // so we don't use a multicall but send separate transactions for closing - const closeAllocationResult = await closeLegacyAllocation( - allocation, - poi, - network, - logger, - ) - - // After closing the legacy allocation, we attempt to create a new horizon allocation - const createAllocationResult = await createHorizonAllocation( - network, - graphNode, - allocationAmount, - logger, - allocation.subgraphDeployment.id, - currentEpoch, - activeAllocations, - network.specification.networkIdentifier, - ) - - return { - txHash: createAllocationResult.txHash, - rewardsAssigned: closeAllocationResult.rewardsAssigned, - newAllocationId: createAllocationResult.allocationId, - } -} - export default { allocations: async ( { filter }: { filter: AllocationFilter }, - { multiNetworks, logger }: IndexerManagementResolverContext, + { network, logger }: IndexerManagementResolverContext, ): Promise => { logger.debug('Execute allocations() query', { filter, }) - if (!multiNetworks) { + if (!network) { throw Error( 'IndexerManagementClient must be in `network` mode to fetch allocations', ) } - const allocationsByNetwork = await multiNetworks.map( - async (network: Network): Promise => { - // Return early if a different protocol network is specifically requested - if ( - filter.protocolNetwork && - filter.protocolNetwork !== network.specification.networkIdentifier - ) { - return [] - } - - const { - networkMonitor, - networkSubgraph, - contracts, - specification: { - indexerOptions: { address }, - }, - } = network - - const [currentEpoch, maxAllocationDuration, epochLength] = await Promise.all([ - networkMonitor.networkCurrentEpoch(), - networkMonitor.maxAllocationDuration(), - contracts.EpochManager.epochLength(), - ]) - - const allocation = filter.allocation - ? filter.allocation === 'all' - ? null - : toAddress(filter.allocation) - : null - - const variables = { - indexer: toAddress(address), - allocation, - status: filter.status, - } - - const context = { - currentEpoch: currentEpoch.epochNumber, - currentEpochStartBlock: currentEpoch.startBlockNumber, - currentEpochElapsedBlocks: epochElapsedBlocks(currentEpoch), - latestBlock: currentEpoch.latestBlock, - maxAllocationDuration: maxAllocationDuration, - blocksPerEpoch: Number(epochLength), - avgBlockTime: 13000, - protocolNetwork: network.specification.networkIdentifier, - } - - return queryAllocations(logger, networkSubgraph, variables, context) + const { + networkMonitor, + networkSubgraph, + contracts, + specification: { + indexerOptions: { address }, }, - ) + } = network + + const [currentEpoch, maxAllocationDuration, epochLength] = await Promise.all([ + networkMonitor.networkCurrentEpoch(), + networkMonitor.maxAllocationDuration(), + contracts.EpochManager.epochLength(), + ]) + + const allocation = filter.allocation + ? filter.allocation === 'all' + ? null + : toAddress(filter.allocation) + : null + + const variables = { + indexer: toAddress(address), + allocation, + status: filter.status, + } + + const context = { + currentEpoch: currentEpoch.epochNumber, + currentEpochStartBlock: currentEpoch.startBlockNumber, + currentEpochElapsedBlocks: epochElapsedBlocks(currentEpoch), + latestBlock: currentEpoch.latestBlock, + maxAllocationDuration: maxAllocationDuration, + blocksPerEpoch: Number(epochLength), + avgBlockTime: 13000, + protocolNetwork: network.specification.networkIdentifier, + } - return Object.values(allocationsByNetwork).flat() + return queryAllocations(logger, networkSubgraph, variables, context) }, createAllocation: async ( { deployment, amount, - protocolNetwork, + protocolNetwork: providedProtocolNetwork, }: { deployment: string amount: string - protocolNetwork: string + protocolNetwork: string | undefined }, - { multiNetworks, graphNode, logger, models }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { graphNode, logger, models } = context + // Get protocol network from context or provided value + const protocolNetwork = getProtocolNetwork(context, providedProtocolNetwork) logger.debug('Execute createAllocation() mutation', { deployment, amount, protocolNetwork, }) - if (!multiNetworks) { - throw Error( - 'IndexerManagementClient must be in `network` mode to fetch allocations', - ) - } - const network = extractNetwork(protocolNetwork, multiNetworks) + const network = getNetwork(context, protocolNetwork) const networkMonitor = network.networkMonitor const allocationAmount = parseGRT(amount) const subgraphDeployment = new SubgraphDeploymentID(deployment) @@ -1478,43 +957,19 @@ export default { try { const currentEpoch = await network.contracts.EpochManager.currentEpoch() - const isHorizon = await network.isHorizon.value() - logger.debug('createAllocation: Checking allocation resolution path', { - isHorizon, - }) - - let txHash: string - let allocationId: Address - if (isHorizon) { - logger.debug('Creating horizon allocation') - const result = await createHorizonAllocation( - network, - graphNode, - allocationAmount, - logger, - subgraphDeployment, - currentEpoch, - activeAllocations, - protocolNetwork, - ) - txHash = result.txHash - allocationId = result.allocationId - } else { - logger.debug('Creating legacy allocation') - const result = await createLegacyAllocation( - network, - graphNode, - allocationAmount, - logger, - subgraphDeployment, - currentEpoch, - activeAllocations, - protocolNetwork, - ) - txHash = result.txHash - allocationId = result.allocationId - } + const result = await createAllocation( + network, + graphNode, + allocationAmount, + logger, + subgraphDeployment, + currentEpoch, + activeAllocations, + protocolNetwork, + ) + const txHash = result.txHash + const allocationId = result.allocationId logger.debug( `Updating indexing rules, so indexer-agent will now manage the active allocation`, @@ -1564,29 +1019,27 @@ export default { blockNumber, publicPOI, force, - protocolNetwork, + protocolNetwork: providedProtocolNetwork, }: { allocation: string poi: string | undefined blockNumber: string | undefined publicPOI: string | undefined force: boolean - protocolNetwork: string + protocolNetwork: string | undefined }, - { logger, models, multiNetworks }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { logger, models } = context + // Get protocol network from context or provided value + const protocolNetwork = getProtocolNetwork(context, providedProtocolNetwork) logger.debug('Execute closeAllocation() mutation', { allocationID: allocation, poi: poi || 'none provided', blockNumber: blockNumber || 'none provided', publicPOI: publicPOI || 'none provided', }) - if (!multiNetworks) { - throw Error( - 'IndexerManagementClient must be in `network` mode to fetch allocations', - ) - } - const network = extractNetwork(protocolNetwork, multiNetworks) + const network = getNetwork(context, protocolNetwork) const networkMonitor = network.networkMonitor const allocationData = await networkMonitor.allocation(allocation) @@ -1595,8 +1048,8 @@ export default { const poiData = await networkMonitor.resolvePOI( allocationData, poi, - allocationData.isLegacy ? undefined : publicPOI, - allocationData.isLegacy || blockNumber === null ? undefined : Number(blockNumber), + publicPOI, + blockNumber === null ? undefined : Number(blockNumber), force, ) logger.debug('POI resolved', { @@ -1609,33 +1062,9 @@ export default { force, }) - logger.debug('closeAllocation: Checking allocation resolution path', { - allocationIsLegacy: allocationData.isLegacy, - }) - - let txHash: string - let rewardsAssigned: bigint - if (allocationData.isLegacy) { - logger.debug('Closing legacy allocation') - const result = await closeLegacyAllocation( - allocationData, - poiData.poi, - network, - logger, - ) - txHash = result.txHash - rewardsAssigned = result.rewardsAssigned - } else { - logger.debug('Closing horizon allocation') - const result = await closeHorizonAllocation( - allocationData, - poiData, - network, - logger, - ) - txHash = result.txHash - rewardsAssigned = result.rewardsAssigned - } + const result = await closeAllocation(allocationData, poiData, network, logger) + const txHash = result.txHash + const rewardsAssigned = result.rewardsAssigned logger.debug( `Updating indexing rules, so indexer-agent keeps the deployment synced but doesn't reallocate to it`, @@ -1682,7 +1111,7 @@ export default { publicPOI, amount, force, - protocolNetwork, + protocolNetwork: providedProtocolNetwork, }: { allocation: string poi: string | undefined @@ -1690,10 +1119,14 @@ export default { publicPOI: string | undefined amount: string force: boolean - protocolNetwork: string + protocolNetwork: string | undefined }, - { logger, models, multiNetworks, graphNode }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + let { logger } = context + const { models } = context + // Get protocol network from context or provided value + const protocolNetwork = getProtocolNetwork(context, providedProtocolNetwork) logger = logger.child({ component: 'reallocateAllocationResolver', }) @@ -1705,14 +1138,8 @@ export default { force, }) - if (!multiNetworks) { - throw Error( - 'IndexerManagementClient must be in `network` mode to fetch allocations', - ) - } - // Obtain the Network object and its associated components and data - const network = extractNetwork(protocolNetwork, multiNetworks) + const network = getNetwork(context, protocolNetwork) const networkMonitor = network.networkMonitor const allocationAmount = parseGRT(amount) @@ -1750,57 +1177,17 @@ export default { force, }) - const isHorizon = await network.isHorizon.value() - - logger.debug('reallocateAllocation: Checking allocation resolution path', { - isHorizon, - allocationIsLegacy: allocationData.isLegacy, - }) - - let txHash: string - let rewardsAssigned: bigint - let newAllocationId: Address - if (!isHorizon) { - logger.debug('Reallocating legacy allocation') - const result = await reallocateLegacyAllocation( - allocationData, - allocationAmount, - activeAllocations, - poiData.poi, - network, - logger, - ) - txHash = result.txHash - rewardsAssigned = result.rewardsAssigned - newAllocationId = result.newAllocationId - } else if (allocationData.isLegacy) { - logger.debug('Migrating legacy allocation to horizon') - const result = await migrateLegacyAllocationToHorizon( - allocationData, - allocationAmount, - activeAllocations, - poiData.poi, - network, - graphNode, - logger, - ) - txHash = result.txHash - rewardsAssigned = result.rewardsAssigned - newAllocationId = result.newAllocationId - } else { - logger.debug('Reallocating horizon allocation') - const result = await reallocateHorizonAllocation( - allocationData, - allocationAmount, - activeAllocations, - poiData, - network, - logger, - ) - txHash = result.txHash - rewardsAssigned = result.rewardsAssigned - newAllocationId = result.newAllocationId - } + const result = await reallocateAllocation( + allocationData, + allocationAmount, + activeAllocations, + poiData, + network, + logger, + ) + const txHash = result.txHash + const rewardsAssigned = result.rewardsAssigned + const newAllocationId = result.newAllocationId logger.debug( `Updating indexing rules, so indexer-agent will now manage the active allocation`, diff --git a/packages/indexer-common/src/indexer-management/resolvers/cost-models.ts b/packages/indexer-common/src/indexer-management/resolvers/cost-models.ts index 720878a38..d09f78ce8 100644 --- a/packages/indexer-common/src/indexer-management/resolvers/cost-models.ts +++ b/packages/indexer-common/src/indexer-management/resolvers/cost-models.ts @@ -78,14 +78,11 @@ export default { setCostModel: async ( { costModel }: { deployment: string; costModel: GraphQLCostModel }, - { models, multiNetworks }: IndexerManagementResolverContext, + { models, network }: IndexerManagementResolverContext, ): Promise => { - if (!multiNetworks) { + if (!network) { throw new Error('No network configuration available') } - if (Object.keys(multiNetworks.inner).length !== 1) { - throw Error('Must be in single network mode to set cost models') - } const update = parseGraphQLCostModel(costModel) // Validate cost model matches 'default => x;' where x is an integer or float diff --git a/packages/indexer-common/src/indexer-management/resolvers/indexer-status.ts b/packages/indexer-common/src/indexer-management/resolvers/indexer-status.ts index 3d90bd748..24dd5a127 100644 --- a/packages/indexer-common/src/indexer-management/resolvers/indexer-status.ts +++ b/packages/indexer-common/src/indexer-management/resolvers/indexer-status.ts @@ -4,13 +4,8 @@ import geohash from 'ngeohash' import gql from 'graphql-tag' import { IndexerManagementResolverContext } from '../client' import { SubgraphDeploymentID } from '@graphprotocol/common-ts' -import { - indexerError, - IndexerErrorCode, - Network, - validateNetworkIdentifier, -} from '@graphprotocol/indexer-common' -import { extractNetwork } from './utils' +import { indexerError, IndexerErrorCode, Network } from '@graphprotocol/indexer-common' +import { getNetwork } from './utils' interface Test { test: (url: string) => string run: (url: string) => Promise @@ -62,56 +57,29 @@ const URL_VALIDATION_TEST: Test = { export default { indexerRegistration: async ( - { protocolNetwork: unvalidatedProtocolNetwork }: { protocolNetwork: string }, - { multiNetworks, logger }: IndexerManagementResolverContext, + { + protocolNetwork: unvalidatedProtocolNetwork, + }: { protocolNetwork: string | undefined }, + context: IndexerManagementResolverContext, ): Promise => { - if (!multiNetworks) { - throw Error( - 'IndexerManagementClient must be in `network` mode to fetch indexer registration information', - ) - } - - const network = extractNetwork(unvalidatedProtocolNetwork, multiNetworks) + const network = getNetwork(context, unvalidatedProtocolNetwork) const protocolNetwork = network.specification.networkIdentifier const address = network.specification.indexerOptions.address const contracts = network.contracts const registrationInfo: RegistrationInfo[] = [] - // Check if the indexer is registered in the legacy service registry - try { - const registered = await contracts.LegacyServiceRegistry.isRegistered(address) - if (registered) { - const service = await contracts.LegacyServiceRegistry.services(address) - registrationInfo.push({ - address, - protocolNetwork, - url: service.url, - location: geohash.decode(service.geoHash), - registered, - isLegacy: true, - __typename: 'IndexerRegistration', - }) - } - } catch (e) { - logger?.debug( - `Could not get legacy service registration for indexer. It's likely that the legacy protocol is not reachable.`, - ) - } - - if (await network.isHorizon.value()) { - const service = await contracts.SubgraphService.indexers(address) - if (service.url.length > 0) { - registrationInfo.push({ - address, - protocolNetwork, - url: service.url, - location: geohash.decode(service.geoHash), - registered: true, - isLegacy: false, - __typename: 'IndexerRegistration', - }) - } + const service = await contracts.SubgraphService.indexers(address) + if (service.url.length > 0) { + registrationInfo.push({ + address, + protocolNetwork, + url: service.url, + location: geohash.decode(service.geoHash), + registered: true, + isLegacy: false, + __typename: 'IndexerRegistration', + }) } if (registrationInfo.length === 0) { @@ -141,16 +109,11 @@ export default { }, indexerAllocations: async ( - { protocolNetwork }: { protocolNetwork: string }, - { multiNetworks, logger }: IndexerManagementResolverContext, + { protocolNetwork: providedProtocolNetwork }: { protocolNetwork: string | undefined }, + context: IndexerManagementResolverContext, ): Promise => { - if (!multiNetworks) { - throw Error( - 'IndexerManagementClient must be in `network` mode to fetch indexer allocations', - ) - } - - const network = extractNetwork(protocolNetwork, multiNetworks) + const { logger } = context + const network = getNetwork(context, providedProtocolNetwork) const address = network.specification.indexerOptions.address try { @@ -218,59 +181,23 @@ export default { indexerEndpoints: async ( { protocolNetwork: unvalidatedProtocolNetwork }: { protocolNetwork: string | null }, - { multiNetworks, logger }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { - if (!multiNetworks) { - throw Error( - 'IndexerManagementClient must be in `network` mode to fetch indexer endpoints', - ) - } + const { logger } = context const endpoints: Endpoints[] = [] - let networkIdentifier: string | null = null - // Validate protocol network + // Get the network - uses context network if not provided + const network = getNetwork(context, unvalidatedProtocolNetwork) try { - if (unvalidatedProtocolNetwork) { - networkIdentifier = validateNetworkIdentifier(unvalidatedProtocolNetwork) - } - } catch (parseError) { - throw new Error( - `Invalid protocol network identifier: '${unvalidatedProtocolNetwork}'. Error: ${parseError}`, - ) + const networkEndpoints = await endpointForNetwork(network) + endpoints.push(networkEndpoints) + } catch (err) { + // Ignore endpoints for this network + logger?.warn(`Failed to detect service endpoints for network`, { + err, + protocolNetwork: network.specification.networkIdentifier, + }) } - - await multiNetworks.map(async (network: Network) => { - // Skip if this query asks for another protocol network - if ( - networkIdentifier && - networkIdentifier !== network.specification.networkIdentifier - ) { - return - } - try { - // Always try to get legacy endpoints, but fail gracefully if they don't exist - try { - const networkEndpoints = await endpointForNetwork(network, false) - endpoints.push(networkEndpoints) - } catch (e) { - logger?.debug( - `Could not get legacy service endpoints for network. It's likely that the legacy protocol is not reachable.`, - ) - } - - // Only get horizon endpoints when horizon is enabled - if (await network.isHorizon.value()) { - const networkEndpoints = await endpointForNetwork(network, true) - endpoints.push(networkEndpoints) - } - } catch (err) { - // Ignore endpoints for this network - logger?.warn(`Failed to detect service endpoints for network`, { - err, - protocolNetwork: network.specification.networkIdentifier, - }) - } - }) return endpoints }, } @@ -329,16 +256,11 @@ function defaultEndpoints(protocolNetwork: string, isHorizon: boolean): Endpoint } } -async function endpointForNetwork( - network: Network, - isHorizon: boolean, -): Promise { +async function endpointForNetwork(network: Network): Promise { const contracts = network.contracts const address = network.specification.indexerOptions.address - const endpoints = defaultEndpoints(network.specification.networkIdentifier, isHorizon) - const service = isHorizon - ? await contracts.SubgraphService.indexers(address) - : await contracts.LegacyServiceRegistry.services(address) + const endpoints = defaultEndpoints(network.specification.networkIdentifier, true) + const service = await contracts.SubgraphService.indexers(address) if (service) { { const { url, tests, ok } = await testURL(service.url, [ diff --git a/packages/indexer-common/src/indexer-management/resolvers/indexing-rules.ts b/packages/indexer-common/src/indexer-management/resolvers/indexing-rules.ts index 2adcdfe8e..787078047 100644 --- a/packages/indexer-common/src/indexer-management/resolvers/indexing-rules.ts +++ b/packages/indexer-common/src/indexer-management/resolvers/indexing-rules.ts @@ -10,9 +10,8 @@ import { IndexerManagementDefaults, IndexerManagementResolverContext } from '../ import { Transaction } from 'sequelize' import { fetchIndexingRules } from '../rules' import { ensureAllocationLifetime, processIdentifier } from '../../' -import { validateNetworkIdentifier } from '../../parsers' import groupBy from 'lodash.groupby' -import { extractNetwork } from './utils' +import { getNetwork, getProtocolNetwork } from './utils' const resetGlobalRule = async ( ruleIdentifier: IndexingRuleIdentifier, @@ -36,15 +35,17 @@ export default { identifier: indexingRuleIdentifier, merged, }: { identifier: IndexingRuleIdentifier; merged: boolean }, - { models }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { models } = context const [identifier] = await processIdentifier(indexingRuleIdentifier.identifier, { all: false, global: true, }) - // Sanitize protocol network identifier - const protocolNetwork = validateNetworkIdentifier( + // Get protocol network from context or provided value + const protocolNetwork = getProtocolNetwork( + context, indexingRuleIdentifier.protocolNetwork, ) @@ -67,39 +68,27 @@ export default { merged, protocolNetwork: uncheckedProtocolNetwork, }: { merged: boolean; protocolNetwork: string | undefined }, - { models }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { - // Convert the input `protocolNetwork` value to a CAIP2-ID - const protocolNetwork = uncheckedProtocolNetwork - ? validateNetworkIdentifier(uncheckedProtocolNetwork) - : undefined + const { models } = context + // Get protocol network from context or provided value + const protocolNetwork = getProtocolNetwork(context, uncheckedProtocolNetwork) return await fetchIndexingRules(models, merged, protocolNetwork) }, setIndexingRule: async ( { rule }: { rule: IndexingRuleCreationAttributes }, - { multiNetworks, models }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { models } = context if (!rule.identifier) { throw Error('Cannot set indexingRule without identifier') } - if (!rule.protocolNetwork) { - throw Error("Cannot set an indexing rule without the field 'protocolNetwork'") - } else { - try { - rule.protocolNetwork = validateNetworkIdentifier(rule.protocolNetwork) - } catch (e) { - throw Error(`Invalid value for the field 'protocolNetwork'. ${e}`) - } - } + // Get protocol network from context or provided value + rule.protocolNetwork = getProtocolNetwork(context, rule.protocolNetwork) - if (!multiNetworks) { - throw Error( - 'IndexerManagementClient must be in `network` mode to set indexing rules', - ) - } - const network = extractNetwork(rule.protocolNetwork, multiNetworks) + const network = getNetwork(context, rule.protocolNetwork) const [isValid, maxSuggestedLifetime] = await ensureAllocationLifetime(rule, network) if (!isValid) { @@ -121,15 +110,17 @@ export default { deleteIndexingRule: async ( { identifier: indexingRuleIdentifier }: { identifier: IndexingRuleIdentifier }, - { models, defaults }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { models, defaults } = context const [identifier] = await processIdentifier(indexingRuleIdentifier.identifier, { all: false, global: true, }) - // Sanitize protocol network identifier - const protocolNetwork = validateNetworkIdentifier( + // Get protocol network from context or provided value + const protocolNetwork = getProtocolNetwork( + context, indexingRuleIdentifier.protocolNetwork, ) @@ -161,13 +152,14 @@ export default { deleteIndexingRules: async ( { identifiers: indexingRuleIdentifiers }: { identifiers: IndexingRuleIdentifier[] }, - { models, defaults }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { models, defaults } = context let totalNumDeleted = 0 - // Sanitize protocol network identifiers + // Normalize protocol network identifiers - use context network if not provided for (const identifier of indexingRuleIdentifiers) { - identifier.protocolNetwork = validateNetworkIdentifier(identifier.protocolNetwork) + identifier.protocolNetwork = getProtocolNetwork(context, identifier.protocolNetwork) } // Batch deletions by the `IndexingRuleIdentifier.protocolNetwork` attribute . diff --git a/packages/indexer-common/src/indexer-management/resolvers/poi-disputes.ts b/packages/indexer-common/src/indexer-management/resolvers/poi-disputes.ts index faf200fcd..48661a472 100644 --- a/packages/indexer-common/src/indexer-management/resolvers/poi-disputes.ts +++ b/packages/indexer-common/src/indexer-management/resolvers/poi-disputes.ts @@ -2,17 +2,20 @@ import { POIDispute, POIDisputeIdentifier, POIDisputeCreationAttributes } from '../models' import { IndexerManagementResolverContext } from '../client' -import { validateNetworkIdentifier } from '../../parsers' import { Op, WhereOptions } from 'sequelize' import groupBy from 'lodash.groupby' +import { getProtocolNetwork } from './utils' export default { dispute: async ( { identifier }: { identifier: POIDisputeIdentifier }, - { models }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { models } = context + // Get protocol network from context or provided value + const protocolNetwork = getProtocolNetwork(context, identifier.protocolNetwork) const dispute = await models.POIDispute.findOne({ - where: { ...identifier }, + where: { allocationID: identifier.allocationID, protocolNetwork }, }) return dispute?.toGraphQL() || dispute }, @@ -23,23 +26,19 @@ export default { minClosedEpoch, protocolNetwork: uncheckedProtocolNetwork, }: { status: string; minClosedEpoch: number; protocolNetwork: string | undefined }, - { models }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { - // Sanitize protocol network identifier - const protocolNetwork = uncheckedProtocolNetwork - ? validateNetworkIdentifier(uncheckedProtocolNetwork) - : undefined + const { models } = context + // Get protocol network from context or provided value + const protocolNetwork = getProtocolNetwork(context, uncheckedProtocolNetwork) // eslint-disable-next-line @typescript-eslint/no-explicit-any const sqlAndExpression: WhereOptions = [ { status }, { closedEpoch: { [Op.gte]: minClosedEpoch } }, + { protocolNetwork }, ] - if (protocolNetwork) { - sqlAndExpression.push({ protocolNetwork }) - } - const disputes = await models.POIDispute.findAll({ where: { [Op.and]: sqlAndExpression }, order: [['allocationAmount', 'DESC']], @@ -49,14 +48,12 @@ export default { storeDisputes: async ( { disputes }: { disputes: POIDisputeCreationAttributes[] }, - { models }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { - // Sanitize protocol network identifiers + const { models } = context + // Normalize protocol network identifiers - use context network if not provided for (const dispute of disputes) { - if (!dispute.protocolNetwork) { - throw new Error(`Dispute is missing the attribute 'protocolNetwork'`) - } - dispute.protocolNetwork = validateNetworkIdentifier(dispute.protocolNetwork) + dispute.protocolNetwork = getProtocolNetwork(context, dispute.protocolNetwork) } const createdDisputes = await models.POIDispute.bulkCreate(disputes, { @@ -74,16 +71,14 @@ export default { deleteDisputes: async ( { identifiers }: { identifiers: POIDisputeIdentifier[] }, - { models }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { models } = context let totalNumDeleted = 0 - // Sanitize protocol network identifiers + // Normalize protocol network identifiers - use context network if not provided for (const identifier of identifiers) { - if (!identifier.protocolNetwork) { - throw new Error(`Dispute is missing the attribute 'protocolNetwork'`) - } - identifier.protocolNetwork = validateNetworkIdentifier(identifier.protocolNetwork) + identifier.protocolNetwork = getProtocolNetwork(context, identifier.protocolNetwork) } // Batch by protocolNetwork diff --git a/packages/indexer-common/src/indexer-management/resolvers/provisions.ts b/packages/indexer-common/src/indexer-management/resolvers/provisions.ts index 00c9e522f..f9056c737 100644 --- a/packages/indexer-common/src/indexer-management/resolvers/provisions.ts +++ b/packages/indexer-common/src/indexer-management/resolvers/provisions.ts @@ -1,11 +1,11 @@ -import { indexerError, IndexerErrorCode, Network } from '@graphprotocol/indexer-common' +import { indexerError, IndexerErrorCode } from '@graphprotocol/indexer-common' /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/ban-types */ import gql from 'graphql-tag' import { IndexerManagementResolverContext } from '@graphprotocol/indexer-common' -import { extractNetwork } from './utils' +import { getNetwork, getProtocolNetwork } from './utils' import { tryParseCustomError } from '../../utils' import { formatGRT, parseGRT } from '@graphprotocol/common-ts' import { ThawRequestType } from '@graphprotocol/toolshed' @@ -112,21 +112,20 @@ const PROVISION_QUERIES = { export default { provisions: async ( { - protocolNetwork, + protocolNetwork: providedProtocolNetwork, }: { - protocolNetwork: string + protocolNetwork: string | undefined }, - { multiNetworks, logger }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { logger } = context + // Get protocol network from context or provided value + const protocolNetwork = getProtocolNetwork(context, providedProtocolNetwork) logger.debug('Execute provisions() query', { protocolNetwork, }) - if (!multiNetworks) { - throw Error('IndexerManagementClient must be in `network` mode to fetch provisions') - } - - const network = extractNetwork(protocolNetwork, multiNetworks) + const network = getNetwork(context, protocolNetwork) if (!(await network.isHorizon.value())) { throw indexerError(IndexerErrorCode.IE082) @@ -136,73 +135,65 @@ export default { const dataService = network.contracts.SubgraphService.target.toString().toLowerCase() const idleStake = await network.contracts.HorizonStaking.getIdleStake(indexer) - const provisionsByNetwork = await multiNetworks.map( - async (network: Network): Promise => { - // Return early if a different protocol network is specifically requested - if ( - protocolNetwork && - protocolNetwork !== network.specification.networkIdentifier - ) { - return [] - } - - const { networkSubgraph } = network + const { networkSubgraph } = network - logger.trace('Query Provisions', { - indexer, - dataService, - }) + logger.trace('Query Provisions', { + indexer, + dataService, + }) - const result = await networkSubgraph.checkedQuery(PROVISION_QUERIES.all, { - indexer, - dataService, - }) + const result = await networkSubgraph.checkedQuery(PROVISION_QUERIES.all, { + indexer, + dataService, + }) - if (result.error) { - logger.error('Querying provisions failed', { - error: result.error, - }) - } + if (result.error) { + logger.error('Querying provisions failed', { + error: result.error, + }) + } - return result.data.provisions.map((provision) => ({ - id: provision.id, - dataService, - indexer, - tokensProvisioned: provision.tokensProvisioned, - tokensAllocated: provision.tokensAllocated, - tokensThawing: provision.tokensThawing, - maxVerifierCut: provision.maxVerifierCut, - thawingPeriod: provision.thawingPeriod, - protocolNetwork: network.specification.networkIdentifier, - idleStake: idleStake.toString(), - })) - }, + return result.data.provisions.map( + (provision: { + id: string + tokensProvisioned: string + tokensAllocated: string + tokensThawing: string + maxVerifierCut: string + thawingPeriod: string + }) => ({ + id: provision.id, + dataService, + indexer, + tokensProvisioned: provision.tokensProvisioned, + tokensAllocated: provision.tokensAllocated, + tokensThawing: provision.tokensThawing, + maxVerifierCut: provision.maxVerifierCut, + thawingPeriod: provision.thawingPeriod, + protocolNetwork: network.specification.networkIdentifier, + idleStake: idleStake.toString(), + }), ) - - return Object.values(provisionsByNetwork).flat() }, addToProvision: async ( { - protocolNetwork, + protocolNetwork: providedProtocolNetwork, amount, }: { - protocolNetwork: string + protocolNetwork: string | undefined amount: string }, - { multiNetworks, logger }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { logger } = context + // Get protocol network from context or provided value + const protocolNetwork = getProtocolNetwork(context, providedProtocolNetwork) logger.debug('Execute addToProvision() mutation', { protocolNetwork, amount, }) - if (!multiNetworks) { - throw Error( - 'IndexerManagementClient must be in `network` mode to add stake to a provision', - ) - } - - const network = extractNetwork(protocolNetwork, multiNetworks) + const network = getNetwork(context, protocolNetwork) const networkMonitor = network.networkMonitor const contracts = network.contracts const transactionManager = network.transactionManager @@ -302,26 +293,23 @@ export default { }, thawFromProvision: async ( { - protocolNetwork, + protocolNetwork: providedProtocolNetwork, amount, }: { - protocolNetwork: string + protocolNetwork: string | undefined amount: string }, - { multiNetworks, logger }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { logger } = context + // Get protocol network from context or provided value + const protocolNetwork = getProtocolNetwork(context, providedProtocolNetwork) logger.debug('Execute thawFromProvision() mutation', { protocolNetwork, amount, }) - if (!multiNetworks) { - throw Error( - 'IndexerManagementClient must be in `network` mode to add stake to a provision', - ) - } - - const network = extractNetwork(protocolNetwork, multiNetworks) + const network = getNetwork(context, protocolNetwork) const networkMonitor = network.networkMonitor const contracts = network.contracts const transactionManager = network.transactionManager @@ -448,21 +436,20 @@ export default { }, thawRequests: async ( { - protocolNetwork, + protocolNetwork: providedProtocolNetwork, }: { - protocolNetwork: string + protocolNetwork: string | undefined }, - { multiNetworks, logger }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { logger } = context + // Get protocol network from context or provided value + const protocolNetwork = getProtocolNetwork(context, providedProtocolNetwork) logger.debug('Execute thawRequests() query', { protocolNetwork, }) - if (!multiNetworks) { - throw Error('IndexerManagementClient must be in `network` mode to fetch provisions') - } - - const network = extractNetwork(protocolNetwork, multiNetworks) + const network = getNetwork(context, protocolNetwork) if (!(await network.isHorizon.value())) { throw indexerError(IndexerErrorCode.IE082) @@ -471,74 +458,61 @@ export default { const indexer = network.specification.indexerOptions.address.toLowerCase() const dataService = network.contracts.SubgraphService.target.toString().toLowerCase() - const thawRequestsByNetwork = await multiNetworks.map( - async (network: Network): Promise => { - // Return early if a different protocol network is specifically requested - if ( - protocolNetwork && - protocolNetwork !== network.specification.networkIdentifier - ) { - return [] - } - - const { networkSubgraph } = network + const { networkSubgraph } = network - logger.trace('Query Thaw Requests', { - indexer, - dataService, - }) - - const result = await networkSubgraph.checkedQuery( - PROVISION_QUERIES.thawRequests, - { - indexer, - dataService, - }, - ) + logger.trace('Query Thaw Requests', { + indexer, + dataService, + }) - if (result.error) { - logger.error('Querying thaw requests failed', { - error: result.error, - }) - } + const result = await networkSubgraph.checkedQuery(PROVISION_QUERIES.thawRequests, { + indexer, + dataService, + }) - const currentBlockTimestamp = - (await network.networkProvider.getBlock('latest'))?.timestamp ?? 0 + if (result.error) { + logger.error('Querying thaw requests failed', { + error: result.error, + }) + } - return result.data.thawRequests.map((thawRequest) => ({ - id: thawRequest.id, - fulfilled: thawRequest.fulfilled, - dataService, - indexer, - shares: thawRequest.shares, - thawingUntil: thawRequest.thawingUntil, - currentBlockTimestamp: currentBlockTimestamp.toString(), - protocolNetwork: network.specification.networkIdentifier, - })) - }, + const currentBlockTimestamp = + (await network.networkProvider.getBlock('latest'))?.timestamp ?? 0 + + return result.data.thawRequests.map( + (thawRequest: { + id: string + fulfilled: string + shares: string + thawingUntil: string + }) => ({ + id: thawRequest.id, + fulfilled: thawRequest.fulfilled, + dataService, + indexer, + shares: thawRequest.shares, + thawingUntil: thawRequest.thawingUntil, + currentBlockTimestamp: currentBlockTimestamp.toString(), + protocolNetwork: network.specification.networkIdentifier, + }), ) - - return Object.values(thawRequestsByNetwork).flat() }, removeFromProvision: async ( { - protocolNetwork, + protocolNetwork: providedProtocolNetwork, }: { - protocolNetwork: string + protocolNetwork: string | undefined }, - { multiNetworks, logger }: IndexerManagementResolverContext, + context: IndexerManagementResolverContext, ): Promise => { + const { logger } = context + // Get protocol network from context or provided value + const protocolNetwork = getProtocolNetwork(context, providedProtocolNetwork) logger.debug('Execute removeFromProvision() mutation', { protocolNetwork, }) - if (!multiNetworks) { - throw Error( - 'IndexerManagementClient must be in `network` mode to add stake to a provision', - ) - } - - const network = extractNetwork(protocolNetwork, multiNetworks) + const network = getNetwork(context, protocolNetwork) const networkMonitor = network.networkMonitor const contracts = network.contracts const transactionManager = network.transactionManager diff --git a/packages/indexer-common/src/indexer-management/resolvers/utils.ts b/packages/indexer-common/src/indexer-management/resolvers/utils.ts index baa061fdf..dbcc55e69 100644 --- a/packages/indexer-common/src/indexer-management/resolvers/utils.ts +++ b/packages/indexer-common/src/indexer-management/resolvers/utils.ts @@ -1,26 +1,50 @@ -import { - MultiNetworks, - Network, - validateNetworkIdentifier, -} from '@graphprotocol/indexer-common' +import { Network, validateNetworkIdentifier } from '@graphprotocol/indexer-common' +import { IndexerManagementResolverContext } from '../client' -export function extractNetwork( - unvalidatedNetworkIdentifier: string, - multiNetworks: MultiNetworks, -): Network { - let networkIdentifier: string - try { - networkIdentifier = validateNetworkIdentifier(unvalidatedNetworkIdentifier) - } catch (parseError) { +/** + * Gets the protocol network identifier from the context's single network. + * Falls back to provided value if given (for backwards compatibility during migration). + */ +export function getProtocolNetwork( + context: IndexerManagementResolverContext, + providedNetwork?: string | null, +): string { + // If provided, validate and use it (backwards compatibility) + if (providedNetwork) { + return validateNetworkIdentifier(providedNetwork) + } + + // Otherwise, get from the single configured network + if (!context.network) { throw new Error( - `Invalid protocol network identifier: '${unvalidatedNetworkIdentifier}'. Error: ${parseError}`, + 'No network configured. Either provide protocolNetwork argument or configure a network.', ) } - const network = multiNetworks.inner[networkIdentifier] - if (!network) { + return context.network.specification.networkIdentifier +} + +/** + * Gets the Network object from context, optionally validating a provided network identifier. + */ +export function getNetwork( + context: IndexerManagementResolverContext, + providedNetwork?: string | null, +): Network { + if (!context.network) { throw new Error( - `Could not find a configured protocol network named ${networkIdentifier}`, + 'No network configured. Either provide protocolNetwork argument or configure a network.', ) } - return network + + // If a network was provided, validate it matches the configured one + if (providedNetwork) { + const validated = validateNetworkIdentifier(providedNetwork) + if (validated !== context.network.specification.networkIdentifier) { + throw new Error( + `Provided network '${validated}' does not match configured network '${context.network.specification.networkIdentifier}'`, + ) + } + } + + return context.network } diff --git a/packages/indexer-common/src/indexer-management/rules.ts b/packages/indexer-common/src/indexer-management/rules.ts index 55a4d0ccd..f48195c3d 100644 --- a/packages/indexer-common/src/indexer-management/rules.ts +++ b/packages/indexer-common/src/indexer-management/rules.ts @@ -4,27 +4,21 @@ import { INDEXING_RULE_GLOBAL, IndexingRule, IndexingRuleAttributes, - MultiNetworks, Network, sequentialTimerMap, } from '@graphprotocol/indexer-common' import { parseIndexingRule } from '../rules' import groupBy from 'lodash.groupby' -import { extractNetwork } from './resolvers/utils' import { IndexingRuleCreationAttributes } from './models' export class RulesManager { - declare multiNetworks: MultiNetworks + declare network: Network declare models: IndexerManagementModels declare logger: Logger - static async create( - multiNetworks: MultiNetworks, - logger: Logger, - models: IndexerManagementModels, - ) { + static async create(network: Network, logger: Logger, models: IndexerManagementModels) { const rulesManager = new RulesManager() - rulesManager.multiNetworks = multiNetworks + rulesManager.network = network rulesManager.logger = logger rulesManager.models = models @@ -36,6 +30,8 @@ export class RulesManager { async monitorRules(): Promise { const logger = this.logger.child({ component: 'RulesMonitor' }) + const protocolNetwork = this.network.specification.networkIdentifier + const rules: Eventual = sequentialTimerMap( { logger, @@ -45,7 +41,7 @@ export class RulesManager { logger.trace('Fetching indexing rules') let rules: IndexingRuleAttributes[] = [] try { - rules = await fetchIndexingRules(this.models, true) + rules = await fetchIndexingRules(this.models, true, protocolNetwork) logger.trace(`Fetched ${rules.length} indexing rules`) } catch (err) { logger.warn('Failed to fetch indexing rules', { err }) @@ -62,10 +58,9 @@ export class RulesManager { join({ rules }).pipe(async ({ rules }) => { logger.info(`Indexing rules found, evaluating allocation lifetime`) for (const rule of rules) { - const network = extractNetwork(rule.protocolNetwork, this.multiNetworks) const [isValid, maxSuggestedLifetime] = await ensureAllocationLifetime( rule, - network, + this.network, ) if (!isValid) { logger.warn(`Invalid rule allocation lifetime. Indexing rewards at risk!`, { diff --git a/packages/indexer-common/src/multi-networks.ts b/packages/indexer-common/src/multi-networks.ts deleted file mode 100644 index f305d2087..000000000 --- a/packages/indexer-common/src/multi-networks.ts +++ /dev/null @@ -1,111 +0,0 @@ -import pReduce from 'p-reduce' -import isEqual from 'lodash.isequal' -import xor from 'lodash.xor' - -// A mapping of different values of type T keyed by their network identifiers -export type NetworkMapped = Record - -// Function to extract the network identifier from a value of type T -type NetworkIdentity = (element: T) => string - -// Wrapper type for performing calls over multiple values of any type, most notably -// Network and Operator instances. -// All public-facing methods should return a `NetworkMapped` or `void`. -export class MultiNetworks { - inner: NetworkMapped - constructor(elements: T[], networkIdentity: NetworkIdentity) { - if (elements.length === 0) { - throw new Error('MultiNetworks component was initialized with empty values') - } - - function reducer(accumulator: NetworkMapped, current: T): NetworkMapped { - const key = networkIdentity(current) - if (key in accumulator) { - throw new Error( - `Duplicate network identifier found while mapping value's network: ${key}`, - ) - } - // TODO: parse and validate network identifiers to standardize them - accumulator[key] = current - return accumulator - } - this.inner = elements.reduce(reducer, {}) - } - - private checkEqualKeys(a: NetworkMapped, b: NetworkMapped) { - const aKeys = Object.keys(a) - const bKeys = Object.keys(b) - if (!isEqual(aKeys, bKeys)) { - const differentKeys = xor(aKeys, bKeys) - throw new Error(`Network Mapped objects have different keys: ${differentKeys}`) - } - } - - async map(func: (value: T) => Promise): Promise> { - const entries: [string, T][] = Object.entries(this.inner) - return pReduce( - entries, - async (acc, pair) => { - const [networkIdentifier, element]: [string, T] = pair - const result = await func(element) - acc[networkIdentifier] = result - return acc - }, - {} as NetworkMapped, - ) - } - - zip(a: NetworkMapped, b: NetworkMapped): NetworkMapped<[U, V]> { - this.checkEqualKeys(a, b) - const result = {} as NetworkMapped<[U, V]> - for (const key in a) { - result[key] = [a[key], b[key]] - } - return result - } - - zip3( - a: NetworkMapped, - b: NetworkMapped, - c: NetworkMapped, - ): NetworkMapped<[U, V, W]> { - this.checkEqualKeys(a, b) - const result = {} as NetworkMapped<[U, V, W]> - for (const key in a) { - result[key] = [a[key], b[key], c[key]] - } - return result - } - - zip4( - a: NetworkMapped, - b: NetworkMapped, - c: NetworkMapped, - d: NetworkMapped, - ): NetworkMapped<[U, V, W, Y]> { - this.checkEqualKeys(a, b) - const result = {} as NetworkMapped<[U, V, W, Y]> - for (const key in a) { - result[key] = [a[key], b[key], c[key], d[key]] - } - return result - } - - async mapNetworkMapped( - nmap: NetworkMapped, - func: (inner: T, value: U) => Promise, - ): Promise> { - return pReduce( - Object.entries(nmap), - async (acc, [networkIdentifier, value]: [string, U]) => { - const inner = this.inner[networkIdentifier] - if (!inner) { - throw new Error(`Network identifier not found: ${networkIdentifier}`) - } - acc[networkIdentifier] = await func(inner, value) - return acc - }, - {} as NetworkMapped, - ) - } -} diff --git a/packages/indexer-common/src/network.ts b/packages/indexer-common/src/network.ts index 72c548907..a055e9b58 100644 --- a/packages/indexer-common/src/network.ts +++ b/packages/indexer-common/src/network.ts @@ -596,11 +596,7 @@ export class Network { await pRetry( async () => { try { - if (await this.isHorizon.value()) { - await this._register(logger, geoHash, url) - } else { - await this._registerLegacy(logger, geoHash, url) - } + await this._register(logger, geoHash, url) } catch (error) { const err = indexerError(IndexerErrorCode.IE012, error) logger.error(INDEXER_ERROR_MESSAGES[IndexerErrorCode.IE012], { @@ -699,80 +695,6 @@ export class Network { logger.info(`Successfully registered indexer`) } - - private async _registerLegacy( - logger: Logger, - geoHash: string, - url: string, - ): Promise { - logger.info(`Register indexer`, { - url, - geoCoordinates: this.specification.indexerOptions.geoCoordinates, - geoHash, - }) - - // Register the indexer (only if it hasn't been registered yet or - // if its URL/geohash is different from what is registered on chain) - const isRegistered = await this.contracts.LegacyServiceRegistry.isRegistered( - this.specification.indexerOptions.address, - ) - if (isRegistered) { - const service = await this.contracts.LegacyServiceRegistry.services( - this.specification.indexerOptions.address, - ) - if (service.url === url && service.geoHash === geoHash) { - logger.debug('Indexer already registered', { - address: this.specification.indexerOptions.address, - serviceRegistry: this.contracts.LegacyServiceRegistry.target, - service, - }) - if (await this.transactionManager.isOperator.value()) { - logger.info(`Indexer already registered, operator status already granted`) - } else { - logger.info(`Indexer already registered, operator status not yet granted`) - } - return - } - } - const receipt = await this.transactionManager.executeTransaction( - () => - this.contracts.LegacyServiceRegistry.registerFor.estimateGas( - this.specification.indexerOptions.address, - this.specification.indexerOptions.url, - geoHash, - ), - (gasLimit) => - this.contracts.LegacyServiceRegistry.registerFor( - this.specification.indexerOptions.address, - this.specification.indexerOptions.url, - geoHash, - { - gasLimit, - }, - ), - logger.child({ function: 'serviceRegistry.registerFor' }), - ) - if (receipt === 'paused' || receipt === 'unauthorized') { - return - } - const events = receipt.logs - const event = events.find((event) => - event.topics.includes( - this.contracts.LegacyServiceRegistry.interface.getEvent('ServiceRegistered') - .topicHash, - ), - ) - logger.info('Event', { - event, - events, - topicHash: - this.contracts.LegacyServiceRegistry.interface.getEvent('ServiceRegistered') - .topicHash, - }) - assert.ok(event) - - logger.info(`Successfully registered indexer (Legacy registration)`) - } } async function connectWallet( @@ -849,15 +771,6 @@ async function connectToProtocolContracts( 'SubgraphService', ] - // Before horizon we need the LegacyServiceRegistry contract as well - const isHorizon = await contracts.HorizonStaking.getMaxThawingPeriod() - .then((maxThawingPeriod) => maxThawingPeriod > 0) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .catch((_) => false) - if (!isHorizon) { - requiredContracts.push('LegacyServiceRegistry') - } - const missingContracts = requiredContracts.filter( (contract) => !(contract in contracts), ) @@ -869,7 +782,6 @@ async function connectToProtocolContracts( process.exit(1) } - // Only list contracts that are used by the indexer logger.info(`Successfully connected to Horizon contracts`, { epochManager: contracts.EpochManager.target, rewardsManager: contracts.RewardsManager.target, @@ -878,9 +790,6 @@ async function connectToProtocolContracts( graphPaymentsEscrow: contracts.PaymentsEscrow.target, }) logger.info(`Successfully connected to Subgraph Service contracts`, { - ...(isHorizon - ? {} - : { legacyServiceRegistry: contracts.LegacyServiceRegistry.target }), subgraphService: contracts.SubgraphService.target, }) return contracts diff --git a/packages/indexer-common/src/query-fees/models.ts b/packages/indexer-common/src/query-fees/models.ts index 132e9240e..2116584d4 100644 --- a/packages/indexer-common/src/query-fees/models.ts +++ b/packages/indexer-common/src/query-fees/models.ts @@ -110,6 +110,12 @@ export interface AllocationReceiptAttributes { allocation: Address fees: string signature: string + /** + * @deprecated LEGACY DEBT: Part of composite primary key for multi-network support. + * Multi-network has been removed but this field remains in the DB schema. + * Always populated with the single configured network. Can be removed in a future + * migration that changes the primary key structure. + */ protocolNetwork: string } @@ -121,6 +127,7 @@ export class AllocationReceipt public allocation!: Address public fees!: string public signature!: string + /** @deprecated LEGACY DEBT: Multi-network removed. See AllocationReceiptAttributes.protocolNetwork */ public protocolNetwork!: string public readonly createdAt!: Date @@ -131,6 +138,12 @@ export interface VoucherAttributes { allocation: Address amount: string signature: string + /** + * @deprecated LEGACY DEBT: Part of composite primary key for multi-network support. + * Multi-network has been removed but this field remains in the DB schema. + * Always populated with the single configured network. Can be removed in a future + * migration that changes the primary key structure. + */ protocolNetwork: string } @@ -141,6 +154,7 @@ export class Voucher extends Model implements VoucherAttribut public readonly createdAt!: Date public readonly updatedAt!: Date + /** @deprecated LEGACY DEBT: Multi-network removed. See VoucherAttributes.protocolNetwork */ public protocolNetwork!: string public readonly allocationSummary?: AllocationSummary @@ -307,6 +321,12 @@ export interface TransferReceiptAttributes { signer: Address fees: string signature: string + /** + * @deprecated LEGACY DEBT: Part of composite primary key for multi-network support. + * Multi-network has been removed but this field remains in the DB schema. + * Always populated with the single configured network. Can be removed in a future + * migration that changes the primary key structure. + */ protocolNetwork: string } @@ -318,6 +338,7 @@ export class TransferReceipt public signer!: Address public fees!: string public signature!: string + /** @deprecated LEGACY DEBT: Multi-network removed. See TransferReceiptAttributes.protocolNetwork */ public protocolNetwork!: string public readonly createdAt!: Date @@ -343,6 +364,12 @@ export interface TransferAttributes { signer: Address allocationClosedAt: Date | null status: TransferStatus + /** + * @deprecated LEGACY DEBT: Part of composite primary key for multi-network support. + * Multi-network has been removed but this field remains in the DB schema. + * Always populated with the single configured network. Can be removed in a future + * migration that changes the primary key structure. + */ protocolNetwork: string } @@ -352,6 +379,7 @@ export class Transfer extends Model implements TransferAttri public signer!: Address public allocationClosedAt!: Date | null public status!: TransferStatus + /** @deprecated LEGACY DEBT: Multi-network removed. See TransferAttributes.protocolNetwork */ public protocolNetwork!: string public readonly createdAt!: Date @@ -373,6 +401,12 @@ export interface AllocationSummaryAttributes { openTransfers: number collectedFees: string withdrawnFees: string + /** + * @deprecated LEGACY DEBT: Part of composite primary key for multi-network support. + * Multi-network has been removed but this field remains in the DB schema. + * Always populated with the single configured network. Can be removed in a future + * migration that changes the primary key structure. + */ protocolNetwork: string } @@ -388,6 +422,7 @@ export class AllocationSummary declare openTransfers: number declare collectedFees: string declare withdrawnFees: string + /** @deprecated LEGACY DEBT: Multi-network removed. See AllocationSummaryAttributes.protocolNetwork */ declare protocolNetwork: string declare readonly createdAt: Date @@ -452,6 +487,10 @@ export function defineQueryFeeModels(sequelize: Sequelize): QueryFeeModels { min: 0.0, }, }, + // LEGACY DEBT: Part of composite primary key for multi-network support. + // Multi-network has been removed but this field remains in the DB schema. + // Always populated with the single configured network. Can be removed in a future + // migration that changes the primary key structure. protocolNetwork: { type: DataTypes.STRING, primaryKey: true, @@ -482,6 +521,10 @@ export function defineQueryFeeModels(sequelize: Sequelize): QueryFeeModels { type: DataTypes.STRING, allowNull: false, }, + // LEGACY DEBT: Part of composite primary key for multi-network support. + // Multi-network has been removed but this field remains in the DB schema. + // Always populated with the single configured network. Can be removed in a future + // migration that changes the primary key structure. protocolNetwork: { type: DataTypes.STRING, primaryKey: true, @@ -693,6 +736,10 @@ export function defineQueryFeeModels(sequelize: Sequelize): QueryFeeModels { min: 0.0, }, }, + // LEGACY DEBT: Part of composite primary key for multi-network support. + // Multi-network has been removed but this field remains in the DB schema. + // Always populated with the single configured network. Can be removed in a future + // migration that changes the primary key structure. protocolNetwork: { type: DataTypes.STRING, primaryKey: true, @@ -734,6 +781,10 @@ export function defineQueryFeeModels(sequelize: Sequelize): QueryFeeModels { ), allowNull: false, }, + // LEGACY DEBT: Part of composite primary key for multi-network support. + // Multi-network has been removed but this field remains in the DB schema. + // Always populated with the single configured network. Can be removed in a future + // migration that changes the primary key structure. protocolNetwork: { type: DataTypes.STRING, primaryKey: true, @@ -781,6 +832,10 @@ export function defineQueryFeeModels(sequelize: Sequelize): QueryFeeModels { type: DataTypes.DECIMAL, allowNull: false, }, + // LEGACY DEBT: Part of composite primary key for multi-network support. + // Multi-network has been removed but this field remains in the DB schema. + // Always populated with the single configured network. Can be removed in a future + // migration that changes the primary key structure. protocolNetwork: { type: DataTypes.STRING, primaryKey: true,