diff --git a/package.json b/package.json index b746ed65e..4ff58fd31 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "dependencies": { "@golevelup/nestjs-modules": "^0.5.0", "@nestjs/platform-express": "8.4.2", + "@nestjs/schedule": "^2.1.0", "bitcoin-address-validation": "^2.2.1", "esbuild": "^0.14.27", "esbuild-runner": "^2.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae0cb74eb..ffd7ca789 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,7 @@ specifiers: '@nestjs/config': ^1.1.0 '@nestjs/core': 8.4.2 '@nestjs/platform-express': 8.4.2 + '@nestjs/schedule': ^2.1.0 '@nestjs/swagger': ^5.2.1 '@oclif/core': ^1 '@oclif/plugin-help': ^5 @@ -83,6 +84,7 @@ specifiers: dependencies: '@golevelup/nestjs-modules': 0.5.0_i3vvyrpmvytyggqwb6cgka6ccm '@nestjs/platform-express': 8.4.2_ithql7ga4sptmu4cnjc25qhlkq + '@nestjs/schedule': 2.1.0_7o3lftxivihh5tsq4a6jxsybhu bitcoin-address-validation: 2.2.1 esbuild: 0.14.27 esbuild-runner: 2.2.1_esbuild@0.14.27 @@ -1278,7 +1280,6 @@ packages: uuid: 8.3.2 transitivePeerDependencies: - debug - dev: true /@nestjs/config/1.2.1_6d336j5z3wdgfwg6g6i43w4msm: resolution: {integrity: sha512-EgaGTXvG4unD5lGWmdSrUFrkGpX32lQGE/8qS60EnL82sIZV7HT1ZL7ib5S86P1nB+DnFDbDhDqTaZ3mivTyOg==} @@ -1327,7 +1328,6 @@ packages: uuid: 8.3.2 transitivePeerDependencies: - encoding - dev: true /@nestjs/mapped-types/1.0.1_xluz7cqyskci5bcbv7rvfyu5ra: resolution: {integrity: sha512-NFvofzSinp00j5rzUd4tf+xi9od6383iY0JP7o0Bnu1fuItAUkWBgc4EKuIQ3D+c2QI3i9pG1kDWAeY27EMGtg==} @@ -1363,6 +1363,19 @@ packages: tslib: 2.3.1 transitivePeerDependencies: - supports-color + + /@nestjs/schedule/2.1.0_7o3lftxivihh5tsq4a6jxsybhu: + resolution: {integrity: sha512-4Xaw56WiW3VsxEPPnj/iDtfjcO+sUZyYAeRxD0gnF5havncxjAnv52Iw7UH3DuzzUA784xPGgGje3Fq0Gu925g==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 + reflect-metadata: ^0.1.12 + dependencies: + '@nestjs/common': 8.4.2_vxit34wn5s2lmlgt65te5kboda + '@nestjs/core': 8.4.2_ygos5qxglvmaixv4w45msfva4a + cron: 2.0.0 + reflect-metadata: 0.1.13 + uuid: 8.3.2 dev: false /@nestjs/schematics/8.0.8_ezuyfu3uj65g35wdjb6342atpm: @@ -1437,7 +1450,6 @@ packages: node-fetch: 2.6.7 transitivePeerDependencies: - encoding - dev: true /@oclif/color/1.0.1: resolution: {integrity: sha512-qjYr+izgWdIVOroiBKqTzQgc1r5Wd9QB1J7yGM2EeelqhBARiiVLRZL45vhV4zdyTRdDkZS0EBzFwQap+nliLA==} @@ -2288,7 +2300,6 @@ packages: /append-field/1.0.0: resolution: {integrity: sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=} - dev: false /arg/4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -2388,7 +2399,6 @@ packages: /async/3.2.0: resolution: {integrity: sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==} - dev: true /asynckit/0.4.0: resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=} @@ -2418,7 +2428,6 @@ packages: follow-redirects: 1.14.9 transitivePeerDependencies: - debug - dev: true /babel-jest/27.5.1_@babel+core@7.17.9: resolution: {integrity: sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==} @@ -2742,7 +2751,6 @@ packages: dependencies: dicer: 0.2.5 readable-stream: 1.1.14 - dev: false /bytes/3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} @@ -2754,7 +2762,6 @@ packages: async: 3.2.0 lodash: 4.17.21 lru-cache: 6.0.0 - dev: true /cacheable-request/6.1.0: resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} @@ -2900,14 +2907,12 @@ packages: /class-transformer/0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} - dev: true /class-validator/0.13.2: resolution: {integrity: sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==} dependencies: libphonenumber-js: 1.9.50 validator: 13.7.0 - dev: true /clean-stack/2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} @@ -3093,7 +3098,6 @@ packages: inherits: 2.0.4 readable-stream: 2.3.7 typedarray: 0.0.6 - dev: false /configstore/5.0.1: resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} @@ -3109,7 +3113,6 @@ packages: /consola/2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} - dev: true /content-disposition/0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} @@ -3234,6 +3237,12 @@ packages: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true + /cron/2.0.0: + resolution: {integrity: sha512-RPeRunBCFr/WEo7WLp8Jnm45F/ziGJiHVvVQEBSDTSGu6uHW49b2FOP2O14DcXlGJRLhwE7TIoDzHHK4KmlL6g==} + dependencies: + luxon: 1.28.0 + dev: false + /cross-fetch/3.1.5: resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} dependencies: @@ -3463,7 +3472,6 @@ packages: dependencies: readable-stream: 1.1.14 streamsearch: 0.1.2 - dev: false /diff-sequences/27.5.1: resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} @@ -3679,7 +3687,6 @@ packages: cpu: [x64] os: [android] requiresBuild: true - dev: false optional: true /esbuild-android-arm64/0.14.27: @@ -3688,7 +3695,6 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: false optional: true /esbuild-darwin-64/0.14.27: @@ -3697,7 +3703,6 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: false optional: true /esbuild-darwin-arm64/0.14.27: @@ -3706,7 +3711,6 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: false optional: true /esbuild-freebsd-64/0.14.27: @@ -3715,7 +3719,6 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true - dev: false optional: true /esbuild-freebsd-arm64/0.14.27: @@ -3724,7 +3727,6 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true - dev: false optional: true /esbuild-linux-32/0.14.27: @@ -3733,7 +3735,6 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-linux-64/0.14.27: @@ -3742,7 +3743,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-linux-arm/0.14.27: @@ -3751,7 +3751,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-linux-arm64/0.14.27: @@ -3760,7 +3759,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-linux-mips64le/0.14.27: @@ -3769,7 +3767,6 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-linux-ppc64le/0.14.27: @@ -3778,7 +3775,6 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-linux-riscv64/0.14.27: @@ -3787,7 +3783,6 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-linux-s390x/0.14.27: @@ -3796,7 +3791,6 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true - dev: false optional: true /esbuild-netbsd-64/0.14.27: @@ -3805,7 +3799,6 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true - dev: false optional: true /esbuild-openbsd-64/0.14.27: @@ -3814,7 +3807,6 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true - dev: false optional: true /esbuild-runner/2.2.1_esbuild@0.14.27: @@ -3834,7 +3826,6 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true - dev: false optional: true /esbuild-windows-32/0.14.27: @@ -3843,7 +3834,6 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true - dev: false optional: true /esbuild-windows-64/0.14.27: @@ -3852,7 +3842,6 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: false optional: true /esbuild-windows-arm64/0.14.27: @@ -3861,7 +3850,6 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: false optional: true /esbuild/0.14.27: @@ -3890,7 +3878,6 @@ packages: esbuild-windows-32: 0.14.27 esbuild-windows-64: 0.14.27 esbuild-windows-arm64: 0.14.27 - dev: false /escalade/3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -4450,7 +4437,6 @@ packages: /fast-safe-stringify/2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - dev: true /fastq/1.13.0: resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} @@ -4554,7 +4540,6 @@ packages: peerDependenciesMeta: debug: optional: true - dev: true /foreach/2.0.5: resolution: {integrity: sha1-C+4AUBiusmDQo6865ljdATbsG5k=} @@ -5542,7 +5527,6 @@ packages: /iterare/1.2.1: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} - dev: true /jake/10.8.4: resolution: {integrity: sha512-MtWeTkl1qGsWUtbl/Jsca/8xSoK3x0UmS82sNbjqxxG/de/M/3b1DntdjHgPMC50enlTNwXOCRqPXLLt5cCfZA==} @@ -6236,7 +6220,6 @@ packages: /libphonenumber-js/1.9.50: resolution: {integrity: sha512-cCzQPChw2XbordcO2LKiw5Htx5leHVfFk/EXkxNHqJfFo7Fndcb1kF5wPJpc316vCJhhikedYnVysMh3Sc7Ocw==} - dev: true /lilconfig/2.0.5: resolution: {integrity: sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==} @@ -6367,7 +6350,10 @@ packages: engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: true + + /luxon/1.28.0: + resolution: {integrity: sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==} + dev: false /macos-release/2.5.0: resolution: {integrity: sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g==} @@ -6566,7 +6552,6 @@ packages: on-finished: 2.4.1 type-is: 1.6.18 xtend: 4.0.2 - dev: false /multibase/0.6.1: resolution: {integrity: sha512-pFfAwyTjbbQgNc3G7D48JkJxWtoJoBMaR4xQUOuB8RnCgRqaYmWNFeJTTvrJ2w51bjLq2zTby6Rqj9TQ9elSUw==} @@ -6667,7 +6652,6 @@ packages: optional: true dependencies: whatwg-url: 5.0.0 - dev: true /node-gyp-build/4.4.0: resolution: {integrity: sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==} @@ -6761,7 +6745,6 @@ packages: /object-hash/3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - dev: true /object-inspect/1.12.2: resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} @@ -6813,7 +6796,6 @@ packages: engines: {node: '>= 0.8'} dependencies: ee-first: 1.1.1 - dev: false /once/1.4.0: resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=} @@ -7052,7 +7034,6 @@ packages: /path-to-regexp/3.2.0: resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==} - dev: true /path-type/4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -7319,7 +7300,6 @@ packages: inherits: 2.0.4 isarray: 0.0.1 string_decoder: 0.10.31 - dev: false /readable-stream/2.3.7: resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} @@ -7377,7 +7357,6 @@ packages: /reflect-metadata/0.1.13: resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} - dev: true /regexpp/3.2.0: resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} @@ -7843,7 +7822,6 @@ packages: /streamsearch/0.1.2: resolution: {integrity: sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=} engines: {node: '>=0.8.0'} - dev: false /strict-uri-encode/1.1.0: resolution: {integrity: sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=} @@ -8197,7 +8175,6 @@ packages: /tr46/0.0.3: resolution: {integrity: sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=} - dev: true /tr46/2.1.0: resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} @@ -8475,7 +8452,6 @@ packages: /typedarray/0.0.6: resolution: {integrity: sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=} - dev: false /typescript/4.6.2: resolution: {integrity: sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==} @@ -8623,7 +8599,6 @@ packages: /uuid/8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true - dev: true /v8-compile-cache-lib/3.0.0: resolution: {integrity: sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==} @@ -8645,7 +8620,6 @@ packages: /validator/13.7.0: resolution: {integrity: sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==} engines: {node: '>= 0.10'} - dev: true /varint/5.0.2: resolution: {integrity: sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==} @@ -8957,7 +8931,6 @@ packages: /webidl-conversions/3.0.1: resolution: {integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=} - dev: true /webidl-conversions/5.0.0: resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} @@ -9048,7 +9021,6 @@ packages: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - dev: true /whatwg-url/8.7.0: resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==} @@ -9248,7 +9220,6 @@ packages: /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true /yaml/1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} diff --git a/src/app-toolkit/app-toolkit.interface.ts b/src/app-toolkit/app-toolkit.interface.ts index bfbd326fd..21d3c1935 100644 --- a/src/app-toolkit/app-toolkit.interface.ts +++ b/src/app-toolkit/app-toolkit.interface.ts @@ -9,6 +9,7 @@ import { DefaultDataProps } from '~position/display.interface'; import { AppTokenPosition, ContractPosition, NonFungibleToken } from '~position/position.interface'; import { AppGroupsDefinition } from '~position/position.service'; import { BaseToken } from '~position/token.interface'; +import { Filters, PriceSelector } from '~token/token-price-selector.interface'; import { Network } from '~types/network.interface'; import { AppToolkitHelperRegistry } from './app-toolkit.helpers'; @@ -30,6 +31,8 @@ export interface IAppToolkit { // Base Tokens + getBaseTokenPriceSelector(filter?: Filters, ctx?: { appId: string }): PriceSelector; + getBaseTokenPrices(network: Network): Promise; getBaseTokenPrice(opts: { network: Network; address: string }): Promise; diff --git a/src/app-toolkit/app-toolkit.service.ts b/src/app-toolkit/app-toolkit.service.ts index 568bc3e1e..38c61f8dd 100644 --- a/src/app-toolkit/app-toolkit.service.ts +++ b/src/app-toolkit/app-toolkit.service.ts @@ -12,6 +12,8 @@ import { PositionKeyService } from '~position/position-key.service'; import { AppTokenPosition, ContractPosition, NonFungibleToken } from '~position/position.interface'; import { AppGroupsDefinition, PositionService } from '~position/position.service'; import { BaseToken } from '~position/token.interface'; +import { PriceSelectorService } from '~token/price-selector.service'; +import { Filters } from '~token/token-price-selector.interface'; import { TokenService } from '~token/token.service'; import { Network } from '~types/network.interface'; @@ -29,6 +31,7 @@ export class AppToolkit implements IAppToolkit { @Inject(PositionService) private readonly positionService: PositionService, @Inject(PositionKeyService) private readonly positionKeyService: PositionKeyService, @Inject(TokenService) private readonly tokenService: TokenService, + @Inject(PriceSelectorService) private readonly priceSelectorService: PriceSelectorService, @Inject(MulticallService) private readonly multicallService: MulticallService, @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, ) { @@ -62,11 +65,15 @@ export class AppToolkit implements IAppToolkit { // Base Tokens getBaseTokenPrices(network: Network) { - return this.tokenService.getTokenPrices(network); + return this.priceSelectorService.create().getAll({ network }); } getBaseTokenPrice(opts: { network: Network; address: string }) { - return this.tokenService.getTokenPrice(opts); + return this.priceSelectorService.create().getOne(opts); + } + + getBaseTokenPriceSelector(filters: Filters = {}) { + return this.priceSelectorService.create(filters); } // Positions diff --git a/src/apps/ethereum/ethereum.module.ts b/src/apps/ethereum/ethereum.module.ts index e111dff9a..849463c79 100644 --- a/src/apps/ethereum/ethereum.module.ts +++ b/src/apps/ethereum/ethereum.module.ts @@ -1,6 +1,5 @@ import { Register } from '~app-toolkit/decorators'; import { AbstractApp } from '~app/app.dynamic-module'; -import { RocketPoolAppModule } from '~apps/rocket-pool'; import { EthereumContractFactory } from './contracts'; import { ETHEREUM_DEFINITION, EthereumAppDefinition } from './ethereum.definition'; diff --git a/src/apps/ethereum/ethereum/ethereum.balance-fetcher.ts b/src/apps/ethereum/ethereum/ethereum.balance-fetcher.ts index e997801f1..87703bc48 100644 --- a/src/apps/ethereum/ethereum/ethereum.balance-fetcher.ts +++ b/src/apps/ethereum/ethereum/ethereum.balance-fetcher.ts @@ -1,14 +1,11 @@ import { Inject } from '@nestjs/common'; import { BigNumber } from 'bignumber.js'; import { gql } from 'graphql-request'; -import { sumBy } from 'lodash'; import { drillBalance } from '~app-toolkit'; import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; import { Register } from '~app-toolkit/decorators'; import { presentBalanceFetcherResponse } from '~app-toolkit/helpers/presentation/balance-fetcher-response.present'; -import { RocketPoolContractFactory } from '~apps/rocket-pool'; -import { rocketMinipoolManagerAddress } from '~apps/rocket-pool/ethereum/rocket-pool.staking.contract-position-fetcher'; import { BalanceFetcher } from '~balance/balance-fetcher.interface'; import { Network } from '~types/network.interface'; @@ -36,7 +33,7 @@ const network = Network.ETHEREUM_MAINNET; @Register.BalanceFetcher(ETHEREUM_DEFINITION.id, network) export class EthereumEthereumBalanceFetcher implements BalanceFetcher { - constructor(@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit) { } + constructor(@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit) {} async getStakedBalances(address: string) { return this.appToolkit.helpers.contractPositionBalanceHelper.getContractPositionBalances({ diff --git a/src/apps/good-ghosting/helpers/good-ghosting.balance-fetcher-helper.ts b/src/apps/good-ghosting/helpers/good-ghosting.balance-fetcher-helper.ts index 70dfebdf4..8532da4b5 100644 --- a/src/apps/good-ghosting/helpers/good-ghosting.balance-fetcher-helper.ts +++ b/src/apps/good-ghosting/helpers/good-ghosting.balance-fetcher-helper.ts @@ -12,8 +12,8 @@ import { ContractType } from '~position/contract.interface'; import { ContractPositionBalance } from '~position/position-balance.interface'; import { isClaimable, isSupplied } from '~position/position.utils'; import { Network } from '~types/network.interface'; -import { NetworkId } from '../helpers/constants'; +import { NetworkId } from '../helpers/constants'; import { GoodGhostingGameConfigFetcherHelper } from '../helpers/good-ghosting.game.config-fetcher'; @Injectable() diff --git a/src/apps/good-ghosting/helpers/good-ghosting.game.config-fetcher.ts b/src/apps/good-ghosting/helpers/good-ghosting.game.config-fetcher.ts index 46d847185..3ad9216b0 100644 --- a/src/apps/good-ghosting/helpers/good-ghosting.game.config-fetcher.ts +++ b/src/apps/good-ghosting/helpers/good-ghosting.game.config-fetcher.ts @@ -3,9 +3,9 @@ import axios from 'axios'; import { SingleStakingFarmDefinition } from '~app-toolkit'; import { CacheOnInterval } from '~cache/cache-on-interval.decorator'; -import { NetworkId } from '../helpers/constants'; import GOOD_GHOSTING_DEFINITION from '../good-ghosting.definition'; +import { NetworkId } from '../helpers/constants'; import { GamesResponse, PlayerBalance, PlayerResponse, BASE_API_URL } from './constants'; diff --git a/src/apps/mean-finance/graphql/getUserPositions.ts b/src/apps/mean-finance/graphql/getUserPositions.ts index b61ff1b46..556641ccd 100644 --- a/src/apps/mean-finance/graphql/getUserPositions.ts +++ b/src/apps/mean-finance/graphql/getUserPositions.ts @@ -2,10 +2,7 @@ import { gql } from 'graphql-request'; export const GET_USER_POSITIONS = gql` query getUserPositions($address: String!, $first: Int, $lastId: String) { - positions( - where: { user: $address, status_in: [ACTIVE, COMPLETED], id_gt: $lastId } - first: $first - ) { + positions(where: { user: $address, status_in: [ACTIVE, COMPLETED], id_gt: $lastId }, first: $first) { id executedSwaps user diff --git a/src/apps/origin-dollar/ethereum/origin-dollar.veogv.token-fetcher.ts b/src/apps/origin-dollar/ethereum/origin-dollar.veogv.token-fetcher.ts index 99a701dc4..379e16d87 100644 --- a/src/apps/origin-dollar/ethereum/origin-dollar.veogv.token-fetcher.ts +++ b/src/apps/origin-dollar/ethereum/origin-dollar.veogv.token-fetcher.ts @@ -1,6 +1,6 @@ import { Inject } from '@nestjs/common'; -import moment from 'moment'; import { ethers } from 'ethers'; +import moment from 'moment'; import { IAppToolkit, APP_TOOLKIT } from '~app-toolkit/app-toolkit.interface'; import { Register } from '~app-toolkit/decorators'; diff --git a/src/apps/rocket-pool/ethereum/rocket-pool.balance-fetcher.ts b/src/apps/rocket-pool/ethereum/rocket-pool.balance-fetcher.ts index 367a1ea60..54106d628 100644 --- a/src/apps/rocket-pool/ethereum/rocket-pool.balance-fetcher.ts +++ b/src/apps/rocket-pool/ethereum/rocket-pool.balance-fetcher.ts @@ -10,6 +10,7 @@ import { Network } from '~types/network.interface'; import { RocketPoolContractFactory } from '../contracts'; import { ROCKET_POOL_DEFINITION } from '../rocket-pool.definition'; + import { rocketMinipoolManagerAddress, rocketNodeStakingAddress, diff --git a/src/apps/rocket-pool/ethereum/rocket-pool.staking.contract-position-fetcher.ts b/src/apps/rocket-pool/ethereum/rocket-pool.staking.contract-position-fetcher.ts index 142e5431f..abb5a4690 100644 --- a/src/apps/rocket-pool/ethereum/rocket-pool.staking.contract-position-fetcher.ts +++ b/src/apps/rocket-pool/ethereum/rocket-pool.staking.contract-position-fetcher.ts @@ -13,7 +13,7 @@ import { Network } from '~types/network.interface'; import { ROCKET_POOL_DEFINITION } from '../rocket-pool.definition'; export const rocketNodeStakingAddress = '0x3019227b2b8493e45bf5d25302139c9a2713bf15'; -export const rocketMinipoolManagerAddress = '0x6293B8abC1F36aFB22406Be5f96D893072A8cF3a'; +export const rocketMinipoolManagerAddress = '0x6293b8abc1f36afb22406be5f96d893072a8cf3a'; export const rocketTokenRPLAddress = '0xd33526068d116ce69f19a9ee46f0bd304f21a51f'; const appId = ROCKET_POOL_DEFINITION.id; diff --git a/src/position/position-fetcher.registry.ts b/src/position/position-fetcher.registry.ts index 4383117cc..c04857360 100644 --- a/src/position/position-fetcher.registry.ts +++ b/src/position/position-fetcher.registry.ts @@ -1,5 +1,5 @@ -import { Inject, OnModuleInit } from '@nestjs/common'; -import { DiscoveryService, Reflector } from '@nestjs/core'; +import { Inject, OnApplicationBootstrap } from '@nestjs/common'; +import { DiscoveryService } from '@nestjs/core'; import { compact } from 'lodash'; import { CACHE_ON_INTERVAL_KEY, CACHE_ON_INTERVAL_TIMEOUT } from '~cache/cache-on-interval.decorator'; @@ -27,7 +27,7 @@ export const buildAppPositionsCacheKey = (opts: { groupId: string; }) => `apps-v3:${opts.type}:${opts.network}:${opts.appId}:${opts.groupId}`; -export class PositionFetcherRegistry implements OnModuleInit { +export class PositionFetcherRegistry implements OnApplicationBootstrap { private registry: Registry< [ContractType, Network, string, string], { fetcher: PositionFetcher; options: PositionOptions } @@ -36,10 +36,9 @@ export class PositionFetcherRegistry implements OnModuleInit { constructor( @Inject(DiscoveryService) private readonly discoveryService: DiscoveryService, @Inject(CacheOnIntervalService) private readonly cacheOnIntervalService: CacheOnIntervalService, - @Inject(Reflector) private readonly reflector: Reflector, ) {} - onModuleInit() { + onApplicationBootstrap() { const wrappers = this.discoveryService.getProviders(); wrappers diff --git a/src/token/price-selector.service.ts b/src/token/price-selector.service.ts new file mode 100644 index 000000000..4dc6af21d --- /dev/null +++ b/src/token/price-selector.service.ts @@ -0,0 +1,107 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { Interval } from '@nestjs/schedule'; +import DataLoader from 'dataloader'; +import Cache from 'file-system-cache'; +import { isNil, map } from 'lodash'; +import moment from 'moment'; + +import { Network } from '~types'; + +import { TokenApiClient } from './token-api.client'; +import { + BaseTokenPrice, + Filters, + GetAll, + GetOne, + PriceSelector, + PriceSelectorFactory, +} from './token-price-selector.interface'; + +type TokenDataLoaderKey = { network: Network; address: string }; + +@Injectable() +export class PriceSelectorService implements PriceSelectorFactory { + private logger = new Logger(PriceSelectorService.name); + private cacheManager = Cache({ + basePath: './.cache', + ns: 'price-selector', + }); + + constructor(@Inject(TokenApiClient) private readonly tokenApiClient: TokenApiClient) {} + + private getCacheKey(network: Network) { + return `$tokens:${network}`; + } + + private async getCachedNetworkTokens(network: Network) { + const tokenMap = (await this.cacheManager.get(this.getCacheKey(network))) as unknown as Record< + string, + BaseTokenPrice + >; + if (!tokenMap) throw new Error(`Could not retrieve "${network}" tokens from cache`); + return tokenMap; + } + + private async getAllFromCache({ network }: Parameters[0], filters: Filters = {}) { + const cacheTokenMap = await this.getCachedNetworkTokens(network); + const tokens = Object.values(cacheTokenMap); + + return tokens.filter(t => { + if (!isNil(filters.exchangeable) && filters.exchangeable !== t.canExchange) return false; + if (!isNil(filters.hidden) && filters.hidden !== t.hide) return false; + return true; + }); + } + + private async getOneFromCache({ network, address }: Parameters[0], filters: Filters = {}) { + const cacheTokenMap = await this.getCachedNetworkTokens(network); + const match = cacheTokenMap[address]; + + if (!match) return null; + if (!isNil(filters.exchangeable) && filters.exchangeable !== match.canExchange) return null; + if (!isNil(filters.hidden) && filters.hidden !== match.hide) return null; + + return match; + } + + create(filters: Filters = {}): PriceSelector { + const tokenDataLoader = new DataLoader(async keys => + Promise.all(keys.map(key => this.getOneFromCache(key, filters))), + ); + + return { + getAll: opts => this.getAllFromCache(opts, filters), + getOne: ({ network, address }: Parameters[0]) => { + return tokenDataLoader.load({ network, address }); + }, + }; + } + + /* Internals */ + + async onApplicationBootstrap() { + try { + await this.updateCache(); + } catch (e) { + this.logger.error(`Could not populate local base token cache on startup ${e.message}`); + } + } + + @Interval(moment.duration(2, 'minutes').asMilliseconds()) + private async updateCache() { + const networks = Object.values(Network); + + await Promise.all( + map(networks, async network => { + const map: Record = {}; + const baseTokens = await this.tokenApiClient.getAllBaseTokenPrices(network); + + baseTokens.forEach(token => { + map[token.address] = token; + }); + + await this.cacheManager.set(this.getCacheKey(network), map as any); + }), + ); + } +} diff --git a/src/token/token-api.client.ts b/src/token/token-api.client.ts new file mode 100644 index 000000000..e304c616c --- /dev/null +++ b/src/token/token-api.client.ts @@ -0,0 +1,24 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import Axios, { AxiosInstance } from 'axios'; + +import { Network } from '~types/network.interface'; + +import { BaseTokenPrice } from './token-price-selector.interface'; + +@Injectable() +export class TokenApiClient { + private readonly axios: AxiosInstance; + + constructor(@Inject(ConfigService) private readonly configService: ConfigService) { + this.axios = Axios.create({ + baseURL: this.configService.get('zapperApi.url'), + params: { api_key: this.configService.get('zapperApi.key') }, + }); + } + + async getAllBaseTokenPrices(network: Network) { + const { data: tokenPrices } = await this.axios.get('/v2/prices', { params: { network } }); + return tokenPrices; + } +} diff --git a/src/token/token-price-selector.interface.ts b/src/token/token-price-selector.interface.ts new file mode 100644 index 000000000..ec5f77175 --- /dev/null +++ b/src/token/token-price-selector.interface.ts @@ -0,0 +1,19 @@ +import { BaseToken } from '~position/token.interface'; +import { Network } from '~types/network.interface'; + +export type BaseTokenPrice = BaseToken & { hide: boolean; canExchange: boolean }; + +export type Filters = { exchangeable?: boolean; hidden?: boolean }; +export type LoggingTags = { appId?: string; network?: Network }; + +export type GetAll = (opts: { network: Network }) => Promise; +export type GetOne = (opts: Parameters[0] & { address: string }) => Promise; + +export interface PriceSelector { + getAll: GetAll; + getOne: GetOne; +} + +export interface PriceSelectorFactory { + create: (filters?: Filters, tags?: LoggingTags) => PriceSelector; +} diff --git a/src/token/token.module.ts b/src/token/token.module.ts index 192302319..5a5126443 100644 --- a/src/token/token.module.ts +++ b/src/token/token.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; +import { PriceSelectorService } from './price-selector.service'; +import { TokenApiClient } from './token-api.client'; import { TokenService } from './token.service'; @Module({ - providers: [TokenService], - exports: [TokenService], + providers: [TokenService, TokenApiClient, PriceSelectorService], + exports: [TokenService, PriceSelectorService], }) export class TokenModule {} diff --git a/src/token/token.service.ts b/src/token/token.service.ts index 404715ca0..d2e0586c0 100644 --- a/src/token/token.service.ts +++ b/src/token/token.service.ts @@ -1,26 +1,17 @@ import { Inject, Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import Axios, { AxiosInstance } from 'axios'; import { Cache } from '~cache/cache.decorator'; -import { BaseToken } from '~position/token.interface'; import { Network } from '~types/network.interface'; +import { TokenApiClient } from './token-api.client'; + @Injectable() export class TokenService { - private readonly axios: AxiosInstance; - - constructor(@Inject(ConfigService) private readonly configService: ConfigService) { - this.axios = Axios.create({ - baseURL: this.configService.get('zapperApi.url'), - params: { api_key: this.configService.get('zapperApi.key') }, - }); - } + constructor(@Inject(TokenApiClient) private readonly tokenApiClient: TokenApiClient) {} @Cache({ key: (network: Network) => `token-prices:${network}`, ttl: 60 }) async getTokenPrices(network: Network) { - const { data: tokenPrices } = await this.axios.get('/v2/prices', { params: { network } }); - return tokenPrices; + return this.tokenApiClient.getAllBaseTokenPrices(network); } async getTokenPrice({ network, address }: { network: Network; address: string }) {