From 842d2ff315a0c711cec493c641ac21990b298255 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Fri, 7 Feb 2025 18:42:08 -0300 Subject: [PATCH 1/7] feat: indexing fees / dips (wip) --- packages/indexer-agent/src/commands/start.ts | 28 +++++ packages/indexer-common/src/index.ts | 1 + .../indexer-common/src/indexing-fees/index.ts | 1 + .../src/indexing-fees/models.ts | 113 ++++++++++++++++++ .../src/network-specification.ts | 3 + 5 files changed, 146 insertions(+) create mode 100644 packages/indexer-common/src/indexing-fees/index.ts create mode 100644 packages/indexer-common/src/indexing-fees/models.ts diff --git a/packages/indexer-agent/src/commands/start.ts b/packages/indexer-agent/src/commands/start.ts index f22943ddb..a9f59dba0 100644 --- a/packages/indexer-agent/src/commands/start.ts +++ b/packages/indexer-agent/src/commands/start.ts @@ -14,6 +14,7 @@ import { createIndexerManagementClient, createIndexerManagementServer, defineIndexerManagementModels, + defineIndexingFeesModels, defineQueryFeeModels, GraphNode, indexerError, @@ -302,6 +303,26 @@ export const start = { default: 1, group: 'Indexer Infrastructure', }) + .option('enable-dips', { + description: 'Whether to enable Indexing Fees (DIPs)', + type: 'boolean', + default: false, + group: 'Indexing Fees ("DIPs")', + }) + .option('dipper-endpoint', { + description: 'Gateway endpoint for DIPs receipts', + type: 'string', + array: false, + required: false, + group: 'Indexing Fees ("DIPs")', + }) + .option('dips-allocation-amount', { + description: 'Amount of GRT to allocate for DIPs', + type: 'number', + default: 1, + required: false, + group: 'Indexing Fees ("DIPs")', + }) .check(argv => { if ( !argv['network-subgraph-endpoint'] && @@ -329,6 +350,9 @@ export const start = { ) { return 'Invalid --rebate-claim-max-batch-size provided. Must be > 0 and an integer.' } + if (argv['enable-dips'] && !argv['dipper-endpoint']) { + return 'Invalid --dipper-endpoint provided. Must be provided when --enable-dips is true.' + } return true }) }, @@ -364,6 +388,9 @@ export async function createNetworkSpecification( allocateOnNetworkSubgraph: argv.allocateOnNetworkSubgraph, register: argv.register, finalityTime: argv.chainFinalizeTime, + enableDips: argv.enableDips, + dipperEndpoint: argv.dipperEndpoint, + dipsAllocationAmount: argv.dipsAllocationAmount, } const transactionMonitoring = { @@ -567,6 +594,7 @@ export async function run( logger.info(`Sync database models`) const managementModels = defineIndexerManagementModels(sequelize) const queryFeeModels = defineQueryFeeModels(sequelize) + const indexingFeesModels = defineIndexingFeesModels(sequelize) await sequelize.sync() logger.info(`Successfully synced database models`) diff --git a/packages/indexer-common/src/index.ts b/packages/indexer-common/src/index.ts index ab3eedd97..9a378416b 100644 --- a/packages/indexer-common/src/index.ts +++ b/packages/indexer-common/src/index.ts @@ -3,6 +3,7 @@ export * from './allocations' export * from './async-cache' export * from './errors' export * from './indexer-management' +export * from './indexing-fees' export * from './graph-node' export * from './operator' export * from './network' diff --git a/packages/indexer-common/src/indexing-fees/index.ts b/packages/indexer-common/src/indexing-fees/index.ts new file mode 100644 index 000000000..ad200c539 --- /dev/null +++ b/packages/indexer-common/src/indexing-fees/index.ts @@ -0,0 +1 @@ +export * from './models' diff --git a/packages/indexer-common/src/indexing-fees/models.ts b/packages/indexer-common/src/indexing-fees/models.ts new file mode 100644 index 000000000..213351ebe --- /dev/null +++ b/packages/indexer-common/src/indexing-fees/models.ts @@ -0,0 +1,113 @@ +import { DataTypes, Sequelize, Model, Association, CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize' + +// Indexing Fees AKA "DIPs" + +export class IndexingAgreement extends Model< + InferAttributes, + InferCreationAttributes +> { + declare id: CreationOptional; + declare signature: Buffer; + declare signed_payload: Buffer; + declare protocol_network: string; + declare chain_id: string; + declare price_per_block: string; + declare price_per_entity: string; + declare subgraph_deployment_id: string; + declare service: string; + declare payee: string; + declare payer: string; + declare created_at: Date; + declare updated_at: Date; + declare cancelled_at: Date | null; + declare signed_cancellation_payload: Buffer | null; + declare current_allocation_id: string | null; + declare last_allocation_id: string | null; +} + +export interface IndexingFeesModels { + IndexingAgreement: typeof IndexingAgreement +} + +export const defineIndexingFeesModels = (sequelize: Sequelize): IndexingFeesModels => { + IndexingAgreement.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + }, + signature: { + type: DataTypes.BLOB, // == BYTEA in postgres + allowNull: false, + }, + signed_payload: { + type: DataTypes.BLOB, // == BYTEA in postgres + allowNull: false, + }, + protocol_network: { + type: DataTypes.STRING(255), + allowNull: false, + }, + chain_id: { + type: DataTypes.STRING(255), + allowNull: false, + }, + price_per_block: { + type: DataTypes.DECIMAL(39), + allowNull: false, + }, + price_per_entity: { + type: DataTypes.DECIMAL(39), + allowNull: false, + }, + subgraph_deployment_id: { + type: DataTypes.STRING(255), + allowNull: false, + }, + service: { + type: DataTypes.CHAR(40), + allowNull: false, + }, + payee: { + type: DataTypes.CHAR(40), + allowNull: false, + }, + payer: { + type: DataTypes.CHAR(40), + allowNull: false, + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + }, + cancelled_at: { + type: DataTypes.DATE, + allowNull: true, + }, + signed_cancellation_payload: { + type: DataTypes.BLOB, + allowNull: true, + }, + current_allocation_id: { + type: DataTypes.CHAR(40), + allowNull: true, + }, + last_allocation_id: { + type: DataTypes.CHAR(40), + allowNull: true, + }, + }, + { + modelName: 'IndexingAgreement', + sequelize, + }, + ) + + return { + ['IndexingAgreement']: IndexingAgreement, + } +} diff --git a/packages/indexer-common/src/network-specification.ts b/packages/indexer-common/src/network-specification.ts index f683cdec5..d16d3b343 100644 --- a/packages/indexer-common/src/network-specification.ts +++ b/packages/indexer-common/src/network-specification.ts @@ -58,6 +58,9 @@ export const IndexerOptions = z allocateOnNetworkSubgraph: z.boolean().default(false), register: z.boolean().default(true), finalityTime: positiveNumber().default(3600), + enableDips: z.boolean().default(false), + dipperEndpoint: z.string().url().optional(), + dipsAllocationAmount: GRT().default(1), }) .strict() export type IndexerOptions = z.infer From 355932e1bedd42f7102785191f708ed8f76c2a42 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Fri, 7 Feb 2025 20:02:43 -0300 Subject: [PATCH 2/7] chore: more work on dips, still wip --- packages/indexer-agent/src/commands/start.ts | 1 + .../src/indexer-management/allocations.ts | 6 +- .../src/indexer-management/models/index.ts | 5 +- .../models/indexing-agreement.ts} | 44 ++++++- .../indexer-common/src/indexing-fees/dips.ts | 110 ++++++++++++++++++ .../indexer-common/src/indexing-fees/index.ts | 2 +- .../src/network-specification.ts | 1 + 7 files changed, 162 insertions(+), 7 deletions(-) rename packages/indexer-common/src/{indexing-fees/models.ts => indexer-management/models/indexing-agreement.ts} (70%) create mode 100644 packages/indexer-common/src/indexing-fees/dips.ts diff --git a/packages/indexer-agent/src/commands/start.ts b/packages/indexer-agent/src/commands/start.ts index a9f59dba0..ce4ee0ebd 100644 --- a/packages/indexer-agent/src/commands/start.ts +++ b/packages/indexer-agent/src/commands/start.ts @@ -391,6 +391,7 @@ export async function createNetworkSpecification( enableDips: argv.enableDips, dipperEndpoint: argv.dipperEndpoint, dipsAllocationAmount: argv.dipsAllocationAmount, + dipsEpochsMargin: argv.dipsEpochsMargin, } const transactionMonitoring = { diff --git a/packages/indexer-common/src/indexer-management/allocations.ts b/packages/indexer-common/src/indexer-management/allocations.ts index 035aa0932..a44db2053 100644 --- a/packages/indexer-common/src/indexer-management/allocations.ts +++ b/packages/indexer-common/src/indexer-management/allocations.ts @@ -15,6 +15,7 @@ import { AllocationStatus, CloseAllocationResult, CreateAllocationResult, + DipsManager, fetchIndexingRules, GraphNode, indexerError, @@ -98,12 +99,15 @@ export type TransactionResult = | ActionFailure[] export class AllocationManager { + private dipsManager: DipsManager constructor( private logger: Logger, private models: IndexerManagementModels, private graphNode: GraphNode, private network: Network, - ) {} + ) { + this.dipsManager = new DipsManager(this.logger, this.models, this.graphNode, this.network, this) + } async executeBatch(actions: Action[]): Promise { const logger = this.logger.child({ function: 'executeBatch' }) diff --git a/packages/indexer-common/src/indexer-management/models/index.ts b/packages/indexer-common/src/indexer-management/models/index.ts index 8d5ec55af..81a59f4d3 100644 --- a/packages/indexer-common/src/indexer-management/models/index.ts +++ b/packages/indexer-common/src/indexer-management/models/index.ts @@ -4,6 +4,7 @@ import { IndexingRuleModels, defineIndexingRuleModels } from './indexing-rule' import { CostModelModels, defineCostModelModels } from './cost-model' import { POIDisputeModels, definePOIDisputeModels } from './poi-dispute' import { ActionModels, defineActionModels } from './action' +import { defineIndexingFeesModels, IndexingFeesModels } from './indexing-agreement' export * from './cost-model' export * from './indexing-rule' @@ -13,7 +14,8 @@ export * from './action' export type IndexerManagementModels = IndexingRuleModels & CostModelModels & POIDisputeModels & - ActionModels + ActionModels & + IndexingFeesModels export const defineIndexerManagementModels = ( sequelize: Sequelize, @@ -24,4 +26,5 @@ export const defineIndexerManagementModels = ( defineIndexingRuleModels(sequelize), definePOIDisputeModels(sequelize), defineActionModels(sequelize), + defineIndexingFeesModels(sequelize), ) diff --git a/packages/indexer-common/src/indexing-fees/models.ts b/packages/indexer-common/src/indexer-management/models/indexing-agreement.ts similarity index 70% rename from packages/indexer-common/src/indexing-fees/models.ts rename to packages/indexer-common/src/indexer-management/models/indexing-agreement.ts index 213351ebe..dfe5c10f7 100644 --- a/packages/indexer-common/src/indexing-fees/models.ts +++ b/packages/indexer-common/src/indexer-management/models/indexing-agreement.ts @@ -11,18 +11,25 @@ export class IndexingAgreement extends Model< declare signed_payload: Buffer; declare protocol_network: string; declare chain_id: string; - declare price_per_block: string; + declare base_price_per_epoch: string; declare price_per_entity: string; declare subgraph_deployment_id: string; declare service: string; declare payee: string; declare payer: string; + declare deadline: Date; + declare duration_epochs: bigint; + declare max_initial_amount: string; + declare max_ongoing_amount_per_epoch: string; + declare min_epochs_per_collection: bigint; + declare max_epochs_per_collection: bigint; declare created_at: Date; declare updated_at: Date; declare cancelled_at: Date | null; declare signed_cancellation_payload: Buffer | null; declare current_allocation_id: string | null; declare last_allocation_id: string | null; + declare last_payment_collected_at: Date | null; } export interface IndexingFeesModels { @@ -37,11 +44,12 @@ export const defineIndexingFeesModels = (sequelize: Sequelize): IndexingFeesMode primaryKey: true, }, signature: { - type: DataTypes.BLOB, // == BYTEA in postgres + type: DataTypes.BLOB, allowNull: false, + unique: true, }, signed_payload: { - type: DataTypes.BLOB, // == BYTEA in postgres + type: DataTypes.BLOB, allowNull: false, }, protocol_network: { @@ -52,7 +60,7 @@ export const defineIndexingFeesModels = (sequelize: Sequelize): IndexingFeesMode type: DataTypes.STRING(255), allowNull: false, }, - price_per_block: { + base_price_per_epoch: { type: DataTypes.DECIMAL(39), allowNull: false, }, @@ -76,6 +84,30 @@ export const defineIndexingFeesModels = (sequelize: Sequelize): IndexingFeesMode type: DataTypes.CHAR(40), allowNull: false, }, + deadline: { + type: DataTypes.DATE, + allowNull: false, + }, + duration_epochs: { + type: DataTypes.BIGINT, + allowNull: false, + }, + max_initial_amount: { + type: DataTypes.DECIMAL(39), + allowNull: false, + }, + max_ongoing_amount_per_epoch: { + type: DataTypes.DECIMAL(39), + allowNull: false, + }, + min_epochs_per_collection: { + type: DataTypes.BIGINT, + allowNull: false, + }, + max_epochs_per_collection: { + type: DataTypes.BIGINT, + allowNull: false, + }, created_at: { type: DataTypes.DATE, allowNull: false, @@ -100,6 +132,10 @@ export const defineIndexingFeesModels = (sequelize: Sequelize): IndexingFeesMode type: DataTypes.CHAR(40), allowNull: true, }, + last_payment_collected_at: { + type: DataTypes.DATE, + allowNull: true, + }, }, { modelName: 'IndexingAgreement', diff --git a/packages/indexer-common/src/indexing-fees/dips.ts b/packages/indexer-common/src/indexing-fees/dips.ts new file mode 100644 index 000000000..28da7d9b1 --- /dev/null +++ b/packages/indexer-common/src/indexing-fees/dips.ts @@ -0,0 +1,110 @@ +import { formatGRT, Logger, SubgraphDeploymentID } from "@graphprotocol/common-ts"; +import { AllocationManager, GraphNode, IndexerManagementModels, IndexingDecisionBasis, IndexingRuleAttributes, Network, SubgraphIdentifierType, upsertIndexingRule } from '@graphprotocol/indexer-common' +import { Op } from "sequelize"; + +export class DipsManager { + constructor( + private logger: Logger, + private models: IndexerManagementModels, + private graphNode: GraphNode, + private network: Network, + private parent: AllocationManager | null, + ) {} + // Cancel an agreement associated to an allocation if it exists + async tryCancelAgreement(allocationId: string) { + const agreement = await this.models.IndexingAgreement.findOne({ + where: { + current_allocation_id: allocationId, + cancelled_at: null, + }, + }) + if (agreement) { + // TODO use dips-proto to cancel agreement via grpc + + // Mark the agreement as cancelled + } + } + // Update the current and last allocation ids for an agreement if it exists + async tryUpdateAgreementAllocation(oldAllocationId: string, newAllocationId: string | null) { + const agreement = await this.models.IndexingAgreement.findOne({ + where: { + current_allocation_id: oldAllocationId, + cancelled_at: null, + }, + }) + if (agreement) { + agreement.current_allocation_id = newAllocationId + agreement.last_allocation_id = oldAllocationId + agreement.last_payment_collected_at = null + await agreement.save() + } + } + // Collect payments for all outstanding agreements + async collectAllPayments() { + const outstandingAgreements = await this.models.IndexingAgreement.findAll({ + where: { + last_payment_collected_at: null, + last_allocation_id: { + [Op.ne]: null, + }, + }, + }) + for (const agreement of outstandingAgreements) { + if (agreement.last_allocation_id) { + await this.tryCollectPayment(agreement.last_allocation_id) + } else { + // This should never happen as we check for this in the query + this.logger.error(`Agreement ${agreement.id} has no last allocation id`) + } + } + } + async tryCollectPayment(lastAllocationId: string) { + // TODO: use dips-proto to collect payment via grpc + + // TODO: store the receipt in the database + // (tap-agent will take care of aggregating it into a RAV) + + // Mark the agreement as having had a payment collected + await this.models.IndexingAgreement.update({ + last_payment_collected_at: new Date(), + }, { + where: { + last_allocation_id: lastAllocationId, + }, + }) + } + async ensureAgreementRules() { + if (!this.parent) { + this.logger.error('DipsManager has no parent AllocationManager, cannot ensure agreement rules') + return + } + // Get all the indexing agreements that are not cancelled + const indexingAgreements = await this.models.IndexingAgreement.findAll({ + where: { + cancelled_at: null, + }, + }) + // For each agreement, check that there is an indexing rule to always + // allocate to the agreement's subgraphDeploymentId, and if not, create one + for (const agreement of indexingAgreements) { + const subgraphDeploymentID = new SubgraphDeploymentID(agreement.subgraph_deployment_id) + // If there is not yet an indexingRule that deems this deployment worth allocating to, make one + if (!(await this.parent.matchingRuleExists(this.logger, subgraphDeploymentID))) { + this.logger.debug( + `Creating indexing rule for agreement ${agreement.id}`, + ) + const indexingRule = { + identifier: agreement.subgraph_deployment_id, + allocationAmount: formatGRT(this.network.specification.indexerOptions.dipsAllocationAmount), + identifierType: SubgraphIdentifierType.DEPLOYMENT, + decisionBasis: IndexingDecisionBasis.ALWAYS, + protocolNetwork: this.network.specification.networkIdentifier, + autoRenewal: true, + allocationLifetime: Math.max(Number(agreement.min_epochs_per_collection), Number(agreement.max_epochs_per_collection) - this.network.specification.indexerOptions.dipsEpochsMargin), + } as Partial + + await upsertIndexingRule(this.logger, this.models, indexingRule) + } + } + } +} diff --git a/packages/indexer-common/src/indexing-fees/index.ts b/packages/indexer-common/src/indexing-fees/index.ts index ad200c539..0b71f1b8e 100644 --- a/packages/indexer-common/src/indexing-fees/index.ts +++ b/packages/indexer-common/src/indexing-fees/index.ts @@ -1 +1 @@ -export * from './models' +export * from './dips' diff --git a/packages/indexer-common/src/network-specification.ts b/packages/indexer-common/src/network-specification.ts index d16d3b343..4db7eb30b 100644 --- a/packages/indexer-common/src/network-specification.ts +++ b/packages/indexer-common/src/network-specification.ts @@ -61,6 +61,7 @@ export const IndexerOptions = z enableDips: z.boolean().default(false), dipperEndpoint: z.string().url().optional(), dipsAllocationAmount: GRT().default(1), + dipsEpochsMargin: positiveNumber().default(1), }) .strict() export type IndexerOptions = z.infer From 812632f5daec64195031c0da98304738ec54df1c Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Fri, 7 Feb 2025 20:06:03 -0300 Subject: [PATCH 3/7] fix: lint --- packages/indexer-agent/src/commands/start.ts | 2 -- .../src/indexer-management/models/indexing-agreement.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/indexer-agent/src/commands/start.ts b/packages/indexer-agent/src/commands/start.ts index ce4ee0ebd..6f9272f43 100644 --- a/packages/indexer-agent/src/commands/start.ts +++ b/packages/indexer-agent/src/commands/start.ts @@ -14,7 +14,6 @@ import { createIndexerManagementClient, createIndexerManagementServer, defineIndexerManagementModels, - defineIndexingFeesModels, defineQueryFeeModels, GraphNode, indexerError, @@ -595,7 +594,6 @@ export async function run( logger.info(`Sync database models`) const managementModels = defineIndexerManagementModels(sequelize) const queryFeeModels = defineQueryFeeModels(sequelize) - const indexingFeesModels = defineIndexingFeesModels(sequelize) await sequelize.sync() logger.info(`Successfully synced database models`) diff --git a/packages/indexer-common/src/indexer-management/models/indexing-agreement.ts b/packages/indexer-common/src/indexer-management/models/indexing-agreement.ts index dfe5c10f7..4d9351779 100644 --- a/packages/indexer-common/src/indexer-management/models/indexing-agreement.ts +++ b/packages/indexer-common/src/indexer-management/models/indexing-agreement.ts @@ -1,4 +1,4 @@ -import { DataTypes, Sequelize, Model, Association, CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize' +import { DataTypes, Sequelize, Model, CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize' // Indexing Fees AKA "DIPs" From 5b4df7597c4acd28e172e7d42af7cbe2e9b36828 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Fri, 7 Feb 2025 20:11:51 -0300 Subject: [PATCH 4/7] fix: format --- .../src/indexer-management/allocations.ts | 8 ++- .../models/indexing-agreement.ts | 57 ++++++++++-------- .../indexer-common/src/indexing-fees/dips.ts | 58 +++++++++++++------ 3 files changed, 79 insertions(+), 44 deletions(-) diff --git a/packages/indexer-common/src/indexer-management/allocations.ts b/packages/indexer-common/src/indexer-management/allocations.ts index a44db2053..d9303345d 100644 --- a/packages/indexer-common/src/indexer-management/allocations.ts +++ b/packages/indexer-common/src/indexer-management/allocations.ts @@ -106,7 +106,13 @@ export class AllocationManager { private graphNode: GraphNode, private network: Network, ) { - this.dipsManager = new DipsManager(this.logger, this.models, this.graphNode, this.network, this) + this.dipsManager = new DipsManager( + this.logger, + this.models, + this.graphNode, + this.network, + this, + ) } async executeBatch(actions: Action[]): Promise { diff --git a/packages/indexer-common/src/indexer-management/models/indexing-agreement.ts b/packages/indexer-common/src/indexer-management/models/indexing-agreement.ts index 4d9351779..180e528a4 100644 --- a/packages/indexer-common/src/indexer-management/models/indexing-agreement.ts +++ b/packages/indexer-common/src/indexer-management/models/indexing-agreement.ts @@ -1,4 +1,11 @@ -import { DataTypes, Sequelize, Model, CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize' +import { + DataTypes, + Sequelize, + Model, + CreationOptional, + InferAttributes, + InferCreationAttributes, +} from 'sequelize' // Indexing Fees AKA "DIPs" @@ -6,30 +13,30 @@ export class IndexingAgreement extends Model< InferAttributes, InferCreationAttributes > { - declare id: CreationOptional; - declare signature: Buffer; - declare signed_payload: Buffer; - declare protocol_network: string; - declare chain_id: string; - declare base_price_per_epoch: string; - declare price_per_entity: string; - declare subgraph_deployment_id: string; - declare service: string; - declare payee: string; - declare payer: string; - declare deadline: Date; - declare duration_epochs: bigint; - declare max_initial_amount: string; - declare max_ongoing_amount_per_epoch: string; - declare min_epochs_per_collection: bigint; - declare max_epochs_per_collection: bigint; - declare created_at: Date; - declare updated_at: Date; - declare cancelled_at: Date | null; - declare signed_cancellation_payload: Buffer | null; - declare current_allocation_id: string | null; - declare last_allocation_id: string | null; - declare last_payment_collected_at: Date | null; + declare id: CreationOptional + declare signature: Buffer + declare signed_payload: Buffer + declare protocol_network: string + declare chain_id: string + declare base_price_per_epoch: string + declare price_per_entity: string + declare subgraph_deployment_id: string + declare service: string + declare payee: string + declare payer: string + declare deadline: Date + declare duration_epochs: bigint + declare max_initial_amount: string + declare max_ongoing_amount_per_epoch: string + declare min_epochs_per_collection: bigint + declare max_epochs_per_collection: bigint + declare created_at: Date + declare updated_at: Date + declare cancelled_at: Date | null + declare signed_cancellation_payload: Buffer | null + declare current_allocation_id: string | null + declare last_allocation_id: string | null + declare last_payment_collected_at: Date | null } export interface IndexingFeesModels { diff --git a/packages/indexer-common/src/indexing-fees/dips.ts b/packages/indexer-common/src/indexing-fees/dips.ts index 28da7d9b1..de9ada303 100644 --- a/packages/indexer-common/src/indexing-fees/dips.ts +++ b/packages/indexer-common/src/indexing-fees/dips.ts @@ -1,6 +1,15 @@ -import { formatGRT, Logger, SubgraphDeploymentID } from "@graphprotocol/common-ts"; -import { AllocationManager, GraphNode, IndexerManagementModels, IndexingDecisionBasis, IndexingRuleAttributes, Network, SubgraphIdentifierType, upsertIndexingRule } from '@graphprotocol/indexer-common' -import { Op } from "sequelize"; +import { formatGRT, Logger, SubgraphDeploymentID } from '@graphprotocol/common-ts' +import { + AllocationManager, + GraphNode, + IndexerManagementModels, + IndexingDecisionBasis, + IndexingRuleAttributes, + Network, + SubgraphIdentifierType, + upsertIndexingRule, +} from '@graphprotocol/indexer-common' +import { Op } from 'sequelize' export class DipsManager { constructor( @@ -20,12 +29,14 @@ export class DipsManager { }) if (agreement) { // TODO use dips-proto to cancel agreement via grpc - // Mark the agreement as cancelled } } // Update the current and last allocation ids for an agreement if it exists - async tryUpdateAgreementAllocation(oldAllocationId: string, newAllocationId: string | null) { + async tryUpdateAgreementAllocation( + oldAllocationId: string, + newAllocationId: string | null, + ) { const agreement = await this.models.IndexingAgreement.findOne({ where: { current_allocation_id: oldAllocationId, @@ -65,17 +76,22 @@ export class DipsManager { // (tap-agent will take care of aggregating it into a RAV) // Mark the agreement as having had a payment collected - await this.models.IndexingAgreement.update({ - last_payment_collected_at: new Date(), - }, { - where: { - last_allocation_id: lastAllocationId, + await this.models.IndexingAgreement.update( + { + last_payment_collected_at: new Date(), }, - }) + { + where: { + last_allocation_id: lastAllocationId, + }, + }, + ) } async ensureAgreementRules() { if (!this.parent) { - this.logger.error('DipsManager has no parent AllocationManager, cannot ensure agreement rules') + this.logger.error( + 'DipsManager has no parent AllocationManager, cannot ensure agreement rules', + ) return } // Get all the indexing agreements that are not cancelled @@ -87,20 +103,26 @@ export class DipsManager { // For each agreement, check that there is an indexing rule to always // allocate to the agreement's subgraphDeploymentId, and if not, create one for (const agreement of indexingAgreements) { - const subgraphDeploymentID = new SubgraphDeploymentID(agreement.subgraph_deployment_id) + const subgraphDeploymentID = new SubgraphDeploymentID( + agreement.subgraph_deployment_id, + ) // If there is not yet an indexingRule that deems this deployment worth allocating to, make one if (!(await this.parent.matchingRuleExists(this.logger, subgraphDeploymentID))) { - this.logger.debug( - `Creating indexing rule for agreement ${agreement.id}`, - ) + this.logger.debug(`Creating indexing rule for agreement ${agreement.id}`) const indexingRule = { identifier: agreement.subgraph_deployment_id, - allocationAmount: formatGRT(this.network.specification.indexerOptions.dipsAllocationAmount), + allocationAmount: formatGRT( + this.network.specification.indexerOptions.dipsAllocationAmount, + ), identifierType: SubgraphIdentifierType.DEPLOYMENT, decisionBasis: IndexingDecisionBasis.ALWAYS, protocolNetwork: this.network.specification.networkIdentifier, autoRenewal: true, - allocationLifetime: Math.max(Number(agreement.min_epochs_per_collection), Number(agreement.max_epochs_per_collection) - this.network.specification.indexerOptions.dipsEpochsMargin), + allocationLifetime: Math.max( + Number(agreement.min_epochs_per_collection), + Number(agreement.max_epochs_per_collection) - + this.network.specification.indexerOptions.dipsEpochsMargin, + ), } as Partial await upsertIndexingRule(this.logger, this.models, indexingRule) From 3894e094d7f705059e1594b8027a8fd7e037652a Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Sat, 8 Feb 2025 01:27:50 -0300 Subject: [PATCH 5/7] chore: implement dipper service client --- packages/indexer-common/package.json | 3 + .../indexing-fees/dipper-service-client.ts | 162 ++++++++++++++++++ .../indexer-common/src/indexing-fees/dips.ts | 87 ++++++++-- packages/indexer-common/src/network.ts | 5 +- .../indexer-common/src/query-fees/models.ts | 13 +- yarn.lock | 123 ++++++++++++- 6 files changed, 379 insertions(+), 14 deletions(-) create mode 100644 packages/indexer-common/src/indexing-fees/dipper-service-client.ts diff --git a/packages/indexer-common/package.json b/packages/indexer-common/package.json index bd4b39556..4651a84ce 100644 --- a/packages/indexer-common/package.json +++ b/packages/indexer-common/package.json @@ -22,8 +22,11 @@ "clean": "rm -rf ./node_modules ./dist ./tsconfig.tsbuildinfo" }, "dependencies": { + "@bufbuild/protobuf": "2.2.3", "@graphprotocol/common-ts": "2.0.11", "@graphprotocol/cost-model": "0.1.18", + "@graphprotocol/dips-proto": "0.2.0", + "@grpc/grpc-js": "^1.12.6", "@semiotic-labs/tap-contracts-bindings": "^1.2.1", "@thi.ng/heaps": "1.2.38", "@types/lodash.clonedeep": "^4.5.7", diff --git a/packages/indexer-common/src/indexing-fees/dipper-service-client.ts b/packages/indexer-common/src/indexing-fees/dipper-service-client.ts new file mode 100644 index 000000000..8ae5d19f6 --- /dev/null +++ b/packages/indexer-common/src/indexing-fees/dipper-service-client.ts @@ -0,0 +1,162 @@ +import { Client, credentials } from '@grpc/grpc-js' +import { UnaryCallback } from '@grpc/grpc-js/build/src/client' +import { DipperServiceClientImpl } from '@graphprotocol/dips-proto/generated/gateway' +import { Wallet } from 'ethers' +import { + _TypedDataEncoder, + arrayify, + defaultAbiCoder, + recoverAddress, +} from 'ethers/lib/utils' +import { toAddress } from '@graphprotocol/common-ts' + +type RpcImpl = (service: string, method: string, data: Uint8Array) => Promise + +interface Rpc { + request: RpcImpl +} + +export const domainSalt = + '0xb4632c657c26dce5d4d7da1d65bda185b14ff8f905ddbb03ea0382ed06c5ef28' +export const chainId = 0xa4b1 // 42161 +export const cancelAgreementDomain = { + name: 'Graph Protocol Indexing Agreement Cancellation', + version: '0', + chainId: chainId, + salt: domainSalt, +} +export const cancelAgreementTypes = { + CancellationRequest: [{ name: 'agreement_id', type: 'bytes16' }], +} + +export const collectPaymentsDomain = { + name: 'Graph Protocol Indexing Agreement Collection', + version: '0', + chainId: chainId, + salt: domainSalt, +} +export const collectPaymentsTypes = { + CollectionRequest: [ + { name: 'agreement_id', type: 'bytes16' }, + { name: 'allocation_id', type: 'address' }, + { name: 'entity_count', type: 'uint64' }, + ], +} + +export const createSignedCancellationRequest = async ( + agreementId: string, + wallet: Wallet, +): Promise => { + const signature = await wallet._signTypedData( + cancelAgreementDomain, + cancelAgreementTypes, + { agreement_id: agreementId }, + ) + return arrayify( + defaultAbiCoder.encode(['tuple(bytes16)', 'bytes'], [[agreementId], signature]), + ) +} + +export const createSignedCollectionRequest = async ( + agreementId: string, + allocationId: string, + entityCount: number, + wallet: Wallet, +): Promise => { + const signature = await wallet._signTypedData( + collectPaymentsDomain, + collectPaymentsTypes, + { agreement_id: agreementId, allocation_id: allocationId, entity_count: entityCount }, + ) + return arrayify( + defaultAbiCoder.encode( + ['tuple(bytes16, address, uint64)', 'bytes'], + [[agreementId, allocationId, entityCount], signature], + ), + ) +} + +export const decodeTapReceipt = (receipt: Uint8Array, verifyingContract: string) => { + const [message, signature] = defaultAbiCoder.decode( + ['tuple(address,uint64,uint64,uint128)', 'bytes'], + receipt, + ) + + const [allocationId, timestampNs, nonce, value] = message + + // Recover the signer address from the signature + // compute the EIP-712 digest of the message + const domain = { + name: 'TAP', + version: '1', + chainId: chainId, + verifyingContract, + } + + const types = { + Receipt: [ + { name: 'allocation_id', type: 'address' }, + { name: 'timestamp_ns', type: 'uint64' }, + { name: 'nonce', type: 'uint64' }, + { name: 'value', type: 'uint128' }, + ], + } + + const digest = _TypedDataEncoder.hash(domain, types, { + allocation_id: allocationId, + timestamp_ns: timestampNs, + nonce: nonce, + value: value, + }) + const signerAddress = recoverAddress(digest, signature) + return { + allocation_id: allocationId, + signer_address: toAddress(signerAddress), + signature: signature, + timestamp_ns: timestampNs, + nonce: nonce, + value: value, + } +} + +export const createRpc = (url: string): Rpc => { + const client = new Client(url, credentials.createInsecure()) + const request: RpcImpl = (service, method, data) => { + // Conventionally in gRPC, the request path looks like + // "package.names.ServiceName/MethodName", + // we therefore construct such a string + const path = `/${service}/${method}` + + return new Promise((resolve, reject) => { + // makeUnaryRequest transmits the result (and error) with a callback + // transform this into a promise! + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const resultCallback: UnaryCallback = (err, res) => { + if (err) { + return reject(err) + } + resolve(res) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function passThrough(argument: any) { + return argument + } + + // Using passThrough as the deserialize functions + client.makeUnaryRequest( + path, + (d) => Buffer.from(d), + passThrough, + data, + resultCallback, + ) + }) + } + + return { request } +} + +export const createDipperServiceClient = (url: string) => { + const rpc = createRpc(url) + return new DipperServiceClientImpl(rpc) +} diff --git a/packages/indexer-common/src/indexing-fees/dips.ts b/packages/indexer-common/src/indexing-fees/dips.ts index de9ada303..bdd03e7d5 100644 --- a/packages/indexer-common/src/indexing-fees/dips.ts +++ b/packages/indexer-common/src/indexing-fees/dips.ts @@ -11,14 +11,35 @@ import { } from '@graphprotocol/indexer-common' import { Op } from 'sequelize' +import { + createDipperServiceClient, + createSignedCancellationRequest, + createSignedCollectionRequest, + decodeTapReceipt, +} from './dipper-service-client' +import { + CollectPaymentStatus, + DipperServiceClientImpl, +} from '@graphprotocol/dips-proto/generated/gateway' +import { IndexingAgreement } from '../indexer-management/models/indexing-agreement' + export class DipsManager { + private dipperServiceClient: DipperServiceClientImpl + constructor( private logger: Logger, private models: IndexerManagementModels, private graphNode: GraphNode, private network: Network, private parent: AllocationManager | null, - ) {} + ) { + if (!this.network.specification.indexerOptions.dipperEndpoint) { + throw new Error('dipperEndpoint is not set') + } + this.dipperServiceClient = createDipperServiceClient( + this.network.specification.indexerOptions.dipperEndpoint, + ) + } // Cancel an agreement associated to an allocation if it exists async tryCancelAgreement(allocationId: string) { const agreement = await this.models.IndexingAgreement.findOne({ @@ -28,8 +49,22 @@ export class DipsManager { }, }) if (agreement) { - // TODO use dips-proto to cancel agreement via grpc - // Mark the agreement as cancelled + try { + const cancellation = await createSignedCancellationRequest( + agreement.id, + this.network.wallet, + ) + await this.dipperServiceClient.CancelAgreement({ + version: 1, + signedCancellation: cancellation, + }) + + // Mark the agreement as cancelled + agreement.cancelled_at = new Date() + await agreement.save() + } catch (error) { + this.logger.error(`Error cancelling agreement ${agreement.id}`, { error }) + } } } // Update the current and last allocation ids for an agreement if it exists @@ -62,18 +97,50 @@ export class DipsManager { }) for (const agreement of outstandingAgreements) { if (agreement.last_allocation_id) { - await this.tryCollectPayment(agreement.last_allocation_id) + await this.tryCollectPayment(agreement) } else { // This should never happen as we check for this in the query this.logger.error(`Agreement ${agreement.id} has no last allocation id`) } } } - async tryCollectPayment(lastAllocationId: string) { - // TODO: use dips-proto to collect payment via grpc - - // TODO: store the receipt in the database - // (tap-agent will take care of aggregating it into a RAV) + async tryCollectPayment(agreement: IndexingAgreement) { + if (!agreement.last_allocation_id) { + this.logger.error(`Agreement ${agreement.id} has no last allocation id`) + return + } + const entityCount = 0 // TODO: get entity count from graph node + const collection = await createSignedCollectionRequest( + agreement.id, + agreement.last_allocation_id, + entityCount, + this.network.wallet, + ) + try { + const response = await this.dipperServiceClient.CollectPayment({ + version: 1, + signedCollection: collection, + }) + if (response.status === CollectPaymentStatus.ACCEPT) { + if (!this.network.tapCollector) { + throw new Error('TapCollector not initialized') + } + // Store the tap receipt in the database + const tapReceipt = decodeTapReceipt( + response.tapReceipt, + this.network.tapCollector?.tapContracts.tapVerifier.address, + ) + await this.network.queryFeeModels.scalarTapReceipts.create(tapReceipt) + } else { + this.logger.error(`Error collecting payment for agreement ${agreement.id}`, { + error: response.status, + }) + } + } catch (error) { + this.logger.error(`Error collecting payment for agreement ${agreement.id}`, { + error, + }) + } // Mark the agreement as having had a payment collected await this.models.IndexingAgreement.update( @@ -82,7 +149,7 @@ export class DipsManager { }, { where: { - last_allocation_id: lastAllocationId, + id: agreement.id, }, }, ) diff --git a/packages/indexer-common/src/network.ts b/packages/indexer-common/src/network.ts index 1b8d436e2..34eaf9b40 100644 --- a/packages/indexer-common/src/network.ts +++ b/packages/indexer-common/src/network.ts @@ -52,7 +52,7 @@ export class Network { specification: spec.NetworkSpecification paused: Eventual isOperator: Eventual - + queryFeeModels: QueryFeeModels private constructor( logger: Logger, contracts: NetworkContracts, @@ -66,6 +66,7 @@ export class Network { specification: spec.NetworkSpecification, paused: Eventual, isOperator: Eventual, + queryFeeModels: QueryFeeModels, ) { this.logger = logger this.contracts = contracts @@ -79,6 +80,7 @@ export class Network { this.specification = specification this.paused = paused this.isOperator = isOperator + this.queryFeeModels = queryFeeModels } static async create( @@ -345,6 +347,7 @@ export class Network { specification, paused, isOperator, + queryFeeModels, ) } diff --git a/packages/indexer-common/src/query-fees/models.ts b/packages/indexer-common/src/query-fees/models.ts index 095d60fe0..a8d3db242 100644 --- a/packages/indexer-common/src/query-fees/models.ts +++ b/packages/indexer-common/src/query-fees/models.ts @@ -13,11 +13,20 @@ export interface ScalarTapReceiptsAttributes { value: bigint error_log?: string } +export interface ScalarTapReceiptsCreationAttributes { + allocation_id: Address + signer_address: Address + signature: Uint8Array + timestamp_ns: bigint + nonce: bigint + value: bigint +} + export class ScalarTapReceipts - extends Model + extends Model implements ScalarTapReceiptsAttributes { - public id!: number + public id!: CreationOptional public allocation_id!: Address public signer_address!: Address public signature!: Uint8Array diff --git a/yarn.lock b/yarn.lock index 3443e3843..88f587cce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -329,6 +329,11 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@bufbuild/protobuf@2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.2.3.tgz#9cd136f6b687e63e9b517b3a54211ece942897ee" + integrity sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg== + "@cspotcode/source-map-consumer@0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" @@ -793,6 +798,11 @@ "@mapbox/node-pre-gyp" "1.0.11" cargo-cp-artifact "0.1.8" +"@graphprotocol/dips-proto@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@graphprotocol/dips-proto/-/dips-proto-0.2.0.tgz#e86ceff0dd30ea8ee3b89e84b5b6ad598301ac13" + integrity sha512-Ppt5XZNCrdTT0PwN1UTYIoDBFS11sC4E83yxK6O6rpOHwsjaCw/bM1vwR7JOl64WC+0Jy0fEhOwWEaRBLtAxaQ== + "@graphprotocol/pino-sentry-simple@0.7.1": version "0.7.1" resolved "https://registry.npmjs.org/@graphprotocol/pino-sentry-simple/-/pino-sentry-simple-0.7.1.tgz" @@ -803,6 +813,24 @@ split2 "^3.1.1" through2 "^3.0.1" +"@grpc/grpc-js@^1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.12.6.tgz#a3586ffdfb6a1f5cd5b4866dec9074c4a1e65472" + integrity sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q== + dependencies: + "@grpc/proto-loader" "^0.7.13" + "@js-sdsl/ordered-map" "^4.4.2" + +"@grpc/proto-loader@^0.7.13": + version "0.7.13" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.13.tgz#f6a44b2b7c9f7b609f5748c6eac2d420e37670cf" + integrity sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.5" + yargs "^17.7.2" + "@humanwhocodes/config-array@^0.11.11", "@humanwhocodes/config-array@^0.11.13": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -1089,6 +1117,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@js-sdsl/ordered-map@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" + integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== + "@lerna/add@6.1.0": version "6.1.0" resolved "https://registry.npmjs.org/@lerna/add/-/add-6.1.0.tgz" @@ -2226,6 +2259,59 @@ resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + "@rushstack/ts-command-line@^4.7.7": version "4.16.0" resolved "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.16.0.tgz" @@ -2751,6 +2837,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.1.tgz#8b589bba9b2af0128796461a0979764562687e6f" integrity sha512-4LcJvuXQlv4lTHnxwyHQZ3uR9Zw2j7m1C9DfuwoTFQQP4Pmu04O6IfLYgMmHoOCt0nosItLLZAH+sOrRE0Bo8g== +"@types/node@>=13.7.0": + version "22.13.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.1.tgz#a2a3fefbdeb7ba6b89f40371842162fac0934f33" + integrity sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew== + dependencies: + undici-types "~6.20.0" + "@types/node@^12.12.54": version "12.20.55" resolved "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz" @@ -7555,6 +7648,11 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +long@^5.0.0: + version "5.2.4" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.4.tgz#ee651d5c7c25901cfca5e67220ae9911695e99b2" + integrity sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg== + loose-envify@^1.0.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" @@ -9096,6 +9194,24 @@ proto-list@~1.2.1: resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz" integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== +protobufjs@^7.2.5: + version "7.4.0" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.4.0.tgz#7efe324ce9b3b61c82aae5de810d287bc08a248a" + integrity sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + protocols@^2.0.0, protocols@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz" @@ -10576,6 +10692,11 @@ underscore@^1.13.1: resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz" integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -11035,7 +11156,7 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.3.1, yargs@^17.6.2: +yargs@^17.3.1, yargs@^17.6.2, yargs@^17.7.2: version "17.7.2" resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== From 9d87b5dabc370adb447035c160428adc8f74eac7 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Sat, 8 Feb 2025 01:48:37 -0300 Subject: [PATCH 6/7] chore: manage dips in allocation actions --- .../src/indexer-management/allocations.ts | 43 +++++++++++++++---- .../indexer-common/src/indexing-fees/dips.ts | 6 +-- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/indexer-common/src/indexer-management/allocations.ts b/packages/indexer-common/src/indexer-management/allocations.ts index d9303345d..4f6daa68d 100644 --- a/packages/indexer-common/src/indexer-management/allocations.ts +++ b/packages/indexer-common/src/indexer-management/allocations.ts @@ -99,20 +99,22 @@ export type TransactionResult = | ActionFailure[] export class AllocationManager { - private dipsManager: DipsManager + private dipsManager: DipsManager | null = null constructor( private logger: Logger, private models: IndexerManagementModels, private graphNode: GraphNode, private network: Network, ) { - this.dipsManager = new DipsManager( - this.logger, - this.models, - this.graphNode, - this.network, - this, - ) + if (this.network.specification.indexerOptions.dipperEndpoint) { + this.dipsManager = new DipsManager( + this.logger, + this.models, + this.graphNode, + this.network, + this, + ) + } } async executeBatch(actions: Action[]): Promise { @@ -521,6 +523,14 @@ export class AllocationManager { await upsertIndexingRule(logger, this.models, indexingRule) } + if (this.dipsManager) { + await this.dipsManager.tryUpdateAgreementAllocation( + deployment, + null, + createAllocationEventLogs.allocationID, + ) + } + return { actionID, type: 'allocate', @@ -677,6 +687,15 @@ export class AllocationManager { await upsertIndexingRule(logger, this.models, neverIndexingRule) + if (this.dipsManager) { + await this.dipsManager.tryCancelAgreement(allocationID) + await this.dipsManager.tryUpdateAgreementAllocation( + allocation.subgraphDeployment.id.toString(), + allocationID, + null, + ) + } + return { actionID, type: 'unallocate', @@ -976,6 +995,14 @@ export class AllocationManager { await upsertIndexingRule(logger, this.models, indexingRule) } + if (this.dipsManager) { + await this.dipsManager.tryUpdateAgreementAllocation( + subgraphDeploymentID.toString(), + allocationID, + createAllocationEventLogs.allocationID, + ) + } + return { actionID, type: 'reallocate', diff --git a/packages/indexer-common/src/indexing-fees/dips.ts b/packages/indexer-common/src/indexing-fees/dips.ts index bdd03e7d5..d19c22ae5 100644 --- a/packages/indexer-common/src/indexing-fees/dips.ts +++ b/packages/indexer-common/src/indexing-fees/dips.ts @@ -69,13 +69,13 @@ export class DipsManager { } // Update the current and last allocation ids for an agreement if it exists async tryUpdateAgreementAllocation( - oldAllocationId: string, + deploymentId: string, + oldAllocationId: string | null, newAllocationId: string | null, ) { const agreement = await this.models.IndexingAgreement.findOne({ where: { - current_allocation_id: oldAllocationId, - cancelled_at: null, + subgraph_deployment_id: deploymentId, }, }) if (agreement) { From 4ee2aabdbdc6f579b7778eb1a9f9760918105d0d Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Fri, 14 Feb 2025 15:31:03 -0300 Subject: [PATCH 7/7] chore: validate receipt signer --- packages/indexer-common/package.json | 2 +- .../src/allocations/escrow-accounts.ts | 31 +++++++++++++++++++ .../indexer-common/src/indexing-fees/dips.ts | 28 ++++++++++++----- ...ient.ts => gateway-dips-service-client.ts} | 6 ++-- yarn.lock | 8 ++--- 5 files changed, 60 insertions(+), 15 deletions(-) rename packages/indexer-common/src/indexing-fees/{dipper-service-client.ts => gateway-dips-service-client.ts} (95%) diff --git a/packages/indexer-common/package.json b/packages/indexer-common/package.json index 4651a84ce..a148ce9ba 100644 --- a/packages/indexer-common/package.json +++ b/packages/indexer-common/package.json @@ -25,7 +25,7 @@ "@bufbuild/protobuf": "2.2.3", "@graphprotocol/common-ts": "2.0.11", "@graphprotocol/cost-model": "0.1.18", - "@graphprotocol/dips-proto": "0.2.0", + "@graphprotocol/dips-proto": "0.2.1", "@grpc/grpc-js": "^1.12.6", "@semiotic-labs/tap-contracts-bindings": "^1.2.1", "@thi.ng/heaps": "1.2.38", diff --git a/packages/indexer-common/src/allocations/escrow-accounts.ts b/packages/indexer-common/src/allocations/escrow-accounts.ts index 1d126fd94..45402171c 100644 --- a/packages/indexer-common/src/allocations/escrow-accounts.ts +++ b/packages/indexer-common/src/allocations/escrow-accounts.ts @@ -13,6 +13,14 @@ export type EscrowAccountResponse = { }[] } +export type EscrowSenderResponse = { + signer: { + sender: { + id: string + } + } +} + export class EscrowAccounts { constructor(private sendersBalances: Map) {} @@ -65,3 +73,26 @@ export const getEscrowAccounts = async ( } return EscrowAccounts.fromResponse(result.data) } + +export const getEscrowSenderForSigner = async ( + tapSubgraph: SubgraphClient, + signer: Address, +): Promise
=> { + const signerLower = signer.toLowerCase() + const result = await tapSubgraph.query( + gql` + query EscrowAccountQuery($signer: ID!) { + signer(id: $signer) { + sender { + id + } + } + } + `, + { signer: signerLower }, + ) + if (!result.data) { + throw `There was an error while querying Tap Subgraph. Errors: ${result.error}` + } + return toAddress(result.data.signer.sender.id) +} diff --git a/packages/indexer-common/src/indexing-fees/dips.ts b/packages/indexer-common/src/indexing-fees/dips.ts index d19c22ae5..43a66ec88 100644 --- a/packages/indexer-common/src/indexing-fees/dips.ts +++ b/packages/indexer-common/src/indexing-fees/dips.ts @@ -1,6 +1,7 @@ import { formatGRT, Logger, SubgraphDeploymentID } from '@graphprotocol/common-ts' import { AllocationManager, + getEscrowSenderForSigner, GraphNode, IndexerManagementModels, IndexingDecisionBasis, @@ -12,19 +13,19 @@ import { import { Op } from 'sequelize' import { - createDipperServiceClient, + createGatewayDipsServiceClient, createSignedCancellationRequest, createSignedCollectionRequest, decodeTapReceipt, -} from './dipper-service-client' +} from './gateway-dips-service-client' import { CollectPaymentStatus, - DipperServiceClientImpl, + GatewayDipsServiceClientImpl, } from '@graphprotocol/dips-proto/generated/gateway' import { IndexingAgreement } from '../indexer-management/models/indexing-agreement' export class DipsManager { - private dipperServiceClient: DipperServiceClientImpl + private gatewayDipsServiceClient: GatewayDipsServiceClientImpl constructor( private logger: Logger, @@ -36,7 +37,7 @@ export class DipsManager { if (!this.network.specification.indexerOptions.dipperEndpoint) { throw new Error('dipperEndpoint is not set') } - this.dipperServiceClient = createDipperServiceClient( + this.gatewayDipsServiceClient = createGatewayDipsServiceClient( this.network.specification.indexerOptions.dipperEndpoint, ) } @@ -54,7 +55,7 @@ export class DipsManager { agreement.id, this.network.wallet, ) - await this.dipperServiceClient.CancelAgreement({ + await this.gatewayDipsServiceClient.CancelAgreement({ version: 1, signedCancellation: cancellation, }) @@ -117,7 +118,7 @@ export class DipsManager { this.network.wallet, ) try { - const response = await this.dipperServiceClient.CollectPayment({ + const response = await this.gatewayDipsServiceClient.CollectPayment({ version: 1, signedCollection: collection, }) @@ -130,6 +131,19 @@ export class DipsManager { response.tapReceipt, this.network.tapCollector?.tapContracts.tapVerifier.address, ) + // TODO: check that the signer of the TAP receipt is a signer + // on the corresponding escrow account for the payer (sender) of the + // indexing agreement + const escrowSender = await getEscrowSenderForSigner( + this.network.tapCollector?.tapSubgraph, + tapReceipt.signer_address, + ) + if (escrowSender !== agreement.payer) { + // TODO: should we cancel the agreement here? + throw new Error( + 'Signer of TAP receipt is not a signer on the indexing agreement', + ) + } await this.network.queryFeeModels.scalarTapReceipts.create(tapReceipt) } else { this.logger.error(`Error collecting payment for agreement ${agreement.id}`, { diff --git a/packages/indexer-common/src/indexing-fees/dipper-service-client.ts b/packages/indexer-common/src/indexing-fees/gateway-dips-service-client.ts similarity index 95% rename from packages/indexer-common/src/indexing-fees/dipper-service-client.ts rename to packages/indexer-common/src/indexing-fees/gateway-dips-service-client.ts index 8ae5d19f6..c158911a5 100644 --- a/packages/indexer-common/src/indexing-fees/dipper-service-client.ts +++ b/packages/indexer-common/src/indexing-fees/gateway-dips-service-client.ts @@ -1,6 +1,6 @@ import { Client, credentials } from '@grpc/grpc-js' import { UnaryCallback } from '@grpc/grpc-js/build/src/client' -import { DipperServiceClientImpl } from '@graphprotocol/dips-proto/generated/gateway' +import { GatewayDipsServiceClientImpl } from '@graphprotocol/dips-proto/generated/gateway' import { Wallet } from 'ethers' import { _TypedDataEncoder, @@ -156,7 +156,7 @@ export const createRpc = (url: string): Rpc => { return { request } } -export const createDipperServiceClient = (url: string) => { +export const createGatewayDipsServiceClient = (url: string) => { const rpc = createRpc(url) - return new DipperServiceClientImpl(rpc) + return new GatewayDipsServiceClientImpl(rpc) } diff --git a/yarn.lock b/yarn.lock index 88f587cce..ab196c76f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -798,10 +798,10 @@ "@mapbox/node-pre-gyp" "1.0.11" cargo-cp-artifact "0.1.8" -"@graphprotocol/dips-proto@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@graphprotocol/dips-proto/-/dips-proto-0.2.0.tgz#e86ceff0dd30ea8ee3b89e84b5b6ad598301ac13" - integrity sha512-Ppt5XZNCrdTT0PwN1UTYIoDBFS11sC4E83yxK6O6rpOHwsjaCw/bM1vwR7JOl64WC+0Jy0fEhOwWEaRBLtAxaQ== +"@graphprotocol/dips-proto@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@graphprotocol/dips-proto/-/dips-proto-0.2.1.tgz#e65b6a8dfdefb5604da96da4eaaeb0fe096a0627" + integrity sha512-1+6+M7P/xsmEHPRtPX5tO6yCdCDkEvg+Owc0/GJfKpmJmFdN+hxYDvAZ4GD1drLilsyxR/iLQxEvHHQsU3FoxA== "@graphprotocol/pino-sentry-simple@0.7.1": version "0.7.1"