Skip to content

Commit

Permalink
Adapters for Intrinio, Unibit, Twelvedata (#442)
Browse files Browse the repository at this point in the history
* unibit adapter

* intrinio adapter - untested

* intrinio: tested + update documentation - only iex needed, others untested

* twelvedata rest api adapter
  • Loading branch information
aalu1418 authored Apr 15, 2021
1 parent c98e932 commit e436c14
Show file tree
Hide file tree
Showing 37 changed files with 1,061 additions and 2 deletions.
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
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'),
}
93 changes: 93 additions & 0 deletions twelvedata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Chainlink External Adapter for Twelvedata

### Environment Variables

| Required? | Name | Description | Options | Defaults to |
| :-------: | :----------: | :------------------------------: | :-----: | :---------------------------: |
|| API_KEY | API key for Twelvedata | | |
| | API_ENDPOINT | The endpoint for your Twelvedata | | `https://api.twelvedata.com/` |

---

### Input Parameters

| Required? | Name | Description | Options | Defaults to |
| :-------: | :------: | :-----------------: | :---------------------------------------------------: | :---------: |
| | endpoint | The endpoint to use | [closing](#Closing-Endpoint), [price](#Price-Endpoint) | `closing` |

---

## Closing Endpoint

This `closing` endpoint provides the closing price of the previous day as detailed in [Twelvedata documentation](https://twelvedata.com/docs#end-of-day-price).

### Input Params

| Required? | Name | Description | Options | Defaults to |
| :-------: | :------------------------------: | :---------------------------------: | :-----: | :---------: |
|| `base`, `from`, `coin`, `market` | The symbol of the currency to query | | |

### Sample Input

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

### Sample Output

```json
{
"jobRunID": "1",
"result": 9.975,
"statusCode": 200,
"data": {
"symbol": "VXX",
"exchange": "CBOE",
"currency": "USD",
"datetime": "2021-04-14",
"close": "9.97500",
"result": 9.975
}
}
```

## Price Endpoint

This `price` endpoint provides the real-time price as detailed in [Twelvedata documentation](https://twelvedata.com/docs#real-time-price).

### Input Params

| Required? | Name | Description | Options | Defaults to |
| :-------: | :------------------------------: | :---------------------------------: | :-----: | :---------: |
|| `base`, `from`, `coin`, `market` | The symbol of the currency to query | | |

### Sample Input

```json
{
"id": "1",
"data": {
"base": "VXX",
"endpoint": "price"
}
}
```

### Sample Output

```json
{
"jobRunID": "1",
"result": 10.0756,
"statusCode": 200,
"data": {
"price": "10.07560",
"result": 10.0756
}
}
```
Loading

0 comments on commit e436c14

Please sign in to comment.