github.com/ethereum-optimism/optimism@v1.7.2/packages/chain-mon/src/wallet-mon/service.ts (about) 1 import { 2 BaseServiceV2, 3 StandardOptions, 4 Gauge, 5 Counter, 6 validators, 7 waitForProvider, 8 } from '@eth-optimism/common-ts' 9 import { getChainId, compareAddrs } from '@eth-optimism/core-utils' 10 import { Provider, TransactionResponse } from '@ethersproject/abstract-provider' 11 import mainnetConfig from '@eth-optimism/contracts-bedrock/deploy-config/mainnet.json' 12 import goerliConfig from '@eth-optimism/contracts-bedrock/deploy-config/goerli.json' 13 14 import { version } from '../../package.json' 15 16 const networks = { 17 1: { 18 name: 'mainnet', 19 l1StartingBlockTag: mainnetConfig.l1StartingBlockTag, 20 accounts: [ 21 { 22 label: 'Proposer', 23 wallet: mainnetConfig.l2OutputOracleProposer, 24 target: '0xdfe97868233d1aa22e815a266982f2cf17685a27', 25 }, 26 { 27 label: 'Batcher', 28 wallet: mainnetConfig.batchSenderAddress, 29 target: mainnetConfig.batchInboxAddress, 30 }, 31 ], 32 }, 33 10: { 34 name: 'goerli', 35 l1StartingBlockTag: goerliConfig.l1StartingBlockTag, 36 accounts: [ 37 { 38 label: 'Proposer', 39 wallet: goerliConfig.l2OutputOracleProposer, 40 target: '0xE6Dfba0953616Bacab0c9A8ecb3a9BBa77FC15c0', 41 }, 42 { 43 label: 'Batcher', 44 wallet: goerliConfig.batchSenderAddress, 45 target: goerliConfig.batchInboxAddress, 46 }, 47 ], 48 }, 49 } 50 51 type WalletMonOptions = { 52 rpc: Provider 53 startBlockNumber: number 54 } 55 56 type WalletMonMetrics = { 57 validatedCalls: Counter 58 unexpectedCalls: Counter 59 unexpectedRpcErrors: Counter 60 } 61 62 type WalletMonState = { 63 chainId: number 64 highestUncheckedBlockNumber: number 65 } 66 67 export class WalletMonService extends BaseServiceV2< 68 WalletMonOptions, 69 WalletMonMetrics, 70 WalletMonState 71 > { 72 constructor(options?: Partial<WalletMonOptions & StandardOptions>) { 73 super({ 74 version, 75 name: 'wallet-mon', 76 loop: true, 77 options: { 78 loopIntervalMs: 1000, 79 ...options, 80 }, 81 optionsSpec: { 82 rpc: { 83 validator: validators.provider, 84 desc: 'Provider for network to monitor balances on', 85 }, 86 startBlockNumber: { 87 validator: validators.num, 88 default: -1, 89 desc: 'L1 block number to start checking from', 90 public: true, 91 }, 92 }, 93 metricsSpec: { 94 validatedCalls: { 95 type: Gauge, 96 desc: 'Transactions from the account checked', 97 labels: ['wallet', 'target', 'nickname'], 98 }, 99 unexpectedCalls: { 100 type: Counter, 101 desc: 'Number of unexpected wallets', 102 labels: ['wallet', 'target', 'nickname', 'transactionHash'], 103 }, 104 unexpectedRpcErrors: { 105 type: Counter, 106 desc: 'Number of unexpected RPC errors', 107 labels: ['section', 'name'], 108 }, 109 }, 110 }) 111 } 112 113 protected async init(): Promise<void> { 114 // Connect to L1. 115 await waitForProvider(this.options.rpc, { 116 logger: this.logger, 117 name: 'L1', 118 }) 119 120 this.state.chainId = await getChainId(this.options.rpc) 121 122 const l1StartingBlockTag = networks[this.state.chainId].l1StartingBlockTag 123 124 if (this.options.startBlockNumber === -1) { 125 const block = await this.options.rpc.getBlock(l1StartingBlockTag) 126 this.state.highestUncheckedBlockNumber = block.number 127 } else { 128 this.state.highestUncheckedBlockNumber = this.options.startBlockNumber 129 } 130 } 131 132 protected async main(): Promise<void> { 133 if ( 134 (await this.options.rpc.getBlockNumber()) < 135 this.state.highestUncheckedBlockNumber 136 ) { 137 this.logger.info('Waiting for new blocks') 138 return 139 } 140 141 const network = networks[this.state.chainId] 142 const accounts = network.accounts 143 144 const block = await this.options.rpc.getBlock( 145 this.state.highestUncheckedBlockNumber 146 ) 147 this.logger.info('Checking block', { 148 number: block.number, 149 }) 150 151 const transactions: TransactionResponse[] = [] 152 for (const txHash of block.transactions) { 153 const t = await this.options.rpc.getTransaction(txHash) 154 transactions.push(t) 155 } 156 157 for (const transaction of transactions) { 158 for (const account of accounts) { 159 if (compareAddrs(account.wallet, transaction.from)) { 160 if (compareAddrs(account.target, transaction.to)) { 161 this.metrics.validatedCalls.inc({ 162 nickname: account.label, 163 wallet: account.address, 164 target: account.target, 165 }) 166 this.logger.info('validated call', { 167 nickname: account.label, 168 wallet: account.address, 169 target: account.target, 170 }) 171 } else { 172 this.metrics.unexpectedCalls.inc({ 173 nickname: account.label, 174 wallet: account.address, 175 target: transaction.to, 176 transactionHash: transaction.hash, 177 }) 178 this.logger.error('Unexpected call detected', { 179 nickname: account.label, 180 address: account.address, 181 target: transaction.to, 182 transactionHash: transaction.hash, 183 }) 184 } 185 } 186 } 187 } 188 this.logger.info('Checked block', { 189 number: this.state.highestUncheckedBlockNumber, 190 }) 191 this.state.highestUncheckedBlockNumber++ 192 } 193 } 194 195 if (require.main === module) { 196 const service = new WalletMonService() 197 service.run() 198 }