github.com/ethereum-optimism/optimism@v1.7.2/packages/chain-mon/src/multisig-mon/service.ts (about) 1 import { exec } from 'child_process' 2 3 import { 4 BaseServiceV2, 5 StandardOptions, 6 Gauge, 7 Counter, 8 validators, 9 } from '@eth-optimism/common-ts' 10 import { Provider } from '@ethersproject/abstract-provider' 11 import { ethers } from 'ethers' 12 13 import Safe from '../abi/IGnosisSafe.0.8.19.json' 14 import OptimismPortal from '../abi/OptimismPortal.json' 15 import { version } from '../../package.json' 16 17 type MultisigMonOptions = { 18 rpc: Provider 19 accounts: string 20 onePassServiceToken: string 21 } 22 23 type MultisigMonMetrics = { 24 safeNonce: Gauge 25 latestPreSignedPauseNonce: Gauge 26 pausedState: Gauge 27 unexpectedRpcErrors: Counter 28 } 29 30 type MultisigMonState = { 31 accounts: Array<{ 32 nickname: string 33 safeAddress: string 34 optimismPortalAddress: string 35 vault: string 36 }> 37 } 38 39 export class MultisigMonService extends BaseServiceV2< 40 MultisigMonOptions, 41 MultisigMonMetrics, 42 MultisigMonState 43 > { 44 constructor(options?: Partial<MultisigMonOptions & StandardOptions>) { 45 super({ 46 version, 47 name: 'multisig-mon', 48 loop: true, 49 options: { 50 loopIntervalMs: 60_000, 51 ...options, 52 }, 53 optionsSpec: { 54 rpc: { 55 validator: validators.provider, 56 desc: 'Provider for network to monitor balances on', 57 }, 58 accounts: { 59 validator: validators.str, 60 desc: 'JSON array of [{ nickname, safeAddress, optimismPortalAddress, vault }] to monitor', 61 public: true, 62 }, 63 onePassServiceToken: { 64 validator: validators.str, 65 desc: '1Password Service Token', 66 }, 67 }, 68 metricsSpec: { 69 safeNonce: { 70 type: Gauge, 71 desc: 'Safe nonce', 72 labels: ['address', 'nickname'], 73 }, 74 latestPreSignedPauseNonce: { 75 type: Gauge, 76 desc: 'Latest pre-signed pause nonce', 77 labels: ['address', 'nickname'], 78 }, 79 pausedState: { 80 type: Gauge, 81 desc: 'OptimismPortal paused state', 82 labels: ['address', 'nickname'], 83 }, 84 unexpectedRpcErrors: { 85 type: Counter, 86 desc: 'Number of unexpected RPC errors', 87 labels: ['section', 'name'], 88 }, 89 }, 90 }) 91 } 92 93 protected async init(): Promise<void> { 94 this.state.accounts = JSON.parse(this.options.accounts) 95 } 96 97 protected async main(): Promise<void> { 98 for (const account of this.state.accounts) { 99 // get the nonce 1pass 100 if (this.options.onePassServiceToken) { 101 await this.getOnePassNonce(account) 102 } 103 104 // get the nonce from deployed safe 105 if (account.safeAddress) { 106 await this.getSafeNonce(account) 107 } 108 109 // get the paused state of the OptimismPortal 110 if (account.optimismPortalAddress) { 111 await this.getPausedState(account) 112 } 113 } 114 } 115 116 private async getPausedState(account: { 117 nickname: string 118 safeAddress: string 119 optimismPortalAddress: string 120 vault: string 121 }) { 122 try { 123 const optimismPortal = new ethers.Contract( 124 account.optimismPortalAddress, 125 OptimismPortal.abi, 126 this.options.rpc 127 ) 128 const paused = await optimismPortal.paused() 129 this.logger.info(`got paused state`, { 130 optimismPortalAddress: account.optimismPortalAddress, 131 nickname: account.nickname, 132 paused, 133 }) 134 135 this.metrics.pausedState.set( 136 { address: account.optimismPortalAddress, nickname: account.nickname }, 137 paused ? 1 : 0 138 ) 139 } catch (err) { 140 this.logger.error(`got unexpected RPC error`, { 141 section: 'pausedState', 142 name: 'getPausedState', 143 err, 144 }) 145 this.metrics.unexpectedRpcErrors.inc({ 146 section: 'pausedState', 147 name: 'getPausedState', 148 }) 149 } 150 } 151 152 private async getOnePassNonce(account: { 153 nickname: string 154 safeAddress: string 155 optimismPortalAddress: string 156 vault: string 157 }) { 158 try { 159 exec( 160 `OP_SERVICE_ACCOUNT_TOKEN=${this.options.onePassServiceToken} op item list --format json --vault="${account.vault}"`, 161 (error, stdout, stderr) => { 162 if (error) { 163 this.logger.error(`got unexpected error from onepass: ${error}`, { 164 section: 'onePassNonce', 165 name: 'getOnePassNonce', 166 }) 167 return 168 } 169 if (stderr) { 170 this.logger.error(`got unexpected error from onepass`, { 171 section: 'onePassNonce', 172 name: 'getOnePassNonce', 173 stderr, 174 }) 175 return 176 } 177 const items = JSON.parse(stdout) 178 let latestNonce = -1 179 this.logger.debug(`items in vault '${account.vault}':`) 180 for (const item of items) { 181 const title = item['title'] 182 this.logger.debug(`- ${title}`) 183 if (title.startsWith('ready-') && title.endsWith('.json')) { 184 const nonce = parseInt(title.substring(6, title.length - 5), 10) 185 if (nonce > latestNonce) { 186 latestNonce = nonce 187 } 188 } 189 } 190 this.metrics.latestPreSignedPauseNonce.set( 191 { address: account.safeAddress, nickname: account.nickname }, 192 latestNonce 193 ) 194 this.logger.debug(`latestNonce: ${latestNonce}`) 195 } 196 ) 197 } catch (err) { 198 this.logger.error(`got unexpected error from onepass`, { 199 section: 'onePassNonce', 200 name: 'getOnePassNonce', 201 err, 202 }) 203 this.metrics.unexpectedRpcErrors.inc({ 204 section: 'onePassNonce', 205 name: 'getOnePassNonce', 206 }) 207 } 208 } 209 210 private async getSafeNonce(account: { 211 nickname: string 212 safeAddress: string 213 optimismPortalAddress: string 214 vault: string 215 }) { 216 try { 217 const safeContract = new ethers.Contract( 218 account.safeAddress, 219 Safe.abi, 220 this.options.rpc 221 ) 222 const safeNonce = await safeContract.nonce() 223 this.logger.info(`got nonce`, { 224 address: account.safeAddress, 225 nickname: account.nickname, 226 nonce: safeNonce.toString(), 227 }) 228 229 this.metrics.safeNonce.set( 230 { address: account.safeAddress, nickname: account.nickname }, 231 parseInt(safeNonce.toString(), 10) 232 ) 233 } catch (err) { 234 this.logger.error(`got unexpected RPC error`, { 235 section: 'safeNonce', 236 name: 'getSafeNonce', 237 err, 238 }) 239 this.metrics.unexpectedRpcErrors.inc({ 240 section: 'safeNonce', 241 name: 'getSafeNonce', 242 }) 243 } 244 } 245 } 246 247 if (require.main === module) { 248 const service = new MultisigMonService() 249 service.run() 250 }