diff --git a/package.json b/package.json index 9f4b753..dc2c53f 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,11 @@ }, "prettier": "@snapshot-labs/prettier-config", "dependencies": { + "@ethersproject/abi": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/providers": "^5.7.2", + "@ethersproject/strings": "^5.7.0", "@graphql-tools/schema": "^8.5.1", "bluebird": "^3.7.2", "connection-string": "^4.3.5", diff --git a/src/checkpoint.ts b/src/checkpoint.ts index cc94824..3e14621 100644 --- a/src/checkpoint.ts +++ b/src/checkpoint.ts @@ -6,7 +6,7 @@ import { Pool as PgPool } from 'pg'; import getGraphQL, { CheckpointsGraphQLObject, MetadataGraphQLObject } from './graphql'; import { GqlEntityController } from './graphql/controller'; import { CheckpointRecord, CheckpointsStore, MetadataId } from './stores/checkpoints'; -import { BaseProvider, StarknetProvider, BlockNotFoundError } from './providers'; +import { BaseIndexer, BlockNotFoundError } from './providers'; import { createLogger, Logger, LogLevel } from './utils/logger'; import { getConfigChecksum, getContractsFromConfig } from './utils/checkpoint'; import { extendSchema } from './utils/graphql'; @@ -15,27 +15,23 @@ import { AsyncMySqlPool, createMySqlPool } from './mysql'; import { createPgPool } from './pg'; import { checkpointConfigSchema } from './schemas'; import { register } from './register'; -import { - ContractSourceConfig, - CheckpointConfig, - CheckpointOptions, - CheckpointWriters, - TemplateSource -} from './types'; - -const BLOCK_PRELOAD = 1000; +import { ContractSourceConfig, CheckpointConfig, CheckpointOptions, TemplateSource } from './types'; + +const BLOCK_PRELOAD_START_RANGE = 1000; +const BLOCK_RELOAD_MIN_RANGE = 10; +const BLOCK_PRELOAD_STEP = 100; +const BLOCK_PRELOAD_TARGET = 10; const BLOCK_PRELOAD_OFFSET = 50; const DEFAULT_FETCH_INTERVAL = 2000; export default class Checkpoint { public config: CheckpointConfig; - public writer: CheckpointWriters; public opts?: CheckpointOptions; public schema: string; private readonly entityController: GqlEntityController; private readonly log: Logger; - private readonly networkProvider: BaseProvider; + private readonly indexer: BaseIndexer; private dbConnection: string; private knex: Knex; @@ -43,13 +39,14 @@ export default class Checkpoint { private pgPool?: PgPool; private checkpointsStore?: CheckpointsStore; private activeTemplates: TemplateSource[] = []; + private preloadStep: number = BLOCK_PRELOAD_START_RANGE; private preloadedBlocks: number[] = []; private preloadEndBlock = 0; private cpBlocksCache: number[] | null; constructor( config: CheckpointConfig, - writer: CheckpointWriters, + indexer: BaseIndexer, schema: string, opts?: CheckpointOptions ) { @@ -59,12 +56,9 @@ export default class Checkpoint { } this.config = config; - this.writer = writer; this.opts = opts; this.schema = extendSchema(schema); - this.validateConfig(); - this.entityController = new GqlEntityController(this.schema, config); this.log = createLogger({ @@ -79,8 +73,14 @@ export default class Checkpoint { : {}) }); - const NetworkProvider = opts?.NetworkProvider || StarknetProvider; - this.networkProvider = new NetworkProvider({ instance: this, log: this.log, abis: opts?.abis }); + this.indexer = indexer; + this.indexer.init({ + instance: this, + log: this.log, + abis: opts?.abis + }); + + this.validateConfig(); this.cpBlocksCache = []; @@ -139,7 +139,7 @@ export default class Checkpoint { } public get sourceContracts() { - return this.networkProvider.formatAddresses(getContractsFromConfig(this.config)); + return this.indexer.getProvider().formatAddresses(getContractsFromConfig(this.config)); } public getCurrentSources(blockNumber: number) { @@ -159,7 +159,7 @@ export default class Checkpoint { this.log.debug('starting'); await this.validateStore(); - await this.networkProvider.init(); + await this.indexer.getProvider().init(); const templateSources = await this.store.getTemplateSources(); await Promise.all( @@ -177,7 +177,7 @@ export default class Checkpoint { const blockNum = await this.getStartBlockNum(); this.preloadEndBlock = - (await this.networkProvider.getLatestBlockNumber()) - BLOCK_PRELOAD_OFFSET; + (await this.indexer.getProvider().getLatestBlockNumber()) - BLOCK_PRELOAD_OFFSET; return await this.next(blockNum); } @@ -332,8 +332,14 @@ export default class Checkpoint { let currentBlock = blockNum; while (currentBlock <= this.preloadEndBlock) { - const endBlock = Math.min(currentBlock + BLOCK_PRELOAD, this.preloadEndBlock); - const checkpoints = await this.networkProvider.getCheckpointsRange(currentBlock, endBlock); + const endBlock = Math.min(currentBlock + this.preloadStep, this.preloadEndBlock); + const checkpoints = await this.indexer + .getProvider() + .getCheckpointsRange(currentBlock, endBlock); + + const increase = + checkpoints.length > BLOCK_PRELOAD_TARGET ? -BLOCK_PRELOAD_STEP : +BLOCK_PRELOAD_STEP; + this.preloadStep = Math.max(BLOCK_RELOAD_MIN_RANGE, this.preloadStep + increase); if (checkpoints.length > 0) { this.preloadedBlocks = checkpoints.map(cp => cp.blockNumber).sort(); @@ -363,7 +369,7 @@ export default class Checkpoint { try { const initialSources = this.getCurrentSources(blockNum); - const nextBlockNumber = await this.networkProvider.processBlock(blockNum); + const nextBlockNumber = await this.indexer.getProvider().processBlock(blockNum); const sources = this.getCurrentSources(nextBlockNumber); if (initialSources.length !== sources.length) { @@ -375,7 +381,7 @@ export default class Checkpoint { if (err instanceof BlockNotFoundError) { if (this.config.optimistic_indexing) { try { - await this.networkProvider.processPool(blockNum); + await this.indexer.getProvider().processPool(blockNum); } catch (err) { this.log.error({ blockNumber: blockNum, err }, 'error occurred during pool processing'); } @@ -480,7 +486,9 @@ export default class Checkpoint { ]; const missingAbis = usedAbis.filter(abi => !this.opts?.abis?.[abi]); - const missingWriters = usedWriters.filter(writer => !this.writer[writer.fn]); + const missingWriters = usedWriters.filter( + writer => !this.indexer.getHandlers().includes(writer.fn) + ); if (missingAbis.length > 0) { throw new Error( @@ -498,7 +506,7 @@ export default class Checkpoint { } private async validateStore() { - const networkIdentifier = await this.networkProvider.getNetworkIdentifier(); + const networkIdentifier = await this.indexer.getProvider().getNetworkIdentifier(); const configChecksum = getConfigChecksum(this.config); const storedNetworkIdentifier = await this.store.getMetadata(MetadataId.NetworkIdentifier); diff --git a/src/index.ts b/src/index.ts index 99b34df..5770712 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ export { LogLevel } from './utils/logger'; export { AsyncMySqlPool } from './mysql'; export { createGetLoader } from './graphql'; export { Model } from './orm'; +export * from './providers'; export * from './types'; export default Checkpoint; diff --git a/src/providers/base.ts b/src/providers/base.ts index 7cd4cd7..aa15f17 100644 --- a/src/providers/base.ts +++ b/src/providers/base.ts @@ -3,10 +3,9 @@ import Checkpoint from '../checkpoint'; import { CheckpointRecord } from '../stores/checkpoints'; import { Logger } from '../utils/logger'; import { AsyncMySqlPool } from '../mysql'; -import { CheckpointConfig, CheckpointWriters, ContractSourceConfig } from '../types'; +import { CheckpointConfig, ContractSourceConfig } from '../types'; -type Instance = { - writer: CheckpointWriters; +export type Instance = { config: CheckpointConfig; getCurrentSources(blockNumber: number): ContractSourceConfig[]; setLastIndexedBlock(blockNum: number); @@ -76,7 +75,28 @@ export class BaseProvider { async getCheckpointsRange(fromBlock: number, toBlock: number): Promise { throw new Error( - `getEventsRange method was not defined when fetching events from ${fromBlock} to ${toBlock}` + `getCheckpointsRange method was not defined when fetching events from ${fromBlock} to ${toBlock}` ); } } + +export class BaseIndexer { + protected provider?: BaseProvider; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + init({ instance, log, abis }: { instance: Instance; log: Logger; abis?: Record }) { + throw new Error('init method was not defined'); + } + + public getProvider() { + if (!this.provider) { + throw new Error('Provider not initialized'); + } + + return this.provider; + } + + public getHandlers(): string[] { + throw new Error('getHandlers method was not defined'); + } +} diff --git a/src/providers/evm/index.ts b/src/providers/evm/index.ts new file mode 100644 index 0000000..e5c759a --- /dev/null +++ b/src/providers/evm/index.ts @@ -0,0 +1,3 @@ +export { EvmProvider } from './provider'; +export { EvmIndexer } from './indexer'; +export * from './types'; diff --git a/src/providers/evm/indexer.ts b/src/providers/evm/indexer.ts new file mode 100644 index 0000000..258e1f9 --- /dev/null +++ b/src/providers/evm/indexer.ts @@ -0,0 +1,21 @@ +import { Logger } from '../../utils/logger'; +import { Instance, BaseIndexer } from '../base'; +import { EvmProvider } from './provider'; +import { Writer } from './types'; + +export class EvmIndexer extends BaseIndexer { + private writers: Record; + + constructor(writers: Record) { + super(); + this.writers = writers; + } + + init({ instance, log, abis }: { instance: Instance; log: Logger; abis?: Record }) { + this.provider = new EvmProvider({ instance, log, abis, writers: this.writers }); + } + + public getHandlers(): string[] { + return Object.keys(this.writers); + } +} diff --git a/src/providers/evm/provider.ts b/src/providers/evm/provider.ts new file mode 100644 index 0000000..8d15026 --- /dev/null +++ b/src/providers/evm/provider.ts @@ -0,0 +1,291 @@ +import { BaseProvider, BlockNotFoundError } from '../base'; +import { getAddress } from '@ethersproject/address'; +import { Log, Provider, StaticJsonRpcProvider } from '@ethersproject/providers'; +import { Interface, LogDescription } from '@ethersproject/abi'; +import { keccak256 } from '@ethersproject/keccak256'; +import { toUtf8Bytes } from '@ethersproject/strings'; +import { CheckpointRecord } from '../../stores/checkpoints'; +import { Writer } from './types'; +import { ContractSourceConfig } from '../../types'; + +type BlockWithTransactions = Awaited>; +type Transaction = BlockWithTransactions['transactions'][number]; +type EventsMap = Record; + +const MAX_BLOCKS_PER_REQUEST = 10000; +export class EvmProvider extends BaseProvider { + private readonly provider: Provider; + private readonly writers: Record; + private processedPoolTransactions = new Set(); + private startupLatestBlockNumber: number | undefined; + + constructor({ + instance, + log, + abis, + writers + }: ConstructorParameters[0] & { writers: Record }) { + super({ instance, log, abis }); + + this.provider = new StaticJsonRpcProvider(this.instance.config.network_node_url); + this.writers = writers; + } + + formatAddresses(addresses: string[]): string[] { + return addresses.map(address => getAddress(address)); + } + + public async init() { + this.startupLatestBlockNumber = await this.getLatestBlockNumber(); + } + + async getNetworkIdentifier(): Promise { + const result = await this.provider.getNetwork(); + return `evm_${result.chainId}`; + } + + async getLatestBlockNumber(): Promise { + return this.provider.getBlockNumber(); + } + + async processBlock(blockNum: number) { + let block: BlockWithTransactions | null; + let eventsMap: EventsMap; + try { + [block, eventsMap] = await Promise.all([ + this.provider.getBlockWithTransactions(blockNum), + this.getEvents(blockNum) + ]); + + if (block === null) { + this.log.info({ blockNumber: blockNum }, 'block not found'); + throw new BlockNotFoundError(); + } + } catch (e) { + this.log.error({ blockNumber: blockNum, err: e }, 'getting block failed... retrying'); + throw e; + } + + await this.handleBlock(block, eventsMap); + + await this.instance.setLastIndexedBlock(block.number); + + return blockNum + 1; + } + + async processPool(blockNumber: number) { + const [block, eventsMap] = await Promise.all([ + this.provider.getBlockWithTransactions('latest'), + this.getEvents('latest') + ]); + + await this.handlePool(block, eventsMap, blockNumber); + } + + private async handleBlock(block: BlockWithTransactions, eventsMap: EventsMap) { + this.log.info({ blockNumber: block.number }, 'handling block'); + + const txsToCheck = block.transactions.filter( + tx => !this.processedPoolTransactions.has(tx.hash) + ); + + for (const [i, tx] of txsToCheck.entries()) { + await this.handleTx(block, block.number, i, tx, tx.hash ? eventsMap[tx.hash] || [] : []); + } + + this.processedPoolTransactions.clear(); + + this.log.debug({ blockNumber: block.number }, 'handling block done'); + } + + private async handlePool( + block: BlockWithTransactions, + eventsMap: EventsMap, + blockNumber: number + ) { + this.log.info('handling pool'); + + const txsToCheck = block.transactions.filter( + tx => !this.processedPoolTransactions.has(tx.hash) + ); + + for (const [i, tx] of txsToCheck.entries()) { + await this.handleTx(null, blockNumber, i, tx, tx.hash ? eventsMap[tx.hash] || [] : []); + + this.processedPoolTransactions.add(tx.hash); + } + + this.log.info('handling pool done'); + } + + private async handleTx( + block: BlockWithTransactions | null, + blockNumber: number, + txIndex: number, + tx: Transaction, + logs: Log[] + ) { + this.log.debug({ txIndex }, 'handling transaction'); + + const writerParams = await this.instance.getWriterParams(); + + if (this.instance.config.tx_fn) { + await this.writers[this.instance.config.tx_fn]({ + blockNumber, + block, + tx, + ...writerParams + }); + } + + if (this.instance.config.global_events) { + const globalEventHandlers = this.instance.config.global_events.reduce((handlers, event) => { + handlers[keccak256(toUtf8Bytes(event.name))] = { + name: event.name, + fn: event.fn + }; + return handlers; + }, {}); + + for (const [eventIndex, event] of logs.entries()) { + const handler = globalEventHandlers[event.topics[0]]; + if (!handler) continue; + + this.log.info( + { contract: event.address, event: handler.name, handlerFn: handler.fn }, + 'found contract event' + ); + + await this.writers[handler.fn]({ + block, + blockNumber, + tx, + rawEvent: event, + eventIndex, + ...writerParams + }); + } + } + + let lastSources = this.instance.getCurrentSources(blockNumber); + const sourcesQueue = [...lastSources]; + + let source: ContractSourceConfig | undefined; + while ((source = sourcesQueue.shift())) { + const contract = getAddress(source.contract); + + for (const [eventIndex, log] of logs.entries()) { + if (contract === getAddress(log.address)) { + for (const sourceEvent of source.events) { + const targetTopic = keccak256(toUtf8Bytes(sourceEvent.name)); + + if (targetTopic === log.topics[0]) { + this.log.info( + { contract: source.contract, event: sourceEvent.name, handlerFn: sourceEvent.fn }, + 'found contract event' + ); + + let parsedEvent: LogDescription | undefined; + if (source.abi && this.abis?.[source.abi]) { + const iface = new Interface(this.abis[source.abi]); + try { + parsedEvent = iface.parseLog(log); + } catch (err) { + this.log.warn( + { contract: source.contract, txType: tx.type, handlerFn: source.deploy_fn }, + 'failed to parse event' + ); + } + } + + await this.writers[sourceEvent.fn]({ + source, + block, + blockNumber, + tx, + rawEvent: log, + event: parsedEvent, + eventIndex, + ...writerParams + }); + } + } + } + } + + const nextSources = this.instance.getCurrentSources(blockNumber); + const newSources = nextSources.filter( + nextSource => !lastSources.find(lastSource => lastSource.contract === nextSource.contract) + ); + + sourcesQueue.push(...newSources); + lastSources = nextSources; + } + + this.log.debug({ txIndex }, 'handling transaction done'); + } + + private async getEvents(blockNumber: number | 'latest'): Promise { + const events = await this.provider.getLogs({ + fromBlock: blockNumber, + toBlock: blockNumber + }); + + return events.reduce((acc, event) => { + if (!acc[event.transactionHash]) acc[event.transactionHash] = []; + + acc[event.transactionHash].push(event); + + return acc; + }, {}); + } + + async getLogs(fromBlock: number, toBlock: number, address: string) { + const result = [] as Log[]; + + let currentFrom = fromBlock; + let currentTo = Math.min(toBlock, currentFrom + MAX_BLOCKS_PER_REQUEST); + while (true) { + try { + const logs = await this.provider.getLogs({ + fromBlock: currentFrom, + toBlock: currentTo, + address + }); + + result.push(...logs); + + if (currentTo === toBlock) break; + currentFrom = currentTo + 1; + currentTo = Math.min(toBlock, currentFrom + MAX_BLOCKS_PER_REQUEST); + } catch (e: any) { + if (!e.body) throw e; + + const body = JSON.parse(e.body); + if (body.error.code !== -32005) throw e; + + currentFrom = parseInt(body.error.data.from, 16); + currentTo = Math.min( + parseInt(body.error.data.to, 16), + currentFrom + MAX_BLOCKS_PER_REQUEST + ); + } + } + + return result.map(log => ({ + blockNumber: log.blockNumber, + contractAddress: log.address + })); + } + + async getCheckpointsRange(fromBlock: number, toBlock: number): Promise { + const events: CheckpointRecord[] = []; + + for (const source of this.instance.getCurrentSources(fromBlock)) { + const addressEvents = await this.getLogs(fromBlock, toBlock, source.contract); + events.push(...addressEvents); + } + + return events; + } +} diff --git a/src/providers/evm/types.ts b/src/providers/evm/types.ts new file mode 100644 index 0000000..e0b05f2 --- /dev/null +++ b/src/providers/evm/types.ts @@ -0,0 +1,15 @@ +import { Provider, Log } from '@ethersproject/providers'; +import { LogDescription } from '@ethersproject/abi'; +import { BaseWriterParams } from '../../types'; + +type BlockWithTransactions = Awaited>; +type Transaction = BlockWithTransactions['transactions'][number]; + +export type Writer = ( + args: { + tx: Transaction; + block: BlockWithTransactions | null; + rawEvent?: Log; + event?: LogDescription; + } & BaseWriterParams +) => Promise; diff --git a/src/providers/index.ts b/src/providers/index.ts index ce81b03..d496696 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -1,2 +1,3 @@ export * from './base'; -export * from './starknet'; +export * as starknet from './starknet'; +export * as evm from './evm'; diff --git a/src/providers/starknet/index.ts b/src/providers/starknet/index.ts index 5104c38..5f68109 100644 --- a/src/providers/starknet/index.ts +++ b/src/providers/starknet/index.ts @@ -1,375 +1,3 @@ -import { RpcProvider, hash, validateAndParseAddress } from 'starknet'; -import { BaseProvider, BlockNotFoundError } from '../base'; -import { parseEvent } from './utils'; -import { CheckpointRecord } from '../../stores/checkpoints'; -import { isFullBlock, isDeployTransaction } from '../../types'; -import { - Block, - FullBlock, - Transaction, - PendingTransaction, - Event, - EventsMap, - ParsedEvent, - ContractSourceConfig -} from '../../types'; - -export class StarknetProvider extends BaseProvider { - private readonly provider: RpcProvider; - private seenPoolTransactions = new Set(); - private processedTransactions = new Set(); - private startupLatestBlockNumber: number | undefined; - - constructor({ instance, log, abis }: ConstructorParameters[0]) { - super({ instance, log, abis }); - - this.provider = new RpcProvider({ - nodeUrl: this.instance.config.network_node_url - }); - } - - public async init() { - this.startupLatestBlockNumber = await this.getLatestBlockNumber(); - } - - formatAddresses(addresses: string[]): string[] { - return addresses.map(address => validateAndParseAddress(address)); - } - - async getNetworkIdentifier(): Promise { - const result = await this.provider.getChainId(); - return `starknet_${result}`; - } - - async getLatestBlockNumber(): Promise { - return this.provider.getBlockNumber(); - } - - async processBlock(blockNum: number) { - let block: Block; - let blockEvents: EventsMap; - try { - [block, blockEvents] = await Promise.all([ - this.provider.getBlockWithTxs(blockNum), - this.getEvents(blockNum) - ]); - - if (!isFullBlock(block) || block.block_number !== blockNum) { - this.log.error({ blockNumber: blockNum }, 'invalid block'); - throw new Error('invalid block'); - } - } catch (e) { - if ((e as Error).message.includes('Block not found') || e instanceof BlockNotFoundError) { - this.log.info({ blockNumber: blockNum }, 'block not found'); - throw new BlockNotFoundError(); - } - - this.log.error({ blockNumber: blockNum, err: e }, 'getting block failed... retrying'); - throw e; - } - - await this.handleBlock(block, blockEvents); - - await this.instance.setLastIndexedBlock(block.block_number); - - return blockNum + 1; - } - - async processPool(blockNumber: number) { - const block = await this.provider.getBlockWithTxs('pending'); - const receipts = await Promise.all( - block.transactions.map(async tx => { - if (!tx.transaction_hash || this.seenPoolTransactions.has(tx.transaction_hash)) { - return null; - } - - try { - return await this.provider.getTransactionReceipt(tx.transaction_hash); - } catch (err) { - this.log.warn( - { transactionHash: tx.transaction_hash, err }, - 'getting transaction receipt failed' - ); - return null; - } - }) - ); - - const txsWithReceipts = block.transactions.filter((_, index) => receipts[index] !== null); - const eventsMap = receipts.reduce((acc, receipt) => { - if (receipt === null) return acc; - - acc[receipt.transaction_hash] = receipt.events; - return acc; - }, {}); - - await this.handlePool(txsWithReceipts, eventsMap, blockNumber); - } - - private async handleBlock(block: FullBlock, eventsMap: EventsMap) { - this.log.info({ blockNumber: block.block_number }, 'handling block'); - - const txsToCheck = block.transactions.filter( - tx => !this.seenPoolTransactions.has(tx.transaction_hash) - ); - - for (const [i, tx] of txsToCheck.entries()) { - await this.handleTx( - block, - block.block_number, - i, - tx, - tx.transaction_hash ? eventsMap[tx.transaction_hash] || [] : [] - ); - } - - this.seenPoolTransactions.clear(); - - this.log.debug({ blockNumber: block.block_number }, 'handling block done'); - } - - private async handlePool(txs: PendingTransaction[], eventsMap: EventsMap, blockNumber: number) { - this.log.info('handling pool'); - - for (const [i, tx] of txs.entries()) { - await this.handleTx( - null, - blockNumber, - i, - tx, - tx.transaction_hash ? eventsMap[tx.transaction_hash] || [] : [] - ); - - this.seenPoolTransactions.add(tx.transaction_hash); - } - - this.log.info('handling pool done'); - } - - private async handleTx( - block: FullBlock | null, - blockNumber: number, - txIndex: number, - tx: Transaction, - events: Event[] - ) { - this.log.debug({ txIndex }, 'handling transaction'); - - if (this.processedTransactions.has(tx.transaction_hash)) { - this.log.warn({ hash: tx.transaction_hash }, 'transaction already processed'); - return; - } - - let wasTransactionProcessed = false; - const writerParams = await this.instance.getWriterParams(); - - if (this.instance.config.tx_fn) { - await this.instance.writer[this.instance.config.tx_fn]({ - blockNumber, - block, - tx, - ...writerParams - }); - - wasTransactionProcessed = true; - } - - if (this.instance.config.global_events) { - const globalEventHandlers = this.instance.config.global_events.reduce((handlers, event) => { - handlers[`0x${hash.starknetKeccak(event.name).toString(16)}`] = { - name: event.name, - fn: event.fn - }; - return handlers; - }, {}); - - for (const [eventIndex, event] of events.entries()) { - const handler = globalEventHandlers[event.keys[0]]; - if (!handler) continue; - - this.log.info( - { contract: event.from_address, event: handler.name, handlerFn: handler.fn }, - 'found contract event' - ); - - await this.instance.writer[handler.fn]({ - block, - blockNumber, - tx, - rawEvent: event, - eventIndex, - ...writerParams - }); - - wasTransactionProcessed = true; - } - } - - let lastSources = this.instance.getCurrentSources(blockNumber); - const sourcesQueue = [...lastSources]; - - let source: ContractSourceConfig | undefined; - while ((source = sourcesQueue.shift())) { - let foundContractData = false; - const contract = validateAndParseAddress(source.contract); - - if ( - isDeployTransaction(tx) && - source.deploy_fn && - contract === validateAndParseAddress(tx.contract_address) - ) { - foundContractData = true; - this.log.info( - { contract: source.contract, txType: tx.type, handlerFn: source.deploy_fn }, - 'found deployment transaction' - ); - - await this.instance.writer[source.deploy_fn]({ - source, - block, - blockNumber, - tx, - ...writerParams - }); - - wasTransactionProcessed = true; - } - - for (const [eventIndex, event] of events.entries()) { - if (contract === validateAndParseAddress(event.from_address)) { - for (const sourceEvent of source.events) { - if (`0x${hash.starknetKeccak(sourceEvent.name).toString(16)}` === event.keys[0]) { - foundContractData = true; - this.log.info( - { contract: source.contract, event: sourceEvent.name, handlerFn: sourceEvent.fn }, - 'found contract event' - ); - - let parsedEvent: ParsedEvent | undefined; - if (source.abi && this.abis?.[source.abi]) { - try { - parsedEvent = parseEvent(this.abis[source.abi], event); - } catch (err) { - this.log.warn( - { contract: source.contract, txType: tx.type, handlerFn: source.deploy_fn }, - 'failed to parse event' - ); - } - } - - await this.instance.writer[sourceEvent.fn]({ - source, - block, - blockNumber, - tx, - rawEvent: event, - event: parsedEvent, - eventIndex, - ...writerParams - }); - - wasTransactionProcessed = true; - } - } - } - } - - if (wasTransactionProcessed) { - this.processedTransactions.add(tx.transaction_hash); - } - - if (foundContractData) { - await this.instance.insertCheckpoints([ - { blockNumber, contractAddress: validateAndParseAddress(source.contract) } - ]); - } - - const nextSources = this.instance.getCurrentSources(blockNumber); - const newSources = nextSources.filter( - nextSource => !lastSources.find(lastSource => lastSource.contract === nextSource.contract) - ); - - sourcesQueue.push(...newSources); - lastSources = nextSources; - } - - this.log.debug({ txIndex }, 'handling transaction done'); - } - - private async getEvents(blockNumber: number): Promise { - const events: Event[] = []; - - let continuationToken: string | undefined; - do { - const result = await this.provider.getEvents({ - from_block: { block_number: blockNumber }, - to_block: { block_number: blockNumber }, - chunk_size: 1000, - continuation_token: continuationToken - }); - - events.push(...result.events); - - continuationToken = result.continuation_token; - } while (continuationToken); - - if ( - events.length === 0 && - this.startupLatestBlockNumber && - blockNumber > this.startupLatestBlockNumber - ) { - throw new BlockNotFoundError(); - } - - return events.reduce((acc, event) => { - if (!acc[event.transaction_hash]) acc[event.transaction_hash] = []; - - acc[event.transaction_hash].push(event); - - return acc; - }, {}); - } - - async getCheckpointsRangeForAddress( - fromBlock: number, - toBlock: number, - address: string - ): Promise { - const events: Event[] = []; - - let continuationToken: string | undefined; - do { - const result = await this.provider.getEvents({ - from_block: { block_number: fromBlock }, - to_block: { block_number: toBlock }, - address: address, - chunk_size: 1000, - continuation_token: continuationToken - }); - - events.push(...result.events); - - continuationToken = result.continuation_token; - } while (continuationToken); - - return events.map(event => ({ - blockNumber: event.block_number, - contractAddress: validateAndParseAddress(event.from_address) - })); - } - - async getCheckpointsRange(fromBlock: number, toBlock: number): Promise { - const events: CheckpointRecord[] = []; - - for (const source of this.instance.getCurrentSources(fromBlock)) { - const addressEvents = await this.getCheckpointsRangeForAddress( - fromBlock, - toBlock, - source.contract - ); - events.push(...addressEvents); - } - - return events; - } -} +export { StarknetProvider } from './provider'; +export { StarknetIndexer } from './indexer'; +export * from './types'; diff --git a/src/providers/starknet/indexer.ts b/src/providers/starknet/indexer.ts new file mode 100644 index 0000000..c3ac265 --- /dev/null +++ b/src/providers/starknet/indexer.ts @@ -0,0 +1,21 @@ +import { Logger } from '../../utils/logger'; +import { Instance, BaseIndexer } from '../base'; +import { StarknetProvider } from '.'; +import { Writer } from './types'; + +export class StarknetIndexer extends BaseIndexer { + private writers: Record; + + constructor(writers: Record) { + super(); + this.writers = writers; + } + + init({ instance, log, abis }: { instance: Instance; log: Logger; abis?: Record }) { + this.provider = new StarknetProvider({ instance, log, abis, writers: this.writers }); + } + + public getHandlers(): string[] { + return Object.keys(this.writers); + } +} diff --git a/src/providers/starknet/provider.ts b/src/providers/starknet/provider.ts new file mode 100644 index 0000000..0615208 --- /dev/null +++ b/src/providers/starknet/provider.ts @@ -0,0 +1,385 @@ +import { RpcProvider, hash, validateAndParseAddress } from 'starknet'; +import { BaseProvider, BlockNotFoundError } from '../base'; +import { parseEvent } from './utils'; +import { CheckpointRecord } from '../../stores/checkpoints'; +import { + Block, + FullBlock, + Transaction, + PendingTransaction, + Event, + EventsMap, + ParsedEvent, + isFullBlock, + isDeployTransaction, + Writer +} from './types'; +import { ContractSourceConfig } from '../../types'; + +export class StarknetProvider extends BaseProvider { + private readonly provider: RpcProvider; + private readonly writers: Record; + private seenPoolTransactions = new Set(); + private processedTransactions = new Set(); + private startupLatestBlockNumber: number | undefined; + + constructor({ + instance, + log, + abis, + writers + }: ConstructorParameters[0] & { + writers: Record; + }) { + super({ instance, log, abis }); + + this.provider = new RpcProvider({ + nodeUrl: this.instance.config.network_node_url + }); + this.writers = writers; + } + + public async init() { + this.startupLatestBlockNumber = await this.getLatestBlockNumber(); + } + + formatAddresses(addresses: string[]): string[] { + return addresses.map(address => validateAndParseAddress(address)); + } + + async getNetworkIdentifier(): Promise { + const result = await this.provider.getChainId(); + return `starknet_${result}`; + } + + async getLatestBlockNumber(): Promise { + return this.provider.getBlockNumber(); + } + + async processBlock(blockNum: number) { + let block: Block; + let blockEvents: EventsMap; + try { + [block, blockEvents] = await Promise.all([ + this.provider.getBlockWithTxs(blockNum), + this.getEvents(blockNum) + ]); + + if (!isFullBlock(block) || block.block_number !== blockNum) { + this.log.error({ blockNumber: blockNum }, 'invalid block'); + throw new Error('invalid block'); + } + } catch (e) { + if ((e as Error).message.includes('Block not found') || e instanceof BlockNotFoundError) { + this.log.info({ blockNumber: blockNum }, 'block not found'); + throw new BlockNotFoundError(); + } + + this.log.error({ blockNumber: blockNum, err: e }, 'getting block failed... retrying'); + throw e; + } + + await this.handleBlock(block, blockEvents); + + await this.instance.setLastIndexedBlock(block.block_number); + + return blockNum + 1; + } + + async processPool(blockNumber: number) { + const block = await this.provider.getBlockWithTxs('pending'); + const receipts = await Promise.all( + block.transactions.map(async tx => { + if (!tx.transaction_hash || this.seenPoolTransactions.has(tx.transaction_hash)) { + return null; + } + + try { + return await this.provider.getTransactionReceipt(tx.transaction_hash); + } catch (err) { + this.log.warn( + { transactionHash: tx.transaction_hash, err }, + 'getting transaction receipt failed' + ); + return null; + } + }) + ); + + const txsWithReceipts = block.transactions.filter((_, index) => receipts[index] !== null); + const eventsMap = receipts.reduce((acc, receipt) => { + if (receipt === null) return acc; + + acc[receipt.transaction_hash] = receipt.events; + return acc; + }, {}); + + await this.handlePool(txsWithReceipts, eventsMap, blockNumber); + } + + private async handleBlock(block: FullBlock, eventsMap: EventsMap) { + this.log.info({ blockNumber: block.block_number }, 'handling block'); + + const txsToCheck = block.transactions.filter( + tx => !this.seenPoolTransactions.has(tx.transaction_hash) + ); + + for (const [i, tx] of txsToCheck.entries()) { + await this.handleTx( + block, + block.block_number, + i, + tx, + tx.transaction_hash ? eventsMap[tx.transaction_hash] || [] : [] + ); + } + + this.seenPoolTransactions.clear(); + + this.log.debug({ blockNumber: block.block_number }, 'handling block done'); + } + + private async handlePool(txs: PendingTransaction[], eventsMap: EventsMap, blockNumber: number) { + this.log.info('handling pool'); + + for (const [i, tx] of txs.entries()) { + await this.handleTx( + null, + blockNumber, + i, + tx, + tx.transaction_hash ? eventsMap[tx.transaction_hash] || [] : [] + ); + + this.seenPoolTransactions.add(tx.transaction_hash); + } + + this.log.info('handling pool done'); + } + + private async handleTx( + block: FullBlock | null, + blockNumber: number, + txIndex: number, + tx: Transaction, + events: Event[] + ) { + this.log.debug({ txIndex }, 'handling transaction'); + + if (this.processedTransactions.has(tx.transaction_hash)) { + this.log.warn({ hash: tx.transaction_hash }, 'transaction already processed'); + return; + } + + let wasTransactionProcessed = false; + const writerParams = await this.instance.getWriterParams(); + + if (this.instance.config.tx_fn) { + await this.writers[this.instance.config.tx_fn]({ + blockNumber, + block, + tx, + ...writerParams + }); + + wasTransactionProcessed = true; + } + + if (this.instance.config.global_events) { + const globalEventHandlers = this.instance.config.global_events.reduce((handlers, event) => { + handlers[`0x${hash.starknetKeccak(event.name).toString(16)}`] = { + name: event.name, + fn: event.fn + }; + return handlers; + }, {}); + + for (const [eventIndex, event] of events.entries()) { + const handler = globalEventHandlers[event.keys[0]]; + if (!handler) continue; + + this.log.info( + { contract: event.from_address, event: handler.name, handlerFn: handler.fn }, + 'found contract event' + ); + + await this.writers[handler.fn]({ + block, + blockNumber, + tx, + rawEvent: event, + eventIndex, + ...writerParams + }); + + wasTransactionProcessed = true; + } + } + + let lastSources = this.instance.getCurrentSources(blockNumber); + const sourcesQueue = [...lastSources]; + + let source: ContractSourceConfig | undefined; + while ((source = sourcesQueue.shift())) { + let foundContractData = false; + const contract = validateAndParseAddress(source.contract); + + if ( + isDeployTransaction(tx) && + source.deploy_fn && + contract === validateAndParseAddress(tx.contract_address) + ) { + this.log.info( + { contract: source.contract, txType: tx.type, handlerFn: source.deploy_fn }, + 'found deployment transaction' + ); + + await this.writers[source.deploy_fn]({ + source, + block, + blockNumber, + tx, + ...writerParams + }); + + wasTransactionProcessed = true; + } + + for (const [eventIndex, event] of events.entries()) { + if (contract === validateAndParseAddress(event.from_address)) { + for (const sourceEvent of source.events) { + if (`0x${hash.starknetKeccak(sourceEvent.name).toString(16)}` === event.keys[0]) { + foundContractData = true; + this.log.info( + { contract: source.contract, event: sourceEvent.name, handlerFn: sourceEvent.fn }, + 'found contract event' + ); + + let parsedEvent: ParsedEvent | undefined; + if (source.abi && this.abis?.[source.abi]) { + try { + parsedEvent = parseEvent(this.abis[source.abi], event); + } catch (err) { + this.log.warn( + { contract: source.contract, txType: tx.type, handlerFn: source.deploy_fn }, + 'failed to parse event' + ); + } + } + + await this.writers[sourceEvent.fn]({ + source, + block, + blockNumber, + tx, + rawEvent: event, + event: parsedEvent, + eventIndex, + ...writerParams + }); + + wasTransactionProcessed = true; + } + } + } + } + + if (wasTransactionProcessed) { + this.processedTransactions.add(tx.transaction_hash); + } + + if (foundContractData) { + await this.instance.insertCheckpoints([ + { blockNumber, contractAddress: validateAndParseAddress(source.contract) } + ]); + } + + const nextSources = this.instance.getCurrentSources(blockNumber); + const newSources = nextSources.filter( + nextSource => !lastSources.find(lastSource => lastSource.contract === nextSource.contract) + ); + + sourcesQueue.push(...newSources); + lastSources = nextSources; + } + + this.log.debug({ txIndex }, 'handling transaction done'); + } + + private async getEvents(blockNumber: number): Promise { + const events: Event[] = []; + + let continuationToken: string | undefined; + do { + const result = await this.provider.getEvents({ + from_block: { block_number: blockNumber }, + to_block: { block_number: blockNumber }, + chunk_size: 1000, + continuation_token: continuationToken + }); + + events.push(...result.events); + + continuationToken = result.continuation_token; + } while (continuationToken); + + if ( + events.length === 0 && + this.startupLatestBlockNumber && + blockNumber > this.startupLatestBlockNumber + ) { + throw new BlockNotFoundError(); + } + + return events.reduce((acc, event) => { + if (!acc[event.transaction_hash]) acc[event.transaction_hash] = []; + + acc[event.transaction_hash].push(event); + + return acc; + }, {}); + } + + async getCheckpointsRangeForAddress( + fromBlock: number, + toBlock: number, + address: string + ): Promise { + const events: Event[] = []; + + let continuationToken: string | undefined; + do { + const result = await this.provider.getEvents({ + from_block: { block_number: fromBlock }, + to_block: { block_number: toBlock }, + address: address, + chunk_size: 1000, + continuation_token: continuationToken + }); + + events.push(...result.events); + + continuationToken = result.continuation_token; + } while (continuationToken); + + return events.map(event => ({ + blockNumber: event.block_number, + contractAddress: validateAndParseAddress(event.from_address) + })); + } + + async getCheckpointsRange(fromBlock: number, toBlock: number): Promise { + const events: CheckpointRecord[] = []; + + for (const source of this.instance.getCurrentSources(fromBlock)) { + const addressEvents = await this.getCheckpointsRangeForAddress( + fromBlock, + toBlock, + source.contract + ); + events.push(...addressEvents); + } + + return events; + } +} diff --git a/src/providers/starknet/types.ts b/src/providers/starknet/types.ts new file mode 100644 index 0000000..8e950d5 --- /dev/null +++ b/src/providers/starknet/types.ts @@ -0,0 +1,32 @@ +import { RPC } from 'starknet'; +import { BaseWriterParams } from '../../types'; + +// Shortcuts to starknet types. +export type Block = RPC.GetBlockWithTxs; +export type Transaction = RPC.GetBlockWithTxs['transactions'][number]; +export type PendingTransaction = RPC.PendingTransactions[number]; +export type Event = RPC.GetEventsResponse['events'][number]; + +// (Partially) narrowed types as real types are not exported from `starknet`. +export type FullBlock = Block & { block_number: number }; +export type DeployTransaction = Transaction & { contract_address: string }; + +export type EventsMap = { [key: string]: Event[] }; +export type ParsedEvent = Record; + +export type Writer = ( + args: { + tx: Transaction; + block: FullBlock | null; + rawEvent?: Event; + event?: ParsedEvent; + } & BaseWriterParams +) => Promise; + +export function isFullBlock(block: Block): block is FullBlock { + return 'block_number' in block; +} + +export function isDeployTransaction(tx: Transaction | PendingTransaction): tx is DeployTransaction { + return tx.type === 'DEPLOY'; +} diff --git a/src/types.ts b/src/types.ts index dd3e301..361cacb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,29 +1,14 @@ import { z } from 'zod'; import { Pool as PgPool } from 'pg'; -import { RPC } from 'starknet'; import Checkpoint from './checkpoint'; import { AsyncMySqlPool } from './mysql'; import { LogLevel } from './utils/logger'; -import { BaseProvider } from './providers'; import { contractSourceConfigSchema, contractTemplateSchema, checkpointConfigSchema } from './schemas'; -// Shortcuts to starknet types. -export type Block = RPC.GetBlockWithTxs; -export type Transaction = RPC.GetBlockWithTxs['transactions'][number]; -export type PendingTransaction = RPC.PendingTransactions[number]; -export type Event = RPC.GetEventsResponse['events'][number]; - -// (Partially) narrowed types as real types are not exported from `starknet`. -export type FullBlock = Block & { block_number: number }; -export type DeployTransaction = Transaction & { contract_address: string }; - -export type EventsMap = { [key: string]: Event[] }; -export type ParsedEvent = Record; - export type TemplateSource = { contractAddress: string; startBlock: number; @@ -47,68 +32,17 @@ export interface CheckpointOptions { dbConnection?: string; // Abis for contracts needed for automatic event parsing abis?: Record; - // BaseProvider based class that defines how blocks are fetched and processed. - NetworkProvider?: typeof BaseProvider; } export type ContractSourceConfig = z.infer; export type ContractTemplate = z.infer; export type CheckpointConfig = z.infer; -/** - * Callback function invoked by checkpoint when a contract event - * is encountered. A writer function should use the `mysql` - * object to write to the database entities based on the require logic. - * - * For example, if a graphql Entity is defined in the schema: - * - * ```graphql - * type Vote { - * id: ID! - * voter: String! - * } - * ``` - * - * Then you can insert into the entity into the database like: - * ```typescript - * await args.mysql.queryAsync('INSERT INTO votes VALUES(?, ?);', ['voteId', 'voters-address']); - * - * // or using pg - * await args.pg.query('INSERT INTO votes VALUES($1, $2);', ['voteId', 'voters-address']); - * ``` - * - * Note, Graphql Entity names are lowercased with an 's' suffix when - * interacting with them in the databas. - *e - */ -export type CheckpointWriter = (args: { - tx: Transaction; - block: FullBlock | null; +export type BaseWriterParams = { blockNumber: number; - event?: ParsedEvent; - rawEvent?: Event; eventIndex?: number; source?: ContractSourceConfig; mysql: AsyncMySqlPool; pg: PgPool; instance: Checkpoint; -}) => Promise; - -/** - * Object map of events to CheckpointWriters. - * - * The CheckpointWriter function will be invoked when an - * event matching a key is found. - * - */ -export interface CheckpointWriters { - [event: string]: CheckpointWriter; -} - -export function isFullBlock(block: Block): block is FullBlock { - return 'block_number' in block; -} - -export function isDeployTransaction(tx: Transaction | PendingTransaction): tx is DeployTransaction { - return tx.type === 'DEPLOY'; -} +}; diff --git a/yarn.lock b/yarn.lock index 53f8a03..8dfef20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -509,6 +509,234 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@ethersproject/abi@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" + integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/abstract-provider@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef" + integrity sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + +"@ethersproject/abstract-signer@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2" + integrity sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + +"@ethersproject/address@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" + integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + +"@ethersproject/base64@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" + integrity sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + +"@ethersproject/basex@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.7.0.tgz#97034dc7e8938a8ca943ab20f8a5e492ece4020b" + integrity sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + +"@ethersproject/bignumber@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" + integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + bn.js "^5.2.1" + +"@ethersproject/bytes@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" + integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/constants@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" + integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + +"@ethersproject/hash@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" + integrity sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/keccak256@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" + integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + js-sha3 "0.8.0" + +"@ethersproject/logger@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" + integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== + +"@ethersproject/networks@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" + integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/properties@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30" + integrity sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/providers@^5.7.2": + version "5.7.2" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" + integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/basex" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + bech32 "1.1.4" + ws "7.4.6" + +"@ethersproject/random@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.7.0.tgz#af19dcbc2484aae078bb03656ec05df66253280c" + integrity sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/rlp@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" + integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/sha2@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.7.0.tgz#9a5f7a7824ef784f7f7680984e593a800480c9fb" + integrity sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + hash.js "1.1.7" + +"@ethersproject/signing-key@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3" + integrity sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + bn.js "^5.2.1" + elliptic "6.5.4" + hash.js "1.1.7" + +"@ethersproject/strings@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2" + integrity sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/transactions@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b" + integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + +"@ethersproject/web@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" + integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w== + dependencies: + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@gar/promisify@^1.0.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -1449,6 +1677,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +bech32@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" + integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== + bignumber.js@9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" @@ -1459,6 +1692,16 @@ bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1481,6 +1724,11 @@ braces@^3.0.2: dependencies: fill-range "^7.0.1" +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + browserslist@^4.20.2: version "4.20.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf" @@ -1832,6 +2080,19 @@ electron-to-chromium@^1.4.284: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.399.tgz#df8a63d1f572124ad8b5d846e38b0532ad7d9d54" integrity sha512-+V1aNvVgoWNWYIbMOiQ1n5fRIaY4SlQ/uRlrsCjLrUwr/3OvQgiX2f5vdav4oArVT9TnttJKcPCqjwPNyZqw/A== +elliptic@6.5.4: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + emittery@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" @@ -2381,6 +2642,14 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + help-me@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/help-me/-/help-me-4.0.1.tgz#b618ca10ae1392508dfad5eca75fce03e25f7616" @@ -2389,6 +2658,15 @@ help-me@^4.0.1: glob "^8.0.0" readable-stream "^3.6.0" +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -2508,7 +2786,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3015,6 +3293,11 @@ js-sdsl@^4.1.4: resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0" integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ== +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -3254,6 +3537,16 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -4538,6 +4831,11 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" +ws@7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"