├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── lerna.json ├── package.json ├── packages └── common-ts │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── .npmignore │ ├── .prettierrc.json │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── attestations │ │ ├── attestations.test.ts │ │ ├── attestations.ts │ │ ├── eip712.ts │ │ └── index.ts │ ├── contracts │ │ ├── chain.ts │ │ ├── index.test.ts │ │ └── index.ts │ ├── database │ │ ├── index.test.ts │ │ └── index.ts │ ├── eventual │ │ ├── __tests__ │ │ │ └── eventual.ts │ │ ├── eventual.ts │ │ └── index.ts │ ├── grt │ │ └── index.ts │ ├── index.ts │ ├── logging │ │ ├── index.test.ts │ │ └── index.ts │ ├── metrics │ │ ├── index.test.ts │ │ └── index.ts │ ├── security │ │ └── index.ts │ ├── subgraph │ │ ├── index.ts │ │ ├── networkSubgraphClient.test.ts │ │ └── networkSubgraphClient.ts │ ├── subgraphs │ │ ├── index.test.ts │ │ └── index.ts │ └── util │ │ ├── addresses.ts │ │ ├── arrays.ts │ │ ├── bytes.ts │ │ ├── equal.ts │ │ └── index.ts │ └── tsconfig.json ├── tsconfig.json └── yarn.lock /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18, 19, 20] 20 | 21 | steps: 22 | - name: Setup PostgreSQL 23 | uses: Harmon758/postgresql-action@v1.0.0 24 | with: 25 | postgresql db: test 26 | postgresql user: test 27 | postgresql password: test 28 | - uses: actions/checkout@v2 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v2 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | - shell: bash 34 | env: 35 | NPM_TOKEN: ${{secrets.npm_token}} 36 | run: echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} > ~/.npmrc && echo 'registry "https://registry.npmjs.org"' >> ~/.yarnrc 37 | - run: yarn install --frozen-lockfile 38 | env: 39 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 40 | - run: npm test 41 | env: 42 | POSTGRES_TEST_HOST: localhost 43 | POSTGRES_TEST_DATABASE: test 44 | POSTGRES_TEST_USERNAME: test 45 | POSTGRES_TEST_PASSWORD: test 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | .npmrc 106 | .yarnrc 107 | 108 | # IDE directories 109 | .idea 110 | 111 | # Vscode directories 112 | .vscode 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2021 The Graph Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Common TypeScript library for Graph Protocol components 2 | 3 | [![CI](https://github.com/graphprotocol/common-ts/workflows/CI/badge.svg)](https://github.com/graphprotocol/common-ts/actions?query=workflow%3ACI) 4 | [![Coverage](https://codecov.io/gh/graphprotocol/common-ts/branch/master/graph/badge.svg)](https://codecov.io/gh/graphprotocol/common-ts) 5 | 6 | ## Usage / Documentation 7 | 8 | More information can be found in the [`@graphprotocol/common-ts` 9 | README](./packages/common-ts/). 10 | 11 | ## Development notes 12 | 13 | ### General notes 14 | 15 | - This repository is managed using [Lerna](https://lerna.js.org/) and [Yarn 16 | workspaces](https://classic.yarnpkg.com/en/docs/workspaces/). 17 | 18 | - [Chan](https://github.com/geut/chan/tree/master/packages/chan) is used to 19 | maintain [changelogs](./packages/common-ts/CHANGELOG.md). 20 | 21 | ### Install dependencies 22 | 23 | ```sh 24 | yarn 25 | ``` 26 | 27 | ### Build 28 | 29 | ```sh 30 | yarn prepublish 31 | ``` 32 | 33 | ### Test 34 | 35 | The following environment variables need to be defined for the test suite to run: 36 | 37 | - `POSTGRES_TEST_HOST` 38 | - `POSTGRES_TEST_PORT` (optional) 39 | - `POSTGRES_TEST_USERNAME` 40 | - `POSTGRES_TEST_PASSWORD` 41 | - `POSTGRES_TEST_DATABASE` 42 | 43 | After that, the test suite can be run with: 44 | 45 | ```sh 46 | yarn test 47 | ``` 48 | 49 | ### Release 50 | 51 | 1. Update the changelog(s). 52 | 2. Run 53 | ```sh 54 | yarn publish 55 | ``` 56 | 57 | ## Copyright 58 | 59 | Copyright © 2020 The Graph Foundation. 60 | 61 | Licensed under the [MIT license](LICENSE). 62 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/common-ts" 4 | ], 5 | "version": "2.0.7" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "bootstrap": "lerna bootstrap", 9 | "prepublish": "lerna run prepublish", 10 | "publish": "lerna run publish", 11 | "test": "lerna run test --stream", 12 | "test:watch": "lerna run test:watch --stream" 13 | }, 14 | "devDependencies": { 15 | "@octokit/core": "^3.2.0", 16 | "lerna": "^4.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/common-ts/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage -------------------------------------------------------------------------------- /packages/common-ts/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: false, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint'], 5 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 6 | } 7 | -------------------------------------------------------------------------------- /packages/common-ts/.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | .idea 3 | -------------------------------------------------------------------------------- /packages/common-ts/.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /packages/common-ts/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "all", 4 | "printWidth": 90, 5 | "singleQuote": true, 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /packages/common-ts/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | - Add ability to enable SSL to database connection with `sslEnabled` (maintain default of `false`) 10 | 11 | ## [2.0.7] - 2023-09-25 12 | ### Changed 13 | - Add L2Curation contract 14 | 15 | ## [2.0.6] - 2023-09-20 16 | ### Added 17 | - Add Robustness checks around contract information 18 | 19 | ## [2.0.5] - 2023-09-19 20 | ### Fixed 21 | - Use the correct interface for staking on contracts 5.x 22 | 23 | ## [2.0.4] - 2023-09-18 24 | ### Changed 25 | - Update node-versions of action runners used by CI workflow 26 | - Upgrade many dependencies 27 | 28 | ## [2.0.2] - 2023-08-23 29 | ### Changed 30 | - Update @graphprotocol/contracts to v2.1.0 31 | - Add support for L2 contracts 32 | - Let connection pool size be configured 33 | - Upgrade sequelize dependency 34 | 35 | ### Fixed 36 | - Remove reservoir contracts 37 | - Don't error out if contract not deployed 38 | - L1/L2 specific contracts weren't being loaded 39 | 40 | ## [1.8.6] - 2022-08-02 41 | ### Changed 42 | - Upgrade sequelize dependency 43 | 44 | ## [1.8.5] - 2022-08-02 45 | ### Changed 46 | - Upgrade @graphprotocol/contracts 47 | 48 | ## [1.8.3] - 2022-04-12 49 | ### Changed 50 | - Upgrade dependencies 51 | - Update test for query versioning to match spec 52 | - Allow set version for EIP721 domain separator 53 | 54 | ## [1.8.2] - 2022-02-23 55 | ### Added 56 | - Add AllocationExchange to NetworkContracts 57 | 58 | ### Changed 59 | - Use TS import for contract addresses 60 | 61 | ## [1.8.1] - 2021-12-22 62 | ### Changed 63 | - Upgrade dependencies 64 | 65 | ### Fixed 66 | - Fix serve metrics test 67 | 68 | ## [1.5.1] - 2021-05-11 69 | ### Added 70 | - `BytesWriter`: Also handle inputs without leading `0x` 71 | 72 | ## [1.5.0] - 2021-05-07 73 | ### Added 74 | - Add attestation encoding, decoding and signer recovery helpers 75 | - Add `BytesWriter` utility to concatenate hexadecimal byte strings 76 | 77 | ## [1.4.2] - 2021-04-14 78 | ### Added 79 | - Add `values()` async generator for eventuals 80 | 81 | ### Fixed 82 | - Fix missing current value in async eventual `values()` generator 83 | - Fix race condition causing `reduce()` eventual to stop working 84 | 85 | ## [1.3.3] - 2021-04-04 86 | ### Changed 87 | - Update ethers to 5.1.0 88 | 89 | ## [1.3.2] - 2021-03-30 90 | ### Changed 91 | - Update @graphprotocol/contracts to 1.2.0 92 | 93 | ## [1.3.1] - 2021-03-30 94 | ### Fixed 95 | - Fix incorrect equality checks in `Eventual`. 96 | 97 | ## [1.3.0] - 2021-01-29 98 | ### Added 99 | - Add `join` eventual that blocks until all values have resolved 100 | 101 | ### Changed 102 | - Update contracts to 1.1.0 (mainnet and testnet) 103 | 104 | ## [0.4.0] - 2020-11-27 105 | ### Changed 106 | - Update `@graphprotocol/contracts` to 0.8.0-testnet-phase2.2 107 | 108 | ## [0.3.12] - 2020-11-27 109 | ### Fixed 110 | - Fix asset holder address 111 | - Add missing dependency on our fork of pino-sentry 112 | 113 | ## [0.3.12-alpha.0] - 2020-11-27 114 | ### Fixed 115 | - Validate subgraph IDs for security reasons 116 | 117 | ### Added 118 | - Add `GRTAssetHolder` to `NetworkContracts` 119 | - Add our own pino-sentry fork for better stack trace support 120 | 121 | ## [0.3.11] - 2020-11-03 122 | ### Fixed 123 | - Fix using trace or debug levels 124 | 125 | ## [0.3.10] - 2020-11-03 126 | ### Added 127 | - Add level option for Sentry error tracking 128 | 129 | ## [0.3.9] - 2020-11-03 130 | ### Fixed 131 | - Fix Sentry integration in Logger 132 | 133 | ## [0.3.8] - 2020-11-03 134 | ### Added 135 | - Add error tracking with Sentry, make it easy to use 136 | 137 | ## [0.3.7] - 2020-11-02 138 | ### Changed 139 | - Add optional error tracker to logging (e.g. for Sentry) 140 | 141 | ## [0.3.6] - 2020-10-30 142 | ### Changed 143 | - Downgrade `@urql/core` and `@urql/exchange-execute` 144 | 145 | ## [0.3.5] - 2020-10-29 146 | ### Changed 147 | - Update and pin all dependencies 148 | 149 | ## [0.3.4] - 2020-10-29 150 | ### Changed 151 | - Pin dependencies to specific versions 152 | 153 | ## [0.3.3] - 2020-10-15 154 | ### Changed 155 | - Update contracts to 0.7.7-testnet-phase2 156 | 157 | ## [0.3.2] - 2020-10-12 158 | ### Changed 159 | - Update contracts to 0.7.5-testnet-phase2 160 | 161 | ## [0.3.1] - 2020-10-12 162 | ### Changed 163 | - Update contracts to 0.7.4-testnet-phase2-production 164 | 165 | ## [0.3.0] - 2020-10-12 166 | ### Added 167 | - Add `filter` eventual 168 | - Allow `timed` to be used with synchronous code 169 | - Add support for asynchronous logging 170 | 171 | ### Changed 172 | - Make `Logger` members public 173 | - Update ethers to 5.0.15 174 | - Update contracts to 0.7.0-testnet-phase2 175 | 176 | ## [0.2.9] - 2020-09-11 177 | ### Fixed 178 | - Fix `timer` eventual to use `setInterval`, not `setTimeout` 179 | 180 | ## [0.2.8] - 2020-09-10 181 | ### Added 182 | - Add `timer` and `reduce` eventuals 183 | 184 | ### Removed 185 | - Remove `poll` eventual 186 | 187 | ## [0.2.7] - 2020-09-10 188 | ### Added 189 | - Add dependency on `lodash.isequal` 190 | 191 | ### Fixed 192 | - Fix equality checks of eventual values, especially for `Map` objects 193 | 194 | ## [0.2.6] - 2020-09-08 195 | ### Changed 196 | - Rerelease to publish to private registry 197 | 198 | ## [0.2.5] - 2020-09-08 199 | ### Removed 200 | - Move indexer management to indexer repository 201 | 202 | ### Added 203 | - Add `Eventual` concept 204 | - Add `Address` type and `toAddress` converter for normalizing addresses 205 | 206 | ## [0.2.4] - 2020-09-01 207 | ### Changed 208 | - Serve metrics from /metrics by default 209 | 210 | ## [0.2.3] - 2020-08-27 211 | ### Added 212 | - More details provided by the `indexerEndpoints` GraphQL API 213 | 214 | ## [0.2.2] - 2020-08-27 215 | ### Added 216 | - Optional `logger` argument for `IndexerManagementClient` 217 | 218 | ### Fixed 219 | - Handle unavailable indexer endpoints correctly 220 | 221 | ## [0.1.0] - 2020-07-30 222 | ### Changed 223 | - Update contracts to 0.4.7-testnet-phase1 224 | 225 | ## [0.0.51] - 2020-07-29 226 | ### Added 227 | - Add optional `port` parameter to `createMetricsServer` 228 | 229 | ## [0.0.50] - 2020-07-23 230 | ### Changed 231 | - Update Connext to 7.0.0 232 | 233 | ## [0.0.49] - 2020-07-15 234 | ### Added 235 | - Add `timed` helper to measure promise execution time 236 | 237 | ### Changed 238 | - Update prom-client from 11.x to 12.x 239 | - Allow loggers to be silent 240 | - Update Connext to 7.0.0-alpha.20 241 | 242 | ## [0.0.48] - 2020-07-10 243 | ### Added 244 | - Add support for manually syncing state channel models to the db 245 | 246 | ### Changed 247 | - Update Connext to 7.0.0-alpha.18 248 | - Update sequelize from 5.x to 6.x 249 | 250 | ## [0.0.47] - 2020-07-06 251 | ### Added 252 | - Add `display` property to `SubgraphDeploymentID` 253 | 254 | ## [0.0.46] - 2020-07-06 255 | ### Changed 256 | - Customize logger interface 257 | 258 | ## [0.0.45] - 2020-07-06 259 | ### Fixed 260 | - Fix tests relying on old logging 261 | 262 | ## [0.0.44] - 2020-07-06 263 | ### Changed 264 | - Switch logging from winston to pino 265 | 266 | ## [0.0.43] - 2020-07-05 267 | ### Changed 268 | - Update Connext to 7.0.0-alpha.11 269 | - Update Connext to 7.0.0-alpha.13 270 | 271 | ## [0.0.41] - 2020-07-03 272 | ### Changed 273 | - Switch to flat module exports 274 | 275 | ## [0.0.40] - 2020-07-03 276 | ### Added 277 | - Add `subgraphs` module with `SubgraphName` and `SubgraphDeploymentID` types 278 | - Configure eslint and automatic code formatting 279 | 280 | ## [0.0.39] - 2020-06-25 281 | ### Changed 282 | - Switch to urql in the `NetworkSubgraphClient` 283 | 284 | ## [0.0.38] - 2020-06-25 285 | ### Changed 286 | - Update Connext to 7.0.0-alpha.3 287 | 288 | ## [0.0.37] - 2020-06-23 289 | ### Changed 290 | - Add TypeChain bindings back in 291 | 292 | ### Added 293 | - Add `subgraph` module with a network subgraph client based on Apollo 294 | 295 | ## [0.0.35] - 2020-06-23 296 | ### Changed 297 | - Revert TypeChain changes, as they don't work transitively 298 | 299 | ## [0.0.34] - 2020-06-23 300 | ### Changed 301 | - Use contract factories for connecting to contracts 302 | 303 | ## [0.0.33] - 2020-06-23 304 | ### Added 305 | - Add `contracts` module based on TypeChain bindings 306 | 307 | ### Changed 308 | - Allow state channel test to take up to 30s 309 | - Update dependencies 310 | 311 | ## [0.0.32] - 2020-06-12 312 | ### Changed 313 | - Update to Connext 7.0.0-alpha.0 314 | - Update to ethers 5.0.0-beta.191 315 | 316 | ## [0.0.31] - 2020-06-03 317 | ### Changed 318 | - Rename subgraphID to subgraphDeploymentID in Attestation 319 | 320 | ## [0.0.30] - 2020-06-01 321 | ### Changed 322 | - Fix hashStruct EIP712 function 323 | 324 | ## [0.0.28] - 2020-05-15 325 | ### Changed 326 | - Update to Connext 6.5.0 327 | 328 | ## [0.0.27] - 2020-05-12 329 | ### Changed 330 | - Reorganization attestation code and export Attestation interface 331 | 332 | ## [0.0.26] - 2020-05-12 333 | ### Added 334 | - Add dependency on bs58 335 | - Add rudimentary EIP-712 implementation 336 | - Add support for creating attestations 337 | 338 | ## [0.0.25] - 2020-05-11 339 | ### Changed 340 | - Update to Connext 6.3.12 341 | 342 | ## [0.0.24] - 2020-05-07 343 | ### Changed 344 | - Update to Connext 6.3.9 345 | - Allow state channels to use separate stores 346 | 347 | ## [0.0.23] - 2020-05-07 348 | ### Changed 349 | - Create state channels using a private key instead of a mnemonic 350 | 351 | ## [0.0.22] - 2020-05-06 352 | ### Changed 353 | - Update to Connext 6.3.8 354 | 355 | ## [0.0.21] - 2020-04-24 356 | ### Changed 357 | - Update to Connext 6.0.9 358 | 359 | ## [0.0.20] - 2020-04-16 360 | ### Changed 361 | - Update to Connext 6.0.3 362 | 363 | ## [0.0.18] - 2020-04-08 364 | ### Changed 365 | - Update to Connext 6.0.0-alpha.10 366 | 367 | ## [0.0.17] - 2020-04-07 368 | ### Added 369 | - Add `connextMessaging` option to `createStateChannel` 370 | 371 | ## [0.0.16] - 2020-04-07 372 | ### Changed 373 | - Update to Connext 6.0.0-alpha.9 374 | 375 | ## [0.0.15] - 2020-04-06 376 | ### Changed 377 | - Update to Connext 6.0.0-alpha.8 378 | 379 | ## [0.0.14] - 2020-04-03 380 | ### Changed 381 | - Update to Connext 6.0.0-alpha.7 382 | 383 | ## [0.0.13] - 2020-03-19 384 | ### Changed 385 | - Bump `@connext/client` and `@connext/types` to 5.2.1 386 | 387 | ## [0.0.12] - 2020-03-11 388 | ### Added 389 | - Add optional `logger` option to createStateChannel 390 | 391 | ### Changed 392 | - Bump `@connext/client` and `@connext/types` to 5.1.1 393 | 394 | ## [0.0.11] - 2020-03-05 395 | ### Changed 396 | - Bump `@connext/client` and `@connext/types` to 5.0.2 397 | 398 | ## [0.0.10] - 2020-02-24 399 | ### Changed 400 | - Update to Connext 4.2.0 401 | 402 | ## [0.0.9] - 2020-02-21 403 | ### Changed 404 | - Update ethers to 4.0.45 405 | 406 | ## [0.0.8] - 2020-02-21 407 | ### Changed 408 | - Update to Connext 4.1.0 409 | 410 | ## [0.0.7] - 2020-02-19 411 | ### Changed 412 | - Downgrade ethers.js to 4.0.41 to be compatible with Connext 413 | 414 | ## [0.0.6] - 2020-02-19 415 | ### Added 416 | - Optional `logLevel` option for `createStateChannel()` 417 | - Export `Record` model from `stateChannels` module 418 | - Implement `reset()` method of state channel store 419 | 420 | ### Fixed 421 | - Upsert existing `Record`s instead of throwing 422 | 423 | ## [0.0.5] - 2020-02-19 424 | ### Changed 425 | - Bump `@connext/client` and `@connext/types` to 4.0.16 426 | 427 | ## [0.0.4] - 2020-02-17 428 | ### Added 429 | - Optional `logging` option for `database.connect()` 430 | 431 | ## [0.0.3] - 2020-02-17 432 | ### Added 433 | - Ethereum provider and Connext node options for state channel setup 434 | 435 | ## [0.0.2] - 2020-02-17 436 | ### Changed 437 | - Export `database`, `logging`, `metrics` and `stateChannel` modules separately 438 | 439 | ## 0.0.1 - 2020-02-17 440 | ### Added 441 | - Common logging module based on winston 442 | - Common metrics module for instrumenting components with Prometheus 443 | - Common database module for simplifying database setup 444 | - Connext client module with Postgres-based store implementation 445 | 446 | [Unreleased]: https://github.com/graphprotocol/common-ts/compare/v2.0.7...HEAD 447 | [2.0.7]: https://github.com/graphprotocol/common-ts/compare/2.0.6...v2.0.7 448 | [2.0.6]: https://github.com/graphprotocol/common-ts/compare/2.0.5...v2.0.6 449 | [2.0.5]: https://github.com/graphprotocol/common-ts/compare/2.0.4...v2.0.5 450 | [2.0.4]: https://github.com/graphprotocol/common-ts/compare/2.0.2...v2.0.4 451 | [2.0.2]: https://github.com/graphprotocol/common-ts/compare/v1.8.6...v2.0.2 452 | [1.8.6]: https://github.com/graphprotocol/common-ts/compare/v1.8.5...v1.8.6 453 | [1.8.5]: https://github.com/graphprotocol/common-ts/compare/v1.8.3...v1.8.5 454 | [1.8.3]: https://github.com/graphprotocol/common-ts/compare/v1.8.2...v1.8.3 455 | [1.8.2]: https://github.com/graphprotocol/common-ts/compare/v1.8.1...v1.8.2 456 | [1.8.1]: https://github.com/graphprotocol/common-ts/compare/v1.5.1...v1.8.1 457 | [1.5.1]: https://github.com/graphprotocol/common/compare/v1.5.0...v1.5.1 458 | [1.5.0]: https://github.com/graphprotocol/common/compare/v1.4.2...v1.5.0 459 | [1.4.2]: https://github.com/graphprotocol/common/compare/v1.3.3...v1.4.2 460 | [1.3.3]: https://github.com/graphprotocol/common/compare/v1.3.2...v1.3.3 461 | [1.3.2]: https://github.com/graphprotocol/common/compare/v1.3.1...v1.3.2 462 | [1.3.1]: https://github.com/graphprotocol/common/compare/v1.3.0...v1.3.1 463 | [1.3.0]: https://github.com/graphprotocol/common/compare/v0.4.0...v1.3.0 464 | [0.4.0]: https://github.com/graphprotocol/common-ts/compare/v0.3.12...v0.4.0 465 | [0.3.12]: https://github.com/graphprotocol/common/compare/v0.3.12-alpha.0...v0.3.12 466 | [0.3.12-alpha.0]: https://github.com/graphprotocol/common/compare/v0.3.11...v0.3.12-alpha.0 467 | [0.3.11]: https://github.com/graphprotocol/common/compare/v0.3.10...v0.3.11 468 | [0.3.10]: https://github.com/graphprotocol/common/compare/v0.3.9...v0.3.10 469 | [0.3.9]: https://github.com/graphprotocol/common/compare/v0.3.8...v0.3.9 470 | [0.3.8]: https://github.com/graphprotocol/common/compare/v0.3.7...v0.3.8 471 | [0.3.7]: https://github.com/graphprotocol/common/compare/v0.3.6...v0.3.7 472 | [0.3.6]: https://github.com/graphprotocol/common/compare/v0.3.5...v0.3.6 473 | [0.3.5]: https://github.com/graphprotocol/common/compare/v0.3.4...v0.3.5 474 | [0.3.4]: https://github.com/graphprotocol/common/compare/v0.3.3...v0.3.4 475 | [0.3.3]: https://github.com/graphprotocol/common/compare/v0.3.2...v0.3.3 476 | [0.3.2]: https://github.com/graphprotocol/common/compare/v0.3.1...v0.3.2 477 | [0.3.1]: https://github.com/graphprotocol/common/compare/v0.3.0...v0.3.1 478 | [0.3.0]: https://github.com/graphprotocol/common/compare/v0.2.9...v0.3.0 479 | [0.2.9]: https://github.com/graphprotocol/common/compare/v0.2.8...v0.2.9 480 | [0.2.8]: https://github.com/graphprotocol/common/compare/v0.2.7...v0.2.8 481 | [0.2.7]: https://github.com/graphprotocol/common/compare/v0.2.6...v0.2.7 482 | [0.2.6]: https://github.com/graphprotocol/common/compare/v0.2.5...v0.2.6 483 | [0.2.5]: https://github.com/graphprotocol/common/compare/v0.2.4...v0.2.5 484 | [0.2.4]: https://github.com/graphprotocol/common/compare/v0.2.3...v0.2.4 485 | [0.2.3]: https://github.com/graphprotocol/common/compare/v0.2.2...v0.2.3 486 | [0.2.2]: https://github.com/graphprotocol/common/compare/v0.1.0...v0.2.2 487 | [0.1.0]: https://github.com/graphprotocol/common/compare/v0.0.51...v0.1.0 488 | [0.0.51]: https://github.com/graphprotocol/common/compare/v0.0.50...v0.0.51 489 | [0.0.50]: https://github.com/graphprotocol/common/compare/v0.0.49...v0.0.50 490 | [0.0.49]: https://github.com/graphprotocol/common/compare/v0.0.48...v0.0.49 491 | [0.0.48]: https://github.com/graphprotocol/common/compare/v0.0.47...v0.0.48 492 | [0.0.47]: https://github.com/graphprotocol/common/compare/v0.0.46...v0.0.47 493 | [0.0.46]: https://github.com/graphprotocol/common/compare/v0.0.45...v0.0.46 494 | [0.0.45]: https://github.com/graphprotocol/common/compare/v0.0.44...v0.0.45 495 | [0.0.44]: https://github.com/graphprotocol/common/compare/v0.0.43...v0.0.44 496 | [0.0.43]: https://github.com/graphprotocol/common/compare/v0.0.41...v0.0.43 497 | [0.0.41]: https://github.com/graphprotocol/common/compare/v0.0.40...v0.0.41 498 | [0.0.40]: https://github.com/graphprotocol/common/compare/v0.0.39...v0.0.40 499 | [0.0.39]: https://github.com/graphprotocol/common/compare/v0.0.38...v0.0.39 500 | [0.0.38]: https://github.com/graphprotocol/common/compare/v0.0.37...v0.0.38 501 | [0.0.37]: https://github.com/graphprotocol/common/compare/v0.0.35...v0.0.37 502 | [0.0.35]: https://github.com/graphprotocol/common/compare/v0.0.34...v0.0.35 503 | [0.0.34]: https://github.com/graphprotocol/common/compare/v0.0.33...v0.0.34 504 | [0.0.33]: https://github.com/graphprotocol/common/compare/v0.0.32...v0.0.33 505 | [0.0.32]: https://github.com/graphprotocol/common/compare/v0.0.31...v0.0.32 506 | [0.0.31]: https://github.com/graphprotocol/common/compare/v0.0.30...v0.0.31 507 | [0.0.30]: https://github.com/graphprotocol/common/compare/v0.0.28...v0.0.30 508 | [0.0.28]: https://github.com/graphprotocol/common/compare/v0.0.27...v0.0.28 509 | [0.0.27]: https://github.com/graphprotocol/common/compare/v0.0.26...v0.0.27 510 | [0.0.26]: https://github.com/graphprotocol/common/compare/v0.0.25...v0.0.26 511 | [0.0.25]: https://github.com/graphprotocol/common/compare/v0.0.24...v0.0.25 512 | [0.0.24]: https://github.com/graphprotocol/common/compare/v0.0.23...v0.0.24 513 | [0.0.23]: https://github.com/graphprotocol/common/compare/v0.0.22...v0.0.23 514 | [0.0.22]: https://github.com/graphprotocol/common/compare/v0.0.21...v0.0.22 515 | [0.0.21]: https://github.com/graphprotocol/common/compare/v0.0.20...v0.0.21 516 | [0.0.20]: https://github.com/graphprotocol/common/compare/v0.0.18...v0.0.20 517 | [0.0.18]: https://github.com/graphprotocol/common/compare/v0.0.17...v0.0.18 518 | [0.0.17]: https://github.com/graphprotocol/common/compare/v0.0.16...v0.0.17 519 | [0.0.16]: https://github.com/graphprotocol/common/compare/v0.0.15...v0.0.16 520 | [0.0.15]: https://github.com/graphprotocol/common/compare/v0.0.14...v0.0.15 521 | [0.0.14]: https://github.com/graphprotocol/common/compare/v0.0.13...v0.0.14 522 | [0.0.13]: https://github.com/graphprotocol/common/compare/v0.0.12...v0.0.13 523 | [0.0.12]: https://github.com/graphprotocol/common/compare/v0.0.11...v0.0.12 524 | [0.0.11]: https://github.com/graphprotocol/common/compare/v0.0.10...v0.0.11 525 | [0.0.10]: https://github.com/graphprotocol/common/compare/v0.0.9...v0.0.10 526 | [0.0.9]: https://github.com/graphprotocol/common/compare/v0.0.8...v0.0.9 527 | [0.0.8]: https://github.com/graphprotocol/common/compare/v0.0.7...v0.0.8 528 | [0.0.7]: https://github.com/graphprotocol/common/compare/v0.0.6...v0.0.7 529 | [0.0.6]: https://github.com/graphprotocol/common/compare/v0.0.5...v0.0.6 530 | [0.0.5]: https://github.com/graphprotocol/common/compare/v0.0.4...v0.0.5 531 | [0.0.4]: https://github.com/graphprotocol/common/compare/v0.0.3...v0.0.4 532 | [0.0.3]: https://github.com/graphprotocol/common/compare/v0.0.2...v0.0.3 533 | [0.0.2]: https://github.com/graphprotocol/common/compare/v0.0.1...v0.0.2 534 | -------------------------------------------------------------------------------- /packages/common-ts/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 The Graph Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/common-ts/README.md: -------------------------------------------------------------------------------- 1 | # Common TypeScript library for Graph Protocol components 2 | 3 | **NOTE: THIS PROJECT IS BETA SOFTWARE.** 4 | 5 | ## Documentation 6 | 7 | `@graphprotocol/common-ts` is a TypeScript utility library for The Graph. It 8 | currently provides the following functionality: 9 | 10 | - Type-safe contract bindings to interact with The Graph Network on mainnet 11 | and rinkeby. 12 | 13 | - Create query response attestations using 14 | [EIP-712](https://eips.ethereum.org/EIPS/eip-712). 15 | 16 | - A GraphQL client to query The Graph Network network subgraph 17 | 18 | - Graph Token (GRT) formatting and parsing. 19 | 20 | - Validation and type-safe handling of subgraph deployment IDs. 21 | 22 | - Type-safe and normalized Ethereum addresses. 23 | 24 | Convenience features: 25 | 26 | - Security ehancement for [Express](https://expressjs.com/) web servers. 27 | 28 | - An easy-to-configure logger based on [pino](https://getpino.io/) with 29 | support for asynchronous logging and [Sentry](https://sentry.io) error 30 | reporting. 31 | 32 | - A consistent way of connecting to [Postgres](https://www.postgresql.org/) 33 | using [Sequelize](https://sequelize.org/). 34 | 35 | - Easy-to-use [Prometheus](https://prometheus.io/) metrics client and server. 36 | 37 | - Eventuals: Asynchronously resolved, observable values that only emit values 38 | if they have changed. These are convenient to monitor, for instance, Graph 39 | Network data and only perform an action if it has changed. 40 | 41 | ## Copyright 42 | 43 | Copyright © 2020-2021 The Graph Foundation. 44 | 45 | Licensed under the [MIT license](./LICENSE). 46 | -------------------------------------------------------------------------------- /packages/common-ts/jest.config.js: -------------------------------------------------------------------------------- 1 | const bail = s => { 2 | throw new Error(s) 3 | } 4 | 5 | module.exports = { 6 | collectCoverage: true, 7 | preset: 'ts-jest', 8 | testEnvironment: 'node', 9 | testPathIgnorePatterns: ['/node_modules/', '/dist/'], 10 | globals: { 11 | __DATABASE__: { 12 | host: process.env.POSTGRES_TEST_HOST || bail('POSTGRES_TEST_HOST is not defined'), 13 | port: parseInt(process.env.POSTGRES_TEST_PORT || '5432'), 14 | username: 15 | process.env.POSTGRES_TEST_USERNAME || 16 | bail('POSTGRES_TEST_USERNAME is not defined'), 17 | password: 18 | process.env.POSTGRES_TEST_PASSWORD || 19 | bail('POSTGRES_TEST_PASSWORD is not defined'), 20 | database: 21 | process.env.POSTGRES_TEST_DATABASE || 22 | bail('POSTGRES_TEST_DATABASE is not defined'), 23 | }, 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /packages/common-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphprotocol/common-ts", 3 | "version": "2.0.11", 4 | "description": "Common TypeScript library for Graph Protocol components", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": "https://github.com/graphprotocol/common-ts", 8 | "author": "Graph Protocol, Inc.", 9 | "license": "MIT/Apache", 10 | "scripts": { 11 | "format": "./node_modules/.bin/prettier --write src/*.ts src/**/*.ts src/**/**/*.ts", 12 | "lint": "./node_modules/.bin/eslint . --ext .ts,.tsx", 13 | "compile": "./node_modules/.bin/tsc", 14 | "prepublish": "yarn format && yarn lint && yarn compile", 15 | "test": "jest --passWithNoTests --detectOpenHandles --verbose --forceExit", 16 | "test:watch": "jest --watch --passWithNoTests --detectOpenHandles --verbose" 17 | }, 18 | "dependencies": { 19 | "@graphprotocol/contracts": "5.3.3", 20 | "@graphprotocol/pino-sentry-simple": "0.7.1", 21 | "@urql/core": "3.1.0", 22 | "@urql/exchange-execute": "2.1.0", 23 | "body-parser": "1.20.2", 24 | "bs58": "5.0.0", 25 | "cors": "2.8.5", 26 | "cross-fetch": "4.0.0", 27 | "ethers": "5.7.0", 28 | "express": "4.18.2", 29 | "graphql": "16.8.0", 30 | "graphql-tag": "2.12.6", 31 | "helmet": "7.0.0", 32 | "morgan": "1.10.0", 33 | "ngeohash": "0.6.3", 34 | "pg": "8.11.3", 35 | "pg-hstore": "2.3.4", 36 | "pino": "7.6.0", 37 | "pino-multi-stream": "6.0.0", 38 | "prom-client": "14.2.0", 39 | "sequelize": "6.33.0" 40 | }, 41 | "devDependencies": { 42 | "@geut/chan": "3.2.9", 43 | "@types/bs58": "4.0.1", 44 | "@types/cors": "2.8.14", 45 | "@types/express": "4.17.17", 46 | "@types/jest": "29.5.4", 47 | "@types/lodash.isequal": "4.5.6", 48 | "@types/morgan": "1.9.5", 49 | "@types/ngeohash": "0.6.4", 50 | "@types/node": "20.6.1", 51 | "@types/pino": "7.0.5", 52 | "@types/pino-multi-stream": "5.1.3", 53 | "@types/supertest": "2.0.12", 54 | "@typescript-eslint/eslint-plugin": "6.7.0", 55 | "@typescript-eslint/parser": "6.7.0", 56 | "eslint": "8.49.0", 57 | "jest": "29.7.0", 58 | "nock": "13.3.3", 59 | "prettier": "3.0.3", 60 | "supertest": "6.3.3", 61 | "ts-jest": "29.1.1", 62 | "typescript": "5.2.2" 63 | }, 64 | "gitHead": "8d9126f4c6c764ad6462c6ca2cc4523a268820fa" 65 | } 66 | -------------------------------------------------------------------------------- /packages/common-ts/src/attestations/attestations.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createAttestation, 3 | encodeAttestation, 4 | decodeAttestation, 5 | recoverAttestation, 6 | } from './attestations' 7 | import { Wallet } from 'ethers' 8 | import { utils } from 'ethers' 9 | import * as bs58 from 'bs58' 10 | 11 | describe('Attestations', () => { 12 | const mnemonic = 13 | 'coyote tattoo slush ball cluster culture bleak news when action cover effort' 14 | 15 | test('Attestations are correct', async () => { 16 | const receipt = { 17 | requestCID: '0xd902c18a1b3590a3d2a8ae4439db376764fda153ca077e339d0427bf776bd463', 18 | responseCID: '0xbe0b5ae5f598fdf631133571d59ef16b443b2fe02e35ca2cb807158069009db9', 19 | subgraphDeploymentID: utils.hexlify( 20 | bs58.decode('QmTXzATwNfgGVukV1fX2T6xw9f6LAYRVWpsdXyRWzUR2H9').slice(2), 21 | ), 22 | } 23 | 24 | const signer = Wallet.fromMnemonic(mnemonic) 25 | const attestation = await createAttestation( 26 | signer.privateKey, 27 | 1, 28 | '0x0000000000000000000000000000000000000000', 29 | receipt, 30 | '0', 31 | ) 32 | 33 | expect(attestation).toStrictEqual({ 34 | requestCID: receipt.requestCID, 35 | responseCID: receipt.responseCID, 36 | subgraphDeploymentID: receipt.subgraphDeploymentID, 37 | v: 27, 38 | r: '0x00bd2f5c3dd7a81dc36a6bf109e4deba55220c0badd5d6e2e1b3aefb48d647e4', 39 | s: '0x34ca501c609bef062785671d594be732ef7af1ddbaffd8f3257a2ad606479769', 40 | }) 41 | }) 42 | 43 | test('encode and decode attestation', async () => { 44 | const receipt = { 45 | requestCID: '0xd902c18a1b3590a3d2a8ae4439db376764fda153ca077e339d0427bf776bd463', 46 | responseCID: '0xbe0b5ae5f598fdf631133571d59ef16b443b2fe02e35ca2cb807158069009db9', 47 | subgraphDeploymentID: utils.hexlify( 48 | bs58.decode('QmTXzATwNfgGVukV1fX2T6xw9f6LAYRVWpsdXyRWzUR2H9').slice(2), 49 | ), 50 | } 51 | const attestation = { 52 | requestCID: receipt.requestCID, 53 | responseCID: receipt.responseCID, 54 | subgraphDeploymentID: receipt.subgraphDeploymentID, 55 | v: 27, 56 | r: '0x00bd2f5c3dd7a81dc36a6bf109e4deba55220c0badd5d6e2e1b3aefb48d647e4', 57 | s: '0x34ca501c609bef062785671d594be732ef7af1ddbaffd8f3257a2ad606479769', 58 | } 59 | 60 | const attestationBytes = encodeAttestation(attestation) 61 | const decodedAttestation = decodeAttestation(attestationBytes) 62 | expect(decodedAttestation).toStrictEqual(attestation) 63 | }) 64 | 65 | test('recover attestation signer', async () => { 66 | const receipt = { 67 | requestCID: '0xd902c18a1b3590a3d2a8ae4439db376764fda153ca077e339d0427bf776bd463', 68 | responseCID: '0xbe0b5ae5f598fdf631133571d59ef16b443b2fe02e35ca2cb807158069009db9', 69 | subgraphDeploymentID: utils.hexlify( 70 | bs58.decode('QmTXzATwNfgGVukV1fX2T6xw9f6LAYRVWpsdXyRWzUR2H9').slice(2), 71 | ), 72 | } 73 | 74 | const signer = Wallet.fromMnemonic(mnemonic) 75 | const chainID = 1 76 | const contractAddress = '0x0000000000000000000000000000000000000000' 77 | const attestation = await createAttestation( 78 | signer.privateKey, 79 | chainID, 80 | contractAddress, 81 | receipt, 82 | '=1.0.0', 83 | ) 84 | const recoveredAddress = recoverAttestation( 85 | chainID, 86 | contractAddress, 87 | attestation, 88 | '=1.0.0', 89 | ) 90 | expect(recoveredAddress).toStrictEqual(await signer.getAddress()) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /packages/common-ts/src/attestations/attestations.ts: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers' 2 | import * as eip712 from './eip712' 3 | 4 | const { 5 | defaultAbiCoder: abi, 6 | arrayify, 7 | concat, 8 | hexlify, 9 | splitSignature, 10 | joinSignature, 11 | } = utils 12 | 13 | const SIG_SIZE_BYTES = 161 14 | const RECEIPT_SIZE_BYTES = 96 15 | const RECEIPT_TYPE_HASH = eip712.typeHash( 16 | 'Receipt(bytes32 requestCID,bytes32 responseCID,bytes32 subgraphDeploymentID)', 17 | ) 18 | 19 | export interface Receipt { 20 | requestCID: string 21 | responseCID: string 22 | subgraphDeploymentID: string 23 | } 24 | 25 | const SALT = '0xa070ffb1cd7409649bf77822cce74495468e06dbfaef09556838bf188679b9c2' 26 | 27 | const encodeReceipt = (receipt: Receipt): string => 28 | eip712.hashStruct( 29 | RECEIPT_TYPE_HASH, 30 | ['bytes32', 'bytes32', 'bytes32'], 31 | [receipt.requestCID, receipt.responseCID, receipt.subgraphDeploymentID], 32 | ) 33 | 34 | export interface Attestation { 35 | requestCID: string 36 | responseCID: string 37 | subgraphDeploymentID: string 38 | v: number 39 | r: string 40 | s: string 41 | } 42 | 43 | export const getDomainSeparator = ( 44 | chainId: number, 45 | disputeManagerAddress: string, 46 | version: string, 47 | ): string => { 48 | const domainSeparator = eip712.domainSeparator({ 49 | name: 'Graph Protocol', 50 | version, 51 | chainId, 52 | verifyingContract: disputeManagerAddress, 53 | salt: SALT, 54 | }) 55 | return domainSeparator 56 | } 57 | 58 | export const createAttestation = async ( 59 | signer: utils.BytesLike, 60 | chainId: number, 61 | disputeManagerAddress: string, 62 | receipt: Receipt, 63 | version: string, 64 | ): Promise => { 65 | const domainSeparator = getDomainSeparator(chainId, disputeManagerAddress, version) 66 | const encodedReceipt = encodeReceipt(receipt) 67 | const message = eip712.encode(domainSeparator, encodedReceipt) 68 | const messageHash = utils.keccak256(message) 69 | const signingKey = new utils.SigningKey(signer) 70 | const { r, s, v } = signingKey.signDigest(messageHash) 71 | 72 | return { 73 | requestCID: receipt.requestCID, 74 | responseCID: receipt.responseCID, 75 | subgraphDeploymentID: receipt.subgraphDeploymentID, 76 | v, 77 | r, 78 | s, 79 | } 80 | } 81 | 82 | export const encodeAttestation = (attestation: Attestation): string => { 83 | const data = arrayify( 84 | abi.encode( 85 | ['bytes32', 'bytes32', 'bytes32'], 86 | [attestation.requestCID, attestation.responseCID, attestation.subgraphDeploymentID], 87 | ), 88 | ) 89 | const sig = joinSignature(attestation) 90 | return hexlify(concat([data, sig])) 91 | } 92 | 93 | export const decodeAttestation = (attestationData: string): Attestation => { 94 | const attestationBytes = arrayify(attestationData) 95 | if (attestationBytes.length !== SIG_SIZE_BYTES) { 96 | throw new Error('Invalid signature length') 97 | } 98 | 99 | const [requestCID, responseCID, subgraphDeploymentID] = abi.decode( 100 | ['bytes32', 'bytes32', 'bytes32'], 101 | attestationBytes, 102 | ) 103 | const sig = splitSignature( 104 | attestationBytes.slice(RECEIPT_SIZE_BYTES, RECEIPT_SIZE_BYTES + SIG_SIZE_BYTES), 105 | ) 106 | 107 | return { 108 | requestCID, 109 | responseCID, 110 | subgraphDeploymentID, 111 | v: sig.v, 112 | r: sig.r, 113 | s: sig.s, 114 | } 115 | } 116 | 117 | export const recoverAttestation = ( 118 | chainId: number, 119 | disputeManagerAddress: string, 120 | attestation: Attestation, 121 | version: string, 122 | ): string => { 123 | const domainSeparator = getDomainSeparator(chainId, disputeManagerAddress, version) 124 | const receipt = { 125 | requestCID: attestation.requestCID, 126 | responseCID: attestation.responseCID, 127 | subgraphDeploymentID: attestation.subgraphDeploymentID, 128 | } 129 | const encodedReceipt = encodeReceipt(receipt) 130 | const message = eip712.encode(domainSeparator, encodedReceipt) 131 | const messageHash = utils.keccak256(message) 132 | return utils.recoverAddress( 133 | messageHash, 134 | joinSignature({ r: attestation.r, s: attestation.s, v: attestation.v }), 135 | ) 136 | } 137 | -------------------------------------------------------------------------------- /packages/common-ts/src/attestations/eip712.ts: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers' 2 | 3 | // Hashes a type signature based on the `typeHash` defined on 4 | // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-hashstruct. 5 | // 6 | // The type signature is expected to follow the `encodeType` format described on 7 | // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-encodetype. 8 | export const typeHash = (typeSignature: string): string => 9 | utils.keccak256(utils.toUtf8Bytes(typeSignature)) 10 | 11 | // Encodes a list of values according to the given types. 12 | // 13 | // See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-encodedata 14 | // for details. 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | const encodeData = (types: string[], values: any[]): string => { 17 | const transformedTypes = [] 18 | const transformedValues = [] 19 | 20 | // Values of types `bytes` and `strings` need to be hashed using keccak256 21 | for (let i = 0; i < types.length; i++) { 22 | if (types[i] === 'string' || types[i] === 'bytes') { 23 | transformedTypes[i] = 'bytes32' 24 | transformedValues[i] = utils.keccak256(utils.toUtf8Bytes(values[i])) 25 | } else { 26 | transformedTypes[i] = types[i] 27 | transformedValues[i] = values[i] 28 | } 29 | } 30 | 31 | return utils.defaultAbiCoder.encode(transformedTypes, transformedValues) 32 | } 33 | 34 | // Hashes a struct based on the hash of a type signature (see `typeHash`), 35 | // a list of struct field types and unencoded values for these fields. 36 | // 37 | // NOTE: Does not support recursion yet. 38 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 39 | export const hashStruct = (typeHash: string, types: string[], values: any[]): string => { 40 | return utils.keccak256(encodeData(['bytes32', ...types], [typeHash, ...values])) 41 | } 42 | 43 | const EIP712_DOMAIN_TYPE_HASH = typeHash( 44 | 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)', 45 | ) 46 | 47 | // An EIP-712 domain struct. 48 | export interface EIP712Domain { 49 | name: string 50 | version: string 51 | chainId: number 52 | verifyingContract: string 53 | salt: string 54 | } 55 | 56 | // Creates a domain separator from an EIP-712 domain struct. 57 | // 58 | // See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-domainseparator 59 | // for more details. 60 | export const domainSeparator = (domain: EIP712Domain): string => 61 | hashStruct( 62 | EIP712_DOMAIN_TYPE_HASH, 63 | ['string', 'string', 'uint256', 'address', 'bytes32'], 64 | [domain.name, domain.version, domain.chainId, domain.verifyingContract, domain.salt], 65 | ) 66 | 67 | // Encodes a message using a domain separator, as described on 68 | // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#specification. 69 | // 70 | // Assumes that the message has been encoded according to EIP-712, 71 | // e.g. using `hashStruct`. 72 | export const encode = (domainSeparator: string, message: string): string => 73 | '0x1901' + domainSeparator.substring(2) + message.substring(2) 74 | -------------------------------------------------------------------------------- /packages/common-ts/src/attestations/index.ts: -------------------------------------------------------------------------------- 1 | export * as eip712 from './eip712' 2 | export * from './attestations' 3 | -------------------------------------------------------------------------------- /packages/common-ts/src/contracts/chain.ts: -------------------------------------------------------------------------------- 1 | class MapWithGetKey extends Map { 2 | getKey(value: K): K | undefined { 3 | for (const [k, v] of this.entries()) { 4 | if (v === value) { 5 | return k 6 | } 7 | } 8 | return 9 | } 10 | } 11 | 12 | // List of supported L1 <> L2 chain mappings 13 | const chainMap = new MapWithGetKey([ 14 | [1, 42161], // Ethereum Mainnet - Arbitrum One 15 | [4, 421611], // Ethereum Rinkeby - Arbitrum Rinkeby 16 | [5, 421613], // Ethereum Goerli - Arbitrum Goerli 17 | [11155111, 421614], // Ethereum Sepolia - Arbitrum Sepolia 18 | [1337, 412346], // Localhost - Arbitrum Localhost 19 | ]) 20 | 21 | export const l1Chains = Array.from(chainMap.keys()) 22 | export const l2Chains = Array.from(chainMap.values()) 23 | export const chains = [...l1Chains, ...l2Chains] 24 | 25 | export const isL1 = (chainId: number): boolean => l1Chains.includes(chainId) 26 | export const isL2 = (chainId: number): boolean => l2Chains.includes(chainId) 27 | export const isSupported = (chainId: number | undefined): boolean => 28 | chainId !== undefined && chains.includes(chainId) 29 | 30 | export const l1ToL2 = (chainId: number): number | undefined => chainMap.get(chainId) 31 | export const l2ToL1 = (chainId: number): number | undefined => chainMap.getKey(chainId) 32 | export const counterpart = (chainId: number): number | undefined => { 33 | if (!isSupported(chainId)) return 34 | return isL1(chainId) ? l1ToL2(chainId) : l2ToL1(chainId) 35 | } 36 | 37 | export default { 38 | l1Chains, 39 | l2Chains, 40 | chains, 41 | isL1, 42 | isL2, 43 | isSupported, 44 | l1ToL2, 45 | l2ToL1, 46 | counterpart, 47 | } 48 | -------------------------------------------------------------------------------- /packages/common-ts/src/contracts/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from 'ethers' 2 | import { connectContracts } from '.' 3 | import * as DEPLOYED_CONTRACTS from '@graphprotocol/contracts/addresses.json' 4 | 5 | jest.mock('ethers') 6 | 7 | const mockSigner = jest.fn() as unknown as Signer 8 | 9 | describe('Contracts', () => { 10 | // Test for each supported protocol network 11 | test.each([1, 5, 42161, 421613, 421614, 11155111])( 12 | 'Connect contracts with explicit addressBook provided [chainId: %p]', 13 | chainId => { 14 | const contracts = connectContracts(mockSigner, chainId, DEPLOYED_CONTRACTS) 15 | expect(contracts).toBeDefined() 16 | }, 17 | ) 18 | test.each([1, 5, 42161, 421613, 421614, 11155111])( 19 | 'Connect contracts without explicit addressBook provided [chainId: %p]', 20 | chainId => { 21 | const contracts = connectContracts(mockSigner, chainId, undefined) 22 | expect(contracts).toBeDefined() 23 | }, 24 | ) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/common-ts/src/contracts/index.ts: -------------------------------------------------------------------------------- 1 | import { providers, Signer } from 'ethers' 2 | import graphChain from './chain' 3 | 4 | // Contract addresses 5 | import * as DEPLOYED_CONTRACTS from '@graphprotocol/contracts/addresses.json' 6 | 7 | // Contract ABIs 8 | import { Curation } from '@graphprotocol/contracts/dist/types/Curation' 9 | import { DisputeManager } from '@graphprotocol/contracts/dist/types/DisputeManager' 10 | import { EpochManager } from '@graphprotocol/contracts/dist/types/EpochManager' 11 | import { GNS } from '@graphprotocol/contracts/dist/types/GNS' 12 | import { RewardsManager } from '@graphprotocol/contracts/dist/types/RewardsManager' 13 | import { ServiceRegistry } from '@graphprotocol/contracts/dist/types/ServiceRegistry' 14 | import { IL1Staking } from '@graphprotocol/contracts/dist/types/IL1Staking' 15 | import { IL2Staking } from '@graphprotocol/contracts/dist/types/IL2Staking' 16 | import { GraphToken } from '@graphprotocol/contracts/dist/types/GraphToken' 17 | import { Controller } from '@graphprotocol/contracts/dist/types/Controller' 18 | import { AllocationExchange } from '@graphprotocol/contracts/dist/types/AllocationExchange' 19 | import { GraphProxyAdmin } from '@graphprotocol/contracts/dist/types/GraphProxyAdmin' 20 | import { SubgraphNFT } from '@graphprotocol/contracts/dist/types/SubgraphNFT' 21 | import { GraphCurationToken } from '@graphprotocol/contracts/dist/types/GraphCurationToken' 22 | import { L1GraphTokenGateway } from '@graphprotocol/contracts/dist/types/L1GraphTokenGateway' 23 | import { BridgeEscrow } from '@graphprotocol/contracts/dist/types/BridgeEscrow' 24 | import { L2GraphToken } from '@graphprotocol/contracts/dist/types/L2GraphToken' 25 | import { L2GraphTokenGateway } from '@graphprotocol/contracts/dist/types/L2GraphTokenGateway' 26 | import { L2Curation } from '@graphprotocol/contracts/dist/types/L2Curation' 27 | 28 | // Contract factories 29 | import { Curation__factory } from '@graphprotocol/contracts/dist/types/factories/Curation__factory' 30 | import { L2Curation__factory } from '@graphprotocol/contracts/dist/types/factories/L2Curation__factory' 31 | import { DisputeManager__factory } from '@graphprotocol/contracts/dist/types/factories/DisputeManager__factory' 32 | import { EpochManager__factory } from '@graphprotocol/contracts/dist/types/factories/EpochManager__factory' 33 | import { GNS__factory } from '@graphprotocol/contracts/dist/types/factories/GNS__factory' 34 | import { RewardsManager__factory } from '@graphprotocol/contracts/dist/types/factories/RewardsManager__factory' 35 | import { ServiceRegistry__factory } from '@graphprotocol/contracts/dist/types/factories/ServiceRegistry__factory' 36 | import { IL1Staking__factory } from '@graphprotocol/contracts/dist/types/factories/IL1Staking__factory' 37 | import { IL2Staking__factory } from '@graphprotocol/contracts/dist/types/factories/IL2Staking__factory' 38 | import { GraphToken__factory } from '@graphprotocol/contracts/dist/types/factories/GraphToken__factory' 39 | import { Controller__factory } from '@graphprotocol/contracts/dist/types/factories/Controller__factory' 40 | import { AllocationExchange__factory } from '@graphprotocol/contracts/dist/types/factories/AllocationExchange__factory' 41 | import { GraphProxyAdmin__factory } from '@graphprotocol/contracts/dist/types/factories/GraphProxyAdmin__factory' 42 | import { SubgraphNFT__factory } from '@graphprotocol/contracts/dist/types/factories/SubgraphNFT__factory' 43 | import { GraphCurationToken__factory } from '@graphprotocol/contracts/dist/types/factories/GraphCurationToken__factory' 44 | import { L1GraphTokenGateway__factory } from '@graphprotocol/contracts/dist/types/factories/L1GraphTokenGateway__factory' 45 | import { BridgeEscrow__factory } from '@graphprotocol/contracts/dist/types/factories/BridgeEscrow__factory' 46 | import { L2GraphToken__factory } from '@graphprotocol/contracts/dist/types/factories/L2GraphToken__factory' 47 | import { L2GraphTokenGateway__factory } from '@graphprotocol/contracts/dist/types/factories/L2GraphTokenGateway__factory' 48 | 49 | export const GraphChain = graphChain 50 | 51 | export interface NetworkContracts { 52 | curation: Curation | L2Curation 53 | disputeManager: DisputeManager 54 | epochManager: EpochManager 55 | gns: GNS 56 | rewardsManager: RewardsManager 57 | serviceRegistry: ServiceRegistry 58 | staking: IL1Staking | IL2Staking 59 | token: GraphToken | L2GraphToken 60 | controller: Controller 61 | allocationExchange: AllocationExchange 62 | graphProxyAdmin: GraphProxyAdmin 63 | subgraphNFT: SubgraphNFT 64 | graphCurationToken: GraphCurationToken 65 | 66 | // Only L1 67 | l1GraphTokenGateway?: L1GraphTokenGateway 68 | bridgeEscrow?: BridgeEscrow 69 | 70 | // Only L2 71 | l2GraphTokenGateway?: L2GraphTokenGateway 72 | } 73 | 74 | export type AddressBook = { [key: string]: { [key: string]: { address: string } } } 75 | 76 | export const connectContracts = async ( 77 | providerOrSigner: providers.Provider | Signer, 78 | chainId: number, 79 | addressBook: AddressBook | undefined, 80 | ): Promise => { 81 | const deployedContracts = addressBook 82 | ? addressBook[`${chainId}`] 83 | : // eslint-disable-next-line @typescript-eslint/no-explicit-any 84 | (DEPLOYED_CONTRACTS as any)[`${chainId}`] 85 | if (!deployedContracts) { 86 | throw new Error(`chainId: '${chainId}' has no deployed contracts`) 87 | } 88 | 89 | const getContractAddress = (contractName: string) => { 90 | if (!deployedContracts[contractName]) { 91 | throw new Error( 92 | `Deployed contract '${contractName}' is undefined for chainId: '${chainId}'`, 93 | ) 94 | } 95 | const address = deployedContracts[contractName].address 96 | if (!address) { 97 | throw new Error( 98 | `Deployed contract '${contractName}' address is undefined for chainId: '${chainId}'`, 99 | ) 100 | } 101 | return address 102 | } 103 | 104 | const GraphTokenFactory = GraphChain.isL1(chainId) 105 | ? GraphToken__factory 106 | : L2GraphToken__factory 107 | 108 | const graphTokenAddress = GraphChain.isL1(chainId) 109 | ? getContractAddress('GraphToken') 110 | : getContractAddress('L2GraphToken') 111 | 112 | const staking = GraphChain.isL1(chainId) 113 | ? IL1Staking__factory.connect(getContractAddress('L1Staking'), providerOrSigner) 114 | : IL2Staking__factory.connect(getContractAddress('L2Staking'), providerOrSigner) 115 | 116 | const gns = GraphChain.isL1(chainId) 117 | ? GNS__factory.connect(getContractAddress('L1GNS'), providerOrSigner) 118 | : GNS__factory.connect(getContractAddress('L2GNS'), providerOrSigner) 119 | 120 | const curation = GraphChain.isL1(chainId) 121 | ? Curation__factory.connect(getContractAddress('Curation'), providerOrSigner) 122 | : L2Curation__factory.connect(getContractAddress('L2Curation'), providerOrSigner) 123 | 124 | const contracts: NetworkContracts = { 125 | disputeManager: DisputeManager__factory.connect( 126 | getContractAddress('DisputeManager'), 127 | providerOrSigner, 128 | ), 129 | epochManager: EpochManager__factory.connect( 130 | getContractAddress('EpochManager'), 131 | providerOrSigner, 132 | ), 133 | gns, 134 | curation, 135 | rewardsManager: RewardsManager__factory.connect( 136 | getContractAddress('RewardsManager'), 137 | providerOrSigner, 138 | ), 139 | serviceRegistry: ServiceRegistry__factory.connect( 140 | getContractAddress('ServiceRegistry'), 141 | providerOrSigner, 142 | ), 143 | staking, 144 | token: GraphTokenFactory.connect(graphTokenAddress, providerOrSigner), 145 | controller: Controller__factory.connect( 146 | getContractAddress('Controller'), 147 | providerOrSigner, 148 | ), 149 | allocationExchange: AllocationExchange__factory.connect( 150 | getContractAddress('AllocationExchange'), 151 | providerOrSigner, 152 | ), 153 | graphProxyAdmin: GraphProxyAdmin__factory.connect( 154 | getContractAddress('GraphProxyAdmin'), 155 | providerOrSigner, 156 | ), 157 | subgraphNFT: SubgraphNFT__factory.connect( 158 | getContractAddress('SubgraphNFT'), 159 | providerOrSigner, 160 | ), 161 | graphCurationToken: GraphCurationToken__factory.connect( 162 | getContractAddress('GraphCurationToken'), 163 | providerOrSigner, 164 | ), 165 | } 166 | 167 | if (GraphChain.isL1(chainId)) { 168 | if (deployedContracts.L1GraphTokenGateway) { 169 | contracts.l1GraphTokenGateway = L1GraphTokenGateway__factory.connect( 170 | getContractAddress('L1GraphTokenGateway'), 171 | providerOrSigner, 172 | ) 173 | } 174 | if (deployedContracts.BridgeEscrow) { 175 | contracts.bridgeEscrow = BridgeEscrow__factory.connect( 176 | getContractAddress('BridgeEscrow'), 177 | providerOrSigner, 178 | ) 179 | } 180 | } else if (GraphChain.isL2(chainId)) { 181 | if (deployedContracts.L2GraphTokenGateway) { 182 | contracts.l2GraphTokenGateway = L2GraphTokenGateway__factory.connect( 183 | getContractAddress('L2GraphTokenGateway'), 184 | providerOrSigner, 185 | ) 186 | } 187 | } 188 | 189 | return contracts 190 | } 191 | -------------------------------------------------------------------------------- /packages/common-ts/src/database/index.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | import { connectDatabase } from '.' 4 | 5 | // Make global Jest variable available 6 | declare const __DATABASE__: any 7 | 8 | describe('Database', () => { 9 | test('Connect', async () => { 10 | const sequelize = await connectDatabase(__DATABASE__) 11 | expect(sequelize).toBeDefined() 12 | }) 13 | 14 | test('Connect with options set', async () => { 15 | const sequelize = await connectDatabase({ 16 | host: 'localhost', 17 | username: 'test', 18 | password: 'test', 19 | database: 'test', 20 | sslEnabled: true, 21 | logging: () => {}, 22 | poolMin: 1, 23 | poolMax: 5, 24 | }) 25 | 26 | expect(sequelize).toBeDefined() 27 | 28 | const poolConfig = sequelize.config.pool 29 | expect(poolConfig?.min).toBe(1) 30 | expect(poolConfig?.max).toBe(5) 31 | 32 | const sslConfig = sequelize.config.ssl 33 | expect(sslConfig).toBe(true) 34 | }) 35 | 36 | test('Connect with default options', async () => { 37 | const sequelize = await connectDatabase({ 38 | host: 'localhost', 39 | username: 'test', 40 | password: 'test', 41 | database: 'test', 42 | logging: () => {}, 43 | }) 44 | 45 | expect(sequelize).toBeDefined() 46 | 47 | const poolConfig = sequelize.config.pool 48 | expect(poolConfig?.min).toBe(0) 49 | expect(poolConfig?.max).toBe(10) 50 | 51 | const sslConfig = sequelize.config.ssl 52 | expect(sslConfig).toBe(false) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /packages/common-ts/src/database/index.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize' 2 | 3 | interface ConnectOptions { 4 | host: string 5 | port?: number 6 | username: string 7 | password: string 8 | database: string 9 | sslEnabled?: boolean 10 | logging?: (sql: string, timing?: number) => void 11 | poolMin?: number 12 | poolMax?: number 13 | } 14 | 15 | export const connectDatabase = async (options: ConnectOptions): Promise => { 16 | const { host, username, password, database, logging } = options 17 | 18 | // Use port 5432 by default 19 | const port = options.port || 5432 20 | const poolMin = options.poolMin || 0 21 | const poolMax = options.poolMax || 10 22 | const sslEnabled = options.sslEnabled || false 23 | 24 | // Connect to the database 25 | const sequelize = new Sequelize({ 26 | dialect: 'postgres', 27 | host, 28 | port, 29 | username, 30 | password, 31 | database, 32 | ssl: sslEnabled, 33 | pool: { 34 | max: poolMax, 35 | min: poolMin, 36 | }, 37 | logging, 38 | }) 39 | 40 | // Test the connection 41 | await sequelize.authenticate() 42 | 43 | // All good, return the connection 44 | return sequelize 45 | } 46 | -------------------------------------------------------------------------------- /packages/common-ts/src/eventual/__tests__/eventual.ts: -------------------------------------------------------------------------------- 1 | import { join, mutable, timer, WritableEventual } from '../eventual' 2 | 3 | describe('Eventual', () => { 4 | test('Value', async () => { 5 | const eventual = mutable([1, 2, 3]) 6 | 7 | // Test that we can read the same value twice 8 | await expect(eventual.value()).resolves.toStrictEqual([1, 2, 3]) 9 | await expect(eventual.value()).resolves.toStrictEqual([1, 2, 3]) 10 | 11 | eventual.push([1, 2, 3, 4]) 12 | 13 | // Test that we can read the new value twice 14 | await expect(eventual.value()).resolves.toStrictEqual([1, 2, 3, 4]) 15 | await expect(eventual.value()).resolves.toStrictEqual([1, 2, 3, 4]) 16 | }) 17 | 18 | test('Map (one consumer)', async () => { 19 | const lower = mutable(['a', 'b', 'c']) 20 | const upper = lower.map(values => values.map(v => v.toUpperCase())) 21 | 22 | await expect(lower.value()).resolves.toStrictEqual(['a', 'b', 'c']) 23 | await expect(upper.value()).resolves.toStrictEqual(['A', 'B', 'C']) 24 | 25 | lower.push(['c', 'd']) 26 | 27 | await expect(lower.value()).resolves.toStrictEqual(['c', 'd']) 28 | await expect(upper.value()).resolves.toStrictEqual(['C', 'D']) 29 | }) 30 | 31 | test('Map (two consumers)', async () => { 32 | const lower = mutable(['a', 'b', 'c']) 33 | const upper = lower.map(values => values.map(v => v.toUpperCase())) 34 | const codes = lower.map(values => values.map(v => v.charCodeAt(0))) 35 | 36 | await expect(lower.value()).resolves.toStrictEqual(['a', 'b', 'c']) 37 | await expect(upper.value()).resolves.toStrictEqual(['A', 'B', 'C']) 38 | await expect(codes.value()).resolves.toStrictEqual([97, 98, 99]) 39 | 40 | lower.push(['c', 'd']) 41 | 42 | await expect(lower.value()).resolves.toStrictEqual(['c', 'd']) 43 | await expect(upper.value()).resolves.toStrictEqual(['C', 'D']) 44 | await expect(codes.value()).resolves.toStrictEqual([99, 100]) 45 | }) 46 | 47 | test('Map (chained consumers)', async () => { 48 | const lower = mutable(['a', 'b', 'c']) 49 | const upper = lower.map(values => values.map(v => v.toUpperCase())) 50 | const codes = upper.map(values => values.map(v => v.charCodeAt(0))) 51 | 52 | await expect(lower.value()).resolves.toStrictEqual(['a', 'b', 'c']) 53 | await expect(upper.value()).resolves.toStrictEqual(['A', 'B', 'C']) 54 | await expect(codes.value()).resolves.toStrictEqual([65, 66, 67]) 55 | 56 | lower.push(['c', 'd']) 57 | 58 | await expect(lower.value()).resolves.toStrictEqual(['c', 'd']) 59 | await expect(upper.value()).resolves.toStrictEqual(['C', 'D']) 60 | await expect(codes.value()).resolves.toStrictEqual([67, 68]) 61 | }) 62 | 63 | test('Try map', async () => { 64 | const numbers: WritableEventual = mutable() 65 | const errors: string[] = [] 66 | const onError = (err: string) => errors.push(err) 67 | 68 | const evenNumbersOnly = numbers.tryMap( 69 | n => { 70 | if (n % 2 === 0) { 71 | return n 72 | } else { 73 | throw `${n} is odd` 74 | } 75 | }, 76 | { onError }, 77 | ) 78 | 79 | for (let i = 0; i < 10; i++) { 80 | numbers.push(i) 81 | await expect(numbers.value()).resolves.toStrictEqual(i) 82 | await expect(evenNumbersOnly.value()).resolves.toStrictEqual( 83 | i % 2 === 0 ? i : i - 1, 84 | ) 85 | } 86 | 87 | expect(errors).toStrictEqual([ 88 | `1 is odd`, 89 | `3 is odd`, 90 | `5 is odd`, 91 | `7 is odd`, 92 | `9 is odd`, 93 | ]) 94 | }) 95 | 96 | test('Filter (even and odd)', async () => { 97 | const source = mutable(0) 98 | const even = source.filter(value => value % 2 === 0) 99 | const odd = source.filter(value => value % 2 !== 0) 100 | 101 | await expect(source.value()).resolves.toStrictEqual(0) 102 | await expect(even.value()).resolves.toStrictEqual(0) 103 | // Note: we cannot check the value of `odd` here because it would block indefinitely 104 | 105 | for (let i = 1; i < 10; i++) { 106 | source.push(i) 107 | 108 | // Always expect the latest value in `source`, the most recent even value 109 | // in `even` and the most recent odd value in `odd`. 110 | await expect(source.value()).resolves.toStrictEqual(i) 111 | await expect(even.value()).resolves.toStrictEqual(i % 2 === 0 ? i : i - 1) 112 | await expect(odd.value()).resolves.toStrictEqual(i % 2 === 0 ? i - 1 : i) 113 | } 114 | }) 115 | 116 | test('Throttle', async () => { 117 | const lower = mutable(['a', 'b', 'c']) 118 | const throttled = lower.throttle(500) 119 | 120 | await expect(lower.value()).resolves.toStrictEqual(['a', 'b', 'c']) 121 | await expect(throttled.value()).resolves.toStrictEqual(['a', 'b', 'c']) 122 | 123 | lower.push(['c', 'd']) 124 | lower.push(['d', 'e']) 125 | lower.push(['e', 'f']) 126 | 127 | await expect(lower.value()).resolves.toStrictEqual(['e', 'f']) 128 | await expect(throttled.value()).resolves.toStrictEqual(['a', 'b', 'c']) 129 | 130 | await new Promise(resolve => setTimeout(resolve, 600)) 131 | 132 | await expect(throttled.value()).resolves.toStrictEqual(['e', 'f']) 133 | }) 134 | 135 | test('Throttle and map', async () => { 136 | const lower = mutable(['a', 'b', 'c']) 137 | const throttled = lower.throttle(500).map(values => values.map(v => v.toUpperCase())) 138 | 139 | await expect(lower.value()).resolves.toStrictEqual(['a', 'b', 'c']) 140 | await expect(throttled.value()).resolves.toStrictEqual(['A', 'B', 'C']) 141 | 142 | lower.push(['c', 'd']) 143 | lower.push(['d', 'e']) 144 | lower.push(['e', 'f']) 145 | 146 | await expect(lower.value()).resolves.toStrictEqual(['e', 'f']) 147 | await expect(throttled.value()).resolves.toStrictEqual(['A', 'B', 'C']) 148 | 149 | await new Promise(resolve => setTimeout(resolve, 600)) 150 | 151 | await expect(throttled.value()).resolves.toStrictEqual(['E', 'F']) 152 | }) 153 | 154 | test('Pipe', async () => { 155 | const lower = mutable(['a', 'b', 'c']) 156 | 157 | const piped = [] as string[][] 158 | lower.pipe(values => { 159 | piped.push(values) 160 | }) 161 | 162 | await expect(lower.value()).resolves.toStrictEqual(['a', 'b', 'c']) 163 | expect(piped).toStrictEqual([['a', 'b', 'c']]) 164 | 165 | lower.push(['c', 'd']) 166 | await new Promise(resolve => setTimeout(resolve, 500)) 167 | expect(piped).toStrictEqual([ 168 | ['a', 'b', 'c'], 169 | ['c', 'd'], 170 | ]) 171 | 172 | lower.push(['e', 'f']) 173 | await new Promise(resolve => setTimeout(resolve, 500)) 174 | expect(piped).toStrictEqual([ 175 | ['a', 'b', 'c'], 176 | ['c', 'd'], 177 | ['e', 'f'], 178 | ]) 179 | }) 180 | 181 | test('Pipe (no race conditions)', async () => { 182 | const lower = mutable(['a', 'b', 'c']) 183 | let slowUpperRunning = false 184 | lower.pipe(async () => { 185 | expect(slowUpperRunning).toBeFalsy() 186 | slowUpperRunning = true 187 | await new Promise(resolve => setTimeout(resolve, 500)).then(() => { 188 | slowUpperRunning = false 189 | }) 190 | }) 191 | 192 | lower.push(['d']) 193 | lower.push(['e']) 194 | 195 | await expect(lower.value()).resolves.toStrictEqual(['e']) 196 | await new Promise(resolve => setTimeout(resolve, 1500)) 197 | }) 198 | 199 | test('Equality with Map objects', async () => { 200 | const source = mutable(new Map([['y', 'x']])) 201 | await expect(source.value()).resolves.toStrictEqual( 202 | new Map([['y', 'x']]), 203 | ) 204 | source.push(new Map([['x', 'y']])) 205 | 206 | // This tests that JS Map objects are compared for equality/inequality 207 | // accurately; we had a bug initially where the comparison was based on 208 | // JSON.stringify, which for Map always returns {}. 209 | await expect(source.value()).resolves.toStrictEqual( 210 | new Map([['x', 'y']]), 211 | ) 212 | }) 213 | 214 | test('Reduce', async () => { 215 | const source = mutable(['a', 'b']) 216 | const reduced = source.reduce((s, values) => s + values.join(''), '') 217 | 218 | await expect(source.value()).resolves.toStrictEqual(['a', 'b']) 219 | await expect(reduced.value()).resolves.toStrictEqual('ab') 220 | 221 | source.push(['c', 'd']) 222 | 223 | await expect(source.value()).resolves.toStrictEqual(['c', 'd']) 224 | await expect(reduced.value()).resolves.toStrictEqual('abcd') 225 | 226 | source.push(['e']) 227 | 228 | await expect(source.value()).resolves.toStrictEqual(['e']) 229 | await expect(reduced.value()).resolves.toStrictEqual('abcde') 230 | }) 231 | 232 | test('Join (all values ready)', async () => { 233 | const letters = mutable(['a', 'b']) 234 | const numbers = mutable([1, 2]) 235 | const joined = join({ letters, numbers }) 236 | 237 | await expect(joined.value()).resolves.toStrictEqual({ 238 | letters: ['a', 'b'], 239 | numbers: [1, 2], 240 | }) 241 | }) 242 | 243 | test('Join (not all values ready initally)', async () => { 244 | const letters = mutable(['a', 'b']) 245 | const numbers = mutable([1, 2]) 246 | const delayed = mutable() 247 | const joined = join({ letters, numbers, delayed }) 248 | 249 | setTimeout(() => delayed.push('ready now'), 500) 250 | 251 | await expect(joined.value()).resolves.toStrictEqual({ 252 | letters: ['a', 'b'], 253 | numbers: [1, 2], 254 | delayed: 'ready now', 255 | }) 256 | }) 257 | 258 | test('Join (multiple updates)', async () => { 259 | const letters = mutable(['a', 'b']) 260 | const numbers = mutable([1, 2]) 261 | const delayed = mutable() 262 | 263 | const joined = join({ letters, numbers, delayed }) 264 | 265 | setTimeout(() => delayed.push('ready now'), 500) 266 | 267 | // Update letters again before the delayed eventual has a value; this should 268 | // not update the join result before the delayed eventual is ready 269 | letters.push(['c', 'd']) 270 | 271 | await expect(joined.value()).resolves.toStrictEqual({ 272 | letters: ['c', 'd'], 273 | numbers: [1, 2], 274 | delayed: 'ready now', 275 | }) 276 | 277 | // Update letters again after the delayed eventual has a value; this should 278 | // update the join result as in any regular case 279 | letters.push(['e', 'f']) 280 | 281 | await expect(joined.value()).resolves.toStrictEqual({ 282 | letters: ['e', 'f'], 283 | numbers: [1, 2], 284 | delayed: 'ready now', 285 | }) 286 | }) 287 | 288 | test('Join (timer)', async () => { 289 | const ticker = timer(100) 290 | const ticks = ticker.reduce(n => ++n, 0) 291 | const ticksViaJoin = join({ ticker }).reduce(n => ++n, 0) 292 | 293 | await new Promise(resolve => setTimeout(resolve, 1000)) 294 | 295 | // We should have seen 9-10 timer events, but as long as we've seen a few 296 | // we're happy 297 | await expect(ticks.value()).resolves.toBeGreaterThan(5) 298 | await expect(ticksViaJoin.value()).resolves.toBeGreaterThan(5) 299 | }) 300 | 301 | test('Values (async generator)', async () => { 302 | const values = Array.from(Array(100).keys()) 303 | 304 | const numbers: WritableEventual = mutable() 305 | for (const value of values) { 306 | setTimeout(() => numbers.push(value), 50 + value * 20) 307 | } 308 | 309 | let prev = -1 310 | for await (const value of numbers.values()) { 311 | expect(value).toBeGreaterThan(prev) 312 | prev = value 313 | if (value >= 99) { 314 | break 315 | } 316 | } 317 | }) 318 | 319 | test('Values (async generator, subscribed after last value)', async () => { 320 | const numbers: WritableEventual = mutable(0) 321 | numbers.push(1) 322 | await expect(numbers.values().next()).resolves.toStrictEqual({ 323 | done: false, 324 | value: 1, 325 | }) 326 | }) 327 | }) 328 | -------------------------------------------------------------------------------- /packages/common-ts/src/eventual/eventual.ts: -------------------------------------------------------------------------------- 1 | import { equal } from '../util' 2 | 3 | export type Awaitable = T | PromiseLike 4 | export type Mapper = (t: T) => Awaitable 5 | export type Filter = (t: T) => Awaitable 6 | export type Reducer = (acc: U, t: T) => Awaitable 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | export type NamedEventuals = { [k: string]: Eventual } & { [K in keyof T]: T[K] } 10 | 11 | export type Join = 12 | Eventual ? U : any }> 14 | 15 | type MutableJoin = 16 | WritableEventual ? U : any }> 18 | 19 | export interface TryMapOptions { 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | onError: (err: any) => void 22 | } 23 | 24 | export type Subscriber = (value: T) => void 25 | 26 | export interface Eventual { 27 | readonly valueReady: boolean 28 | value(): Promise 29 | 30 | subscribe(subscriber: Subscriber): void 31 | 32 | map(f: Mapper): Eventual 33 | tryMap(f: Mapper, options: TryMapOptions): Eventual 34 | filter(f: Filter): Eventual 35 | pipe(f: (t: T) => Awaitable): void 36 | throttle(interval: number): Eventual 37 | reduce(f: Reducer, initial: U): Eventual 38 | 39 | // An asynchronous generator over values pushed into the Eventual 40 | // over time. Note: There is no guarantee that all values will be 41 | // emitted; some may be skipped e.g. if multiple different values 42 | // are pushed into the eventual at almost the same time; only the 43 | // last of these may be emitted in this case. 44 | values(): AsyncGenerator 45 | } 46 | 47 | export interface WritableEventual extends Eventual { 48 | push(value: T): void 49 | } 50 | 51 | export class EventualValue implements WritableEventual { 52 | inner: T | undefined 53 | 54 | promise: Promise | undefined 55 | resolvePromise?: (value: T) => void 56 | 57 | subscribers: Subscriber[] = [] 58 | 59 | constructor(initial?: T) { 60 | this.inner = initial 61 | this.promise = new Promise(resolve => { 62 | if (this.inner !== undefined) { 63 | resolve(this.inner) 64 | } else { 65 | this.resolvePromise = resolve 66 | } 67 | }) 68 | } 69 | 70 | get valueReady(): boolean { 71 | return this.inner !== undefined 72 | } 73 | 74 | value(): Promise { 75 | if (this.promise) { 76 | return this.promise 77 | } else { 78 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 79 | return Promise.resolve(this.inner!) 80 | } 81 | } 82 | 83 | subscribe(subscriber: Subscriber): void { 84 | this.subscribers.push(subscriber) 85 | if (this.inner !== undefined) { 86 | subscriber(this.inner) 87 | } 88 | } 89 | 90 | push(value: T): void { 91 | if (!equal(this.inner, value)) { 92 | this.inner = value 93 | this.resolvePromise?.call(this, value) 94 | this.promise = undefined 95 | this.resolvePromise = undefined 96 | this.subscribers.forEach(subscriber => subscriber(value)) 97 | } 98 | } 99 | 100 | map(f: Mapper): Eventual { 101 | return map(this, f) 102 | } 103 | 104 | tryMap(f: Mapper, options: TryMapOptions): Eventual { 105 | return tryMap(this, f, options) 106 | } 107 | 108 | filter(f: Filter): Eventual { 109 | return filter(this, f) 110 | } 111 | 112 | pipe(f: (t: T) => Awaitable): void { 113 | return pipe(this, f) 114 | } 115 | 116 | throttle(interval: number): Eventual { 117 | return throttle(this, interval) 118 | } 119 | 120 | reduce(f: Reducer, initial: U): Eventual { 121 | return reduce(this, f, initial) 122 | } 123 | 124 | async *values(): AsyncGenerator { 125 | // Creates a promise and exposes its `resolve` method so it can be triggered 126 | // externally 127 | function defer() { 128 | let resolve: ((t: T) => void) | null = null 129 | const promise = new Promise(_resolve => { 130 | resolve = _resolve 131 | }) 132 | 133 | const deferred: { 134 | promise: Promise 135 | resolve: (t: T) => void 136 | } = { 137 | promise, 138 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 139 | resolve: resolve!, 140 | } 141 | 142 | return deferred 143 | } 144 | 145 | // Create the initial promise 146 | let next = defer() 147 | 148 | // Delay this ever so slightly to allow `await next.promise` to be executed 149 | // before we resolve the first value. Otherwise we'd skip the value at the 150 | // time `values()` is called, because `await next.promise` would await the 151 | // second, not the initial promise. 152 | setTimeout( 153 | () => 154 | // Whenever there is a new value, resolve the current promise 155 | // and replace it with a new one 156 | this.pipe(t => { 157 | next.resolve(t) 158 | next = defer() 159 | }), 160 | 0, 161 | ) 162 | 163 | while (true) { 164 | yield await next.promise 165 | } 166 | } 167 | } 168 | 169 | export function mutable(initial?: T): WritableEventual { 170 | return new EventualValue(initial) 171 | } 172 | 173 | export function map( 174 | source: Eventual, 175 | mapper: (t: T) => Awaitable, 176 | ): Eventual { 177 | const output: WritableEventual = mutable() 178 | 179 | let previousT: T | undefined 180 | let latestT: T | undefined 181 | let mapPromise: Promise | undefined 182 | 183 | source.subscribe(t => { 184 | latestT = t 185 | if (mapPromise === undefined) { 186 | mapPromise = (async () => { 187 | while (!equal(latestT, previousT)) { 188 | previousT = latestT 189 | output.push(await mapper(latestT)) 190 | } 191 | mapPromise = undefined 192 | })() 193 | } 194 | }) 195 | 196 | return output 197 | } 198 | 199 | export function tryMap( 200 | source: Eventual, 201 | mapper: (t: T) => Awaitable, 202 | { onError }: TryMapOptions, 203 | ): Eventual { 204 | const output: WritableEventual = mutable() 205 | 206 | let previousT: T | undefined 207 | let latestT: T | undefined 208 | let promiseActive = false 209 | 210 | source.subscribe(t => { 211 | latestT = t 212 | if (!promiseActive) { 213 | promiseActive = true 214 | ;(async () => { 215 | while (!equal(latestT, previousT)) { 216 | try { 217 | previousT = latestT 218 | output.push(await mapper(latestT)) 219 | } catch (err) { 220 | onError(err) 221 | } 222 | } 223 | promiseActive = false 224 | })() 225 | } 226 | }) 227 | 228 | return output 229 | } 230 | 231 | export function filter(source: Eventual, f: Filter): Eventual { 232 | const output: WritableEventual = mutable() 233 | 234 | let previousT: T | undefined 235 | let latestT: T | undefined 236 | let mapPromise: Promise | undefined 237 | 238 | source.subscribe(t => { 239 | latestT = t 240 | if (mapPromise === undefined) { 241 | mapPromise = (async () => { 242 | while (!equal(latestT, previousT)) { 243 | previousT = latestT 244 | if (await f(latestT)) { 245 | output.push(latestT) 246 | } 247 | } 248 | mapPromise = undefined 249 | })() 250 | } 251 | }) 252 | 253 | return output 254 | } 255 | 256 | export function pipe(source: Eventual, fn: (t: T) => Awaitable): void { 257 | map(source, fn) 258 | } 259 | 260 | export function throttle(source: Eventual, interval: number): Eventual { 261 | const output: WritableEventual = mutable() 262 | 263 | let latestT: T | undefined 264 | let timeout: NodeJS.Timeout | undefined 265 | let lastPushed = Date.now() 266 | 267 | source.subscribe(t => { 268 | if (!output.valueReady) { 269 | latestT = t 270 | output.push(t) 271 | lastPushed = Date.now() 272 | } else if (!equal(t, latestT)) { 273 | latestT = t 274 | 275 | if (!timeout) { 276 | timeout = setTimeout( 277 | () => { 278 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 279 | output.push(latestT!) 280 | lastPushed = Date.now() 281 | timeout = undefined 282 | }, 283 | Math.max(0, Math.min(interval, Date.now() - lastPushed)), 284 | ) 285 | } 286 | } 287 | }) 288 | 289 | return output 290 | } 291 | 292 | export function timer(milliseconds: number): Eventual { 293 | const time = mutable(Date.now()) 294 | setInterval(() => time.push(Date.now()), milliseconds) 295 | return time 296 | } 297 | 298 | export function reduce( 299 | source: Eventual, 300 | reducer: (acc: U, t: T) => Awaitable, 301 | initial: U, 302 | ): Eventual { 303 | const output = mutable(initial) 304 | 305 | let acc: U = initial 306 | let previousT: T | undefined 307 | let latestT: T | undefined 308 | 309 | let promiseActive = false 310 | 311 | source.subscribe(t => { 312 | latestT = t 313 | if (!promiseActive) { 314 | promiseActive = true 315 | ;(async () => { 316 | while (!equal(latestT, previousT)) { 317 | previousT = latestT 318 | acc = await reducer(acc, latestT) 319 | output.push(acc) 320 | } 321 | promiseActive = false 322 | })() 323 | } 324 | }) 325 | 326 | return output 327 | } 328 | 329 | export function join(sources: NamedEventuals): Join { 330 | const output: MutableJoin = mutable() 331 | 332 | const keys = Object.keys(sources) as Array 333 | 334 | const sourceValues: { 335 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 336 | [key in keyof T]: T[key] extends Eventual ? U : any 337 | } = keys.reduce((out, key) => { 338 | out[key] = undefined 339 | return out 340 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 341 | }, {} as any) 342 | 343 | for (const key of keys) { 344 | sources[key].subscribe(value => { 345 | sourceValues[key] = value 346 | 347 | if (!keys.some(key => sourceValues[key] === undefined)) { 348 | // NOTE: creating a new JS object is important, otherwise 349 | // `output.inner` and `sourceValues` will be the same object 350 | // and therefore always be considered identical 351 | output.push({ ...sourceValues }) 352 | } 353 | }) 354 | } 355 | 356 | return output 357 | } 358 | -------------------------------------------------------------------------------- /packages/common-ts/src/eventual/index.ts: -------------------------------------------------------------------------------- 1 | export * from './eventual' 2 | -------------------------------------------------------------------------------- /packages/common-ts/src/grt/index.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from 'ethers' 2 | import { parseUnits, formatUnits } from 'ethers/lib/utils' 3 | 4 | export const formatGRT = (value: BigNumberish): string => formatUnits(value, 18) 5 | 6 | export const parseGRT = (grt: string): BigNumber => parseUnits(grt, 18) 7 | -------------------------------------------------------------------------------- /packages/common-ts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logging' 2 | export * from './metrics' 3 | export * from './database' 4 | export * from './attestations' 5 | export * from './contracts' 6 | export * from './subgraph' 7 | export * from './subgraphs' 8 | export * from './grt' 9 | export * from './security' 10 | export * from './eventual' 11 | export * from './util' 12 | -------------------------------------------------------------------------------- /packages/common-ts/src/logging/index.test.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '.' 2 | 3 | describe('Logging', () => { 4 | test('Create logger', () => { 5 | const logger = createLogger({ name: 'test' }) 6 | expect(logger).toBeDefined() 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /packages/common-ts/src/logging/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | 4 | import pino from 'pino' 5 | import pinoMultiStream from 'pino-multi-stream' 6 | import * as pinoSentry from '@graphprotocol/pino-sentry-simple' 7 | 8 | export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' 9 | 10 | export interface LoggerSentryOptions { 11 | dsn: string 12 | serverName: string 13 | release: string 14 | tracesSampleRate: number 15 | debug: boolean 16 | level: 'debug' | 'info' | 'warning' | 'error' | 'fatal' 17 | tagKeys?: string[] 18 | excludeKeys?: string[] 19 | } 20 | 21 | export interface LoggerOptions { 22 | name: string 23 | level?: LogLevel 24 | async?: boolean 25 | sentry?: LoggerSentryOptions 26 | } 27 | 28 | export class Logger { 29 | options: LoggerOptions 30 | inner: pino.Logger 31 | 32 | constructor(options: LoggerOptions) { 33 | this.options = options 34 | 35 | const loggerOptions = { name: options.name, level: options.level || 'debug' } 36 | 37 | const stream = options.async 38 | ? pino.destination({ minLength: 4096, sync: false }) 39 | : pino.destination() 40 | 41 | if (options.sentry) { 42 | const streams = [ 43 | { stream, level: loggerOptions.level }, 44 | { 45 | stream: options.async 46 | ? pinoSentry.createWriteStreamAsync({ ...options.sentry }) 47 | : pinoSentry.createWriteStream({ ...options.sentry }), 48 | }, 49 | ] 50 | this.inner = pinoMultiStream({ 51 | ...loggerOptions, 52 | streams, 53 | }) 54 | } else { 55 | this.inner = pino(loggerOptions, stream) 56 | } 57 | } 58 | 59 | child(bindings: pino.Bindings): Logger { 60 | const inner = this.inner.child(bindings) 61 | const logger = new Logger(this.options) 62 | logger.inner = inner 63 | return logger 64 | } 65 | 66 | trace(msg: string, o?: object, ...args: any[]): void { 67 | if (o) { 68 | this.inner.trace(o, msg, ...args) 69 | } else { 70 | this.inner.trace(msg, ...args) 71 | } 72 | } 73 | 74 | debug(msg: string, o?: object, ...args: any[]): void { 75 | if (o) { 76 | this.inner.debug(o, msg, ...args) 77 | } else { 78 | this.inner.debug(msg, ...args) 79 | } 80 | } 81 | 82 | info(msg: string, o?: object, ...args: any[]): void { 83 | if (o) { 84 | this.inner.info(o, msg, ...args) 85 | } else { 86 | this.inner.info(msg, ...args) 87 | } 88 | } 89 | 90 | warn(msg: string, o?: object, ...args: any[]): void { 91 | if (o) { 92 | this.inner.warn(o, msg, ...args) 93 | } else { 94 | this.inner.warn(msg, ...args) 95 | } 96 | } 97 | 98 | warning(msg: string, o?: object, ...args: any[]): void { 99 | if (o) { 100 | this.inner.warn(o, msg, ...args) 101 | } else { 102 | this.inner.warn(msg, ...args) 103 | } 104 | } 105 | 106 | error(msg: string, o?: object, ...args: any[]): void { 107 | if (o) { 108 | this.inner.error(o, msg, ...args) 109 | } else { 110 | this.inner.error(msg, ...args) 111 | } 112 | } 113 | 114 | fatal(msg: string, o?: object, ...args: any[]): void { 115 | if (o) { 116 | this.inner.fatal(o, msg, ...args) 117 | } else { 118 | this.inner.fatal(msg, ...args) 119 | } 120 | } 121 | 122 | crit(msg: string, o?: object, ...args: any[]): void { 123 | if (o) { 124 | this.inner.fatal(o, msg, ...args) 125 | } else { 126 | this.inner.fatal(msg, ...args) 127 | } 128 | } 129 | 130 | critical(msg: string, o?: object, ...args: any[]): void { 131 | if (o) { 132 | this.inner.fatal(o, msg, ...args) 133 | } else { 134 | this.inner.fatal(msg, ...args) 135 | } 136 | } 137 | } 138 | 139 | export const createLogger = (options: LoggerOptions): Logger => new Logger(options) 140 | -------------------------------------------------------------------------------- /packages/common-ts/src/metrics/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Registry } from 'prom-client' 2 | import { createMetrics, createMetricsServer } from '.' 3 | import request from 'supertest' 4 | import { createLogger } from '../logging' 5 | 6 | describe('Metrics', () => { 7 | test('Create metrics', () => { 8 | const metrics = createMetrics() 9 | expect(metrics.client).toBeDefined() 10 | expect(metrics.registry).toBeInstanceOf(Registry) 11 | metrics.registry.clear() 12 | }) 13 | 14 | test('Serve metrics', async () => { 15 | const { client, registry } = createMetrics() 16 | const logger = createLogger({ name: 'test' }) 17 | const server = createMetricsServer({ logger, registry, port: 51235 }) 18 | 19 | try { 20 | // Create two metrics for testing 21 | const counter = new client.Counter({ 22 | name: 'counter', 23 | help: 'counter help', 24 | registers: [registry], 25 | }) 26 | const gauge = new client.Gauge({ 27 | name: 'gauge', 28 | help: 'gauge help', 29 | registers: [registry], 30 | }) 31 | 32 | counter.inc() 33 | counter.inc() 34 | gauge.set(100) 35 | 36 | // Verify that the registered metrics are served at `/` 37 | const response = await request(server).get('/metrics').send() 38 | expect(response.status).toEqual(200) 39 | expect(response.text).toMatch(/counter 2/) 40 | expect(response.text).toMatch(/gauge 100/) 41 | } finally { 42 | server.close() 43 | registry.clear() 44 | } 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /packages/common-ts/src/metrics/index.ts: -------------------------------------------------------------------------------- 1 | import prometheus, { 2 | collectDefaultMetrics, 3 | LabelValues, 4 | Histogram, 5 | Registry, 6 | } from 'prom-client' 7 | import express from 'express' 8 | import { Server } from 'net' 9 | import { Logger } from '..' 10 | 11 | export interface Metrics { 12 | client: typeof prometheus 13 | registry: Registry 14 | } 15 | 16 | export const createMetrics = (): Metrics => { 17 | // Collect default metrics (event loop lag, memory, file descriptors etc.) 18 | collectDefaultMetrics() 19 | 20 | return { client: prometheus, registry: prometheus.register } 21 | } 22 | 23 | export interface MetricsServerOptions { 24 | logger: Logger 25 | registry: Registry 26 | port?: number 27 | route?: string 28 | } 29 | 30 | export const createMetricsServer = (options: MetricsServerOptions): Server => { 31 | const logger = options.logger.child({ component: 'MetricsServer' }) 32 | 33 | const app = express() 34 | 35 | app.get(options.route || '/metrics', async (_, res) => { 36 | res.set('Content-Type', options.registry.contentType) 37 | res.status(200).send(await options.registry.metrics()) 38 | }) 39 | 40 | const port = options.port || 7300 41 | const server = app.listen(port, () => { 42 | logger.debug('Listening on port', { port }) 43 | }) 44 | 45 | return server 46 | } 47 | 48 | export async function timed( 49 | metric: Histogram | undefined, 50 | labels: LabelValues | undefined, 51 | promise: T | Promise, 52 | ): Promise { 53 | const timer = metric?.startTimer(labels) 54 | try { 55 | return await promise 56 | } finally { 57 | if (timer) { 58 | timer(labels) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/common-ts/src/security/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Express } from 'express' 2 | import helmet from 'helmet' 3 | 4 | const rejectBadHeaders = (req: Request, res: Response, next: () => void) => { 5 | if (req.headers['challenge-bypass-token']) { 6 | res.status(400).send('Bad Request') 7 | } else { 8 | next() 9 | } 10 | } 11 | 12 | export const secureExpressApp = (app: Express): void => { 13 | // This is meant to provide some security across a wide range of 14 | // attacks. It is mentioned in the express docs under 15 | // Production Best Practices: Security 16 | // https://expressjs.com/en/advanced/best-practice-security.html 17 | // It's not entirely clear that this is helpful 18 | app.use(helmet()) 19 | 20 | // Fix a known bad 21 | app.use(rejectBadHeaders) 22 | } 23 | -------------------------------------------------------------------------------- /packages/common-ts/src/subgraph/index.ts: -------------------------------------------------------------------------------- 1 | export * from './networkSubgraphClient' 2 | -------------------------------------------------------------------------------- /packages/common-ts/src/subgraph/networkSubgraphClient.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | import gql from 'graphql-tag' 3 | import { createNetworkSubgraphClient } from './networkSubgraphClient' 4 | 5 | describe('Network Subgraph Client', () => { 6 | test('Sends queries to the configured URL', async () => { 7 | nock('http://foo.bar/') 8 | .post('/baz/ruux') 9 | .reply(200, { data: { ok: true } }) 10 | 11 | const client = await createNetworkSubgraphClient({ url: 'http://foo.bar/baz/ruux' }) 12 | 13 | expect( 14 | client 15 | .query( 16 | gql` 17 | { 18 | ok 19 | } 20 | `, 21 | [], 22 | undefined, 23 | ) 24 | .toPromise(), 25 | ).resolves.toMatchObject({ 26 | data: { ok: true }, 27 | }) 28 | 29 | expect(nock.isDone()) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /packages/common-ts/src/subgraph/networkSubgraphClient.ts: -------------------------------------------------------------------------------- 1 | import { createClient, Client } from '@urql/core' 2 | import fetch from 'cross-fetch' 3 | 4 | export interface NetworkSubgraphClientOptions { 5 | url: string 6 | } 7 | 8 | export const createNetworkSubgraphClient = async ( 9 | options: NetworkSubgraphClientOptions, 10 | ): Promise => { 11 | return createClient({ url: options.url, fetch }) 12 | } 13 | -------------------------------------------------------------------------------- /packages/common-ts/src/subgraphs/index.test.ts: -------------------------------------------------------------------------------- 1 | import { SubgraphDeploymentID, SubgraphNameOrDeploymentID } from '.' 2 | 3 | describe('Subgraph deployment IDs', () => { 4 | test('Type safety', () => { 5 | const original = '0x4d31d21d389263c98d1e83a031e8fed17cdcef15bd62ee8153f34188a83c7b1c' 6 | const id: SubgraphNameOrDeploymentID = new SubgraphDeploymentID(original) 7 | 8 | // This wouldn't compile if TypeScript didn't recognize `id` having 9 | // kind == 'deployment-id' 10 | expect(id.bytes32).toEqual(original) 11 | }) 12 | 13 | test('Conversion from bytes32', () => { 14 | const original = '0x4d31d21d389263c98d1e83a031e8fed17cdcef15bd62ee8153f34188a83c7b1c' 15 | const id = new SubgraphDeploymentID(original) 16 | 17 | expect(`${id}`).toEqual(original) 18 | expect(id.bytes32).toEqual(original) 19 | expect(id.ipfsHash).toEqual('QmTXzATwNfgGVukV1fX2T6xw9f6LAYRVWpsdXyRWzUR2H9') 20 | 21 | const original2 = '0x32c4e64f2b5ecfedbcd41c1d1c469f837d2f3f4f9cdaff496fc7332d92090449' 22 | const id2 = new SubgraphDeploymentID(original2) 23 | 24 | expect(id2.bytes32).toEqual(original2) 25 | expect(id2.ipfsHash).toEqual('QmRkqEVeZ8bRmMfvBHJvoB4NbnPgXNcuszLZWNNF49skY8') 26 | }) 27 | 28 | test('Conversion from IPFS hash', () => { 29 | const original = 'QmTXzATwNfgGVukV1fX2T6xw9f6LAYRVWpsdXyRWzUR2H9' 30 | const id = new SubgraphDeploymentID(original) 31 | 32 | expect(`${id}`).toEqual( 33 | '0x4d31d21d389263c98d1e83a031e8fed17cdcef15bd62ee8153f34188a83c7b1c', 34 | ) 35 | expect(id.bytes32).toEqual( 36 | '0x4d31d21d389263c98d1e83a031e8fed17cdcef15bd62ee8153f34188a83c7b1c', 37 | ) 38 | expect(id.ipfsHash).toEqual(original) 39 | 40 | const original2 = 'QmRkqEVeZ8bRmMfvBHJvoB4NbnPgXNcuszLZWNNF49skY8' 41 | const id2 = new SubgraphDeploymentID(original2) 42 | 43 | expect(`${id2}`).toEqual( 44 | '0x32c4e64f2b5ecfedbcd41c1d1c469f837d2f3f4f9cdaff496fc7332d92090449', 45 | ) 46 | expect(id2.bytes32).toEqual( 47 | '0x32c4e64f2b5ecfedbcd41c1d1c469f837d2f3f4f9cdaff496fc7332d92090449', 48 | ) 49 | expect(id2.ipfsHash).toEqual(original2) 50 | }) 51 | 52 | test('Invalid values', () => { 53 | const cases = [ 54 | // IPFS too long 55 | 'QmY8Uzg61ttrogeTyCKLcDDR4gKNK44g9qkGDqeProkSHEE', 56 | // IPFS too short 57 | 'QmY8Uzg61ttrogeTyCKLcDDR4gKNK44g9qkGDqeProkSH', 58 | // IPFS Invalid characters 59 | "QmY8Uzg61ttrogeTyCKLcDDR4gKNK44g9qkGDqePro'SHE", 60 | // Bytes32 too long 61 | '0xaa937267f33a096e959296c675eae0c8898d549f9a5de9930149de8950c03203a', 62 | // Bytes32 too short 63 | '0xaa937267f33a096e959296c675eae0c8898d549f9a5de9930149de8950c0320', 64 | // Bytes32 invalid characters 65 | '0xaa937267f33a096e959296c675eae0c8898d549f9a5de9930149de8+50c03Y03', 66 | ] 67 | 68 | for (const id of cases) { 69 | expect(() => new SubgraphDeploymentID(id)).toThrow( 70 | new Error(`Invalid subgraph deployment ID: ${id}`), 71 | ) 72 | } 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /packages/common-ts/src/subgraphs/index.ts: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers' 2 | import base58 from 'bs58' 3 | 4 | export class SubgraphName { 5 | kind: 'name' = 'name' as const 6 | value: string 7 | 8 | constructor(name: string) { 9 | this.value = name 10 | } 11 | 12 | toString(): string { 13 | return this.value 14 | } 15 | } 16 | 17 | // Security: Input validation 18 | const bytes32Check = /^0x[0-9a-f]{64}$/ 19 | const multiHashCheck = /^Qm[1-9a-km-zA-HJ-NP-Z]{44}$/ 20 | 21 | export class SubgraphDeploymentID { 22 | kind: 'deployment-id' = 'deployment-id' as const 23 | 24 | // Hexadecimal (bytes32) representation of the subgraph deployment Id 25 | value: string 26 | 27 | constructor(id: string) { 28 | let value 29 | // Security: Input validation 30 | if (multiHashCheck.test(id)) { 31 | value = utils.hexlify(base58.decode(id).slice(2)) 32 | } else if (bytes32Check.test(id)) { 33 | value = id 34 | } 35 | 36 | if (value != null) { 37 | this.value = value 38 | } else { 39 | throw new Error(`Invalid subgraph deployment ID: ${id}`) 40 | } 41 | } 42 | 43 | toString(): string { 44 | return this.value 45 | } 46 | 47 | get display(): { bytes32: string; ipfsHash: string } { 48 | return { 49 | bytes32: this.bytes32, 50 | ipfsHash: this.ipfsHash, 51 | } 52 | } 53 | 54 | get bytes32(): string { 55 | return this.value 56 | } 57 | 58 | get ipfsHash(): string { 59 | return base58.encode([0x12, 0x20, ...utils.arrayify(this.value)]) 60 | } 61 | } 62 | 63 | export type SubgraphNameOrDeploymentID = SubgraphName | SubgraphDeploymentID 64 | -------------------------------------------------------------------------------- /packages/common-ts/src/util/addresses.ts: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers' 2 | 3 | /** 4 | * A normalized address in checksum format. 5 | */ 6 | export type Address = string & { _isAddress: void } 7 | 8 | /** 9 | * Converts an address to checksum format and returns a typed instance. 10 | */ 11 | export const toAddress = (s: Address | string): Address => 12 | typeof s === 'string' ? (utils.getAddress(s) as Address) : s 13 | -------------------------------------------------------------------------------- /packages/common-ts/src/util/arrays.ts: -------------------------------------------------------------------------------- 1 | import { equal } from './equal' 2 | 3 | function uniqueFilter(value: T, index: number, array: T[]): boolean { 4 | return array.findIndex(v => equal(value, v)) === index 5 | } 6 | 7 | export function uniqueValues(array: T[]): T[] { 8 | return array.filter(uniqueFilter) 9 | } 10 | -------------------------------------------------------------------------------- /packages/common-ts/src/util/bytes.ts: -------------------------------------------------------------------------------- 1 | // Writes bytes to a fixed size buffer with a capacity that is known upfront. 2 | export class BytesWriter { 3 | private offset = 0 4 | private data: Uint8Array 5 | 6 | constructor(capacity: number) { 7 | this.data = new Uint8Array(capacity) 8 | } 9 | 10 | // Write a hex string to this buffer 11 | // The hex should start with 0x and have an even number of hexadecimal characters. 12 | // This does no validation at all. Make sure the input is correct and that the 13 | // writer has capacity. 14 | writeHex(hex: string): void { 15 | // Help the optimizer maybe by lifting local variables 16 | let offset = this.offset 17 | const data = this.data 18 | 19 | // TODO: Performance 20 | // There are no great answers in JS, 21 | // but individual characters might be better. Need to profile. 22 | let iRead = 0 23 | if (hex.startsWith('0x')) { 24 | iRead = 2 25 | } 26 | while (iRead < hex.length) { 27 | const num = hex.slice(iRead, iRead + 2) 28 | data[offset++] = parseInt(num, 16) 29 | iRead += 2 30 | } 31 | this.offset = offset 32 | } 33 | 34 | writeZeroes(bytes: number): void { 35 | this.offset += bytes 36 | } 37 | 38 | unwrap(): Uint8Array { 39 | if (this.offset !== this.data.length) { 40 | throw new Error('Did not write correct number of bytes') 41 | } 42 | return this.data 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/common-ts/src/util/equal.ts: -------------------------------------------------------------------------------- 1 | import { isDeepStrictEqual } from 'util' 2 | 3 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any 4 | export const equal = (a: any, b: any) => isDeepStrictEqual(a, b) 5 | -------------------------------------------------------------------------------- /packages/common-ts/src/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from './equal' 2 | export * from './arrays' 3 | export * from './addresses' 4 | export * from './bytes' 5 | -------------------------------------------------------------------------------- /packages/common-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "target": "es2015", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "composite": true, 13 | "lib": ["es2015", "es6", "esnext.asynciterable", "dom"], 14 | "types": ["jest", "node"] 15 | }, 16 | "include": ["src/**/*.ts"], 17 | "exclude": [], 18 | "references": [] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./packages", 4 | "paths": { 5 | "@graphprotocol/*": ["./*/src"] 6 | }, 7 | "resolveJsonModule": true, 8 | "skipLibCheck": true, 9 | } 10 | } 11 | --------------------------------------------------------------------------------