├── .env.example ├── .eslintrc ├── .github └── workflows │ └── docker-ci.yml ├── .gitignore ├── .prettierrc ├── README.md ├── _docker ├── api.Dockerfile └── cron.Dockerfile ├── package-lock.json ├── package.json ├── src ├── api.ts ├── api │ ├── controller.ts │ ├── errorMiddleware.ts │ ├── routes.ts │ └── setup.ts ├── compare.ts ├── cron.ts ├── jobs │ └── sync.job.ts ├── models │ └── token.model.ts └── utils │ ├── errors.util.ts │ ├── httpRequest.util.ts │ ├── logger.util.ts │ └── osReport.util.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | PORT=8000 2 | DB_URL=mongodb://localhost:27017,127.0.0.1:27018/utl?replicaSet=rs0 3 | CDN_URL=https://cdn.jsdelivr.net/gh/solflare-wallet/token-list/solana-tokenlist.json 4 | CRON_SYNC="0 * * * * *" 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "parser": "@typescript-eslint/parser", 7 | "plugins": ["import", "@typescript-eslint"], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "rules": { 14 | "@typescript-eslint/no-explicit-any": "off", 15 | "import/order": [ 16 | "error", 17 | { 18 | "newlines-between": "always", 19 | "groups": [["builtin", "external"], "parent", "sibling", "index"], 20 | "warnOnUnassignedImports": true, 21 | "alphabetize": { 22 | "order": "asc" 23 | } 24 | } 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/docker-ci.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build And Push CI 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version (v1.0.4)' 8 | required: true 9 | 10 | 11 | env: 12 | REGISTRY: gcr.io 13 | GCP_PID: ${{ secrets.GCP_PID }} 14 | GCP_KEY: ${{ secrets.GCP_CICD_KEY }} 15 | 16 | jobs: 17 | # 18 | # test: 19 | # runs-on: ubuntu-latest 20 | # steps: 21 | # - uses: actions/checkout@v2 22 | # - name: Install modules 23 | # run: npm i 24 | # - name: Run tests 25 | # run: npm run test:detect 26 | 27 | docker-push-api: 28 | # needs: test 29 | runs-on: ubuntu-latest 30 | permissions: 31 | contents: read 32 | pull-requests: read 33 | steps: 34 | - uses: actions/checkout@v2 35 | 36 | - name: Build and push Docker Panel API image 37 | uses: docker/build-push-action@v1 38 | with: 39 | username: _json_key 40 | password: ${{ env.GCP_KEY }} 41 | registry: ${{ env.REGISTRY }} 42 | repository: ${{ env.GCP_PID }}/utl-api 43 | dockerfile: "_docker/api.Dockerfile" 44 | push: true 45 | tag_with_sha: true 46 | tags: latest, ${{ github.event.inputs.version }} 47 | 48 | 49 | docker-push-cron: 50 | # needs: test 51 | runs-on: ubuntu-latest 52 | permissions: 53 | contents: read 54 | pull-requests: read 55 | steps: 56 | - uses: actions/checkout@v2 57 | 58 | - name: Build and push Docker Panel API Cron image 59 | uses: docker/build-push-action@v1 60 | with: 61 | username: _json_key 62 | password: ${{ env.GCP_KEY }} 63 | registry: ${{ env.REGISTRY }} 64 | repository: ${{ env.GCP_PID }}/utl-api-cron 65 | dockerfile: "_docker/cron.Dockerfile" 66 | push: true 67 | tag_with_sha: true 68 | tags: latest, ${{ github.event.inputs.version }} 69 | 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.xml 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # CMake 25 | cmake-build-debug/ 26 | 27 | # Mongo Explorer plugin: 28 | .idea/**/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Cursive Clojure plugin 45 | .idea/replstate.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | fabric.properties 52 | ### VisualStudio template 53 | ## Ignore Visual Studio temporary files, build results, and 54 | ## files generated by popular Visual Studio add-ons. 55 | ## 56 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 57 | 58 | # User-specific files 59 | *.suo 60 | *.user 61 | *.userosscache 62 | *.sln.docstates 63 | 64 | # User-specific files (MonoDevelop/Xamarin Studio) 65 | *.userprefs 66 | 67 | # Build results 68 | [Dd]ebug/ 69 | [Dd]ebugPublic/ 70 | [Rr]elease/ 71 | [Rr]eleases/ 72 | x64/ 73 | x86/ 74 | bld/ 75 | [Bb]in/ 76 | [Oo]bj/ 77 | [Ll]og/ 78 | 79 | # Visual Studio 2015 cache/options directory 80 | .vs/ 81 | # Uncomment if you have tasks that create the project's static files in wwwroot 82 | #wwwroot/ 83 | 84 | # MSTest test Results 85 | [Tt]est[Rr]esult*/ 86 | [Bb]uild[Ll]og.* 87 | 88 | # NUNIT 89 | *.VisualState.xml 90 | TestResult.xml 91 | 92 | # Build Results of an ATL Project 93 | [Dd]ebugPS/ 94 | [Rr]eleasePS/ 95 | dlldata.c 96 | 97 | # Benchmark Results 98 | BenchmarkDotNet.Artifacts/ 99 | 100 | # .NET Core 101 | project.lock.json 102 | project.fragment.lock.json 103 | artifacts/ 104 | **/Properties/launchSettings.json 105 | 106 | *_i.c 107 | *_p.c 108 | *_i.h 109 | *.ilk 110 | *.meta 111 | *.obj 112 | *.pch 113 | *.pdb 114 | *.pgc 115 | *.pgd 116 | *.rsp 117 | *.sbr 118 | *.tlb 119 | *.tli 120 | *.tlh 121 | *.tmp 122 | *.tmp_proj 123 | *.log 124 | *.vspscc 125 | *.vssscc 126 | .builds 127 | *.pidb 128 | *.svclog 129 | *.scc 130 | 131 | # Chutzpah Test files 132 | _Chutzpah* 133 | 134 | # Visual C++ cache files 135 | ipch/ 136 | *.aps 137 | *.ncb 138 | *.opendb 139 | *.opensdf 140 | *.sdf 141 | *.cachefile 142 | *.VC.db 143 | *.VC.VC.opendb 144 | 145 | # Visual Studio profiler 146 | *.psess 147 | *.vsp 148 | *.vspx 149 | *.sap 150 | 151 | # Visual Studio Trace Files 152 | *.e2e 153 | 154 | # TFS 2012 Local Workspace 155 | $tf/ 156 | 157 | # Guidance Automation Toolkit 158 | *.gpState 159 | 160 | # ReSharper is a .NET coding add-in 161 | _ReSharper*/ 162 | *.[Rr]e[Ss]harper 163 | *.DotSettings.user 164 | 165 | # JustCode is a .NET coding add-in 166 | .JustCode 167 | 168 | # TeamCity is a build add-in 169 | _TeamCity* 170 | 171 | # DotCover is a Code Coverage Tool 172 | *.dotCover 173 | 174 | # AxoCover is a Code Coverage Tool 175 | .axoCover/* 176 | !.axoCover/settings.json 177 | 178 | # Visual Studio code coverage results 179 | *.coverage 180 | *.coveragexml 181 | 182 | # NCrunch 183 | _NCrunch_* 184 | .*crunch*.local.xml 185 | nCrunchTemp_* 186 | 187 | # MightyMoose 188 | *.mm.* 189 | AutoTest.Net/ 190 | 191 | # Web workbench (sass) 192 | .sass-cache/ 193 | 194 | # Installshield output folder 195 | [Ee]xpress/ 196 | 197 | # DocProject is a documentation generator add-in 198 | DocProject/buildhelp/ 199 | DocProject/Help/*.HxT 200 | DocProject/Help/*.HxC 201 | DocProject/Help/*.hhc 202 | DocProject/Help/*.hhk 203 | DocProject/Help/*.hhp 204 | DocProject/Help/Html2 205 | DocProject/Help/html 206 | 207 | # Click-Once directory 208 | publish/ 209 | 210 | # Publish Web Output 211 | *.[Pp]ublish.xml 212 | *.azurePubxml 213 | # Note: Comment the next line if you want to checkin your web deploy settings, 214 | # but database connection strings (with potential passwords) will be unencrypted 215 | *.pubxml 216 | *.publishproj 217 | 218 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 219 | # checkin your Azure Web App publish settings, but sensitive information contained 220 | # in these scripts will be unencrypted 221 | PublishScripts/ 222 | 223 | # NuGet Packages 224 | *.nupkg 225 | # The packages folder can be ignored because of Package Restore 226 | **/[Pp]ackages/* 227 | # except build/, which is used as an MSBuild target. 228 | !**/[Pp]ackages/build/ 229 | # Uncomment if necessary however generally it will be regenerated when needed 230 | #!**/[Pp]ackages/repositories.config 231 | # NuGet v3's project.json files produces more ignorable files 232 | *.nuget.props 233 | *.nuget.targets 234 | 235 | # Microsoft Azure Build Output 236 | csx/ 237 | *.build.csdef 238 | 239 | # Microsoft Azure Emulator 240 | ecf/ 241 | rcf/ 242 | 243 | # Windows Store app package directories and files 244 | AppPackages/ 245 | BundleArtifacts/ 246 | Package.StoreAssociation.xml 247 | _pkginfo.txt 248 | *.appx 249 | 250 | # Visual Studio cache files 251 | # files ending in .cache can be ignored 252 | *.[Cc]ache 253 | # but keep track of directories ending in .cache 254 | !*.[Cc]ache/ 255 | 256 | # Others 257 | ClientBin/ 258 | ~$* 259 | *~ 260 | *.dbmdl 261 | *.dbproj.schemaview 262 | *.jfm 263 | *.pfx 264 | *.publishsettings 265 | orleans.codegen.cs 266 | 267 | # Since there are multiple workflows, uncomment next line to ignore bower_components 268 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 269 | #bower_components/ 270 | 271 | # RIA/Silverlight projects 272 | Generated_Code/ 273 | 274 | # Backup & report files from converting an old project file 275 | # to a newer Visual Studio version. Backup files are not needed, 276 | # because we have git ;-) 277 | _UpgradeReport_Files/ 278 | Backup*/ 279 | UpgradeLog*.XML 280 | UpgradeLog*.htm 281 | 282 | # SQL Server files 283 | *.mdf 284 | *.ldf 285 | *.ndf 286 | 287 | # Business Intelligence projects 288 | *.rdl.data 289 | *.bim.layout 290 | *.bim_*.settings 291 | 292 | # Microsoft Fakes 293 | FakesAssemblies/ 294 | 295 | # GhostDoc plugin setting file 296 | *.GhostDoc.xml 297 | 298 | # Node.js Tools for Visual Studio 299 | .ntvs_analysis.dat 300 | node_modules/ 301 | 302 | # Typescript v1 declaration files 303 | typings/ 304 | 305 | # Visual Studio 6 build log 306 | *.plg 307 | 308 | # Visual Studio 6 workspace options file 309 | *.opt 310 | 311 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 312 | *.vbw 313 | 314 | # Visual Studio LightSwitch build output 315 | **/*.HTMLClient/GeneratedArtifacts 316 | **/*.DesktopClient/GeneratedArtifacts 317 | **/*.DesktopClient/ModelManifest.xml 318 | **/*.Server/GeneratedArtifacts 319 | **/*.Server/ModelManifest.xml 320 | _Pvt_Extensions 321 | 322 | # Paket dependency manager 323 | .paket/paket.exe 324 | paket-files/ 325 | 326 | # FAKE - F# Make 327 | .fake/ 328 | 329 | # JetBrains Rider 330 | .idea/ 331 | *.sln.iml 332 | 333 | # IDE - VSCode 334 | .vscode/* 335 | !.vscode/settings.json 336 | !.vscode/tasks.json 337 | !.vscode/launch.json 338 | !.vscode/extensions.json 339 | 340 | # CodeRush 341 | .cr/ 342 | 343 | # Python Tools for Visual Studio (PTVS) 344 | __pycache__/ 345 | *.pyc 346 | 347 | # Cake - Uncomment if you are using it 348 | # tools/** 349 | # !tools/packages.config 350 | 351 | # Tabs Studio 352 | *.tss 353 | 354 | # Telerik's JustMock configuration file 355 | *.jmconfig 356 | 357 | # BizTalk build output 358 | *.btp.cs 359 | *.btm.cs 360 | *.odx.cs 361 | *.xsd.cs 362 | 363 | # OpenCover UI analysis results 364 | OpenCover/ 365 | coverage/ 366 | 367 | ### macOS template 368 | # General 369 | .DS_Store 370 | .AppleDouble 371 | .LSOverride 372 | 373 | # Icon must end with two \r 374 | Icon 375 | 376 | # Thumbnails 377 | ._* 378 | 379 | # Files that might appear in the root of a volume 380 | .DocumentRevisions-V100 381 | .fseventsd 382 | .Spotlight-V100 383 | .TemporaryItems 384 | .Trashes 385 | .VolumeIcon.icns 386 | .com.apple.timemachine.donotpresent 387 | 388 | # Directories potentially created on remote AFP share 389 | .AppleDB 390 | .AppleDesktop 391 | Network Trash Folder 392 | Temporary Items 393 | .apdisk 394 | 395 | ======= 396 | # Local 397 | .env 398 | dist 399 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #

