[price-service] add /get_vaa endpoint that serves cache and db vaas (#482)

* add /get_vaa endpoint that serves cache and db vaas

* update env vars

* fix precommit errors

* fix precommit errors

* rename removeExpiredValuesFromVaasCache to runCacheCleanupLoop

* move initialized envOrErr to constructor or func arg

* use setInterval

* update pyth-price-service env vars on tilt devnet

* use undefined instead of null

* use status code

* make web-api an env var

* update env var

* fix precommit

* fix precommit

* fix changes

* remove env vars from tilt-devnet

* address changes

* fix lint issues

* fix linting issues again

* bump package version

* remove unused imports
This commit is contained in:
Daniel Chew 2023-01-14 00:08:27 +09:00 committed by GitHub
parent 8b9682ccac
commit ad864c697a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 289 additions and 62 deletions

View File

@ -17,3 +17,6 @@ PROM_PORT=8081
# The default is to log with level info. # The default is to log with level info.
#LOG_LEVEL=debug #LOG_LEVEL=debug
REMOVE_EXPIRED_VALUES_INTERVAL_SECONDS=60
CACHE_TTL_SECONDS=300

View File

@ -37,6 +37,10 @@ services:
READINESS_SPY_SYNC_TIME_SECONDS: "20" READINESS_SPY_SYNC_TIME_SECONDS: "20"
READINESS_NUM_LOADED_SYMBOLS: "50" READINESS_NUM_LOADED_SYMBOLS: "50"
LOG_LEVEL: warning LOG_LEVEL: warning
DB_API_CLUSTER: pythnet
REMOVE_EXPIRED_VALUES_INTERVAL_SECONDS: "60"
CACHE_TTL_SECONDS: "300"
DB_API_ENDPOINT: "https://web-api.pyth.network"
healthcheck: healthcheck:
test: test:
[ [

View File

@ -37,6 +37,10 @@ services:
READINESS_SPY_SYNC_TIME_SECONDS: "20" READINESS_SPY_SYNC_TIME_SECONDS: "20"
READINESS_NUM_LOADED_SYMBOLS: "50" READINESS_NUM_LOADED_SYMBOLS: "50"
LOG_LEVEL: warning LOG_LEVEL: warning
DB_API_CLUSTER: devnet
REMOVE_EXPIRED_VALUES_INTERVAL_SECONDS: "60"
CACHE_TTL_SECONDS: "300"
DB_API_ENDPOINT: "https://web-api.pyth.network"
healthcheck: healthcheck:
test: test:
[ [

View File

@ -1,12 +1,12 @@
{ {
"name": "@pythnetwork/pyth-price-service", "name": "@pythnetwork/pyth-price-service",
"version": "2.2.4", "version": "2.3.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@pythnetwork/pyth-price-service", "name": "@pythnetwork/pyth-price-service",
"version": "2.2.4", "version": "2.3.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@certusone/wormhole-sdk": "^0.1.4", "@certusone/wormhole-sdk": "^0.1.4",
@ -27,6 +27,7 @@
"joi": "^17.6.0", "joi": "^17.6.0",
"lru-cache": "^7.14.1", "lru-cache": "^7.14.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"node-fetch": "^2.6.1",
"prom-client": "^14.0.1", "prom-client": "^14.0.1",
"response-time": "^2.3.2", "response-time": "^2.3.2",
"winston": "^3.3.3", "winston": "^3.3.3",
@ -36,6 +37,7 @@
"@types/jest": "^27.5.0", "@types/jest": "^27.5.0",
"@types/long": "^4.0.1", "@types/long": "^4.0.1",
"@types/node": "^16.6.1", "@types/node": "^16.6.1",
"@types/node-fetch": "^2.6.2",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
"jest": "^28.0.3", "jest": "^28.0.3",
"prettier": "^2.3.2", "prettier": "^2.3.2",
@ -634,6 +636,14 @@
"rxjs": "^7.3.0" "rxjs": "^7.3.0"
} }
}, },
"node_modules/@certusone/wormhole-sdk/node_modules/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"dependencies": {
"follow-redirects": "^1.14.4"
}
},
"node_modules/@certusone/wormhole-spydk": { "node_modules/@certusone/wormhole-spydk": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-spydk/-/wormhole-spydk-0.0.1.tgz", "resolved": "https://registry.npmjs.org/@certusone/wormhole-spydk/-/wormhole-spydk-0.0.1.tgz",
@ -2908,6 +2918,30 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.27.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.27.tgz",
"integrity": "sha512-C1pD3kgLoZ56Uuy5lhfOxie4aZlA3UMGLX9rXteq4WitEZH6Rl80mwactt9QG0w0gLFlN/kLBTFnGXtDVWvWQw==" "integrity": "sha512-C1pD3kgLoZ56Uuy5lhfOxie4aZlA3UMGLX9rXteq4WitEZH6Rl80mwactt9QG0w0gLFlN/kLBTFnGXtDVWvWQw=="
}, },
"node_modules/@types/node-fetch": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz",
"integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==",
"dev": true,
"dependencies": {
"@types/node": "*",
"form-data": "^3.0.0"
}
},
"node_modules/@types/node-fetch/node_modules/form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@types/prettier": { "node_modules/@types/prettier": {
"version": "2.6.0", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.0.tgz",
@ -3242,14 +3276,6 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true "dev": true
}, },
"node_modules/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"dependencies": {
"follow-redirects": "^1.14.4"
}
},
"node_modules/babel-jest": { "node_modules/babel-jest": {
"version": "28.0.3", "version": "28.0.3",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.0.3.tgz", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.0.3.tgz",
@ -4033,6 +4059,25 @@
"node-fetch": "2.6.7" "node-fetch": "2.6.7"
} }
}, },
"node_modules/cross-fetch/node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -4567,9 +4612,9 @@
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.14.9", "version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -7241,22 +7286,11 @@
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="
}, },
"node_modules/node-fetch": { "node_modules/node-fetch": {
"version": "2.6.7", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": { "engines": {
"node": "4.x || >=6.0.0" "node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
} }
}, },
"node_modules/node-gyp-build": { "node_modules/node-gyp-build": {
@ -8589,7 +8623,7 @@
"node_modules/tr46": { "node_modules/tr46": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
}, },
"node_modules/triple-beam": { "node_modules/triple-beam": {
"version": "1.3.0", "version": "1.3.0",
@ -8877,12 +8911,12 @@
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
}, },
"node_modules/whatwg-url": { "node_modules/whatwg-url": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": { "dependencies": {
"tr46": "~0.0.3", "tr46": "~0.0.3",
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"
@ -9483,6 +9517,16 @@
"js-base64": "^3.6.1", "js-base64": "^3.6.1",
"protobufjs": "^6.11.2", "protobufjs": "^6.11.2",
"rxjs": "^7.3.0" "rxjs": "^7.3.0"
},
"dependencies": {
"axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"requires": {
"follow-redirects": "^1.14.4"
}
}
} }
}, },
"@certusone/wormhole-spydk": { "@certusone/wormhole-spydk": {
@ -11124,6 +11168,29 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.27.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.27.tgz",
"integrity": "sha512-C1pD3kgLoZ56Uuy5lhfOxie4aZlA3UMGLX9rXteq4WitEZH6Rl80mwactt9QG0w0gLFlN/kLBTFnGXtDVWvWQw==" "integrity": "sha512-C1pD3kgLoZ56Uuy5lhfOxie4aZlA3UMGLX9rXteq4WitEZH6Rl80mwactt9QG0w0gLFlN/kLBTFnGXtDVWvWQw=="
}, },
"@types/node-fetch": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz",
"integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==",
"dev": true,
"requires": {
"@types/node": "*",
"form-data": "^3.0.0"
},
"dependencies": {
"form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"@types/prettier": { "@types/prettier": {
"version": "2.6.0", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.0.tgz",
@ -11430,14 +11497,6 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true "dev": true
}, },
"axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"requires": {
"follow-redirects": "^1.14.4"
}
},
"babel-jest": { "babel-jest": {
"version": "28.0.3", "version": "28.0.3",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.0.3.tgz", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.0.3.tgz",
@ -12067,6 +12126,16 @@
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"requires": { "requires": {
"node-fetch": "2.6.7" "node-fetch": "2.6.7"
},
"dependencies": {
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
}
}
} }
}, },
"cross-spawn": { "cross-spawn": {
@ -12506,9 +12575,9 @@
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
}, },
"follow-redirects": { "follow-redirects": {
"version": "1.14.9", "version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
}, },
"form-data": { "form-data": {
"version": "4.0.0", "version": "4.0.0",
@ -14499,12 +14568,9 @@
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="
}, },
"node-fetch": { "node-fetch": {
"version": "2.6.7", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
"requires": {
"whatwg-url": "^5.0.0"
}
}, },
"node-gyp-build": { "node-gyp-build": {
"version": "4.4.0", "version": "4.4.0",
@ -15491,7 +15557,7 @@
"tr46": { "tr46": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
}, },
"triple-beam": { "triple-beam": {
"version": "1.3.0", "version": "1.3.0",
@ -15692,12 +15758,12 @@
"webidl-conversions": { "webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
}, },
"whatwg-url": { "whatwg-url": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"requires": { "requires": {
"tr46": "~0.0.3", "tr46": "~0.0.3",
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"

View File

@ -1,6 +1,6 @@
{ {
"name": "@pythnetwork/pyth-price-service", "name": "@pythnetwork/pyth-price-service",
"version": "2.2.4", "version": "2.3.0",
"description": "Pyth Price Service", "description": "Pyth Price Service",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@ -18,6 +18,7 @@
"@types/jest": "^27.5.0", "@types/jest": "^27.5.0",
"@types/long": "^4.0.1", "@types/long": "^4.0.1",
"@types/node": "^16.6.1", "@types/node": "^16.6.1",
"@types/node-fetch": "^2.6.2",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
"jest": "^28.0.3", "jest": "^28.0.3",
"prettier": "^2.3.2", "prettier": "^2.3.2",
@ -46,6 +47,7 @@
"joi": "^17.6.0", "joi": "^17.6.0",
"lru-cache": "^7.14.1", "lru-cache": "^7.14.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"node-fetch": "^2.6.1",
"prom-client": "^14.0.1", "prom-client": "^14.0.1",
"response-time": "^2.3.2", "response-time": "^2.3.2",
"winston": "^3.3.3", "winston": "^3.3.3",

View File

@ -2,11 +2,12 @@ import { HexString, Price, PriceFeed } from "@pythnetwork/pyth-sdk-js";
import { Express } from "express"; import { Express } from "express";
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import request from "supertest"; import request from "supertest";
import { PriceInfo, PriceStore } from "../listen"; import { PriceInfo, PriceStore, VaaCache } from "../listen";
import { RestAPI } from "../rest"; import { RestAPI } from "../rest";
let app: Express; let app: Express;
let priceInfoMap: Map<string, PriceInfo>; let priceInfoMap: Map<string, PriceInfo>;
let vaasCache: VaaCache;
function expandTo64Len(id: string): string { function expandTo64Len(id: string): string {
return id.repeat(64).substring(0, 64); return id.repeat(64).substring(0, 64);
@ -56,6 +57,12 @@ beforeAll(async () => {
dummyPriceInfoPair(expandTo64Len("3456"), 2, "bad01bad"), dummyPriceInfoPair(expandTo64Len("3456"), 2, "bad01bad"),
dummyPriceInfoPair(expandTo64Len("10101"), 3, "bidbidbid"), dummyPriceInfoPair(expandTo64Len("10101"), 3, "bidbidbid"),
]); ]);
vaasCache = new VaaCache();
vaasCache.set(
expandTo64Len("abcd"),
1,
Buffer.from("a1b2c3d4", "hex").toString("base64")
);
const priceInfo: PriceStore = { const priceInfo: PriceStore = {
getLatestPriceInfo: (priceFeedId: string) => { getLatestPriceInfo: (priceFeedId: string) => {
@ -63,6 +70,9 @@ beforeAll(async () => {
}, },
addUpdateListener: (_callback: (priceInfo: PriceInfo) => any) => undefined, addUpdateListener: (_callback: (priceInfo: PriceInfo) => any) => undefined,
getPriceIds: () => new Set(), getPriceIds: () => new Set(),
getVaa: (vaasCacheKey: string, publishTime: number) => {
return vaasCache.get(vaasCacheKey, publishTime);
},
}; };
const api = new RestAPI({ port: 8889 }, priceInfo, () => true); const api = new RestAPI({ port: 8889 }, priceInfo, () => true);

View File

@ -108,6 +108,7 @@ beforeAll(async () => {
getLatestPriceInfo: (_priceFeedId: string) => undefined, getLatestPriceInfo: (_priceFeedId: string) => undefined,
addUpdateListener: (_callback: (priceInfo: PriceInfo) => any) => undefined, addUpdateListener: (_callback: (priceInfo: PriceInfo) => any) => undefined,
getPriceIds: () => new Set(priceInfos.map((info) => info.priceFeed.id)), getPriceIds: () => new Set(priceInfos.map((info) => info.priceFeed.id)),
getVaa: (_vaasCacheKey: string) => undefined,
}; };
api = new WebSocketAPI(priceInfo); api = new WebSocketAPI(priceInfo);

View File

@ -52,6 +52,8 @@ async function run() {
const restAPI = new RestAPI( const restAPI = new RestAPI(
{ {
port: parseInt(envOrErr("REST_PORT"), 10), port: parseInt(envOrErr("REST_PORT"), 10),
dbApiEndpoint: process.env.DB_API_ENDPOINT,
dbApiCluster: process.env.DB_API_CLUSTER,
}, },
listener, listener,
isReady, isReady,
@ -61,6 +63,7 @@ async function run() {
const wsAPI = new WebSocketAPI(listener, promClient); const wsAPI = new WebSocketAPI(listener, promClient);
listener.run(); listener.run();
listener.runCacheCleanupLoop();
const server = await restAPI.run(); const server = await restAPI.run();
wsAPI.run(server); wsAPI.run(server);
} }

View File

@ -7,21 +7,21 @@ import {
import { importCoreWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm"; import { importCoreWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
import {
getBatchSummary,
parseBatchPriceAttestation,
priceAttestationToPriceFeed,
} from "@pythnetwork/p2w-sdk-js";
import { import {
FilterEntry, FilterEntry,
SubscribeSignedVAAResponse, SubscribeSignedVAAResponse,
} from "@certusone/wormhole-spydk/lib/cjs/proto/spy/v1/spy"; } from "@certusone/wormhole-spydk/lib/cjs/proto/spy/v1/spy";
import { ClientReadableStream } from "@grpc/grpc-js"; import { ClientReadableStream } from "@grpc/grpc-js";
import {
getBatchSummary,
parseBatchPriceAttestation,
priceAttestationToPriceFeed,
} from "@pythnetwork/p2w-sdk-js";
import { HexString, PriceFeed } from "@pythnetwork/pyth-sdk-js"; import { HexString, PriceFeed } from "@pythnetwork/pyth-sdk-js";
import LRUCache from "lru-cache";
import { sleep, TimestampInSec } from "./helpers"; import { sleep, TimestampInSec } from "./helpers";
import { logger } from "./logging"; import { logger } from "./logging";
import { PromClient } from "./promClient"; import { PromClient } from "./promClient";
import LRUCache from "lru-cache";
export type PriceInfo = { export type PriceInfo = {
vaa: Buffer; vaa: Buffer;
@ -37,6 +37,7 @@ export interface PriceStore {
getPriceIds(): Set<HexString>; getPriceIds(): Set<HexString>;
getLatestPriceInfo(priceFeedId: HexString): PriceInfo | undefined; getLatestPriceInfo(priceFeedId: HexString): PriceInfo | undefined;
addUpdateListener(callback: (priceInfo: PriceInfo) => any): void; addUpdateListener(callback: (priceInfo: PriceInfo) => any): void;
getVaa(priceFeedId: string, publishTime: number): VaaConfig | undefined;
} }
type ListenerReadinessConfig = { type ListenerReadinessConfig = {
@ -48,10 +49,74 @@ type ListenerConfig = {
spyServiceHost: string; spyServiceHost: string;
filtersRaw?: string; filtersRaw?: string;
readiness: ListenerReadinessConfig; readiness: ListenerReadinessConfig;
webApiEndpoint?: string;
webApiCluster?: string;
}; };
type VaaKey = string; type VaaKey = string;
type VaaConfig = {
publishTime: number;
vaa: string;
};
export class VaaCache {
private cache: Map<string, VaaConfig[]>;
private ttl: number;
constructor(ttl: number = 300) {
this.cache = new Map();
this.ttl = ttl;
}
set(key: VaaKey, publishTime: number, vaa: string): void {
if (this.cache.has(key)) {
this.cache.get(key)!.push({ publishTime, vaa });
} else {
this.cache.set(key, [{ publishTime, vaa }]);
}
}
get(key: VaaKey, publishTime: number): VaaConfig | undefined {
if (!this.cache.has(key)) {
return undefined;
} else {
const vaaConf = this.find(this.cache.get(key)!, publishTime);
return vaaConf;
}
}
find(arr: VaaConfig[], publishTime: number): VaaConfig | undefined {
if (arr.length === 0 || publishTime < arr[0].publishTime) {
return undefined;
}
let left = 0;
let right = arr.length - 1;
let nextLargest = -1;
while (left <= right) {
const middle = Math.floor((left + right) / 2);
if (arr[middle].publishTime === publishTime) {
return arr[middle];
} else if (arr[middle].publishTime < publishTime) {
left = middle + 1;
} else {
nextLargest = middle;
right = middle - 1;
}
}
return nextLargest !== -1 ? arr[nextLargest] : undefined;
}
async removeExpiredValues() {
const now = Math.floor(Date.now() / 1000);
for (const arr of this.cache.values()) {
arr.filter((vaaConf) => now - vaaConf.publishTime < this.ttl);
}
}
}
export class Listener implements PriceStore { export class Listener implements PriceStore {
// Mapping of Price Feed Id to Vaa // Mapping of Price Feed Id to Vaa
private priceFeedVaaMap = new Map<string, PriceInfo>(); private priceFeedVaaMap = new Map<string, PriceInfo>();
@ -62,6 +127,7 @@ export class Listener implements PriceStore {
private readinessConfig: ListenerReadinessConfig; private readinessConfig: ListenerReadinessConfig;
private updateCallbacks: ((priceInfo: PriceInfo) => any)[]; private updateCallbacks: ((priceInfo: PriceInfo) => any)[];
private observedVaas: LRUCache<VaaKey, boolean>; private observedVaas: LRUCache<VaaKey, boolean>;
private vaasCache: VaaCache;
constructor(config: ListenerConfig, promClient?: PromClient) { constructor(config: ListenerConfig, promClient?: PromClient) {
this.promClient = promClient; this.promClient = promClient;
@ -73,6 +139,7 @@ export class Listener implements PriceStore {
max: 10000, // At most 10000 items max: 10000, // At most 10000 items
ttl: 60 * 1000, // 60 seconds ttl: 60 * 1000, // 60 seconds
}); });
this.vaasCache = new VaaCache();
} }
private loadFilters(filtersRaw?: string) { private loadFilters(filtersRaw?: string) {
@ -105,6 +172,10 @@ export class Listener implements PriceStore {
logger.info("loaded " + this.filters.length + " filters"); logger.info("loaded " + this.filters.length + " filters");
} }
async runCacheCleanupLoop(interval: number = 60) {
setInterval(this.vaasCache.removeExpiredValues, interval * 1000);
}
async run() { async run() {
logger.info( logger.info(
"pyth_relay starting up, will listen for signed VAAs from " + "pyth_relay starting up, will listen for signed VAAs from " +
@ -185,13 +256,13 @@ export class Listener implements PriceStore {
const vaaEmitterAddressHex = Buffer.from( const vaaEmitterAddressHex = Buffer.from(
parsedVaa.emitter_address parsedVaa.emitter_address
).toString("hex"); ).toString("hex");
const vaaKey: VaaKey = `${parsedVaa.emitter_chain}#${vaaEmitterAddressHex}#${parsedVaa.sequence}`; const observedVaasKey: VaaKey = `${parsedVaa.emitter_chain}#${vaaEmitterAddressHex}#${parsedVaa.sequence}`;
if (this.observedVaas.has(vaaKey)) { if (this.observedVaas.has(observedVaasKey)) {
return; return;
} }
this.observedVaas.set(vaaKey, true); this.observedVaas.set(observedVaasKey, true);
this.promClient?.incReceivedVaa(); this.promClient?.incReceivedVaa();
let batchAttestation; let batchAttestation;
@ -223,6 +294,11 @@ export class Listener implements PriceStore {
const cachedPriceInfo = this.priceFeedVaaMap.get(key); const cachedPriceInfo = this.priceFeedVaaMap.get(key);
if (this.isNewPriceInfo(cachedPriceInfo, priceInfo)) { if (this.isNewPriceInfo(cachedPriceInfo, priceInfo)) {
this.vaasCache.set(
priceInfo.priceFeed.id,
priceInfo.publishTime,
priceInfo.vaa.toString("base64")
);
this.priceFeedVaaMap.set(key, priceInfo); this.priceFeedVaaMap.set(key, priceInfo);
if (cachedPriceInfo !== undefined) { if (cachedPriceInfo !== undefined) {
@ -252,6 +328,10 @@ export class Listener implements PriceStore {
); );
} }
getVaa(priceFeedId: string, publishTime: number): VaaConfig | undefined {
return this.vaasCache.get(priceFeedId, publishTime);
}
getLatestPriceInfo(priceFeedId: string): PriceInfo | undefined { getLatestPriceInfo(priceFeedId: string): PriceInfo | undefined {
return this.priceFeedVaaMap.get(priceFeedId); return this.priceFeedVaaMap.get(priceFeedId);
} }

View File

@ -5,6 +5,7 @@ import { Joi, schema, validate, ValidationError } from "express-validation";
import { Server } from "http"; import { Server } from "http";
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import morgan from "morgan"; import morgan from "morgan";
import fetch from "node-fetch";
import { TimestampInSec } from "./helpers"; import { TimestampInSec } from "./helpers";
import { PriceStore } from "./listen"; import { PriceStore } from "./listen";
import { logger } from "./logging"; import { logger } from "./logging";
@ -36,14 +37,18 @@ export class RestAPI {
private priceFeedVaaInfo: PriceStore; private priceFeedVaaInfo: PriceStore;
private isReady: (() => boolean) | undefined; private isReady: (() => boolean) | undefined;
private promClient: PromClient | undefined; private promClient: PromClient | undefined;
private dbApiEndpoint?: string;
private dbApiCluster?: string;
constructor( constructor(
config: { port: number }, config: { port: number; dbApiEndpoint?: string; dbApiCluster?: string },
priceFeedVaaInfo: PriceStore, priceFeedVaaInfo: PriceStore,
isReady?: () => boolean, isReady?: () => boolean,
promClient?: PromClient promClient?: PromClient
) { ) {
this.port = config.port; this.port = config.port;
this.dbApiEndpoint = config.dbApiEndpoint;
this.dbApiCluster = config.dbApiCluster;
this.priceFeedVaaInfo = priceFeedVaaInfo; this.priceFeedVaaInfo = priceFeedVaaInfo;
this.isReady = isReady; this.isReady = isReady;
this.promClient = promClient; this.promClient = promClient;
@ -113,6 +118,51 @@ export class RestAPI {
"api/latest_vaas?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&.." "api/latest_vaas?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&.."
); );
const getVaaInputSchema: schema = {
query: Joi.object({
id: Joi.string()
.regex(/^(0x)?[a-f0-9]{64}$/)
.required(),
publish_time: Joi.number().required(),
}).required(),
};
app.get(
"/api/get_vaa",
validate(getVaaInputSchema),
(req: Request, res: Response) => {
const priceFeedId = req.query.id as string;
const publishTime = Number(req.query.publish_time as string);
const vaa = this.priceFeedVaaInfo.getVaa(priceFeedId, publishTime);
// if publishTime is older than cache ttl or vaa is not found, fetch from db
if (!vaa) {
// cache miss
if (this.dbApiEndpoint && this.dbApiCluster) {
fetch(
`${this.dbApiEndpoint}/vaa?id=${priceFeedId}&publishTime=${publishTime}&cluster=${this.dbApiCluster}`
)
.then((r: any) => r.json())
.then((arr: any) => {
if (arr.length > 0 && arr[0]) {
res.json(arr[0]);
} else {
res.status(StatusCodes.NOT_FOUND).send("VAA not found");
}
});
}
} else {
// cache hit
const processedVaa = {
publishTime: new Date(vaa.publishTime),
vaa: vaa.vaa,
};
res.json(processedVaa);
}
}
);
endpoints.push(
"api/get_vaa?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>"
);
const latestPriceFeedsInputSchema: schema = { const latestPriceFeedsInputSchema: schema = {
query: Joi.object({ query: Joi.object({
ids: Joi.array() ids: Joi.array()

View File

@ -72,3 +72,7 @@ spec:
value: "6" value: "6"
- name: LOG_LEVEL - name: LOG_LEVEL
value: debug value: debug
- name: REMOVE_EXPIRED_VALUES_INTERVAL_SECONDS
value: "60"
- name: CACHE_TTL_SECONDS
value: "300"