Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Intrinio, Twelvedata, Unibit to master (before EAEE changes, after v0.2.0) #446

Merged
merged 3 commits into from
Apr 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/strategy/adapters.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@
"ethwrite",
"geodb",
"therundown",
"tradingeconomics-stream"
"tradingeconomics-stream",
"unibit",
"intrinio",
"twelvedata"
]
},
"2-step": {
Expand Down
6 changes: 5 additions & 1 deletion amberdata/src/endpoint/price.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const addressMapping: { [symbol: string]: string } = {
WBTC: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599',
RAI: '0x03ab458634910aad20ef5f1c8ee96f1d6ac54919',
WETH: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
RGT: '0xD291E7a03283640FDc51b121aC401383A46cC623',
RARI: '0xFca59Cd816aB1eaD66534D82bc21E7515cE441CF',
}

const customParams = {
Expand All @@ -38,7 +40,9 @@ export const execute: ExecuteWithConfig<Config> = async (input, config) => {
if (
includes.length > 0 &&
((includes[0].toLowerCase() === 'wbtc' && coin.toLowerCase() === 'digg') ||
(includes[0].toLowerCase() === 'weth' && coin.toLowerCase() === 'rai'))
(includes[0].toLowerCase() === 'weth' && coin.toLowerCase() === 'rai') ||
(includes[0].toLowerCase() === 'weth' && coin.toLowerCase() === 'rgt') ||
(includes[0].toLowerCase() === 'weth' && coin.toLowerCase() === 'rari'))
) {
const fromAddress = addressMapping[coin.toUpperCase()]
const toAddress = addressMapping[includes[0].toUpperCase()]
Expand Down
2 changes: 1 addition & 1 deletion composite/token-allocation/src/data-providers/amberdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { util } from '@chainlink/ea-bootstrap'
const getPriceData = async (symbol: string) => {
const url = `https://web3api.io/api/v2/market/tokens/prices/${symbol.toLowerCase()}/latest`
const headers = {
'X-API-KEY': util.getRequiredEnv('API_KEY'),
'X-API-KEY': util.getRandomRequiredEnv('API_KEY'),
}
const config = {
url,
Expand Down
2 changes: 1 addition & 1 deletion composite/token-allocation/src/data-providers/coinapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const getPriceData = async (symbol: string, currency: string) => {
const config = {
url,
params: {
apikey: util.getRequiredEnv('API_KEY'),
apikey: util.getRandomRequiredEnv('API_KEY'),
},
}
const response = await Requester.request(config)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Requester } from '@chainlink/external-adapter'
import { util } from '@chainlink/ea-bootstrap'

// Coin IDs fetched from the ID map: https://coinmarketcap.com/api/documentation/v1/#operation/getV1CryptocurrencyMap
const presetIds: { [symbol: string]: number } = {
Expand Down Expand Up @@ -37,7 +38,7 @@ const getPriceData = async (assets: string[], convert: string) => {
const _getPriceData = async (params: any): Promise<any> => {
const url = 'https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest'
const headers = {
'X-CMC_PRO_API_KEY': process.env.API_KEY,
'X-CMC_PRO_API_KEY': util.getRandomRequiredEnv('API_KEY'),
}
const config = {
url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const getPriceData = async (symbols: string, currency: string) => {
const params = {
tsyms: currency.toUpperCase(),
fsyms: symbols,
api_key: util.getRequiredEnv('API_KEY'),
api_key: util.getRandomRequiredEnv('API_KEY'),
}
const config = {
url,
Expand Down
2 changes: 1 addition & 1 deletion composite/token-allocation/src/data-providers/kaiko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const getPriceData = async (symbol: string, currency: string) => {
interval: '1m',
}
const headers = {
'X-Api-Key': util.getRequiredEnv('API_KEY'),
'X-Api-Key': util.getRandomRequiredEnv('API_KEY'),
'User-Agent': 'Chainlink',
}
const timeout = 5000
Expand Down
2 changes: 1 addition & 1 deletion composite/token-allocation/src/data-providers/nomics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const getPriceData = async (symbols: string, currency: string) => {
const params = {
ids: symbols,
convert: currency.toUpperCase(),
key: util.getRequiredEnv('API_KEY'),
key: util.getRandomRequiredEnv('API_KEY'),
}
const config = {
url,
Expand Down
3 changes: 3 additions & 0 deletions intrinio/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
...require('../.eslintrc.ts.js'),
}
43 changes: 43 additions & 0 deletions intrinio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Chainlink External Adapter for Intrinio

This adapter uses the Intrinio WS stream

### Environment variables

| Required? | Name | Description | Options | Defaults to |
| :-------: | :------------: | :--------------------------------------------------------------------------------------------------------------------: | :--------------------: | :---------: |
| ✅ | `API_KEY` | Your API client key | | |
| ✅ | `SYMBOLS` | A comma delimited list of symbols to fetch prices for. E.g: "MSFT,AAPL" | | |
| | `API_PROVIDER` | Intrinio allows subscription to different [channels/resources](https://github.com/intrinio/intrinio-realtime-node-sdk) | `iex`, `quodd`, `fxcm` | `iex` |

**NOTE: `quodd` and `fxcm` have not been tested. `iex` is the recommended (and default) websocket subscription**

### Input Params

| Required? | Name | Description | Options | Defaults to |
| :-------: | :------------------------: | :------------------------------: | :--------------: | :---------: |
| ✅ | `base`, `from`, or `asset` | The symbol of the asset to query | one of `SYMBOLS` | |

### Sample Input

```json
{
"id": "1",
"data": {
"base": "FTSE"
}
}
```

### Sample Output

```json
{
"jobRunID": "1",
"data": {
"result": 6663.73
},
"result": 6663.73,
"statusCode": 200
}
```
49 changes: 49 additions & 0 deletions intrinio/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@chainlink/intrinio-adapter",
"version": "0.0.1",
"description": "Chainlink intrinio adapter.",
"keywords": [
"Chainlink",
"LINK",
"blockchain",
"oracle",
"intrinio"
],
"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": {
"prepublishOnly": "yarn build && yarn test:unit",
"setup": "yarn build",
"build": "tsc -b",
"lint": "eslint --ignore-path ../.eslintignore . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint --ignore-path ../.eslintignore . --ext .js,.jsx,.ts,.tsx --fix",
"test": "mocha --exit --timeout 3000 -r ts-node/register 'test/**/*.test.ts'",
"test:unit": "mocha --exit --grep @integration --invert -r ts-node/register 'test/**/*.test.ts'",
"test:integration": "mocha --exit --timeout 3000 --grep @integration -r ts-node/register 'test/**/*.test.ts'",
"server": "node -e 'require(\"./index.js\").server()'",
"server:dist": "node -e 'require(\"./dist/index.js\").server()'",
"start": "yarn server:dist"
},
"devDependencies": {
"@types/chai": "^4.2.11",
"@types/express": "^4.17.6",
"@types/mocha": "^7.0.2",
"@types/node": "^14.0.13",
"@typescript-eslint/eslint-plugin": "^3.9.0",
"@typescript-eslint/parser": "^3.9.0",
"ts-node": "^8.10.2",
"typescript": "^3.9.7"
},
"dependencies": {
"express": "^4.17.1",
"intrinio-realtime": "^2.3.0"
}
}
83 changes: 83 additions & 0 deletions intrinio/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Requester, Validator, AdapterError } from '@chainlink/external-adapter'
import IntrinioRealtime from 'intrinio-realtime'
import { AdapterRequest } from '@chainlink/types'
import { Config, makeConfig, PROVIDER_OPTIONS } from './config'

const prices: { [symbol: string]: { bid: number; ask: number } } = {}

const subscribe = (assets: string[], config: Config) => {
const client = new IntrinioRealtime({
api_key: config.key,
provider: config.provider,
})

client.join(assets)

client.onQuote((quote: any) => {
// https://github.com/intrinio/intrinio-realtime-node-sdk
// handle different responses from different providers
switch (config.provider) {
case PROVIDER_OPTIONS[1]: //quodd (untested data provider)
prices[quote.ticker] = {
bid: quote?.bid_price_4d || prices[quote.ticker].bid,
ask: quote?.ask_price_4d || prices[quote.ticker].ask,
}
break
case PROVIDER_OPTIONS[2]: //fxcm (untested data provider)
prices[quote.code] = {
bid: quote.bid_price,
ask: quote.ask_price,
}
break
case PROVIDER_OPTIONS[0]: //iex
default:
if (quote.type == 'last') return
prices[quote.ticker] = {
...prices[quote.ticker],
[quote.type]: quote.price,
}
break
}
})
}

export const startService = (config: Config): void => {
const symbols = config.symbols.toUpperCase().split(',')
subscribe(symbols, config)
}

const customParams = {
base: ['base', 'from', 'asset'],
}

export const execute = async (input: AdapterRequest, config: Config) => {
const symbols = config.symbols.toUpperCase().split(',')
const validator = new Validator(input, customParams)
if (validator.error) throw validator.error

const jobRunID = validator.validated.id
const symbol = validator.validated.data.base.toUpperCase()

if (!symbols.includes(symbol))
throw new AdapterError({
jobRunID,
message: `Requested ${symbol} not in SYMBOLS environment variable`,
})

const bid = Requester.validateResultNumber(prices, [symbol, 'bid'])
const ask = Requester.validateResultNumber(prices, [symbol, 'ask'])
const price = (bid + ask) / 2

const response = {
data: {
result: price,
},
result: price,
status: 200,
}
return Requester.success(jobRunID, response)
}

export const makeExecute = (config?: Config) => {
return async (request: AdapterRequest) => execute(request, config || makeConfig())
}
24 changes: 24 additions & 0 deletions intrinio/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { util } from '@chainlink/ea-bootstrap'

export const NAME = 'INTRINIO'

export const PROVIDER_OPTIONS = ['iex', 'quodd', 'fxcm']

export type Config = {
key: string
symbols: string
provider: string
}

export const makeConfig = (prefix?: string): Config => {
let provider = util.getEnv('API_PROVIDER', prefix)
if (!PROVIDER_OPTIONS.includes(provider)) {
provider = PROVIDER_OPTIONS[0]
}

return {
key: util.getRequiredEnv('API_KEY', prefix),
symbols: util.getRequiredEnv('SYMBOLS', prefix),
provider: provider.toLowerCase(),
}
}
19 changes: 19 additions & 0 deletions intrinio/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as bootstrap from '@chainlink/ea-bootstrap'
import { makeExecute, startService } from './adapter'
import { AdapterRequest, Execute, ExecuteSync } from '@chainlink/types'
import { Requester } from '@chainlink/external-adapter'
import { makeConfig } from './config'

// Execution helper async => sync
const executeSync = (execute: Execute): ExecuteSync => {
return (data: AdapterRequest, callback: any) => {
return execute(data)
.then((result) => callback(result.statusCode, result))
.catch((error) => callback(error.statusCode || 500, Requester.errored(data.id, error)))
}
}

export const server = (): void => {
startService(makeConfig())
bootstrap.server.initHandler(executeSync(makeExecute()))()
}
31 changes: 31 additions & 0 deletions intrinio/test/adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Requester } from '@chainlink/external-adapter'
import { assertError } from '@chainlink/adapter-test-helpers'
import { AdapterRequest } from '@chainlink/types'
import { makeExecute } from '../src/adapter'

describe('execute', () => {
const jobID = '1'
const execute = makeExecute({ key: '', symbols: '', provider: '' })

context('validation error', () => {
const requests = [
{ name: 'empty body', testData: {} },
{ name: 'empty data', testData: { data: {} } },
{
name: 'base not supplied',
testData: { id: jobID, data: { quote: 'USD' } },
},
]

requests.forEach((req) => {
it(`${req.name}`, async () => {
try {
await execute(req.testData as AdapterRequest)
} catch (error) {
const errorResp = Requester.errored(jobID, error)
assertError({ expected: 400, actual: errorResp.statusCode }, errorResp, jobID)
}
})
})
})
})
10 changes: 10 additions & 0 deletions intrinio/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"typeRoots": ["../node_modules/@types", "../typings", "./typings"]
},
"include": ["src/**/*"],
"exclude": ["dist", "**/*.spec.ts", "**/*.test.ts"]
}
1 change: 1 addition & 0 deletions intrinio/typings/intrinio/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module 'intrinio-realtime'
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@
"geodb",
"therundown",
"tradingeconomics-stream",
"blockstream"
"blockstream",
"unibit",
"intrinio",
"twelvedata"
],
"scripts": {
"lint": "yarn workspaces run lint",
Expand Down
3 changes: 3 additions & 0 deletions twelvedata/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
...require('../.eslintrc.ts.js'),
}
Loading