Solflare 2 | 3 | # Unified Token List API 4 | 5 | The Token List API is an API that will consume the generated UTL and expose endpoints for efficient querying and searching. 6 | It will include endpoints where you can input a list of mint addresses, and receive data of those mints in one request, search endpoints etc without needing to pull the whole token list client-side. The goal of this API is to be very performant and to not require clients to download the whole token list. 7 | 8 | 9 | ## Setup 10 | Implemented as a simple Express.JS API, only a connection to MongoDB database is required. 11 | 12 | **API service** 13 | 14 | ```shell 15 | docker build -t utl-api -f _docker/api.Dockerfile . 16 | docker run --env DB_URL="mongodb://user:pass@localhost/utl" --env NODE_ENV=production -p 8080:80 utl-api 17 | ``` 18 | 19 | 20 | **CRON service** 21 | 22 | Used to peridically sync with published token list on CDN. `CRON_SYNC` defines frequency of sync cron job. 23 | 24 | ```shell 25 | docker build -t utl-api-cron -f _docker/cron.Dockerfile . 26 | docker run --env DB_URL="mongodb://user:pass@localhost/utl" --CDN_URL="https://cdn.jsdelivr.net/gh/solflare-wallet/token-list@latest/solana-tokenlist.json" --env NODE_ENV=production --env CRON_SYNC="0 */10 * * * *" utl-api-cron 27 | ``` 28 | 29 | 30 | ## Public Instance 31 | 32 | Solflare provides a public community instance of this API that is free for use. 33 | It pulls token list from [Solfare Token List](https://github.com/solflare-wallet/token-list) CDN. 34 | ``` 35 | https://token-list-api.solana.cloud 36 | ``` 37 | 38 | 39 | ## Endpoints 40 | 41 | ### List all 42 | 43 | Used to list all tokens. 44 | 45 | **URL** : `/v1/list` or `/v1/list?chainId=103` 46 | 47 | **Method** : `GET` 48 | 49 | **Response** 50 | 51 | ```json 52 | { 53 | "content": [ 54 | { 55 | "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 56 | "chainId": 101, 57 | "name": "USD Coin", 58 | "symbol": "USDC", 59 | "verified": true, 60 | "decimals": 6, 61 | "holders": 100000, 62 | "logoURI": "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png?1547042389", 63 | "tags": [] 64 | }, 65 | { 66 | "address": "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R", 67 | "chainId": 101, 68 | "name": "Raydium", 69 | "symbol": "RAY", 70 | "verified": true, 71 | "decimals": 6, 72 | "holders": 100000, 73 | "logoURI": "https://assets.coingecko.com/coins/images/13928/large/PSigc4ie_400x400.jpg?1612875614", 74 | "tags": [] 75 | }, 76 | ] 77 | } 78 | ``` 79 | 80 | 81 | ### Search by content 82 | 83 | Used to search tokens by name/symbol. You can use `start` and `limit` for pagination. 84 | 85 | **URL** : `/v1/search?query=slrs&start=0&limit` or `/v1/search?query=slrs&start=0&limit&chainId=101` 86 | 87 | **Method** : `GET` 88 | 89 | **Response** 90 | 91 | ```json 92 | { 93 | "content": [ 94 | { 95 | "address": "SLRSSpSLUTP7okbCUBYStWCo1vUgyt775faPqz8HUMr", 96 | "chainId": 101, 97 | "name": "Solrise Finance", 98 | "symbol": "SLRS", 99 | "verified": true, 100 | "decimals": 6, 101 | "holders": 40604, 102 | "logoURI": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/SLRSSpSLUTP7okbCUBYStWCo1vUgyt775faPqz8HUMr/logo.png", 103 | "tags": [] 104 | }, 105 | { 106 | "address": "GtFtWCcLYtWQT8NLRwEfUqc9sgVnq4SbuSnMCpwcutNk", 107 | "chainId": 101, 108 | "name": "tuSLRS", 109 | "symbol": "tuSLRS", 110 | "verified": true, 111 | "decimals": 6, 112 | "holders": 1117, 113 | "logoURI": "https://raw.githubusercontent.com/sol-farm/token-logos/main/tuSLRS.png", 114 | "tags": [ 115 | "tulip-protocol", 116 | "lending", 117 | "collateral-tokens" 118 | ] 119 | } 120 | ] 121 | } 122 | ``` 123 | 124 | 125 | 126 | 127 | ### Get by mints 128 | 129 | Used to get all tokens from array of mint addresses. 130 | 131 | **URL** : `/v1/mints` or `/v1/mints?chainId=101` 132 | 133 | **Method** : `POST` 134 | 135 | **Request** 136 | ```json 137 | { 138 | "addresses": [ 139 | "SLRSSpSLUTP7okbCUBYStWCo1vUgyt775faPqz8HUMr", 140 | "GtFtWCcLYtWQT8NLRwEfUqc9sgVnq4SbuSnMCpwcutNk" 141 | ] 142 | } 143 | ``` 144 | 145 | **Response** 146 | 147 | ```json 148 | { 149 | "content": [ 150 | { 151 | "address": "SLRSSpSLUTP7okbCUBYStWCo1vUgyt775faPqz8HUMr", 152 | "chainId": 101, 153 | "name": "Solrise Finance", 154 | "symbol": "SLRS", 155 | "verified": true, 156 | "decimals": 6, 157 | "holders": 40604, 158 | "logoURI": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/SLRSSpSLUTP7okbCUBYStWCo1vUgyt775faPqz8HUMr/logo.png", 159 | "tags": [] 160 | }, 161 | { 162 | "address": "GtFtWCcLYtWQT8NLRwEfUqc9sgVnq4SbuSnMCpwcutNk", 163 | "chainId": 101, 164 | "name": "tuSLRS", 165 | "symbol": "tuSLRS", 166 | "verified": true, 167 | "decimals": 6, 168 | "holders": 1117, 169 | "logoURI": "https://raw.githubusercontent.com/sol-farm/token-logos/main/tuSLRS.png", 170 | "tags": [ 171 | "tulip-protocol", 172 | "lending", 173 | "collateral-tokens" 174 | ] 175 | } 176 | ] 177 | } 178 | ``` 179 | 180 | 181 | 182 | ## Related repos 183 | - [Token List Aggregator](https://github.com/solflare-wallet/utl-aggregator) 184 | - [Token List API](https://github.com/solflare-wallet/utl-api) 185 | - [Token List SDK](https://github.com/solflare-wallet/utl-sdk) 186 | - [Solfare Token List](https://github.com/solflare-wallet/token-list) 187 | -------------------------------------------------------------------------------- /_docker/api.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:17.4 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | 7 | RUN npm i --production 8 | 9 | COPY . . 10 | 11 | EXPOSE 80 12 | 13 | CMD npm run start:api 14 | -------------------------------------------------------------------------------- /_docker/cron.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:17.4 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | 7 | RUN npm i --production 8 | 9 | COPY . . 10 | 11 | EXPOSE 80 12 | 13 | CMD npm run start:cron 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@solflare-wallet/utl-api", 3 | "version": "1.0.6", 4 | "scripts": { 5 | "start:api": "ts-node src/api.ts", 6 | "dev:api": "nodemon src/api.ts", 7 | "start:cron": "ts-node src/cron.ts", 8 | "dev:cron": "nodemon src/cron.ts", 9 | "test": "jest", 10 | "test:detect": "jest --detectOpenHandles", 11 | "compare": "ts-node src/compare.ts" 12 | }, 13 | "author": "Solflare Developers ", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@google-cloud/logging-winston": "^4.2.3", 17 | "@solana/web3.js": "^1.42.0", 18 | "@types/axios": "^0.14.0", 19 | "@types/cors": "^2.8.12", 20 | "@types/cron": "^2.0.0", 21 | "@types/express": "^4.17.13", 22 | "@types/lodash": "^4.14.182", 23 | "@types/mongoose": "^5.11.97", 24 | "@types/morgan": "^1.9.3", 25 | "axios": "^0.27.2", 26 | "cors": "^2.8.5", 27 | "cron": "^2.0.0", 28 | "dotenv": "^16.0.1", 29 | "express": "^4.18.1", 30 | "joi": "^17.6.0", 31 | "lodash": "^4.17.21", 32 | "mongoose": "^6.4.0", 33 | "morgan": "^1.10.0", 34 | "node-cache": "^5.1.2", 35 | "nodemon": "^2.0.16", 36 | "ts-node": "^10.7.0", 37 | "typescript": "^4.6.4", 38 | "winston": "^3.7.2" 39 | }, 40 | "devDependencies": { 41 | "@typescript-eslint/eslint-plugin": "^5.25.0", 42 | "@typescript-eslint/parser": "^5.25.0", 43 | "eslint": "^8.15.0", 44 | "eslint-plugin-import": "^2.26.0", 45 | "eslint-plugin-node": "^11.1.0", 46 | "prettier": "^2.6.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | import mongoose from 'mongoose' 3 | dotenv.config() 4 | 5 | import ApiSetup from './api/setup' 6 | 7 | mongoose 8 | .connect(process.env.DB_URL as string) 9 | .then((db) => { 10 | console.log(`Connected to ${db.connections[0].name} - mongodb`) 11 | ApiSetup.listen(process.env.PORT ?? 80, () => { 12 | console.log(`Express running on port ${process.env.PORT}`) 13 | }) 14 | }) 15 | .catch((error) => { 16 | console.log('There was an error connecting to db') 17 | console.log(error) 18 | }) 19 | -------------------------------------------------------------------------------- /src/api/controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Response, Request } from 'express' 2 | import Joi from 'joi' 3 | 4 | import TokenModel from '../models/token.model' 5 | 6 | export async function fetchAll( 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ) { 11 | try { 12 | const query = await Joi.object({ 13 | chainId: Joi.number().integer().valid(101, 102, 103), 14 | }).validateAsync(req.query) 15 | 16 | const tokens = await TokenModel.find({ 17 | verified: true, 18 | ...(query.chainId ? { chainId: query.chainId } : {}), 19 | }) 20 | 21 | return res.send({ 22 | content: tokens, 23 | }) 24 | } catch (error) { 25 | next(error) 26 | } 27 | } 28 | 29 | export async function fetchByMint( 30 | req: Request, 31 | res: Response, 32 | next: NextFunction 33 | ) { 34 | try { 35 | const body = await Joi.object({ 36 | addresses: Joi.array().items(Joi.string()).min(1).required(), 37 | }).validateAsync(req.body) 38 | 39 | const query = await Joi.object({ 40 | chainId: Joi.number().integer().valid(101, 102, 103), 41 | }).validateAsync(req.query) 42 | 43 | const tokens = await TokenModel.find({ 44 | address: { 45 | $in: body.addresses, 46 | }, 47 | ...(query.chainId ? { chainId: query.chainId } : {}), 48 | }).exec() 49 | 50 | return res.send({ 51 | content: tokens, 52 | }) 53 | } catch (error) { 54 | next(error) 55 | } 56 | } 57 | 58 | export async function searchByContent( 59 | req: Request, 60 | res: Response, 61 | next: NextFunction 62 | ) { 63 | try { 64 | const data = await Joi.object({ 65 | query: Joi.string().required(), 66 | start: Joi.number().integer().min(0).required(), 67 | limit: Joi.number().integer().min(1).required(), 68 | chainId: Joi.number().integer().valid(101, 102, 103), 69 | }).validateAsync(req.query) 70 | 71 | // Special case if only "+" is passed as query than act like its search all 72 | const tokens = 73 | data.query && data.query.length === 1 && data.query === ' ' 74 | ? await TokenModel.find({ 75 | ...(data.chainId ? { chainId: data.chainId } : {}), 76 | }) 77 | .sort({ 78 | verified: -1, 79 | holders: -1, 80 | }) 81 | .skip(data.start) 82 | .limit(data.limit) 83 | : await TokenModel.find( 84 | { 85 | ...(data.chainId ? { chainId: data.chainId } : {}), 86 | $or: [ 87 | { 88 | $text: { 89 | $search: escapeRegex(data.query.trim()), 90 | }, 91 | }, 92 | { 93 | address: escapeRegex(data.query.trim()), 94 | }, 95 | ], 96 | }, 97 | { score: { $meta: 'textScore' } } 98 | ) 99 | .sort({ 100 | verified: -1, 101 | score: { $meta: 'textScore' }, 102 | holders: -1, 103 | }) 104 | .skip(data.start) 105 | .limit(data.limit) 106 | 107 | return res.send({ 108 | content: tokens, 109 | }) 110 | } catch (error) { 111 | next(error) 112 | } 113 | } 114 | 115 | function escapeRegex(string: string) { 116 | return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&') 117 | } 118 | -------------------------------------------------------------------------------- /src/api/errorMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express' 2 | import { ValidationError } from 'joi' 3 | 4 | import { 5 | HttpError, 6 | HttpInternalServerError, 7 | HttpValidationError, 8 | } from '../utils/errors.util' 9 | import LoggerUtil from '../utils/logger.util' 10 | 11 | async function handler( 12 | error: Error, 13 | req: Request, 14 | res: Response, 15 | Next: NextFunction 16 | ) { 17 | console.log('CAPTURE ERROR') 18 | let stackTrace = undefined 19 | 20 | if (!['production', 'prod'].includes(process.env.NODE_ENV as string)) { 21 | stackTrace = error.stack 22 | } 23 | 24 | if (error instanceof ValidationError) { 25 | error = new HttpValidationError( 26 | error.details[0].message, 27 | error.details[0] 28 | ) 29 | } 30 | 31 | if (!(error instanceof HttpError)) { 32 | LoggerUtil.error(error) 33 | error = new HttpInternalServerError() 34 | } 35 | 36 | if (error instanceof HttpError) { 37 | res.status(error.statusCode).json({ 38 | type: error.constructor.name, 39 | message: error.message, 40 | code: error.statusCode, 41 | data: error.data ? error.data : {}, 42 | stackTrace, 43 | }) 44 | } else { 45 | res.status(500).json({}) 46 | } 47 | } 48 | 49 | export default handler 50 | -------------------------------------------------------------------------------- /src/api/routes.ts: -------------------------------------------------------------------------------- 1 | import { Express, NextFunction, Request, Response } from 'express' 2 | 3 | import { HttpNotFound } from '../utils/errors.util' 4 | 5 | import * as Controller from './controller' 6 | 7 | const routes = (app: Express) => { 8 | app.use((req: Request, res: Response, next: NextFunction) => { 9 | res.setHeader('Access-Control-Allow-Origin', '*') 10 | res.setHeader( 11 | 'Access-Control-Allow-Methods', 12 | 'GET, POST, OPTIONS, PUT, PATCH, DELETE' 13 | ) 14 | res.setHeader( 15 | 'Access-Control-Allow-Headers', 16 | 'X-Requested-With, content-type, x-access-token, authorization' 17 | ) 18 | res.setHeader('Access-Control-Allow-Credentials', 'true') 19 | res.removeHeader('X-Powered-By') 20 | next() 21 | }) 22 | 23 | app.get('/v1/list', Controller.fetchAll) 24 | app.get('/v1/search', Controller.searchByContent) 25 | app.post('/v1/mints', Controller.fetchByMint) 26 | 27 | app.use(function (req: Request, res: Response, next: NextFunction) { 28 | return next(new HttpNotFound()) 29 | }) 30 | } 31 | 32 | export default routes 33 | -------------------------------------------------------------------------------- /src/api/setup.ts: -------------------------------------------------------------------------------- 1 | import cors from 'cors' 2 | import express from 'express' 3 | 4 | import errorMiddleware from './errorMiddleware' 5 | import routes from './routes' 6 | 7 | const app = express() 8 | app.use(cors()) 9 | app.use(express.json({ limit: '2mb' })) 10 | 11 | routes(app) 12 | 13 | app.use(errorMiddleware) 14 | 15 | export default app 16 | -------------------------------------------------------------------------------- /src/compare.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import dotenv from 'dotenv' 3 | dotenv.config() 4 | 5 | import { Token } from './jobs/sync.job' 6 | 7 | async function getTokensFromUrl(url: string) { 8 | const { data } = await axios.get<{ 9 | tokens: Token[] 10 | }>(url) 11 | 12 | return data.tokens 13 | } 14 | 15 | async function compare() { 16 | const currentTokens = await getTokensFromUrl(process.env.CDN_URL as string) 17 | const futureTokens = await getTokensFromUrl( 18 | process.env.FUTURE_CDN_URL as string 19 | ) 20 | 21 | const currentMints = currentTokens.map( 22 | (token) => `${token.address}:${token.chainId}` 23 | ) 24 | 25 | const futureMints = futureTokens.map( 26 | (token) => `${token.address}:${token.chainId}` 27 | ) 28 | 29 | const deleteTokens = currentMints.filter((tokenMint) => { 30 | return !futureMints.includes(tokenMint) 31 | }) 32 | 33 | const updateTokens = currentMints.filter((tokenMint) => { 34 | return futureMints.includes(tokenMint) 35 | }) 36 | 37 | const insertTokens = currentMints.filter((tokenMint) => { 38 | return !currentMints.includes(tokenMint) 39 | }) 40 | 41 | for (const deletedToken of deleteTokens) { 42 | console.log(deletedToken) 43 | } 44 | 45 | console.log( 46 | `Deleting ${deleteTokens.length} | Insert ${insertTokens.length} | Keep: ${updateTokens.length}` 47 | ) 48 | } 49 | 50 | compare() 51 | -------------------------------------------------------------------------------- /src/cron.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | dotenv.config() 3 | 4 | import mongoose from 'mongoose' 5 | 6 | import * as SyncJob from './jobs/sync.job' 7 | import * as OsReportUtil from './utils/osReport.util' 8 | 9 | dotenv.config() 10 | 11 | setInterval(OsReportUtil.task('utl-api-cron'), OsReportUtil.interval) 12 | 13 | mongoose 14 | .connect(process.env.DB_URL as string) 15 | .then((db) => { 16 | console.log(`Connected to ${db.connections[0].name} - mongodb`) 17 | SyncJob.cronJob().start() 18 | }) 19 | .catch((error) => { 20 | console.log('There was an error connecting to db') 21 | console.log(error) 22 | }) 23 | -------------------------------------------------------------------------------- /src/jobs/sync.job.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { CronJob } from 'cron' 3 | import _ from 'lodash' 4 | import mongoose from 'mongoose' 5 | 6 | import TokenModel from '../models/token.model' 7 | import LoggerUtil from '../utils/logger.util' 8 | 9 | 10 | export interface Token { 11 | address: string 12 | name: string 13 | symbol: string 14 | logoURI: string | null 15 | chainId: number 16 | decimals: number 17 | holders: number | null 18 | verified: boolean 19 | tags: string[] 20 | extensions: object 21 | } 22 | 23 | let jobRunning: null|number = null; 24 | 25 | async function handle() { 26 | LoggerUtil.info(`${name} | Start ${Date.now()}`) 27 | 28 | const cdnUrl = process.env.CDN_URL 29 | if (!cdnUrl) { 30 | LoggerUtil.info(`${name} | No CDN URL provided`) 31 | throw new Error('No CDN URL provided') 32 | } 33 | 34 | const response = await axios.get<{ 35 | tokens: Token[] 36 | }>(cdnUrl) 37 | 38 | const newTokens = response.data.tokens.filter((token) => { 39 | return token.address && token.chainId && token.name && token.symbol && token.decimals 40 | }) 41 | 42 | const currentTokens = await TokenModel.find({}) 43 | 44 | LoggerUtil.info( 45 | `${name} | New count: ${newTokens.length} | Current count: ${currentTokens.length}` 46 | ) 47 | 48 | const newMints = newTokens.map( 49 | (token) => `${token.address}:${token.chainId}` 50 | ) 51 | const currentMints = currentTokens.map( 52 | (token) => `${token.address}:${token.chainId}` 53 | ) 54 | 55 | const deleteTokens = []; 56 | // currentTokens.filter((token) => { 57 | // return !newMints.includes(`${token.address}:${token.chainId}`) 58 | // }) 59 | 60 | const updateTokens = currentTokens.filter((token) => { 61 | return newMints.includes(`${token.address}:${token.chainId}`) 62 | }) 63 | 64 | const insertTokens = newTokens.filter((token) => { 65 | return !currentMints.includes(`${token.address}:${token.chainId}`) 66 | }) 67 | 68 | LoggerUtil.info( 69 | `${name} | TO BE Deleted: ${deleteTokens.length} | Updated: ${updateTokens.length} | Created: ${insertTokens.length}` 70 | ) 71 | 72 | if (process.env.SYNC_SAVE !== '1') { 73 | LoggerUtil.info(`${name} | Aborted`) 74 | return 75 | } 76 | 77 | 78 | let session;//; = await mongoose.connection.startSession() 79 | // try { 80 | // session.startTransaction() 81 | // 82 | // await TokenModel.deleteMany( 83 | // { _id: { $in: deleteTokens.map((token) => token._id) } }, 84 | // { session } 85 | // ) 86 | // await session.commitTransaction() 87 | // } 88 | // catch (error: any) { 89 | // await session.abortTransaction() 90 | // LoggerUtil.info(`${name} | error deleting from db ${error.message}`) 91 | // return; 92 | // } finally { 93 | // await session.endSession() 94 | // } 95 | 96 | 97 | const insertTokensBatches = _.chunk(insertTokens, 2000); 98 | for (const insertTokensBatch of insertTokensBatches) { 99 | session = await mongoose.connection.startSession() 100 | try { 101 | session.startTransaction() 102 | 103 | 104 | for (const token of insertTokensBatch) { 105 | await TokenModel.create( 106 | [ 107 | { 108 | address: token.address, 109 | name: token.name, 110 | symbol: token.symbol, 111 | decimals: token.decimals, 112 | chainId: token.chainId, 113 | verified: token.verified, 114 | logoURI: token.logoURI ?? null, 115 | holders: token.holders, 116 | tags: token.tags, 117 | extensions: token.extensions, 118 | }, 119 | ], 120 | { session } 121 | ) 122 | } 123 | 124 | 125 | await session.commitTransaction() 126 | } catch (error: any) { 127 | await session.abortTransaction() 128 | LoggerUtil.info(`${name} | error inserting to db ${error.message}`) 129 | break; 130 | } finally { 131 | await session.endSession() 132 | } 133 | } 134 | 135 | const updateTokensBatches = _.chunk(updateTokens, 1000); 136 | for (const updateTokensBatch of updateTokensBatches) { 137 | session = await mongoose.connection.startSession() 138 | try { 139 | session.startTransaction() 140 | 141 | for (const token of updateTokensBatch) { 142 | const newToken = newTokens.find( 143 | (t) => 144 | t.address === token.address && t.chainId === token.chainId 145 | ) 146 | 147 | if (!newToken) { 148 | LoggerUtil.info( 149 | `${name} | Couldnt find new token from current: ${token.address}` 150 | ) 151 | continue 152 | } 153 | 154 | await TokenModel.updateOne( 155 | { 156 | address: token.address, 157 | chainId: token.chainId, 158 | }, 159 | { 160 | $set: { 161 | name: newToken.name, 162 | symbol: newToken.symbol, 163 | decimals: newToken.decimals, 164 | verified: newToken.verified, 165 | logoURI: newToken.logoURI ?? null, 166 | holders: newToken.holders, 167 | tags: newToken.tags, 168 | extensions: newToken.extensions, 169 | }, 170 | }, 171 | { session } 172 | ) 173 | } 174 | await session.commitTransaction() 175 | } catch (error: any) { 176 | await session.abortTransaction() 177 | LoggerUtil.info(`${name} | error updating to db ${error.message}`) 178 | break; 179 | } finally { 180 | await session.endSession() 181 | } 182 | } 183 | 184 | LoggerUtil.info( 185 | `${name} | Deleted: ${deleteTokens.length} | Updated: ${updateTokens.length} | Created: ${insertTokens.length}` 186 | ) 187 | } 188 | 189 | /* istanbul ignore next */ 190 | export const cronJob = () => 191 | new CronJob( 192 | process.env.CRON_SYNC ?? '0 * * * * *', 193 | async () => { 194 | if (jobRunning) { 195 | LoggerUtil.info(`${name} | Skip already running from ${jobRunning}`) 196 | return 197 | } 198 | jobRunning = Date.now(); 199 | try { 200 | await handle() // 30 days 201 | } catch (error: any) { 202 | LoggerUtil.info(`${name} | Failed: ${error.message}`) 203 | } finally { 204 | jobRunning = null 205 | } 206 | 207 | }, 208 | null, 209 | true, 210 | 'UTC' 211 | ) 212 | 213 | export const name = 'utl-api-cron-sync' 214 | -------------------------------------------------------------------------------- /src/models/token.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | export interface IToken extends mongoose.Document { 4 | address: string 5 | chainId: number 6 | name: string 7 | symbol: string 8 | verified: boolean 9 | decimals: number 10 | holders: number | null 11 | logoURI: string | null 12 | tags: string[] 13 | extensions?: { 14 | coingeckoId: string 15 | } 16 | } 17 | 18 | export const TokenSchema = new mongoose.Schema( 19 | { 20 | address: { 21 | type: String, 22 | required: true, 23 | }, 24 | chainId: { 25 | type: Number, 26 | required: true, 27 | }, 28 | name: { 29 | type: String, 30 | required: true, 31 | }, 32 | symbol: { 33 | type: String, 34 | required: true, 35 | }, 36 | verified: { 37 | type: Boolean, 38 | required: true, 39 | }, 40 | decimals: { 41 | type: Number, 42 | required: true, 43 | }, 44 | holders: { 45 | type: Number, 46 | default: null, 47 | }, 48 | logoURI: { 49 | type: String, 50 | default: null, 51 | }, 52 | tags: { 53 | type: [String], 54 | required: true, 55 | default: [], 56 | }, 57 | extensions: { 58 | type: mongoose.Schema.Types.Mixed, 59 | }, 60 | }, 61 | { 62 | timestamps: true, 63 | toJSON: { 64 | transform: function (doc, ret, options) { 65 | delete ret._id 66 | delete ret.__v 67 | delete ret.createdAt 68 | delete ret.updatedAt 69 | return ret 70 | }, 71 | }, 72 | } 73 | ) 74 | 75 | TokenSchema.index({ address: 1, chainId: 1 }, { unique: true }) 76 | TokenSchema.index({ name: 'text', symbol: 'text' }, { weights: { name: 5, symbol: 10 }}) 77 | TokenSchema.index({ chainId: 1 }) 78 | TokenSchema.index({ holders: -1 }) 79 | TokenSchema.index({ verified: -1 }) 80 | TokenSchema.index({ tags: 1 }) 81 | 82 | const TokenModel = mongoose.model('token', TokenSchema) 83 | export default TokenModel 84 | -------------------------------------------------------------------------------- /src/utils/errors.util.ts: -------------------------------------------------------------------------------- 1 | export const codes = { 2 | UNAUTHORIZED: 401, 3 | BAD_REQUEST: 400, 4 | NOT_FOUND: 404, 5 | TOO_MANY_REQUESTS: 429, 6 | INTERNAL_SERVER_ERROR: 500, 7 | } 8 | 9 | export class HttpError extends Error { 10 | name: string 11 | statusCode: number 12 | data: unknown 13 | 14 | constructor( 15 | message: string, 16 | name: string | null, 17 | statusCode: number, 18 | data: unknown 19 | ) { 20 | super(message) 21 | this.name = name ?? this.constructor.name 22 | this.statusCode = statusCode 23 | this.data = data 24 | Error.captureStackTrace(this, HttpError) 25 | } 26 | } 27 | 28 | export class HttpTooManyRequests extends HttpError { 29 | constructor(message: string | null = null, data: unknown = null) { 30 | super( 31 | message ?? 'Too Many Requests', 32 | null, 33 | codes.TOO_MANY_REQUESTS, 34 | data ?? {} 35 | ) 36 | } 37 | } 38 | 39 | export class HttpUnauthorized extends HttpError { 40 | constructor(message: string | null = null, data: unknown = null) { 41 | super(message ?? 'Unauthorized', null, codes.UNAUTHORIZED, data ?? {}) 42 | } 43 | } 44 | 45 | export class HttpBadRequest extends HttpError { 46 | constructor(message: string | null = null, data: unknown = null) { 47 | super(message ?? 'Bad request', null, codes.BAD_REQUEST, data ?? {}) 48 | } 49 | } 50 | 51 | export class HttpValidationError extends HttpError { 52 | constructor(message: string | null = null, data: unknown = null) { 53 | super( 54 | message ?? 'Validation error', 55 | null, 56 | codes.BAD_REQUEST, 57 | data ?? {} 58 | ) 59 | } 60 | } 61 | 62 | export class HttpNotFound extends HttpError { 63 | constructor(message: string | null = null, data: unknown = null) { 64 | super(message ?? 'Not Found', null, codes.NOT_FOUND, data ?? {}) 65 | } 66 | } 67 | 68 | /* istanbul ignore next */ 69 | export class HttpInternalServerError extends HttpError { 70 | constructor(message: string | null = null, data: unknown = null) { 71 | super( 72 | message ?? 'Internal server error', 73 | null, 74 | codes.INTERNAL_SERVER_ERROR, 75 | data ?? {} 76 | ) 77 | } 78 | } 79 | 80 | export default { 81 | codes, 82 | HttpUnauthorized, 83 | HttpError, 84 | HttpBadRequest, 85 | HttpValidationError, 86 | HttpNotFound, 87 | HttpInternalServerError, 88 | HttpTooManyRequests, 89 | } 90 | -------------------------------------------------------------------------------- /src/utils/httpRequest.util.ts: -------------------------------------------------------------------------------- 1 | import Axios, { AxiosError, AxiosRequestConfig } from 'axios' 2 | import _ from 'lodash' 3 | 4 | import LoggerUtil from './logger.util' 5 | 6 | /* istanbul ignore next */ 7 | const SlimAxiosError = ( 8 | error: unknown, 9 | url: string, 10 | data: unknown | null = null 11 | ) => { 12 | const slimError = new Error((error as Error).message) as any 13 | 14 | const axios = { 15 | url, 16 | data: _.truncate(JSON.stringify(data), { 17 | length: 1000, 18 | }), 19 | response: {}, 20 | request: {}, 21 | meta: {}, 22 | } 23 | 24 | if (error instanceof AxiosError) { 25 | if (error.response !== undefined) { 26 | axios.response = { 27 | status: error.response.status, 28 | data: _.truncate(JSON.stringify(error.response.data), { 29 | length: 1000, 30 | }), 31 | } 32 | } 33 | 34 | if (error.request !== undefined) { 35 | axios.request = _.pick(error.request, [ 36 | 'host', 37 | 'path', 38 | 'method', 39 | 'protocol', 40 | ]) 41 | } 42 | } 43 | 44 | slimError.meta = { axios } 45 | 46 | LoggerUtil.error(slimError.message, slimError.meta) 47 | return slimError 48 | } 49 | 50 | /* istanbul ignore next */ 51 | export const get = async (url: string, config: AxiosRequestConfig) => { 52 | try { 53 | return await Axios.get(url, config) 54 | } catch (error) { 55 | throw SlimAxiosError(error, url) 56 | } 57 | } 58 | 59 | /* istanbul ignore next */ 60 | export const del = async (url: string, config: AxiosRequestConfig) => { 61 | try { 62 | return await Axios.delete(url, config) 63 | } catch (error) { 64 | throw SlimAxiosError(error, url) 65 | } 66 | } 67 | 68 | export const post = async ( 69 | url: string, 70 | data: unknown, 71 | config: AxiosRequestConfig 72 | ) => { 73 | try { 74 | return await Axios.post(url, data, config) 75 | } catch (error) { 76 | throw SlimAxiosError(error, url, data) 77 | } 78 | } 79 | 80 | /* istanbul ignore next */ 81 | export const put = async ( 82 | url: string, 83 | data: unknown, 84 | config: AxiosRequestConfig 85 | ) => { 86 | try { 87 | return await Axios.put(url, data, config) 88 | } catch (error) { 89 | throw SlimAxiosError(error, url, data) 90 | } 91 | } 92 | 93 | /* istanbul ignore next */ 94 | export const patch = async ( 95 | url: string, 96 | data: unknown, 97 | config: AxiosRequestConfig 98 | ) => { 99 | try { 100 | return await Axios.patch(url, data, config) 101 | } catch (error) { 102 | throw SlimAxiosError(error, url, data) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/utils/logger.util.ts: -------------------------------------------------------------------------------- 1 | import { LoggingWinston } from '@google-cloud/logging-winston' 2 | import winston from 'winston' 3 | 4 | const transports = [] 5 | 6 | if (process.env.GCP_PROJECT) { 7 | /* istanbul ignore next */ 8 | transports.push(new LoggingWinston()) 9 | } else { 10 | transports.push(new winston.transports.Console()) 11 | } 12 | 13 | export const Logger = winston.createLogger({ 14 | level: 'info', 15 | format: winston.format.combine( 16 | winston.format.errors({ stack: true }), 17 | winston.format.json() 18 | ), 19 | transports, 20 | silent: process.env.JEST_WORKER_ID !== undefined, 21 | }) 22 | 23 | export default Logger 24 | -------------------------------------------------------------------------------- /src/utils/osReport.util.ts: -------------------------------------------------------------------------------- 1 | import os from 'os' 2 | 3 | import LoggerUtil from './logger.util' 4 | 5 | export const task = (processName: string) => { 6 | return () => { 7 | const memoryRatio = 8 | Math.round((os.freemem() / os.totalmem()) * 100) / 100 9 | LoggerUtil.info(`${processName} | os | mem.r: ${memoryRatio}`) 10 | } 11 | } 12 | 13 | export const interval = 5000 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "forceConsistentCasingInFileNames": true, 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "target": "es6", 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "resolveJsonModule": true 12 | } 13 | } 14 | --------------------------------------------------------------------------------