From 97e87232941e39122add96188bd5ffa92af38bdf Mon Sep 17 00:00:00 2001 From: Ford Date: Tue, 2 Jan 2024 10:54:39 -0800 Subject: [PATCH] WIP --- packages/indexer-common/package.json | 1 + .../src/__tests__/transactions.test.ts | 113 ++++++++++++++++ .../src/allocations/query-fees.ts | 11 +- .../src/indexer-management/allocations.ts | 6 +- .../resolvers/allocations.ts | 24 ++-- packages/indexer-common/src/network.ts | 11 +- packages/indexer-common/src/transactions.ts | 126 +++++++++++++++++- yarn.lock | 26 +++- 8 files changed, 283 insertions(+), 35 deletions(-) create mode 100644 packages/indexer-common/src/__tests__/transactions.test.ts diff --git a/packages/indexer-common/package.json b/packages/indexer-common/package.json index 2ba42dbb5..82c8d6b68 100644 --- a/packages/indexer-common/package.json +++ b/packages/indexer-common/package.json @@ -22,6 +22,7 @@ "clean": "rm -rf ./node_modules ./dist ./tsconfig.tsbuildinfo" }, "dependencies": { + "@arbitrum/sdk": "^3.1.13", "@graphprotocol/common-ts": "2.0.9", "@graphprotocol/cost-model": "0.1.16", "@thi.ng/heaps": "1.2.38", diff --git a/packages/indexer-common/src/__tests__/transactions.test.ts b/packages/indexer-common/src/__tests__/transactions.test.ts new file mode 100644 index 000000000..d60ec2a0d --- /dev/null +++ b/packages/indexer-common/src/__tests__/transactions.test.ts @@ -0,0 +1,113 @@ +import { Overrides } from 'ethers' +import { + connectContracts, + createLogger, + createMetrics, + Logger, + mutable, + NetworkContracts, +} from '@graphprotocol/common-ts' +import { connectWallet, Network, TransactionManager } from '@graphprotocol/indexer-common' +import { TransactionMonitoring } from '../network-specification' +import geohash from 'ngeohash' + +// Make global Jest variables available +// eslint-disable-next-line @typescript-eslint/no-explicit-any +declare const __LOG_LEVEL__: never + +let contracts: NetworkContracts +let logger: Logger +let transactionManager: TransactionManager + +const setup = async () => { + logger = createLogger({ + name: 'transactions.test.ts', + async: false, + level: __LOG_LEVEL__, + }) + const metrics = createMetrics() + const provider = await Network.provider( + logger, + metrics, + 'arbsepolia', + // 'https://sepolia.publicgoods.network', + 'https://sepolia-rollup.arbitrum.io/rpc', + // 'https://wiser-evocative-energy.arbitrum-sepolia.quiknode.pro/fa97af810dfc2e91b4dedecdd381e92a82ef70e3/', + 1000, + ) + const testPhrase = + 'myth like bonus scare over problem client lizard pioneer submit female collect' + const wallet = await connectWallet(provider, 'arbsepolia', testPhrase, logger) + transactionManager = new TransactionManager( + provider, + wallet, + mutable(false), + mutable(true), + TransactionMonitoring.parse({}), + ) + contracts = await connectContracts(wallet, 421614, undefined) +} + +describe('Transaction Manager tests', () => { + beforeAll(setup) + + // Use higher timeout because tests make requests to an open RPC provider + jest.setTimeout(30_000) + + test('Identify Arbitrum provider', async () => { + await expect(transactionManager.isArbitrumChain()).resolves.toEqual(true) + }) + + test('Get gas price', async () => { + const gasP = await transactionManager.ethereum.getFeeData() + console.log('FeeData', gasP) + expect(gasP).toEqual(true) + }) + + test('Arbitrum gas estimation', async () => { + const contractAddress = contracts.serviceRegistry.address + const txData = await contracts.serviceRegistry.populateTransaction.register( + 'http://testindexer.hi', + geohash.encode(100, 100), + ) + const estimatedFee = await transactionManager.arbGasEstimation( + logger, + contractAddress, + txData.data!, + ) + console.log('ef', estimatedFee) + + const gasP = await transactionManager.ethereum.getFeeData() + console.log('FeeData', gasP) + await expect(estimatedFee).toEqual(4) + }) + + test('Estimate gas usage of contract function', async () => { + const overrides = [ + { maxPriorityFeePerGas: 0 }, + { }, + // { maxPriorityFeePerGas: 0, maxFeePerGas: 100000000 }, + ] + for (const override of overrides) { + const gasEstimate = await contracts.serviceRegistry.estimateGas.register( + 'http://testindexer.hi', + geohash.encode(100, 100), + override, + ) + console.log('gasEstimate', gasEstimate.toString()) + } + + await expect(overrides).toEqual({ maxFeePerGas: 0 }) + }) + + test('Calculate transaction overrides', async () => { + const estimationFn = (overrides: Overrides | undefined) => + contracts.serviceRegistry.estimateGas.register( + 'http://testindexer.hi', + geohash.encode(100, 100), + overrides, + ) + + await expect(transactionManager.txOverrides(estimationFn)).resolves.toHaveProperty('maxPriorityFeePerGas', 0) + }) +}) diff --git a/packages/indexer-common/src/allocations/query-fees.ts b/packages/indexer-common/src/allocations/query-fees.ts index 22ed08e9d..47317acbb 100644 --- a/packages/indexer-common/src/allocations/query-fees.ts +++ b/packages/indexer-common/src/allocations/query-fees.ts @@ -21,7 +21,7 @@ import { specification as spec, } from '..' import { DHeap } from '@thi.ng/heaps' -import { BigNumber, BigNumberish, Contract } from 'ethers' +import { BigNumber, Contract } from 'ethers' import { Op } from 'sequelize' import pReduce from 'p-reduce' @@ -567,11 +567,10 @@ export class AllocationReceiptCollector implements ReceiptCollector { try { // Submit the voucher on chain const txReceipt = await this.transactionManager.executeTransaction( - () => this.allocationExchange.estimateGas.redeemMany(onchainVouchers), - async (gasLimit: BigNumberish) => - this.allocationExchange.redeemMany(onchainVouchers, { - gasLimit, - }), + (overrides) => + this.allocationExchange.estimateGas.redeemMany(onchainVouchers, overrides), + async (overrides) => + this.allocationExchange.redeemMany(onchainVouchers, overrides), logger.child({ action: 'redeemMany' }), ) diff --git a/packages/indexer-common/src/indexer-management/allocations.ts b/packages/indexer-common/src/indexer-management/allocations.ts index 5e08ed1d4..09cf4c184 100644 --- a/packages/indexer-common/src/indexer-management/allocations.ts +++ b/packages/indexer-common/src/indexer-management/allocations.ts @@ -147,9 +147,9 @@ export class AllocationManager { logger.trace('Prepared transaction calldata', { callData }) return await this.network.transactionManager.executeTransaction( - async () => this.network.contracts.staking.estimateGas.multicall(callData), - async (gasLimit) => - this.network.contracts.staking.multicall(callData, { gasLimit }), + async (overrides) => + this.network.contracts.staking.estimateGas.multicall(callData, overrides), + async (overrides) => this.network.contracts.staking.multicall(callData, overrides), this.logger.child({ actions: `${JSON.stringify(validatedActions.map((action) => action.id))}`, function: 'staking.multicall', diff --git a/packages/indexer-common/src/indexer-management/resolvers/allocations.ts b/packages/indexer-common/src/indexer-management/resolvers/allocations.ts index 3bd945a46..d1a55c710 100644 --- a/packages/indexer-common/src/indexer-management/resolvers/allocations.ts +++ b/packages/indexer-common/src/indexer-management/resolvers/allocations.ts @@ -544,7 +544,7 @@ export default { }) const receipt = await transactionManager.executeTransaction( - async () => + async (overrides) => contracts.staking.estimateGas.allocateFrom( address, subgraphDeployment.bytes32, @@ -552,8 +552,9 @@ export default { allocationId, utils.hexlify(Array(32).fill(0)), proof, + overrides, ), - async (gasLimit) => + async (overrides) => contracts.staking.allocateFrom( address, subgraphDeployment.bytes32, @@ -561,7 +562,7 @@ export default { allocationId, utils.hexlify(Array(32).fill(0)), proof, - { gasLimit }, + overrides, ), logger.child({ action: 'allocate' }), ) @@ -700,12 +701,15 @@ export default { logger.debug('Sending closeAllocation transaction') const receipt = await transactionManager.executeTransaction( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - () => contracts.staking.estimateGas.closeAllocation(allocationData.id, poi!), - (gasLimit) => + (overrides) => + contracts.staking.estimateGas.closeAllocation( + allocationData.id, + poi!, + overrides, + ), + (overrides) => // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - contracts.staking.closeAllocation(allocationData.id, poi!, { - gasLimit, - }), + contracts.staking.closeAllocation(allocationData.id, poi!, overrides), logger, ) @@ -1009,8 +1013,8 @@ export default { ].map((tx) => tx.data as string) const receipt = await transactionManager.executeTransaction( - async () => contracts.staking.estimateGas.multicall(callData), - async (gasLimit) => contracts.staking.multicall(callData, { gasLimit }), + async (overrides) => contracts.staking.estimateGas.multicall(callData, overrides), + async (overrides) => contracts.staking.multicall(callData, overrides), logger.child({ function: 'closeAndAllocate', }), diff --git a/packages/indexer-common/src/network.ts b/packages/indexer-common/src/network.ts index 0104f8844..12fda2aa8 100644 --- a/packages/indexer-common/src/network.ts +++ b/packages/indexer-common/src/network.ts @@ -375,20 +375,19 @@ export class Network { } } const receipt = await this.transactionManager.executeTransaction( - () => + (overrides) => this.contracts.serviceRegistry.estimateGas.registerFor( this.specification.indexerOptions.address, this.specification.indexerOptions.url, geoHash, + overrides, ), - (gasLimit) => + (overrides) => this.contracts.serviceRegistry.registerFor( this.specification.indexerOptions.address, this.specification.indexerOptions.url, geoHash, - { - gasLimit, - }, + overrides, ), logger.child({ function: 'serviceRegistry.registerFor' }), ) @@ -417,7 +416,7 @@ export class Network { } } -async function connectWallet( +export async function connectWallet( networkProvider: providers.Provider, networkIdentifier: string, mnemonic: string, diff --git a/packages/indexer-common/src/transactions.ts b/packages/indexer-common/src/transactions.ts index d120b993c..9060ff190 100644 --- a/packages/indexer-common/src/transactions.ts +++ b/packages/indexer-common/src/transactions.ts @@ -1,8 +1,9 @@ import { BigNumber, - BigNumberish, + BytesLike, ContractReceipt, ContractTransaction, + Overrides, providers, utils, Wallet, @@ -17,11 +18,14 @@ import { toAddress, } from '@graphprotocol/common-ts' import delay from 'delay' +import gql from 'graphql-tag' +import { NodeInterface__factory } from '@arbitrum/sdk/dist/lib/abi/factories/NodeInterface__factory' +import { NODE_INTERFACE_ADDRESS } from '@arbitrum/sdk/dist/lib/dataEntities/constants' + import { TransactionMonitoring } from './network-specification' import { IndexerError, indexerError, IndexerErrorCode } from './errors' import { TransactionConfig, TransactionType } from './types' import { NetworkSubgraph } from './network-subgraph' -import gql from 'graphql-tag' export class TransactionManager { ethereum: providers.BaseProvider @@ -53,8 +57,8 @@ export class TransactionManager { } async executeTransaction( - gasEstimation: () => Promise, - transaction: (gasLimit: BigNumberish) => Promise, + gasEstimation: (overrides: Overrides | undefined) => Promise, + transaction: (overrides: Overrides) => Promise, logger: Logger, ): Promise { if (await this.paused.value()) { @@ -71,9 +75,9 @@ export class TransactionManager { let output: providers.TransactionReceipt | undefined = undefined const feeData = await this.waitForGasPricesBelowThreshold(logger) - const paddedGasLimit = Math.ceil((await gasEstimation()).toNumber() * 1.5) + const overrides = await this.txOverrides(gasEstimation) - const txPromise = transaction(paddedGasLimit) + const txPromise = transaction(overrides) let tx = await txPromise let txRequest: providers.TransactionRequest | undefined = undefined @@ -249,6 +253,116 @@ export class TransactionManager { ) } } + + async isArbitrumChain(): Promise { + const chainId = (await this.ethereum.getNetwork()).chainId + const arbitrumChains = [42161, 421613, 421614] + return arbitrumChains.includes(chainId) + } + + async arbGasEstimation( + logger: Logger, + contractAddress: string, + txData: BytesLike, + ): Promise { + logger.info('STARTINGGGG') + const arbitrumNodeInterface = NodeInterface__factory.connect( + NODE_INTERFACE_ADDRESS, + this.ethereum, + ) + logger.info('hiiiii') + try { + const gasEstimateComponents = + await arbitrumNodeInterface.callStatic.gasEstimateComponents( + contractAddress, + false, + txData, + {}, + ) + logger.info('COMPONENTS', { gasEstimateComponents }) + + // Getting useful values for calculating the formula + const l1GasEstimated = gasEstimateComponents.gasEstimateForL1 + const l2GasUsed = gasEstimateComponents.gasEstimate.sub( + gasEstimateComponents.gasEstimateForL1, + ) + const l2EstimatedPrice = gasEstimateComponents.baseFee + const l1EstimatedPrice = gasEstimateComponents.l1BaseFeeEstimate.mul(16) + + logger.info('yo', { + l1GasEstimated, + l2GasUsed, + l1EstimatedPrice, + l2EstimatedPrice, + }) + // Calculating some extra values to be able to apply all variables of the formula + // ------------------------------------------------------------------------------- + // NOTE: This one might be a bit confusing, but l1GasEstimated (B in the formula) is calculated based on l2 gas fees + // const l1Cost = l1GasEstimated.mul(l2EstimatedPrice) + // NOTE: This is similar to 140 + utils.hexDataLength(txData); + const l1Size = 140 + utils.hexDataLength(txData) + // const l1Size = + // l1EstimatedPrice == BigNumber.from(0) ? 0 : l1Cost.div(l1EstimatedPrice) + + const P = l2EstimatedPrice + const L2G = l2GasUsed + const L1P = l1EstimatedPrice + const L1S = l1Size + + // L1C (L1 Cost) = L1P * L1S + const L1C = L1P.mul(L1S) + + // B (Extra Buffer) = L1C / P + const B = L1C.div(P) + + // G (Gas Limit) = L2G + B + const G = L2G.add(B) + + // TXFEES (Transaction fees) = P * G + const TXFEES = P.mul(G) + + logger.info('Gas estimation components', { gasEstimateComponents }) + logger.info('-------------------') + logger.info( + `Full gas estimation = ${gasEstimateComponents.gasEstimate.toNumber()} units`, + ) + logger.info(`L2 Gas (L2G) = ${L2G.toNumber()} units`) + logger.info(`L1 estimated Gas (L1G) = ${l1GasEstimated.toNumber()} units`) + + logger.info(`P (L2 Gas Price) = ${utils.formatUnits(P, 'gwei')} gwei`) + logger.info( + `L1P (L1 estimated calldata price per byte) = ${utils.formatUnits( + L1P, + 'gwei', + )} gwei`, + ) + logger.info(`L1S (L1 Calldata size in bytes) = ${L1S} bytes`) + + logger.info('-------------------') + logger.info(`Transaction estimated fees to pay = ${utils.formatEther(TXFEES)} ETH`) + + return TXFEES + } catch (e) { + logger.info('WTFFFFF', { error: e }) + return BigNumber.from(0) + } + } + + async txOverrides( + gasEstimation: (overrides: Overrides | undefined) => Promise, + ): Promise { + const customMaxPriorityFeePerGas = (await this.isArbitrumChain()) ? 0 : undefined + const paddedGasLimit = Math.ceil( + ( + await gasEstimation({ maxPriorityFeePerGas: customMaxPriorityFeePerGas }) + ).toNumber(), + ) + return { + gasLimit: paddedGasLimit, + maxPriorityFeePerGas: customMaxPriorityFeePerGas, + } + } + async waitForGasPricesBelowThreshold(logger: Logger): Promise { let attempt = 1 let aboveThreshold = true diff --git a/yarn.lock b/yarn.lock index 28e47f540..92a5508f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33,6 +33,17 @@ tslib "^2.3.0" zen-observable-ts "^1.2.0" +"@arbitrum/sdk@^3.1.13": + version "3.1.13" + resolved "https://registry.npmjs.org/@arbitrum/sdk/-/sdk-3.1.13.tgz#a0d3d9a7b387f42547c63f6f066d8a6c4dd945cc" + integrity sha512-oE/j8ThWWEdFfV0helmR8lD0T67/CY1zMCt6RVslaCLrytFdbg3QsrHs/sQE3yiCXgisQlsx3qomCgh8PfBo8Q== + dependencies: + "@ethersproject/address" "^5.0.8" + "@ethersproject/bignumber" "^5.1.1" + "@ethersproject/bytes" "^5.0.8" + async-mutex "^0.4.0" + ethers "^5.1.0" + "@ardatan/sync-fetch@^0.0.1": version "0.0.1" resolved "https://registry.npmjs.org/@ardatan/sync-fetch/-/sync-fetch-0.0.1.tgz#3385d3feedceb60a896518a1db857ec1e945348f" @@ -424,7 +435,7 @@ "@ethersproject/logger" "^5.7.0" "@ethersproject/properties" "^5.7.0" -"@ethersproject/address@5.7.0", "@ethersproject/address@^5.7.0": +"@ethersproject/address@5.7.0", "@ethersproject/address@^5.0.8", "@ethersproject/address@^5.7.0": version "5.7.0" resolved "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== @@ -450,7 +461,7 @@ "@ethersproject/bytes" "^5.7.0" "@ethersproject/properties" "^5.7.0" -"@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.7.0": +"@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.1.1", "@ethersproject/bignumber@^5.7.0": version "5.7.0" resolved "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== @@ -459,7 +470,7 @@ "@ethersproject/logger" "^5.7.0" bn.js "^5.2.1" -"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.7.0": +"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.0.8", "@ethersproject/bytes@^5.7.0": version "5.7.0" resolved "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== @@ -3827,6 +3838,13 @@ astral-regex@^2.0.0: resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async-mutex@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz#ae8048cd4d04ace94347507504b3cf15e631c25f" + integrity sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA== + dependencies: + tslib "^2.4.0" + async@^3.2.3: version "3.2.4" resolved "https://registry.npmjs.org/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" @@ -5278,7 +5296,7 @@ ethereum-cryptography@^2.0.0, ethereum-cryptography@^2.1.2: "@scure/bip32" "1.3.1" "@scure/bip39" "1.2.1" -ethers@5.7.0, ethers@^5.6.0: +ethers@5.7.0, ethers@^5.1.0, ethers@^5.6.0: version "5.7.0" resolved "https://registry.npmjs.org/ethers/-/ethers-5.7.0.tgz#0055da174b9e076b242b8282638bc94e04b39835" integrity sha512-5Xhzp2ZQRi0Em+0OkOcRHxPzCfoBfgtOQA+RUylSkuHbhTEaQklnYi2hsWbRgs3ztJsXVXd9VKBcO1ScWL8YfA==