From ff575c54b545af1a660b700676c7b51de08fe206 Mon Sep 17 00:00:00 2001 From: Yuru Shao Date: Sat, 6 Apr 2024 01:21:27 -0700 Subject: [PATCH] api: return real time prices (#56) Implementation based on example code at https://github.com/pyth-network/pyth-crosschain/tree/main/target_chains/ethereum/sdk/js --- api/package.json | 2 + api/pnpm-lock.yaml | 140 +++++++++++++++++++++++++++++++++++++-- api/server.js | 32 +++++++-- api/tests/server.test.js | 21 +++++- 4 files changed, 183 insertions(+), 12 deletions(-) diff --git a/api/package.json b/api/package.json index 25969a3f..5cb31ec4 100644 --- a/api/package.json +++ b/api/package.json @@ -11,7 +11,9 @@ "author": "", "license": "ISC", "dependencies": { + "@pythnetwork/pyth-evm-js": "^1.38.0", "@scure/base": "^1.1.6", + "bn.js": "^5.2.1", "canvas": "^2.11.2", "cors": "^2.8.5", "express": "^4.19.2" diff --git a/api/pnpm-lock.yaml b/api/pnpm-lock.yaml index 3c78e1fa..2cdbd067 100644 --- a/api/pnpm-lock.yaml +++ b/api/pnpm-lock.yaml @@ -5,9 +5,15 @@ settings: excludeLinksFromLockfile: false dependencies: + '@pythnetwork/pyth-evm-js': + specifier: ^1.38.0 + version: 1.38.0 '@scure/base': specifier: ^1.1.6 version: 1.1.6 + bn.js: + specifier: ^5.2.1 + version: 5.2.1 canvas: specifier: ^2.11.2 version: 2.11.2 @@ -329,6 +335,13 @@ packages: '@babel/helper-plugin-utils': 7.24.0 dev: true + /@babel/runtime@7.24.4: + resolution: {integrity: sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + dev: false + /@babel/template@7.24.0: resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} engines: {node: '>=6.9.0'} @@ -647,6 +660,37 @@ packages: - supports-color dev: false + /@pythnetwork/price-service-client@1.9.0: + resolution: {integrity: sha512-SLm3IFcfmy9iMqHeT4Ih6qMNZhJEefY14T9yTlpsH2D/FE5+BaGGnfcexUifVlfH6M7mwRC4hEFdNvZ6ebZjJg==} + dependencies: + '@pythnetwork/price-service-sdk': 1.6.0 + '@types/ws': 8.5.10 + axios: 1.6.8 + axios-retry: 3.9.1 + isomorphic-ws: 4.0.1(ws@8.16.0) + ts-log: 2.2.5 + ws: 8.16.0 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + dev: false + + /@pythnetwork/price-service-sdk@1.6.0: + resolution: {integrity: sha512-3mtlxvT/V3uCqmAGFTRAoJ4u0AXGHgIZVBs4uyRxysURTgywJk4yxoD3xPhaUjd5sfRTon546ZZ67L7oyUFMUg==} + dev: false + + /@pythnetwork/pyth-evm-js@1.38.0: + resolution: {integrity: sha512-jvxtq/0SxS2V2QnXXcDf0JSDkVjL2lShzON4xEL3hQx+svGJkE4mqgSrz+0bfj9WOi9xCGADKgu8I6r+wqPBdA==} + dependencies: + '@pythnetwork/price-service-client': 1.9.0 + buffer: 6.0.3 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + dev: false + /@scure/base@1.1.6: resolution: {integrity: sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==} dev: false @@ -722,12 +766,17 @@ packages: resolution: {integrity: sha512-sD+ia2ubTeWrOu+YMF+MTAB7E+O7qsMqAbMfW7DG3K1URwhZ5hN1pLlRVGbf4wDFzSfikL05M17EyorS86jShw==} dependencies: undici-types: 5.26.5 - dev: true /@types/stack-utils@2.0.3: resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} dev: true + /@types/ws@8.5.10: + resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + dependencies: + '@types/node': 20.12.3 + dev: false + /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} dev: true @@ -824,7 +873,23 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: true + + /axios-retry@3.9.1: + resolution: {integrity: sha512-8PJDLJv7qTTMMwdnbMvrLYuvB47M81wRtxQmEdV5w4rgbTXTt+vtPkXwajOfOdSyv/wZICJOC+/UhXH4aQ/R+w==} + dependencies: + '@babel/runtime': 7.24.4 + is-retry-allowed: 2.2.0 + dev: false + + /axios@1.6.8: + resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false /babel-jest@29.7.0(@babel/core@7.24.3): resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} @@ -901,11 +966,19 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + /binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} dev: true + /bn.js@5.2.1: + resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + dev: false + /body-parser@1.20.2: resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -960,6 +1033,13 @@ packages: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + /bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1107,7 +1187,6 @@ packages: engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 - dev: true /component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} @@ -1240,7 +1319,6 @@ packages: /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - dev: true /delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} @@ -1459,6 +1537,16 @@ packages: path-exists: 4.0.0 dev: true + /follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -1466,7 +1554,6 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: true /formidable@2.1.2: resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} @@ -1658,6 +1745,10 @@ packages: safer-buffer: 2.1.2 dev: false + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + /ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} dev: true @@ -1733,6 +1824,11 @@ packages: engines: {node: '>=0.12.0'} dev: true + /is-retry-allowed@2.2.0: + resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} + engines: {node: '>=10'} + dev: false + /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -1742,6 +1838,14 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /isomorphic-ws@4.0.1(ws@8.16.0): + resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} + peerDependencies: + ws: '*' + dependencies: + ws: 8.16.0 + dev: false + /istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -2613,6 +2717,10 @@ packages: ipaddr.js: 1.9.1 dev: false + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} dev: true @@ -2662,6 +2770,10 @@ packages: picomatch: 2.3.1 dev: true + /regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + dev: false + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -3000,6 +3112,10 @@ packages: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: false + /ts-log@2.2.5: + resolution: {integrity: sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==} + dev: false + /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} @@ -3024,7 +3140,6 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true /unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} @@ -3116,6 +3231,19 @@ packages: signal-exit: 3.0.7 dev: true + /ws@8.16.0: + resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} diff --git a/api/server.js b/api/server.js index 9af0595e..8b1fb22e 100644 --- a/api/server.js +++ b/api/server.js @@ -3,6 +3,7 @@ const { createCanvas, loadImage } = require("canvas"); const { validatePubkey } = require("./validation"); const { priceHistory, fundPerformance } = require("./prices"); const cors = require("cors"); +const pyth = require("@pythnetwork/pyth-evm-js"); BASE_URL = "https://api.glam.systems"; @@ -23,6 +24,29 @@ app.get("/_/health", (req, res) => { res.send("ok"); }); +app.get("/prices", async (req, res) => { + const connection = new pyth.EvmPriceServiceConnection( + "https://hermes.pyth.network" + ); + // You can find the ids of prices at https://pyth.network/developers/price-feed-ids#pyth-evm-stable + const priceIds = [ + "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", // BTC/USD price id + "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", // ETH/USD price id + "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d", // SOL/USD price id + "0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a" // USDC/USD price id + ]; + const priceFeeds = await connection.getLatestPriceFeeds(priceIds); + res.set("content-type", "application/json"); + res.send( + JSON.stringify({ + btc: parseFloat(priceFeeds[0].price.price) / 1e8, + eth: parseFloat(priceFeeds[1].price.price) / 1e8, + usdc: parseFloat(priceFeeds[2].price.price) / 1e8, + sol: parseFloat(priceFeeds[3].price.price) / 1e8 + }) + ); +}); + app.get("/fund/:pubkey/perf", async (req, res) => { const { w_btc = 0.4, w_eth = 0, w_sol = 0.6 } = req.query; // TODO: validate input @@ -38,9 +62,9 @@ app.get("/fund/:pubkey/perf", async (req, res) => { const { closingPrices: solClosingPrices } = await priceHistory( "Crypto.SOL/USD" ); - const { closingPrices: usdcClosingPrices } = await priceHistory( - "Crypto.USDC/USD" - ); + // const { closingPrices: usdcClosingPrices } = await priceHistory( + // "Crypto.USDC/USD" + // ); const { weightedChanges, btcChanges, ethChanges, solChanges } = fundPerformance( @@ -55,7 +79,7 @@ app.get("/fund/:pubkey/perf", async (req, res) => { res.send( JSON.stringify({ timestamps, - usdcClosingPrices, + // usdcClosingPrices, fundPerformance: weightedChanges, btcPerformance: btcChanges, ethPerformance: ethChanges, diff --git a/api/tests/server.test.js b/api/tests/server.test.js index ebb6dc21..1d778bdd 100644 --- a/api/tests/server.test.js +++ b/api/tests/server.test.js @@ -18,16 +18,33 @@ describe("Test /fund/:pubkey/perf", () => { server.close(); }); - it("", async () => { + it("Expected response", async () => { const res = await requestWithSupertest.get("/fund/xyz/perf"); expect(res.status).toEqual(200); expect(res.body).toEqual({ timestamps: expect.any(Array), fundPerformance: expect.any(Array), - usdcClosingPrices: expect.any(Array), + // usdcClosingPrices: expect.any(Array), btcPerformance: expect.any(Array), ethPerformance: expect.any(Array), solPerformance: expect.any(Array) }); }); }); + +describe("Test /prices", () => { + afterAll(() => { + server.close(); + }); + + it("Expected response", async () => { + const res = await requestWithSupertest.get("/prices"); + expect(res.status).toEqual(200); + expect(res.body).toEqual({ + btc: expect.any(Number), + eth: expect.any(Number), + sol: expect.any(Number), + usdc: expect.any(Number) + }); + }); +});