├── .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 | #
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 |
--------------------------------------------------------------------------------