From 1f07aa6f63d6d5f932492265535fdefda485c37c Mon Sep 17 00:00:00 2001 From: Subarna Singh Date: Mon, 16 Dec 2024 15:47:58 +0530 Subject: [PATCH] DF 20736- GMx Glv EA (#3614) * yarn cache updated * GMX Glv EA * update * tests * fix unsupported assets error * fixes post review * ea-framework version update * READ regenerated * revert framework update * param override * framework update * update tsconfig.json * yarn update * comment setInterval to stop indefinite testing * skip Soak Test * changes post review * changes post review * package update * fix integration test --------- Co-authored-by: Karen Stepanyan --- .changeset/tough-radios-melt.md | 5 + .pnp.cjs | 22 + packages/composites/glv-token/CHANGELOG.md | 0 packages/composites/glv-token/README.md | 63 ++ packages/composites/glv-token/package.json | 42 + .../glv-token/src/config/glvReaderAbi.json | 783 ++++++++++++++++++ .../composites/glv-token/src/config/index.ts | 80 ++ .../glv-token/src/endpoint/index.ts | 1 + .../glv-token/src/endpoint/price.ts | 36 + packages/composites/glv-token/src/index.ts | 13 + .../glv-token/src/transport/price.ts | 372 +++++++++ .../glv-token/src/transport/utils.ts | 70 ++ .../composites/glv-token/test-payload.json | 5 + .../__snapshots__/adapter.test.ts.snap | 27 + .../test/integration/adapter.test.ts | 118 +++ .../glv-token/test/integration/fixtures.ts | 308 +++++++ .../glv-token/test/unit/utils.test.ts | 19 + packages/composites/glv-token/tsconfig.json | 9 + .../composites/glv-token/tsconfig.test.json | 7 + .../get-changed-adapters/soakTestBlacklist.ts | 1 + packages/tsconfig.json | 3 + packages/tsconfig.test.json | 3 + yarn.lock | 15 + 23 files changed, 2002 insertions(+) create mode 100644 .changeset/tough-radios-melt.md create mode 100644 packages/composites/glv-token/CHANGELOG.md create mode 100644 packages/composites/glv-token/README.md create mode 100644 packages/composites/glv-token/package.json create mode 100644 packages/composites/glv-token/src/config/glvReaderAbi.json create mode 100644 packages/composites/glv-token/src/config/index.ts create mode 100644 packages/composites/glv-token/src/endpoint/index.ts create mode 100644 packages/composites/glv-token/src/endpoint/price.ts create mode 100644 packages/composites/glv-token/src/index.ts create mode 100644 packages/composites/glv-token/src/transport/price.ts create mode 100644 packages/composites/glv-token/src/transport/utils.ts create mode 100644 packages/composites/glv-token/test-payload.json create mode 100644 packages/composites/glv-token/test/integration/__snapshots__/adapter.test.ts.snap create mode 100644 packages/composites/glv-token/test/integration/adapter.test.ts create mode 100644 packages/composites/glv-token/test/integration/fixtures.ts create mode 100644 packages/composites/glv-token/test/unit/utils.test.ts create mode 100644 packages/composites/glv-token/tsconfig.json create mode 100644 packages/composites/glv-token/tsconfig.test.json diff --git a/.changeset/tough-radios-melt.md b/.changeset/tough-radios-melt.md new file mode 100644 index 0000000000..ce63f7e080 --- /dev/null +++ b/.changeset/tough-radios-melt.md @@ -0,0 +1,5 @@ +--- +'@chainlink/glv-token-adapter': major +--- + +GLV EA diff --git a/.pnp.cjs b/.pnp.cjs index d2db7e5a76..6e7d8e04b9 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -70,6 +70,10 @@ const RAW_RUNTIME_STATE = "name": "@chainlink/dydx-rewards-adapter",\ "reference": "workspace:packages/composites/dydx-rewards"\ },\ + {\ + "name": "@chainlink/glv-token-adapter",\ + "reference": "workspace:packages/composites/glv-token"\ + },\ {\ "name": "@chainlink/gm-token-adapter",\ "reference": "workspace:packages/composites/gm-token"\ @@ -1060,6 +1064,7 @@ const RAW_RUNTIME_STATE = ["@chainlink/gemini-adapter", ["workspace:packages/sources/gemini"]],\ ["@chainlink/genesis-volatility-adapter", ["workspace:packages/sources/genesis-volatility"]],\ ["@chainlink/geodb-adapter", ["workspace:packages/sources/geodb"]],\ + ["@chainlink/glv-token-adapter", ["workspace:packages/composites/glv-token"]],\ ["@chainlink/gm-token-adapter", ["workspace:packages/composites/gm-token"]],\ ["@chainlink/google-bigquery-adapter", ["workspace:packages/sources/google-bigquery"]],\ ["@chainlink/google-weather-adapter", ["workspace:packages/composites/google-weather"]],\ @@ -7308,6 +7313,23 @@ const RAW_RUNTIME_STATE = "linkType": "SOFT"\ }]\ ]],\ + ["@chainlink/glv-token-adapter", [\ + ["workspace:packages/composites/glv-token", {\ + "packageLocation": "./packages/composites/glv-token/",\ + "packageDependencies": [\ + ["@chainlink/glv-token-adapter", "workspace:packages/composites/glv-token"],\ + ["@chainlink/external-adapter-framework", "npm:1.7.7"],\ + ["@types/jest", "npm:27.5.2"],\ + ["@types/node", "npm:16.18.119"],\ + ["decimal.js", "npm:10.4.3"],\ + ["ethers", "npm:5.7.2"],\ + ["nock", "npm:13.5.5"],\ + ["tslib", "npm:2.4.1"],\ + ["typescript", "patch:typescript@npm%3A5.6.3#optional!builtin::version=5.6.3&hash=8c6c40"]\ + ],\ + "linkType": "SOFT"\ + }]\ + ]],\ ["@chainlink/gm-token-adapter", [\ ["workspace:packages/composites/gm-token", {\ "packageLocation": "./packages/composites/gm-token/",\ diff --git a/packages/composites/glv-token/CHANGELOG.md b/packages/composites/glv-token/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/composites/glv-token/README.md b/packages/composites/glv-token/README.md new file mode 100644 index 0000000000..cfced7836a --- /dev/null +++ b/packages/composites/glv-token/README.md @@ -0,0 +1,63 @@ +# GLV + +![0.0.0](https://img.shields.io/github/package-json/v/smartcontractkit/external-adapters-js?filename=packages/composites/glv-token/package.json) ![v3](https://img.shields.io/badge/framework%20version-v3-blueviolet) + +This document was generated automatically. Please see [README Generator](../../scripts#readme-generator) for more info. + +## Environment Variables + +| Required? | Name | Description | Type | Options | Default | +| :-------: | :-------------------------: | :---------------------------------------------------------------------------------------: | :----: | :-----: | :------------------------------------------: | +| ✅ | ARBITRUM_RPC_URL | RPC url of Arbitrum node | string | | | +| ✅ | ARBITRUM_CHAIN_ID | The chain id to connect to | number | | `42161` | +| ✅ | DATASTORE_CONTRACT_ADDRESS | Address of Data Store contract | string | | `0xFD70de6b91282D8017aA4E741e9Ae325CAb992d8` | +| ✅ | READER_CONTRACT_ADDRESS | Address of Reader contract | string | | `0x0537C767cDAC0726c76Bb89e92904fe28fd02fE1` | +| ✅ | GLV_READER_CONTRACT_ADDRESS | Address of Glv Reader Contract | string | | `0x6a9505D0B44cFA863d9281EA5B0b34cB36243b45` | +| ✅ | TIINGO_ADAPTER_URL | URL of Tiingo EA | string | | | +| ✅ | NCFX_ADAPTER_URL | URL of NCFX EA | string | | | +| ✅ | COINMETRICS_ADAPTER_URL | URL of Coinmetrics EA | string | | | +| ✅ | MIN_REQUIRED_SOURCE_SUCCESS | Minimum number of source EAs that need to successfully return a value. | number | | `2` | +| ✅ | MARKET_INFO_API | URL market meta data supported by Glv | string | | `https://arbitrum-api.gmxinfra.io/markets` | +| ✅ | TOKEN_INFO_API | URL to token meta data supported by Glv | string | | `https://arbitrum-api.gmxinfra.io/tokens` | +| | BACKGROUND_EXECUTE_MS | The amount of time the background execute should sleep before performing the next request | number | | `10000` | + +--- + +## Data Provider Rate Limits + +There are no rate limits for this adapter. + +--- + +## Input Parameters + +| Required? | Name | Description | Type | Options | Default | +| :-------: | :------: | :-----------------: | :----: | :----------------------: | :-----: | +| | endpoint | The endpoint to use | string | [price](#price-endpoint) | `price` | + +## Price Endpoint + +`price` is the only supported name for this endpoint. + +### Input Params + +| Required? | Name | Aliases | Description | Type | Options | Default | Depends On | Not Valid With | +| :-------: | :--: | :-----: | :------------------: | :----: | :-----: | :-----: | :--------: | :------------: | +| ✅ | glv | | Glv contract address | string | | | | | + +### Example + +Request: + +```json +{ + "data": { + "endpoint": "price", + "glv": "0x528A5bac7E746C9A509A1f4F6dF58A03d44279F9" + } +} +``` + +--- + +MIT License diff --git a/packages/composites/glv-token/package.json b/packages/composites/glv-token/package.json new file mode 100644 index 0000000000..56b3b9775f --- /dev/null +++ b/packages/composites/glv-token/package.json @@ -0,0 +1,42 @@ +{ + "name": "@chainlink/glv-token-adapter", + "version": "0.0.0", + "description": "Chainlink glv-token adapter.", + "keywords": [ + "Chainlink", + "LINK", + "blockchain", + "oracle", + "glv-token" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "repository": { + "url": "https://github.com/smartcontractkit/external-adapters-js", + "type": "git" + }, + "license": "MIT", + "scripts": { + "clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo", + "prepack": "yarn build", + "build": "tsc -b", + "server": "node -e 'require(\"./index.js\").server()'", + "server:dist": "node -e 'require(\"./dist/index.js\").server()'", + "start": "yarn server:dist" + }, + "devDependencies": { + "@types/jest": "27.5.2", + "@types/node": "16.18.119", + "nock": "13.5.5", + "typescript": "5.6.3" + }, + "dependencies": { + "@chainlink/external-adapter-framework": "1.7.7", + "decimal.js": "^10.3.1", + "ethers": "^5.4.6", + "tslib": "2.4.1" + } +} diff --git a/packages/composites/glv-token/src/config/glvReaderAbi.json b/packages/composites/glv-token/src/config/glvReaderAbi.json new file mode 100644 index 0000000000..f923db7e7c --- /dev/null +++ b/packages/composites/glv-token/src/config/glvReaderAbi.json @@ -0,0 +1,783 @@ +[ + { + "inputs": [], + "name": "EmptyMarketTokenSupply", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "glv", + "type": "address" + }, + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "GlvNegativeMarketPoolValue", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract DataStore", + "name": "dataStore", + "type": "address" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "start", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "end", + "type": "uint256" + } + ], + "name": "getAccountGlvDeposits", + "outputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "glv", + "type": "address" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "callbackContract", + "type": "address" + }, + { + "internalType": "address", + "name": "uiFeeReceiver", + "type": "address" + }, + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "address", + "name": "initialLongToken", + "type": "address" + }, + { + "internalType": "address", + "name": "initialShortToken", + "type": "address" + }, + { + "internalType": "address[]", + "name": "longTokenSwapPath", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "shortTokenSwapPath", + "type": "address[]" + } + ], + "internalType": "struct GlvDeposit.Addresses", + "name": "addresses", + "type": "tuple" + }, + { + "components": [ + { "internalType": "uint256", "name": "marketTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "initialLongTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "initialShortTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minGlvTokens", "type": "uint256" }, + { "internalType": "uint256", "name": "updatedAtTime", "type": "uint256" }, + { "internalType": "uint256", "name": "executionFee", "type": "uint256" }, + { "internalType": "uint256", "name": "callbackGasLimit", "type": "uint256" } + ], + "internalType": "struct GlvDeposit.Numbers", + "name": "numbers", + "type": "tuple" + }, + { + "components": [ + { "internalType": "bool", "name": "shouldUnwrapNativeToken", "type": "bool" }, + { "internalType": "bool", "name": "isMarketTokenDeposit", "type": "bool" } + ], + "internalType": "struct GlvDeposit.Flags", + "name": "flags", + "type": "tuple" + } + ], + "internalType": "struct GlvDeposit.Props[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract DataStore", "name": "dataStore", "type": "address" }, + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "uint256", "name": "start", "type": "uint256" }, + { "internalType": "uint256", "name": "end", "type": "uint256" } + ], + "name": "getAccountGlvWithdrawals", + "outputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "glv", "type": "address" }, + { "internalType": "address", "name": "market", "type": "address" }, + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "address", "name": "receiver", "type": "address" }, + { "internalType": "address", "name": "callbackContract", "type": "address" }, + { "internalType": "address", "name": "uiFeeReceiver", "type": "address" }, + { "internalType": "address[]", "name": "longTokenSwapPath", "type": "address[]" }, + { "internalType": "address[]", "name": "shortTokenSwapPath", "type": "address[]" } + ], + "internalType": "struct GlvWithdrawal.Addresses", + "name": "addresses", + "type": "tuple" + }, + { + "components": [ + { "internalType": "uint256", "name": "glvTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minLongTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minShortTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "updatedAtTime", "type": "uint256" }, + { "internalType": "uint256", "name": "executionFee", "type": "uint256" }, + { "internalType": "uint256", "name": "callbackGasLimit", "type": "uint256" } + ], + "internalType": "struct GlvWithdrawal.Numbers", + "name": "numbers", + "type": "tuple" + }, + { + "components": [ + { "internalType": "bool", "name": "shouldUnwrapNativeToken", "type": "bool" } + ], + "internalType": "struct GlvWithdrawal.Flags", + "name": "flags", + "type": "tuple" + } + ], + "internalType": "struct GlvWithdrawal.Props[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract DataStore", "name": "dataStore", "type": "address" }, + { "internalType": "address", "name": "glv", "type": "address" } + ], + "name": "getGlv", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "glvToken", "type": "address" }, + { "internalType": "address", "name": "longToken", "type": "address" }, + { "internalType": "address", "name": "shortToken", "type": "address" } + ], + "internalType": "struct Glv.Props", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract DataStore", "name": "dataStore", "type": "address" }, + { "internalType": "bytes32", "name": "salt", "type": "bytes32" } + ], + "name": "getGlvBySalt", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "glvToken", "type": "address" }, + { "internalType": "address", "name": "longToken", "type": "address" }, + { "internalType": "address", "name": "shortToken", "type": "address" } + ], + "internalType": "struct Glv.Props", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract DataStore", "name": "dataStore", "type": "address" }, + { "internalType": "bytes32", "name": "key", "type": "bytes32" } + ], + "name": "getGlvDeposit", + "outputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "glv", "type": "address" }, + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "address", "name": "receiver", "type": "address" }, + { "internalType": "address", "name": "callbackContract", "type": "address" }, + { "internalType": "address", "name": "uiFeeReceiver", "type": "address" }, + { "internalType": "address", "name": "market", "type": "address" }, + { "internalType": "address", "name": "initialLongToken", "type": "address" }, + { "internalType": "address", "name": "initialShortToken", "type": "address" }, + { "internalType": "address[]", "name": "longTokenSwapPath", "type": "address[]" }, + { "internalType": "address[]", "name": "shortTokenSwapPath", "type": "address[]" } + ], + "internalType": "struct GlvDeposit.Addresses", + "name": "addresses", + "type": "tuple" + }, + { + "components": [ + { "internalType": "uint256", "name": "marketTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "initialLongTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "initialShortTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minGlvTokens", "type": "uint256" }, + { "internalType": "uint256", "name": "updatedAtTime", "type": "uint256" }, + { "internalType": "uint256", "name": "executionFee", "type": "uint256" }, + { "internalType": "uint256", "name": "callbackGasLimit", "type": "uint256" } + ], + "internalType": "struct GlvDeposit.Numbers", + "name": "numbers", + "type": "tuple" + }, + { + "components": [ + { "internalType": "bool", "name": "shouldUnwrapNativeToken", "type": "bool" }, + { "internalType": "bool", "name": "isMarketTokenDeposit", "type": "bool" } + ], + "internalType": "struct GlvDeposit.Flags", + "name": "flags", + "type": "tuple" + } + ], + "internalType": "struct GlvDeposit.Props", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract DataStore", "name": "dataStore", "type": "address" }, + { "internalType": "uint256", "name": "start", "type": "uint256" }, + { "internalType": "uint256", "name": "end", "type": "uint256" } + ], + "name": "getGlvDeposits", + "outputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "glv", "type": "address" }, + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "address", "name": "receiver", "type": "address" }, + { "internalType": "address", "name": "callbackContract", "type": "address" }, + { "internalType": "address", "name": "uiFeeReceiver", "type": "address" }, + { "internalType": "address", "name": "market", "type": "address" }, + { "internalType": "address", "name": "initialLongToken", "type": "address" }, + { "internalType": "address", "name": "initialShortToken", "type": "address" }, + { "internalType": "address[]", "name": "longTokenSwapPath", "type": "address[]" }, + { "internalType": "address[]", "name": "shortTokenSwapPath", "type": "address[]" } + ], + "internalType": "struct GlvDeposit.Addresses", + "name": "addresses", + "type": "tuple" + }, + { + "components": [ + { "internalType": "uint256", "name": "marketTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "initialLongTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "initialShortTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minGlvTokens", "type": "uint256" }, + { "internalType": "uint256", "name": "updatedAtTime", "type": "uint256" }, + { "internalType": "uint256", "name": "executionFee", "type": "uint256" }, + { "internalType": "uint256", "name": "callbackGasLimit", "type": "uint256" } + ], + "internalType": "struct GlvDeposit.Numbers", + "name": "numbers", + "type": "tuple" + }, + { + "components": [ + { "internalType": "bool", "name": "shouldUnwrapNativeToken", "type": "bool" }, + { "internalType": "bool", "name": "isMarketTokenDeposit", "type": "bool" } + ], + "internalType": "struct GlvDeposit.Flags", + "name": "flags", + "type": "tuple" + } + ], + "internalType": "struct GlvDeposit.Props[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract DataStore", + "name": "dataStore", + "type": "address" + }, + { + "internalType": "address", + "name": "glv", + "type": "address" + } + ], + "name": "getGlvInfo", + "outputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "glvToken", + "type": "address" + }, + { + "internalType": "address", + "name": "longToken", + "type": "address" + }, + { + "internalType": "address", + "name": "shortToken", + "type": "address" + } + ], + "internalType": "struct Glv.Props", + "name": "glv", + "type": "tuple" + }, + { + "internalType": "address[]", + "name": "markets", + "type": "address[]" + } + ], + "internalType": "struct GlvReader.GlvInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract DataStore", "name": "dataStore", "type": "address" }, + { "internalType": "uint256", "name": "start", "type": "uint256" }, + { "internalType": "uint256", "name": "end", "type": "uint256" } + ], + "name": "getGlvInfoList", + "outputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "glvToken", "type": "address" }, + { "internalType": "address", "name": "longToken", "type": "address" }, + { "internalType": "address", "name": "shortToken", "type": "address" } + ], + "internalType": "struct Glv.Props", + "name": "glv", + "type": "tuple" + }, + { "internalType": "address[]", "name": "markets", "type": "address[]" } + ], + "internalType": "struct GlvReader.GlvInfo[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract DataStore", "name": "dataStore", "type": "address" }, + { "internalType": "bytes32", "name": "key", "type": "bytes32" } + ], + "name": "getGlvShift", + "outputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "glv", "type": "address" }, + { "internalType": "address", "name": "fromMarket", "type": "address" }, + { "internalType": "address", "name": "toMarket", "type": "address" } + ], + "internalType": "struct GlvShift.Addresses", + "name": "addresses", + "type": "tuple" + }, + { + "components": [ + { "internalType": "uint256", "name": "marketTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minMarketTokens", "type": "uint256" }, + { "internalType": "uint256", "name": "updatedAtTime", "type": "uint256" } + ], + "internalType": "struct GlvShift.Numbers", + "name": "numbers", + "type": "tuple" + } + ], + "internalType": "struct GlvShift.Props", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract DataStore", "name": "dataStore", "type": "address" }, + { "internalType": "uint256", "name": "start", "type": "uint256" }, + { "internalType": "uint256", "name": "end", "type": "uint256" } + ], + "name": "getGlvShifts", + "outputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "glv", "type": "address" }, + { "internalType": "address", "name": "fromMarket", "type": "address" }, + { "internalType": "address", "name": "toMarket", "type": "address" } + ], + "internalType": "struct GlvShift.Addresses", + "name": "addresses", + "type": "tuple" + }, + { + "components": [ + { "internalType": "uint256", "name": "marketTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minMarketTokens", "type": "uint256" }, + { "internalType": "uint256", "name": "updatedAtTime", "type": "uint256" } + ], + "internalType": "struct GlvShift.Numbers", + "name": "numbers", + "type": "tuple" + } + ], + "internalType": "struct GlvShift.Props[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract DataStore", + "name": "dataStore", + "type": "address" + }, + { + "internalType": "address[]", + "name": "marketAddresses", + "type": "address[]" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "min", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "max", + "type": "uint256" + } + ], + "internalType": "struct Price.Props[]", + "name": "indexTokenPrices", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "uint256", "name": "min", "type": "uint256" }, + { "internalType": "uint256", "name": "max", "type": "uint256" } + ], + "internalType": "struct Price.Props", + "name": "longTokenPrice", + "type": "tuple" + }, + { + "components": [ + { "internalType": "uint256", "name": "min", "type": "uint256" }, + { "internalType": "uint256", "name": "max", "type": "uint256" } + ], + "internalType": "struct Price.Props", + "name": "shortTokenPrice", + "type": "tuple" + }, + { + "internalType": "address", + "name": "glv", + "type": "address" + }, + { + "internalType": "bool", + "name": "maximize", + "type": "bool" + } + ], + "name": "getGlvTokenPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract DataStore", "name": "dataStore", "type": "address" }, + { "internalType": "address[]", "name": "marketAddresses", "type": "address[]" }, + { + "components": [ + { "internalType": "uint256", "name": "min", "type": "uint256" }, + { "internalType": "uint256", "name": "max", "type": "uint256" } + ], + "internalType": "struct Price.Props[]", + "name": "indexTokenPrices", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "uint256", "name": "min", "type": "uint256" }, + { "internalType": "uint256", "name": "max", "type": "uint256" } + ], + "internalType": "struct Price.Props", + "name": "longTokenPrice", + "type": "tuple" + }, + { + "components": [ + { "internalType": "uint256", "name": "min", "type": "uint256" }, + { "internalType": "uint256", "name": "max", "type": "uint256" } + ], + "internalType": "struct Price.Props", + "name": "shortTokenPrice", + "type": "tuple" + }, + { "internalType": "address", "name": "glv", "type": "address" }, + { "internalType": "bool", "name": "maximize", "type": "bool" } + ], + "name": "getGlvValue", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract DataStore", "name": "dataStore", "type": "address" }, + { "internalType": "bytes32", "name": "key", "type": "bytes32" } + ], + "name": "getGlvWithdrawal", + "outputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "glv", "type": "address" }, + { "internalType": "address", "name": "market", "type": "address" }, + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "address", "name": "receiver", "type": "address" }, + { "internalType": "address", "name": "callbackContract", "type": "address" }, + { "internalType": "address", "name": "uiFeeReceiver", "type": "address" }, + { "internalType": "address[]", "name": "longTokenSwapPath", "type": "address[]" }, + { "internalType": "address[]", "name": "shortTokenSwapPath", "type": "address[]" } + ], + "internalType": "struct GlvWithdrawal.Addresses", + "name": "addresses", + "type": "tuple" + }, + { + "components": [ + { "internalType": "uint256", "name": "glvTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minLongTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minShortTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "updatedAtTime", "type": "uint256" }, + { "internalType": "uint256", "name": "executionFee", "type": "uint256" }, + { "internalType": "uint256", "name": "callbackGasLimit", "type": "uint256" } + ], + "internalType": "struct GlvWithdrawal.Numbers", + "name": "numbers", + "type": "tuple" + }, + { + "components": [ + { "internalType": "bool", "name": "shouldUnwrapNativeToken", "type": "bool" } + ], + "internalType": "struct GlvWithdrawal.Flags", + "name": "flags", + "type": "tuple" + } + ], + "internalType": "struct GlvWithdrawal.Props", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract DataStore", + "name": "dataStore", + "type": "address" + }, + { + "internalType": "uint256", + "name": "start", + "type": "uint256" + }, + { "internalType": "uint256", "name": "end", "type": "uint256" } + ], + "name": "getGlvWithdrawals", + "outputs": [ + { + "components": [ + { + "components": [ + { "internalType": "address", "name": "glv", "type": "address" }, + { "internalType": "address", "name": "market", "type": "address" }, + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "address", "name": "receiver", "type": "address" }, + { "internalType": "address", "name": "callbackContract", "type": "address" }, + { "internalType": "address", "name": "uiFeeReceiver", "type": "address" }, + { "internalType": "address[]", "name": "longTokenSwapPath", "type": "address[]" }, + { "internalType": "address[]", "name": "shortTokenSwapPath", "type": "address[]" } + ], + "internalType": "struct GlvWithdrawal.Addresses", + "name": "addresses", + "type": "tuple" + }, + { + "components": [ + { "internalType": "uint256", "name": "glvTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minLongTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minShortTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "updatedAtTime", "type": "uint256" }, + { "internalType": "uint256", "name": "executionFee", "type": "uint256" }, + { "internalType": "uint256", "name": "callbackGasLimit", "type": "uint256" } + ], + "internalType": "struct GlvWithdrawal.Numbers", + "name": "numbers", + "type": "tuple" + }, + { + "components": [ + { "internalType": "bool", "name": "shouldUnwrapNativeToken", "type": "bool" } + ], + "internalType": "struct GlvWithdrawal.Flags", + "name": "flags", + "type": "tuple" + } + ], + "internalType": "struct GlvWithdrawal.Props[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract DataStore", "name": "dataStore", "type": "address" }, + { "internalType": "uint256", "name": "start", "type": "uint256" }, + { "internalType": "uint256", "name": "end", "type": "uint256" } + ], + "name": "getGlvs", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "glvToken", + "type": "address" + }, + { + "internalType": "address", + "name": "longToken", + "type": "address" + }, + { + "internalType": "address", + "name": "shortToken", + "type": "address" + } + ], + "internalType": "struct Glv.Props[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/packages/composites/glv-token/src/config/index.ts b/packages/composites/glv-token/src/config/index.ts new file mode 100644 index 0000000000..ed94984e01 --- /dev/null +++ b/packages/composites/glv-token/src/config/index.ts @@ -0,0 +1,80 @@ +import { AdapterConfig } from '@chainlink/external-adapter-framework/config' +import { validator } from '@chainlink/external-adapter-framework/validation/utils' + +export const config = new AdapterConfig( + { + ARBITRUM_RPC_URL: { + description: 'RPC url of Arbitrum node', + type: 'string', + required: true, + }, + ARBITRUM_CHAIN_ID: { + description: 'The chain id to connect to', + type: 'number', + required: true, + default: 42161, + }, + DATASTORE_CONTRACT_ADDRESS: { + description: 'Address of Data Store contract', + type: 'string', + required: true, + default: '0xFD70de6b91282D8017aA4E741e9Ae325CAb992d8', + }, + GLV_READER_CONTRACT_ADDRESS: { + description: 'Address of Glv Reader Contract', + type: 'string', + required: true, + default: '0x6a9505D0B44cFA863d9281EA5B0b34cB36243b45', + }, + TIINGO_ADAPTER_URL: { + description: 'URL of Tiingo EA', + type: 'string', + required: true, + }, + NCFX_ADAPTER_URL: { + description: 'URL of NCFX EA', + type: 'string', + required: true, + }, + COINMETRICS_ADAPTER_URL: { + description: 'URL of Coinmetrics EA', + type: 'string', + required: true, + }, + MIN_REQUIRED_SOURCE_SUCCESS: { + description: 'Minimum number of source EAs that need to successfully return a value.', + type: 'number', + required: true, + default: 2, + validate: validator.integer({ min: 1, max: 3 }), + }, + MARKET_INFO_API: { + description: 'URL market meta data supported by Glv', + type: 'string', + required: true, + default: 'https://arbitrum-api.gmxinfra.io/markets', + }, + TOKEN_INFO_API: { + description: 'URL to token meta data supported by Glv ', + type: 'string', + required: true, + default: 'https://arbitrum-api.gmxinfra.io/tokens', + }, + METADATA_REFRESH_INTERVAL_MS: { + description: 'The amount of time the metadata should be refreshed', + type: 'number', + default: 60 * 60 * 3 * 1000, // 3 hours + }, + BACKGROUND_EXECUTE_MS: { + description: + 'The amount of time the background execute should sleep before performing the next request', + type: 'number', + default: 10_000, + }, + }, + { + envDefaultOverrides: { + RETRY: 3, + }, + }, +) diff --git a/packages/composites/glv-token/src/endpoint/index.ts b/packages/composites/glv-token/src/endpoint/index.ts new file mode 100644 index 0000000000..11a44912b4 --- /dev/null +++ b/packages/composites/glv-token/src/endpoint/index.ts @@ -0,0 +1 @@ +export { endpoint as price } from './price' diff --git a/packages/composites/glv-token/src/endpoint/price.ts b/packages/composites/glv-token/src/endpoint/price.ts new file mode 100644 index 0000000000..14d9e27028 --- /dev/null +++ b/packages/composites/glv-token/src/endpoint/price.ts @@ -0,0 +1,36 @@ +import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { InputParameters } from '@chainlink/external-adapter-framework/validation' +import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util' +import { config } from '../config' +import { glvTokenTransport } from '../transport/price' + +export const inputParameters = new InputParameters( + { + glv: { + required: true, + type: 'string', + description: 'Glv address', + }, + }, + [ + { + glv: '0x528A5bac7E746C9A509A1f4F6dF58A03d44279F9', + }, + ], +) + +export type BaseEndpointTypes = { + Parameters: typeof inputParameters.definition + Response: SingleNumberResultResponse & { + Data: { + sources: Record + } + } + Settings: typeof config.settings +} + +export const endpoint = new AdapterEndpoint({ + name: 'price', + transport: glvTokenTransport, + inputParameters, +}) diff --git a/packages/composites/glv-token/src/index.ts b/packages/composites/glv-token/src/index.ts new file mode 100644 index 0000000000..e0d17dd88c --- /dev/null +++ b/packages/composites/glv-token/src/index.ts @@ -0,0 +1,13 @@ +import { expose, ServerInstance } from '@chainlink/external-adapter-framework' +import { Adapter } from '@chainlink/external-adapter-framework/adapter' +import { config } from './config' +import { price } from './endpoint' + +export const adapter = new Adapter({ + defaultEndpoint: price.name, + name: 'GLV_TOKEN', + config, + endpoints: [price], +}) + +export const server = (): Promise => expose(adapter) diff --git a/packages/composites/glv-token/src/transport/price.ts b/packages/composites/glv-token/src/transport/price.ts new file mode 100644 index 0000000000..7332a0ba2d --- /dev/null +++ b/packages/composites/glv-token/src/transport/price.ts @@ -0,0 +1,372 @@ +import { TransportDependencies } from '@chainlink/external-adapter-framework/transports' +import { ResponseCache } from '@chainlink/external-adapter-framework/cache/response' +import { Requester } from '@chainlink/external-adapter-framework/util/requester' + +import { BaseEndpointTypes, inputParameters } from '../endpoint/price' +import { ethers, utils } from 'ethers' +import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription' +import { + EndpointContext, + LwbaResponseDataFields, +} from '@chainlink/external-adapter-framework/adapter' +import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util' +import { + toFixed, + median, + PriceData, + SIGNED_PRICE_DECIMALS, + Source, + Token, + Market, + mapSymbol, + mapParameter, +} from './utils' +import glvAbi from '../config/glvReaderAbi.json' +import { AdapterDataProviderError } from '@chainlink/external-adapter-framework/validation/error' + +const logger = makeLogger('GlvToken') + +type RequestParams = typeof inputParameters.validated + +export type GlvTokenTransportTypes = BaseEndpointTypes + +interface glvInformation { + glvToken: string + longToken: Token + shortToken: Token + markets: Record +} + +export class GlvTokenTransport extends SubscriptionTransport { + name!: string + responseCache!: ResponseCache + requester!: Requester + provider!: ethers.providers.JsonRpcProvider + glvReaderContract!: ethers.Contract + settings!: GlvTokenTransportTypes['Settings'] + + tokensMap: Record = {} + marketsMap: Record = {} + decimals: Record = {} + + async initialize( + dependencies: TransportDependencies, + adapterSettings: GlvTokenTransportTypes['Settings'], + endpointName: string, + transportName: string, + ): Promise { + await super.initialize(dependencies, adapterSettings, endpointName, transportName) + this.settings = adapterSettings + this.provider = new ethers.providers.JsonRpcProvider( + adapterSettings.ARBITRUM_RPC_URL, + adapterSettings.ARBITRUM_CHAIN_ID, + ) + this.requester = dependencies.requester + + this.glvReaderContract = new ethers.Contract( + adapterSettings.GLV_READER_CONTRACT_ADDRESS, + glvAbi, + this.provider, + ) + + await this.tokenInfo() + await this.marketInfo() + + if (this.settings.METADATA_REFRESH_INTERVAL_MS > 0) { + setInterval(() => { + this.tokenInfo() + this.marketInfo() + }, this.settings.METADATA_REFRESH_INTERVAL_MS) + } + } + + async tokenInfo() { + const requestConfig = { + url: this.settings.TOKEN_INFO_API, + method: 'GET', + } + + const response = await this.requester.request<{ tokens: Token[] }>( + JSON.stringify(requestConfig), + requestConfig, + ) + + const data: Token[] = response.response.data.tokens + data.map((token) => { + this.tokensMap[token.address] = token + this.decimals[token.symbol] = token.decimals + }) + } + + async marketInfo() { + const requestConfig = { + url: this.settings.MARKET_INFO_API, + method: 'GET', + } + + const response = await this.requester.request<{ markets: Market[] }>( + JSON.stringify(requestConfig), + requestConfig, + ) + + const data: Market[] = response.response.data.markets + data.map((market) => { + this.marketsMap[market.marketToken] = market + }) + } + + async backgroundHandler(context: EndpointContext, entries: RequestParams[]) { + await Promise.all(entries.map(async (param) => this.handleRequest(param))) + await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS) + } + + async handleRequest(param: RequestParams) { + let response: AdapterResponse + try { + response = await this._handleRequest(param) + } catch (e) { + const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred' + logger.error(e, errorMessage) + response = { + statusCode: 502, + errorMessage, + timestamps: { + providerDataRequestedUnixMs: 0, + providerDataReceivedUnixMs: 0, + providerIndicatedTimeUnixMs: undefined, + }, + } + } + await this.responseCache.write(this.name, [{ params: param, response }]) + } + + async _handleRequest( + param: RequestParams, + ): Promise> { + const providerDataRequestedUnixMs = Date.now() + const glv_address = param.glv + + const glvInfo = await this.glvReaderContract.getGlvInfo( + this.settings.DATASTORE_CONTRACT_ADDRESS, + glv_address, + ) + + const glv: glvInformation = { + glvToken: glvInfo.glv.glvToken, + longToken: mapSymbol(glvInfo.glv.longToken, this.tokensMap), + shortToken: mapSymbol(glvInfo.glv.shortToken, this.tokensMap), + markets: {}, + } + + for (let i = 0; i < glvInfo.markets.length; i++) { + glv.markets[glvInfo.markets[i]] = mapSymbol(glvInfo.markets[i], this.marketsMap) + } + + const assets: Array = [glv.longToken.symbol, glv.shortToken.symbol] + Object.keys(glv.markets).forEach((m) => { + assets.push(mapSymbol(glv.markets[m].indexToken, this.tokensMap).symbol) + }) + + assets.sort() + const priceResult = await this.fetchPrices([...new Set(assets)], providerDataRequestedUnixMs) + + const indexTokensPrices: Array[] = [] + Object.keys(glv.markets).forEach((m) => { + const symbol = mapSymbol(glv.markets[m].indexToken, this.tokensMap).symbol + indexTokensPrices.push([priceResult.prices[symbol].ask, priceResult.prices[symbol].bid]) + }) + + const glvTokenPriceContractParams = [ + this.settings.DATASTORE_CONTRACT_ADDRESS, + glvInfo.markets, + indexTokensPrices, + [priceResult.prices[glv.longToken.symbol].ask, priceResult.prices[glv.longToken.symbol].bid], + [ + priceResult.prices[glv.shortToken.symbol].ask, + priceResult.prices[glv.shortToken.symbol].bid, + ], + glv_address, + ] + + // Prices have a spread from min to max. The last param (maximize-true/false) decides whether to maximize the market token price + // or not. We get both values and return the median. + + const [[maximizedValue], [minimizedValue]] = await Promise.all([ + this.glvReaderContract.getGlvTokenPrice(...glvTokenPriceContractParams, true), + this.glvReaderContract.getGlvTokenPrice(...glvTokenPriceContractParams, false), + ]) + + const maximizedPrice = Number(utils.formatUnits(maximizedValue, SIGNED_PRICE_DECIMALS)) + const minimizedPrice = Number(utils.formatUnits(minimizedValue, SIGNED_PRICE_DECIMALS)) + const result = median([minimizedPrice, maximizedPrice]) + + return { + data: { + result: result, + sources: priceResult.sources, + }, + statusCode: 200, + result: result, + timestamps: { + providerDataRequestedUnixMs, + providerDataReceivedUnixMs: Date.now(), + providerIndicatedTimeUnixMs: undefined, + }, + } + } + + // Fetches the lwba price info from multiple source EAs, calculates the median for bids and asks per asset and fixes the price precision + private async fetchPrices(assets: string[], dataRequestedTimestamp: number) { + // priceData holds raw bid/ask values per asset from source EAs response + const priceData = {} as PriceData + + const sources = [ + { url: this.settings.TIINGO_ADAPTER_URL, name: 'tiingo' }, + { url: this.settings.COINMETRICS_ADAPTER_URL, name: 'coinmetrics' }, + { url: this.settings.NCFX_ADAPTER_URL, name: 'ncfx' }, + ] + + //priceProviders contains assets with a list of sources where asset price was successfully fetched + const priceProviders: Record = {} + + const promises = [] + + for (let i = 0; i < sources.length; i++) { + const source = sources[i] + const assetPromises = assets.map(async (asset) => { + const mappedAsset = mapParameter(source.name, asset) + const base = this.unwrapAsset(mappedAsset) + const requestConfig = { + url: source.url, + method: 'POST', + data: { + data: { + endpoint: 'crypto-lwba', + base, + quote: 'USD', + }, + }, + } + + // try/catch is needed in a case if one of source EAs fails to return a response, + // we will still get and calculate the median price based on responses of remaining EAs (based on MIN_REQUIRED_SOURCE_SUCCESS setting) + try { + const response = await this.requester.request<{ data: LwbaResponseDataFields['Data'] }>( + JSON.stringify(requestConfig), + requestConfig, + ) + const { bid, ask } = response.response.data.data + + priceData[asset] = { + bids: [...(priceData[asset]?.bids || []), bid], + asks: [...(priceData[asset]?.asks || []), ask], + } + + priceProviders[asset] = priceProviders[asset] + ? [...new Set([...priceProviders[asset], source.name])] + : [source.name] + } catch (error) { + const e = error as Error + logger.error( + `Error fetching data for ${asset} from ${source.name}, url - ${source.url}: ${e.message}`, + ) + } + }) + + promises.push(...assetPromises) + } + + await Promise.all(promises) + + this.validateRequiredResponses(priceProviders, sources, assets, dataRequestedTimestamp) + + const medianValues = this.calculateMedian(assets, priceData) + + const prices: Record> = {} + + medianValues.map( + (v) => + (prices[v.asset] = { + ...v, + ask: toFixed(v.ask, this.decimals[v.asset as keyof typeof this.decimals]), + bid: toFixed(v.bid, this.decimals[v.asset as keyof typeof this.decimals]), + }), + ) + + return { + prices, + sources: priceProviders, + } + } + + private calculateMedian(assets: string[], priceData: PriceData) { + return assets.map((asset) => { + // Since most of the gm markets have the same long and index tokens, we need to remove duplicate values from duplicate requests + const medianBid = median([...new Set(priceData[asset].bids)]) + const medianAsk = median([...new Set(priceData[asset].asks)]) + return { asset, bid: medianBid, ask: medianAsk } + }) + } + + private unwrapAsset(asset: string) { + if (asset === 'WBTC.b') { + return 'BTC' + } + if (asset === 'WETH') { + return 'ETH' + } + return asset + } + + private validateRequiredResponses( + priceProviders: Record = {}, + sources: Source[], + assets: string[], + dataRequestedTimestamp: number, + ) { + const allSource = sources.map((s) => s.name) + if (!Object.entries(priceProviders)?.length) { + throw new AdapterDataProviderError( + { + statusCode: 502, + message: `Missing responses from '${allSource.join(',')}' for all assets.`, + }, + { + providerDataRequestedUnixMs: dataRequestedTimestamp, + providerDataReceivedUnixMs: Date.now(), + providerIndicatedTimeUnixMs: undefined, + }, + ) + } + + assets.forEach((asset) => { + const base = this.unwrapAsset(asset) + const respondedSources = priceProviders[base] + + if (respondedSources.length < this.settings.MIN_REQUIRED_SOURCE_SUCCESS) { + const missingSources = allSource.filter((s) => !respondedSources.includes(s)) + throw new AdapterDataProviderError( + { + statusCode: 502, + message: `Cannot calculate median price for '${asset}'. At least ${ + this.settings.MIN_REQUIRED_SOURCE_SUCCESS + } EAs are required to provide a response but response was received only from ${ + respondedSources.length + } EA(s). Missing responses from '${missingSources.join(',')}'.`, + }, + { + providerDataRequestedUnixMs: dataRequestedTimestamp, + providerDataReceivedUnixMs: Date.now(), + providerIndicatedTimeUnixMs: undefined, + }, + ) + } + }) + } + + getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number { + return adapterSettings.WARMUP_SUBSCRIPTION_TTL + } +} + +export const glvTokenTransport = new GlvTokenTransport() diff --git a/packages/composites/glv-token/src/transport/utils.ts b/packages/composites/glv-token/src/transport/utils.ts new file mode 100644 index 0000000000..66cff57f67 --- /dev/null +++ b/packages/composites/glv-token/src/transport/utils.ts @@ -0,0 +1,70 @@ +import Decimal from 'decimal.js' + +// The signed prices represent the price of one unit of the token using a value with 30 decimals of precision. +export const SIGNED_PRICE_DECIMALS = 30 + +export type PriceData = { [asset: string]: { bids: number[]; asks: number[] } } + +export type Source = { url: string; name: string } + +export const median = (values: number[]): number => { + if (values.length === 0) { + throw new Error('Input array is empty') + } + + values = [...values].sort((a, b) => a - b) + + const half = Math.floor(values.length / 2) + + return values.length % 2 ? values[half] : (values[half - 1] + values[half]) / 2 +} + +/* +Formats a given number with a specified precision without leading zeros and decimal point. +Price decimals = SIGNED_PRICE_DECIMALS - token decimals + +toFixed(14.84329267, 18) -> '14843292670000' +toFixed(0.99999558, 6) -> '999995580000000000000000' + */ +export const toFixed = (number: number, decimals: number): string => { + const n = new Decimal(number) + return n + .toFixed(SIGNED_PRICE_DECIMALS - decimals) + .replace('.', '') + .replace(/^0+/, '') +} + +export interface Token { + symbol: string + address: string + decimals: number + synthetic: null | boolean +} + +export interface Market { + marketToken: string + indexToken: string + longToken: string + shortToken: string + isListed: boolean +} + +export function mapSymbol(address: string, symbolMap: Record) { + return symbolMap[address] +} + +const adapterParamOverride: Record> = { + coinmetrics: { + TAO: 'tao_bittensor', + }, + tiingo: { + FLOKI: 'floki2', + }, +} + +export function mapParameter(source: string, param: string) { + if (source in adapterParamOverride && param in adapterParamOverride[source]) { + return adapterParamOverride[source][param] + } + return param +} diff --git a/packages/composites/glv-token/test-payload.json b/packages/composites/glv-token/test-payload.json new file mode 100644 index 0000000000..e09b5a6de9 --- /dev/null +++ b/packages/composites/glv-token/test-payload.json @@ -0,0 +1,5 @@ +{ + "requests": [{ + "glv": "0x528A5bac7E746C9A509A1f4F6dF58A03d44279F9" + }] +} \ No newline at end of file diff --git a/packages/composites/glv-token/test/integration/__snapshots__/adapter.test.ts.snap b/packages/composites/glv-token/test/integration/__snapshots__/adapter.test.ts.snap new file mode 100644 index 0000000000..50ab0400f3 --- /dev/null +++ b/packages/composites/glv-token/test/integration/__snapshots__/adapter.test.ts.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`execute price endpoint should return success 1`] = ` +{ + "data": { + "result": 1.289471279771571, + "sources": { + "ETH": [ + "tiingo", + "coinmetrics", + "ncfx", + ], + "USDC": [ + "tiingo", + "coinmetrics", + "ncfx", + ], + }, + }, + "result": 1.289471279771571, + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; diff --git a/packages/composites/glv-token/test/integration/adapter.test.ts b/packages/composites/glv-token/test/integration/adapter.test.ts new file mode 100644 index 0000000000..c9d651e38d --- /dev/null +++ b/packages/composites/glv-token/test/integration/adapter.test.ts @@ -0,0 +1,118 @@ +import { + TestAdapter, + setEnvVariables, +} from '@chainlink/external-adapter-framework/util/testing-utils' +import * as nock from 'nock' +import { + mockCoinmetricsEAResponseSuccess, + mockNCFXEAResponseSuccess, + mockTiingoEAResponseSuccess, +} from './fixtures' +import { ethers } from 'ethers' + +jest.mock('ethers', () => ({ + ...jest.requireActual('ethers'), + ethers: { + providers: { + JsonRpcProvider: function (): ethers.providers.JsonRpcProvider { + return {} as ethers.providers.JsonRpcProvider + }, + }, + Contract: function () { + return { + getGlvInfo: () => { + return { + glv: { + glvToken: '0x528A5bac7E746C9A509A1f4F6dF58A03d44279F9', + longToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', + shortToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + }, + markets: ['0x70d95587d40A2caf56bd97485aB3Eec10Bee6336'], + } + }, + getGlvTokenPrice: ( + contract_addr: string, + markets: string[], + indexTokenPrices: number[], + shortTokenPrices: number[], + glv_address: string, + maximize: boolean, + ) => { + if (maximize) { + return [ + { + _hex: '0x104681a4a04ccfed9f2e8d144e', + _isBigNumber: true, + }, + { + _hex: '0x1023b38579fd89d222cb251e176c4826', + _isBigNumber: true, + }, + { _hex: '0x0dc3092ed23a446cb3fe99', _isBigNumber: true }, + ] + } + return [ + { + _hex: '0x1047154bfc2ec20ad4c399c9cd', + _isBigNumber: true, + }, + { + _hex: '0x102445f11382dc2e29e06362a1c7a8d6', + _isBigNumber: true, + }, + { _hex: '0x0dc3092ed23a446cb3fe99', _isBigNumber: true }, + ] + }, + } + }, + }, +})) + +describe('execute', () => { + let spy: jest.SpyInstance + let testAdapter: TestAdapter + let oldEnv: NodeJS.ProcessEnv + + beforeAll(async () => { + oldEnv = JSON.parse(JSON.stringify(process.env)) + process.env.TIINGO_ADAPTER_URL = process.env.TIINGO_ADAPTER_URL ?? 'http://localhost:8081' + process.env.NCFX_ADAPTER_URL = process.env.NCFX_ADAPTER_URL ?? 'http://localhost:8082' + process.env.COINMETRICS_ADAPTER_URL = + process.env.COINMETRICS_ADAPTER_URL ?? 'http://localhost:8083' + process.env.ARBITRUM_RPC_URL = process.env.ARBITRUM_RPC_URL ?? 'http://localhost:3040' + process.env.RETRY = process.env.RETRY ?? '0' + process.env.BACKGROUND_EXECUTE_MS = '0' + process.env.METADATA_REFRESH_INTERVAL_MS = '0' + + const mockDate = new Date('2001-01-01T11:11:11.111Z') + spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) + + const adapter = (await import('./../../src')).adapter + adapter.rateLimiting = undefined + testAdapter = await TestAdapter.startWithMockedCache(adapter, { + testAdapter: {} as TestAdapter, + }) + }) + + afterAll(async () => { + setEnvVariables(oldEnv) + await testAdapter.api.close() + nock.restore() + nock.cleanAll() + spy.mockRestore() + }) + + describe('price endpoint', () => { + it('should return success', async () => { + const data = { + glv: '0x528A5bac7E746C9A509A1f4F6dF58A03d44279F9', + } + mockTiingoEAResponseSuccess('ETH') + mockNCFXEAResponseSuccess('ETH') + mockCoinmetricsEAResponseSuccess('ETH') + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + }) +}) diff --git a/packages/composites/glv-token/test/integration/fixtures.ts b/packages/composites/glv-token/test/integration/fixtures.ts new file mode 100644 index 0000000000..129bb066d2 --- /dev/null +++ b/packages/composites/glv-token/test/integration/fixtures.ts @@ -0,0 +1,308 @@ +import nock from 'nock' + +export const mockTiingoEAResponseSuccess = (base): nock.Scope => + nock('http://localhost:8081', { + encodedQueryParams: true, + }) + .post('/', { + data: { + endpoint: 'crypto-lwba', + base, + quote: 'USD', + }, + }) + .reply( + 200, + () => ({ + data: { + ask: 15.694322872166047, + bid: 15.763680197921362, + mid: 15.729001535, + }, + result: null, + statusCode: 200, + timestamps: { + providerDataReceivedUnixMs: 2028, + providerDataStreamEstablishedUnixMs: 2020, + providerIndicatedTimeUnixMs: 1680187094577, + }, + }), + [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ], + ) + .persist() + .post('/', { + data: { + endpoint: 'crypto-lwba', + base: 'USDC', + quote: 'USD', + }, + }) + .reply( + 200, + () => ({ + data: { + ask: 1.0012, + bid: 1.01, + mid: 1.0056, + }, + result: null, + statusCode: 200, + timestamps: { + providerDataReceivedUnixMs: 2028, + providerDataStreamEstablishedUnixMs: 2020, + providerIndicatedTimeUnixMs: 1680187094577, + }, + }), + [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ], + ) + .persist() + +export const mockNCFXEAResponseSuccess = (base): nock.Scope => + nock('http://localhost:8082', { + encodedQueryParams: true, + }) + .post('/', { + data: { + endpoint: 'crypto-lwba', + base, + quote: 'USD', + }, + }) + .reply( + 200, + () => ({ + data: { + ask: 15.614322872166047, + bid: 15.863680197921362, + mid: 15.739001535, + }, + result: 15.739001535, + statusCode: 200, + timestamps: { + providerDataReceivedUnixMs: 2028, + providerDataStreamEstablishedUnixMs: 2020, + providerIndicatedTimeUnixMs: 1680187094577, + }, + }), + [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ], + ) + .persist() + .post('/', { + data: { + endpoint: 'crypto-lwba', + base: 'USDC', + quote: 'USD', + }, + }) + .reply( + 200, + () => ({ + data: { + ask: 1.001, + bid: 1.002, + mid: 1.0015, + }, + result: null, + statusCode: 200, + timestamps: { + providerDataReceivedUnixMs: 2028, + providerDataStreamEstablishedUnixMs: 2020, + providerIndicatedTimeUnixMs: 1680187094577, + }, + }), + [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ], + ) + .persist() + +export const mockCoinmetricsEAResponseSuccess = (base): nock.Scope => + nock('http://localhost:8083', { + encodedQueryParams: true, + }) + .post('/', { + data: { + endpoint: 'crypto-lwba', + base, + quote: 'USD', + }, + }) + .reply( + 200, + () => ({ + data: { + ask: 15.59, + bid: 15.64, + mid: 15.61, + }, + result: 15.739001535, + statusCode: 200, + timestamps: { + providerDataReceivedUnixMs: 2028, + providerDataStreamEstablishedUnixMs: 2020, + providerIndicatedTimeUnixMs: 1680187094577, + }, + }), + [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ], + ) + .persist() + .post('/', { + data: { + endpoint: 'crypto-lwba', + base: 'USDC', + quote: 'USD', + }, + }) + .reply( + 200, + () => ({ + data: { + ask: 1, + bid: 1.002, + mid: 1.001, + }, + result: null, + statusCode: 200, + timestamps: { + providerDataReceivedUnixMs: 2028, + providerDataStreamEstablishedUnixMs: 2020, + providerIndicatedTimeUnixMs: 1680187094577, + }, + }), + [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ], + ) + .persist() + +export const mockNCFXEAResponseFailure = (base): nock.Scope => + nock('http://localhost:8082', { + encodedQueryParams: true, + }) + .post('/', { + data: { + endpoint: 'crypto-lwba', + base, + quote: 'USD', + }, + }) + .reply(500, () => ({}), [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ]) + .persist() + .post('/', { + data: { + endpoint: 'crypto-lwba', + base: 'USDC', + quote: 'USD', + }, + }) + .reply(500, () => ({}), [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ]) + .persist() + +export const mockCoinmetricsEAResponseFailure = (base): nock.Scope => + nock('http://localhost:8083', { + encodedQueryParams: true, + }) + .post('/', { + data: { + endpoint: 'crypto-lwba', + base, + quote: 'USD', + }, + }) + .reply(500, () => ({}), [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ]) + .persist() + .post('/', { + data: { + endpoint: 'crypto-lwba', + base: 'USDC', + quote: 'USD', + }, + }) + .reply(500, () => ({}), [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ]) + .persist() diff --git a/packages/composites/glv-token/test/unit/utils.test.ts b/packages/composites/glv-token/test/unit/utils.test.ts new file mode 100644 index 0000000000..ef896a5290 --- /dev/null +++ b/packages/composites/glv-token/test/unit/utils.test.ts @@ -0,0 +1,19 @@ +import { toFixed, median } from '../../src/transport/utils' + +describe('toFixed', () => { + it('should return a string with correct precision', () => { + let res = toFixed(0.62296417, 12) + expect(res).toBe('622964170000000000') + res = toFixed(44.3422343, 8) + expect(res).toBe('443422343000000000000000') + }) +}) + +describe('median', () => { + it('should return correct median value', () => { + let res = median([1, 2, 3, 4, 5]) + expect(res).toBe(3) + res = median([1, 2, 3, 4]) + expect(res).toBe(2.5) + }) +}) diff --git a/packages/composites/glv-token/tsconfig.json b/packages/composites/glv-token/tsconfig.json new file mode 100644 index 0000000000..f59363fd76 --- /dev/null +++ b/packages/composites/glv-token/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*", "src/**/*.json"], + "exclude": ["dist", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/composites/glv-token/tsconfig.test.json b/packages/composites/glv-token/tsconfig.test.json new file mode 100644 index 0000000000..e3de28cb5c --- /dev/null +++ b/packages/composites/glv-token/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*", "**/test", "src/**/*.json"], + "compilerOptions": { + "noEmit": true + } +} diff --git a/packages/scripts/src/get-changed-adapters/soakTestBlacklist.ts b/packages/scripts/src/get-changed-adapters/soakTestBlacklist.ts index f194e603ab..57439b902e 100644 --- a/packages/scripts/src/get-changed-adapters/soakTestBlacklist.ts +++ b/packages/scripts/src/get-changed-adapters/soakTestBlacklist.ts @@ -39,6 +39,7 @@ export const SoakTestBlacklist: string[] = [ 'fmpcloud', 'frxeth-exchange-rate', 'genesis-volatility', + 'glv-token', // Composite EA - Missing underlying source EAs 'gm-token', // Composite EA - Missing underlying source EAs 'google-bigquery', 'google-weather', diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 5668855a4d..337e0f4743 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -44,6 +44,9 @@ { "path": "./composites/dydx-rewards" }, + { + "path": "./composites/glv-token" + }, { "path": "./composites/gm-token" }, diff --git a/packages/tsconfig.test.json b/packages/tsconfig.test.json index d4c1802557..0835ed7e6f 100644 --- a/packages/tsconfig.test.json +++ b/packages/tsconfig.test.json @@ -44,6 +44,9 @@ { "path": "./composites/dydx-rewards/tsconfig.test.json" }, + { + "path": "./composites/glv-token/tsconfig.test.json" + }, { "path": "./composites/gm-token/tsconfig.test.json" }, diff --git a/yarn.lock b/yarn.lock index d912ca7216..ae93667c80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4164,6 +4164,21 @@ __metadata: languageName: unknown linkType: soft +"@chainlink/glv-token-adapter@workspace:packages/composites/glv-token": + version: 0.0.0-use.local + resolution: "@chainlink/glv-token-adapter@workspace:packages/composites/glv-token" + dependencies: + "@chainlink/external-adapter-framework": "npm:1.7.7" + "@types/jest": "npm:27.5.2" + "@types/node": "npm:16.18.119" + decimal.js: "npm:^10.3.1" + ethers: "npm:^5.4.6" + nock: "npm:13.5.5" + tslib: "npm:2.4.1" + typescript: "npm:5.6.3" + languageName: unknown + linkType: soft + "@chainlink/gm-token-adapter@workspace:packages/composites/gm-token": version: 0.0.0-use.local resolution: "@chainlink/gm-token-adapter@workspace:packages/composites/gm-token"