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  }