2021-08-12 09:58:13 +00:00
|
|
|
//
|
|
|
|
// Copyright 2021 Vulcanize, Inc.
|
|
|
|
//
|
|
|
|
|
2021-10-20 10:36:03 +00:00
|
|
|
import assert from 'assert';
|
2021-08-19 07:57:32 +00:00
|
|
|
import { ValueTransformer } from 'typeorm';
|
2021-10-20 10:36:03 +00:00
|
|
|
import yargs from 'yargs';
|
|
|
|
import { hideBin } from 'yargs/helpers';
|
2022-07-20 05:07:17 +00:00
|
|
|
import { utils, providers } from 'ethers';
|
2022-10-20 13:16:56 +00:00
|
|
|
import JSONbig from 'json-bigint';
|
2021-12-02 07:52:29 +00:00
|
|
|
import Decimal from 'decimal.js';
|
2023-11-06 09:30:24 +00:00
|
|
|
import ApolloBigInt from 'apollo-type-bigint';
|
|
|
|
import { GraphQLResolveInfo, GraphQLScalarType, ValueNode } from 'graphql';
|
2022-11-15 09:26:08 +00:00
|
|
|
import _ from 'lodash';
|
2021-10-20 10:36:03 +00:00
|
|
|
|
|
|
|
import { DEFAULT_CONFIG_PATH } from './constants';
|
2022-11-15 09:26:08 +00:00
|
|
|
import { GQLCacheConfig, Config } from './config';
|
2021-10-20 10:36:03 +00:00
|
|
|
import { JobQueue } from './job-queue';
|
2022-11-25 10:24:35 +00:00
|
|
|
import { GraphDecimal } from './graph/graph-decimal';
|
2021-11-17 12:44:02 +00:00
|
|
|
import * as EthDecoder from './eth';
|
2022-10-20 13:16:56 +00:00
|
|
|
import { ResultEvent } from './indexer';
|
2023-11-09 13:12:37 +00:00
|
|
|
import { EventInterface, EthFullBlock, EthFullTransaction } from './types';
|
2022-11-15 09:26:08 +00:00
|
|
|
import { BlockHeight } from './database';
|
2023-11-09 13:12:37 +00:00
|
|
|
import { Transaction } from './graph/utils';
|
2022-10-20 13:16:56 +00:00
|
|
|
|
|
|
|
const JSONbigNative = JSONbig({ useNativeBigInt: true });
|
2022-09-07 12:59:04 +00:00
|
|
|
|
2021-08-04 13:12:59 +00:00
|
|
|
/**
|
|
|
|
* Method to wait for specified time.
|
|
|
|
* @param time Time to wait in milliseconds
|
|
|
|
*/
|
|
|
|
export const wait = async (time: number): Promise<void> => new Promise(resolve => setTimeout(resolve, time));
|
2021-08-19 07:57:32 +00:00
|
|
|
|
2021-12-02 07:52:29 +00:00
|
|
|
/**
|
|
|
|
* Transformer used by typeorm entity for GraphDecimal type fields.
|
|
|
|
*/
|
|
|
|
export const graphDecimalTransformer: ValueTransformer = {
|
|
|
|
to: (value?: GraphDecimal) => {
|
2023-05-15 06:40:27 +00:00
|
|
|
if (value !== undefined && value !== null) {
|
2021-12-02 07:52:29 +00:00
|
|
|
return value.toFixed();
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
},
|
|
|
|
from: (value?: string) => {
|
2023-05-15 06:40:27 +00:00
|
|
|
if (value !== undefined && value !== null) {
|
2021-12-02 07:52:29 +00:00
|
|
|
return new GraphDecimal(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-08-19 07:57:32 +00:00
|
|
|
/**
|
2021-09-27 04:43:50 +00:00
|
|
|
* Transformer used by typeorm entity for Decimal type fields.
|
2021-08-19 07:57:32 +00:00
|
|
|
*/
|
|
|
|
export const decimalTransformer: ValueTransformer = {
|
|
|
|
to: (value?: Decimal) => {
|
2023-05-15 06:40:27 +00:00
|
|
|
if (value !== undefined && value !== null) {
|
2021-08-19 07:57:32 +00:00
|
|
|
return value.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
},
|
|
|
|
from: (value?: string) => {
|
2023-05-15 06:40:27 +00:00
|
|
|
if (value !== undefined && value !== null) {
|
2021-08-19 07:57:32 +00:00
|
|
|
return new Decimal(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
};
|
2021-09-27 04:43:50 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Transformer used by typeorm entity for bigint type fields.
|
|
|
|
*/
|
|
|
|
export const bigintTransformer: ValueTransformer = {
|
|
|
|
to: (value?: bigint) => {
|
2023-05-15 06:40:27 +00:00
|
|
|
if (value !== undefined && value !== null) {
|
2021-09-27 04:43:50 +00:00
|
|
|
return value.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
},
|
|
|
|
from: (value?: string) => {
|
2023-05-15 06:40:27 +00:00
|
|
|
if (value !== undefined && value !== null) {
|
2021-09-27 04:43:50 +00:00
|
|
|
return BigInt(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
};
|
2021-10-20 10:36:03 +00:00
|
|
|
|
2021-11-17 11:31:09 +00:00
|
|
|
export const bigintArrayTransformer: ValueTransformer = {
|
|
|
|
to: (valueArray?: bigint[]) => {
|
2023-04-20 08:01:41 +00:00
|
|
|
if (valueArray !== undefined) {
|
2021-11-17 11:31:09 +00:00
|
|
|
return valueArray.map(value => bigintTransformer.to(value));
|
|
|
|
}
|
|
|
|
|
|
|
|
return valueArray;
|
|
|
|
},
|
|
|
|
from: (valueArray?: string[]) => {
|
2023-04-20 08:01:41 +00:00
|
|
|
if (valueArray !== undefined) {
|
2021-11-17 11:31:09 +00:00
|
|
|
return valueArray.map(value => bigintTransformer.from(value));
|
|
|
|
}
|
|
|
|
|
|
|
|
return valueArray;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export const decimalArrayTransformer: ValueTransformer = {
|
|
|
|
to: (valueArray?: Decimal[]) => {
|
2023-04-20 08:01:41 +00:00
|
|
|
if (valueArray !== undefined) {
|
2021-11-17 11:31:09 +00:00
|
|
|
return valueArray.map(value => decimalTransformer.to(value));
|
|
|
|
}
|
|
|
|
|
|
|
|
return valueArray;
|
|
|
|
},
|
|
|
|
from: (valueArray?: string[]) => {
|
2023-04-20 08:01:41 +00:00
|
|
|
if (valueArray !== undefined) {
|
2021-11-17 11:31:09 +00:00
|
|
|
return valueArray.map(value => decimalTransformer.from(value));
|
|
|
|
}
|
|
|
|
|
|
|
|
return valueArray;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-10-20 10:36:03 +00:00
|
|
|
export const resetJobs = async (config: Config): Promise<void> => {
|
|
|
|
const { jobQueue: jobQueueConfig } = config;
|
|
|
|
|
|
|
|
const { dbConnectionString, maxCompletionLagInSecs } = jobQueueConfig;
|
|
|
|
assert(dbConnectionString, 'Missing job queue db connection string');
|
|
|
|
|
|
|
|
const jobQueue = new JobQueue({ dbConnectionString, maxCompletionLag: maxCompletionLagInSecs });
|
|
|
|
await jobQueue.start();
|
2023-11-06 09:31:48 +00:00
|
|
|
// Delete all active and pending (before completed) jobs
|
2023-11-06 06:04:48 +00:00
|
|
|
await jobQueue.deleteAllJobs('completed');
|
2021-10-20 10:36:03 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export const getResetYargs = (): yargs.Argv => {
|
|
|
|
return yargs(hideBin(process.argv))
|
|
|
|
.parserConfiguration({
|
|
|
|
'parse-numbers': false
|
|
|
|
}).options({
|
|
|
|
configFile: {
|
|
|
|
alias: 'f',
|
|
|
|
type: 'string',
|
|
|
|
require: true,
|
|
|
|
demandOption: true,
|
|
|
|
describe: 'configuration file path (toml)',
|
|
|
|
default: DEFAULT_CONFIG_PATH
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
2021-10-25 14:21:16 +00:00
|
|
|
|
2022-07-20 05:07:17 +00:00
|
|
|
export const getCustomProvider = (url?: utils.ConnectionInfo | string, network?: providers.Networkish): providers.JsonRpcProvider => {
|
2023-11-09 13:12:37 +00:00
|
|
|
const provider = new providers.StaticJsonRpcProvider(url, network);
|
2021-10-25 14:21:16 +00:00
|
|
|
provider.formatter = new CustomFormatter();
|
|
|
|
return provider;
|
|
|
|
};
|
|
|
|
|
|
|
|
class CustomFormatter extends providers.Formatter {
|
|
|
|
blockTag (blockTag: any): string {
|
|
|
|
if (blockTag == null) { return 'latest'; }
|
|
|
|
|
|
|
|
if (blockTag === 'earliest') { return '0x0'; }
|
|
|
|
|
|
|
|
if (blockTag === 'latest' || blockTag === 'pending') {
|
|
|
|
return blockTag;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof (blockTag) === 'number' || utils.isHexString(blockTag)) {
|
|
|
|
// Return value if hex string is of block hash length.
|
|
|
|
if (utils.isHexString(blockTag) && utils.hexDataLength(blockTag) === 32) {
|
|
|
|
return blockTag;
|
|
|
|
}
|
|
|
|
|
|
|
|
return utils.hexValue(<number | string>blockTag);
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Error('invalid blockTag');
|
|
|
|
}
|
|
|
|
}
|
2021-11-17 12:44:02 +00:00
|
|
|
|
2023-11-09 13:12:37 +00:00
|
|
|
export const getFullBlock = (ethFullBlock: EthFullBlock): any => {
|
2022-09-07 12:59:04 +00:00
|
|
|
// Decode the header data.
|
2023-11-09 13:12:37 +00:00
|
|
|
const header = EthDecoder.decodeHeader(EthDecoder.decodeData(ethFullBlock.blockByMhKey.data));
|
2021-11-17 12:44:02 +00:00
|
|
|
assert(header);
|
|
|
|
|
|
|
|
return {
|
2023-11-09 13:12:37 +00:00
|
|
|
headerId: ethFullBlock.id,
|
|
|
|
cid: ethFullBlock.cid,
|
|
|
|
blockNumber: ethFullBlock.blockNumber,
|
|
|
|
blockHash: ethFullBlock.blockHash,
|
|
|
|
parentHash: ethFullBlock.parentHash,
|
|
|
|
timestamp: ethFullBlock.timestamp,
|
|
|
|
stateRoot: ethFullBlock.stateRoot,
|
|
|
|
td: ethFullBlock.td,
|
|
|
|
txRoot: ethFullBlock.txRoot,
|
|
|
|
receiptRoot: ethFullBlock.receiptRoot,
|
|
|
|
uncleHash: ethFullBlock.uncleRoot,
|
2021-11-17 12:44:02 +00:00
|
|
|
difficulty: header.Difficulty.toString(),
|
|
|
|
gasLimit: header.GasLimit.toString(),
|
2021-11-30 11:05:01 +00:00
|
|
|
gasUsed: header.GasUsed.toString(),
|
2021-12-01 10:48:38 +00:00
|
|
|
author: header.Beneficiary,
|
2023-11-09 13:12:37 +00:00
|
|
|
size: ethFullBlock.size,
|
2022-08-09 05:20:25 +00:00
|
|
|
baseFee: header.BaseFee?.toString()
|
2021-11-17 12:44:02 +00:00
|
|
|
};
|
|
|
|
};
|
2021-12-29 07:51:39 +00:00
|
|
|
|
2023-11-09 13:12:37 +00:00
|
|
|
export const getFullTransaction = (txHash: string, ethFullTransactions: EthFullTransaction[]): Transaction => {
|
|
|
|
const ethFullTransaction = ethFullTransactions.find(ethFullTransaction => ethFullTransaction.ethTransactionCidByTxHash.txHash === txHash);
|
|
|
|
assert(ethFullTransaction);
|
|
|
|
|
2023-11-08 12:02:26 +00:00
|
|
|
let {
|
|
|
|
ethTransactionCidByTxHash: fullTx,
|
|
|
|
data: txData
|
2023-11-09 13:12:37 +00:00
|
|
|
} = ethFullTransaction;
|
2021-12-29 07:51:39 +00:00
|
|
|
|
2023-11-08 12:02:26 +00:00
|
|
|
// Check if txData does not exist when using ipld-eth-client
|
|
|
|
if (!txData) {
|
|
|
|
assert(fullTx.blockByMhKey);
|
2021-12-29 07:51:39 +00:00
|
|
|
|
2023-11-08 12:02:26 +00:00
|
|
|
// Decode the transaction data.
|
|
|
|
// TODO: Get required tx data directly from ipld-eth-server
|
|
|
|
txData = utils.parseTransaction(EthDecoder.decodeData(fullTx.blockByMhKey.data));
|
|
|
|
}
|
2021-12-29 07:51:39 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
hash: txHash,
|
|
|
|
from: fullTx.src,
|
|
|
|
to: fullTx.dst,
|
|
|
|
index: fullTx.index,
|
2022-07-11 05:59:33 +00:00
|
|
|
value: txData.value.toString(),
|
|
|
|
gasLimit: txData.gasLimit.toString(),
|
|
|
|
gasPrice: txData.gasPrice?.toString(),
|
2022-08-09 05:20:25 +00:00
|
|
|
input: txData.data,
|
|
|
|
maxPriorityFeePerGas: txData.maxPriorityFeePerGas?.toString(),
|
|
|
|
maxFeePerGas: txData.maxFeePerGas?.toString()
|
2021-12-29 07:51:39 +00:00
|
|
|
};
|
|
|
|
};
|
2022-08-26 06:32:39 +00:00
|
|
|
|
2022-10-19 09:54:14 +00:00
|
|
|
export const jsonBigIntStringReplacer = (_: string, value: any): any => {
|
2022-08-26 06:32:39 +00:00
|
|
|
if (typeof value === 'bigint') {
|
|
|
|
return value.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
};
|
2022-10-20 13:16:56 +00:00
|
|
|
|
|
|
|
export const getResultEvent = (event: EventInterface): ResultEvent => {
|
|
|
|
const block = event.block;
|
|
|
|
const eventFields = JSONbigNative.parse(event.eventInfo);
|
|
|
|
const { tx, eventSignature } = JSONbigNative.parse(event.extraInfo);
|
|
|
|
|
|
|
|
return {
|
|
|
|
block: {
|
|
|
|
cid: block.cid,
|
|
|
|
hash: block.blockHash,
|
|
|
|
number: block.blockNumber,
|
|
|
|
timestamp: block.blockTimestamp,
|
|
|
|
parentHash: block.parentHash
|
|
|
|
},
|
|
|
|
|
|
|
|
tx: {
|
|
|
|
hash: event.txHash,
|
|
|
|
from: tx.src,
|
|
|
|
to: tx.dst,
|
|
|
|
index: tx.index
|
|
|
|
},
|
|
|
|
|
|
|
|
contract: event.contract,
|
|
|
|
|
|
|
|
eventIndex: event.index,
|
|
|
|
eventSignature,
|
|
|
|
event: {
|
|
|
|
__typename: `${event.eventName}Event`,
|
|
|
|
...eventFields
|
|
|
|
},
|
|
|
|
|
|
|
|
// TODO: Return proof only if requested.
|
|
|
|
proof: JSON.parse(event.proof)
|
|
|
|
};
|
|
|
|
};
|
2022-11-15 09:26:08 +00:00
|
|
|
|
|
|
|
export const setGQLCacheHints = (info: GraphQLResolveInfo, block: BlockHeight, gqlCache: GQLCacheConfig): void => {
|
|
|
|
if (!gqlCache || !gqlCache.enabled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-11-17 06:32:08 +00:00
|
|
|
let maxAge: number;
|
|
|
|
if (_.isEmpty(block)) {
|
|
|
|
assert(gqlCache.maxAge, 'Missing server gqlCache.maxAge');
|
|
|
|
maxAge = gqlCache.maxAge;
|
|
|
|
} else {
|
|
|
|
assert(gqlCache.timeTravelMaxAge, 'Missing server gqlCache.timeTravelMaxAge');
|
|
|
|
maxAge = gqlCache.timeTravelMaxAge;
|
|
|
|
}
|
2022-11-15 09:26:08 +00:00
|
|
|
|
|
|
|
info.cacheControl.setCacheHint({ maxAge });
|
|
|
|
};
|
2023-11-06 09:30:24 +00:00
|
|
|
|
|
|
|
class GraphQLBigIntType extends ApolloBigInt {
|
|
|
|
constructor () {
|
|
|
|
super('bigInt');
|
|
|
|
}
|
|
|
|
|
|
|
|
name = 'BigInt';
|
|
|
|
description = 'BigInt custom scalar type';
|
|
|
|
|
|
|
|
parseLiteral = function (ast: ValueNode) {
|
|
|
|
if (ast.kind === 'IntValue' || ast.kind === 'StringValue') {
|
|
|
|
return global.BigInt(ast.value);
|
|
|
|
} else {
|
|
|
|
throw new TypeError(`BigInt cannot represent value kind: ${ast.kind}`);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
parseValue = function (value: any) {
|
|
|
|
if (value === '') {
|
|
|
|
throw new TypeError('The value cannot be converted from BigInt because it is empty string');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof value !== 'number' && typeof value !== 'bigint' && typeof value !== 'string') {
|
|
|
|
throw new TypeError(
|
|
|
|
`The value ${value} cannot be converted to a BigInt because it is not an integer`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
return global.BigInt(value);
|
|
|
|
} catch {
|
|
|
|
throw new TypeError(
|
|
|
|
`The value ${value} cannot be converted to a BigInt because it is not an integer`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export const GraphQLBigInt = new GraphQLBigIntType();
|
|
|
|
|
|
|
|
export const GraphQLBigDecimal = new GraphQLScalarType({
|
|
|
|
name: 'BigDecimal',
|
|
|
|
description: 'BigDecimal custom scalar type',
|
|
|
|
parseValue (value) {
|
|
|
|
// value from the client
|
|
|
|
return new Decimal(value);
|
|
|
|
},
|
|
|
|
serialize (value: Decimal) {
|
|
|
|
// value sent to the client
|
|
|
|
return value.toFixed();
|
|
|
|
}
|
|
|
|
});
|