diff --git a/packages/indexer-agent/src/commands/start.ts b/packages/indexer-agent/src/commands/start.ts
index f22943ddb..6f9272f43 100644
--- a/packages/indexer-agent/src/commands/start.ts
+++ b/packages/indexer-agent/src/commands/start.ts
@@ -302,6 +302,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 +349,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 +387,10 @@ export async function createNetworkSpecification(
allocateOnNetworkSubgraph: argv.allocateOnNetworkSubgraph,
register: argv.register,
finalityTime: argv.chainFinalizeTime,
+ enableDips: argv.enableDips,
+ dipperEndpoint: argv.dipperEndpoint,
+ dipsAllocationAmount: argv.dipsAllocationAmount,
+ dipsEpochsMargin: argv.dipsEpochsMargin,
}
const transactionMonitoring = {
diff --git a/packages/indexer-common/package.json b/packages/indexer-common/package.json
index bd4b39556..a148ce9ba 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.1",
+ "@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/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/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/indexer-management/allocations.ts b/packages/indexer-common/src/indexer-management/allocations.ts
index 035aa0932..4f6daa68d 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,23 @@ export type TransactionResult =
| ActionFailure[]
export class AllocationManager {
+ private dipsManager: DipsManager | null = null
constructor(
private logger: Logger,
private models: IndexerManagementModels,
private graphNode: GraphNode,
private network: Network,
- ) {}
+ ) {
+ if (this.network.specification.indexerOptions.dipperEndpoint) {
+ 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' })
@@ -511,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',
@@ -667,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',
@@ -966,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/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/indexer-management/models/indexing-agreement.ts b/packages/indexer-common/src/indexer-management/models/indexing-agreement.ts
new file mode 100644
index 000000000..180e528a4
--- /dev/null
+++ b/packages/indexer-common/src/indexer-management/models/indexing-agreement.ts
@@ -0,0 +1,156 @@
+import {
+ DataTypes,
+ Sequelize,
+ Model,
+ 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 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 {
+ IndexingAgreement: typeof IndexingAgreement
+}
+
+export const defineIndexingFeesModels = (sequelize: Sequelize): IndexingFeesModels => {
+ IndexingAgreement.init(
+ {
+ id: {
+ type: DataTypes.UUID,
+ primaryKey: true,
+ },
+ signature: {
+ type: DataTypes.BLOB,
+ allowNull: false,
+ unique: true,
+ },
+ signed_payload: {
+ type: DataTypes.BLOB,
+ allowNull: false,
+ },
+ protocol_network: {
+ type: DataTypes.STRING(255),
+ allowNull: false,
+ },
+ chain_id: {
+ type: DataTypes.STRING(255),
+ allowNull: false,
+ },
+ base_price_per_epoch: {
+ 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,
+ },
+ 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,
+ },
+ 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,
+ },
+ last_payment_collected_at: {
+ type: DataTypes.DATE,
+ allowNull: true,
+ },
+ },
+ {
+ modelName: 'IndexingAgreement',
+ sequelize,
+ },
+ )
+
+ return {
+ ['IndexingAgreement']: 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..43a66ec88
--- /dev/null
+++ b/packages/indexer-common/src/indexing-fees/dips.ts
@@ -0,0 +1,213 @@
+import { formatGRT, Logger, SubgraphDeploymentID } from '@graphprotocol/common-ts'
+import {
+ AllocationManager,
+ getEscrowSenderForSigner,
+ GraphNode,
+ IndexerManagementModels,
+ IndexingDecisionBasis,
+ IndexingRuleAttributes,
+ Network,
+ SubgraphIdentifierType,
+ upsertIndexingRule,
+} from '@graphprotocol/indexer-common'
+import { Op } from 'sequelize'
+
+import {
+ createGatewayDipsServiceClient,
+ createSignedCancellationRequest,
+ createSignedCollectionRequest,
+ decodeTapReceipt,
+} from './gateway-dips-service-client'
+import {
+ CollectPaymentStatus,
+ GatewayDipsServiceClientImpl,
+} from '@graphprotocol/dips-proto/generated/gateway'
+import { IndexingAgreement } from '../indexer-management/models/indexing-agreement'
+
+export class DipsManager {
+ private gatewayDipsServiceClient: GatewayDipsServiceClientImpl
+
+ 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.gatewayDipsServiceClient = createGatewayDipsServiceClient(
+ 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({
+ where: {
+ current_allocation_id: allocationId,
+ cancelled_at: null,
+ },
+ })
+ if (agreement) {
+ try {
+ const cancellation = await createSignedCancellationRequest(
+ agreement.id,
+ this.network.wallet,
+ )
+ await this.gatewayDipsServiceClient.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
+ async tryUpdateAgreementAllocation(
+ deploymentId: string,
+ oldAllocationId: string | null,
+ newAllocationId: string | null,
+ ) {
+ const agreement = await this.models.IndexingAgreement.findOne({
+ where: {
+ subgraph_deployment_id: deploymentId,
+ },
+ })
+ 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)
+ } 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(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.gatewayDipsServiceClient.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,
+ )
+ // 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}`, {
+ 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(
+ {
+ last_payment_collected_at: new Date(),
+ },
+ {
+ where: {
+ id: agreement.id,
+ },
+ },
+ )
+ }
+ 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/gateway-dips-service-client.ts b/packages/indexer-common/src/indexing-fees/gateway-dips-service-client.ts
new file mode 100644
index 000000000..c158911a5
--- /dev/null
+++ b/packages/indexer-common/src/indexing-fees/gateway-dips-service-client.ts
@@ -0,0 +1,162 @@
+import { Client, credentials } from '@grpc/grpc-js'
+import { UnaryCallback } from '@grpc/grpc-js/build/src/client'
+import { GatewayDipsServiceClientImpl } 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 createGatewayDipsServiceClient = (url: string) => {
+ const rpc = createRpc(url)
+ return new GatewayDipsServiceClientImpl(rpc)
+}
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..0b71f1b8e
--- /dev/null
+++ b/packages/indexer-common/src/indexing-fees/index.ts
@@ -0,0 +1 @@
+export * from './dips'
diff --git a/packages/indexer-common/src/network-specification.ts b/packages/indexer-common/src/network-specification.ts
index f683cdec5..4db7eb30b 100644
--- a/packages/indexer-common/src/network-specification.ts
+++ b/packages/indexer-common/src/network-specification.ts
@@ -58,6 +58,10 @@ 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),
+ dipsEpochsMargin: positiveNumber().default(1),
})
.strict()
export type IndexerOptions = z.infer
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..ab196c76f 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.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"
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==