diff --git a/.github/strategy/adapters.json b/.github/strategy/adapters.json index f565d2e0f9..8236b10514 100644 --- a/.github/strategy/adapters.json +++ b/.github/strategy/adapters.json @@ -106,7 +106,8 @@ "defi-pulse", "dns-record-check", "outlier-detection", - "crypto-volatility-index" + "crypto-volatility-index", + "bitcoin-json-rpc" ] }, "synth-index": { diff --git a/CHANGELOG.md b/CHANGELOG.md index 52d7ebfcc8..c155586e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - `paxos` to get Paxos asset supply attestations - `outlier-detection`: composite adapter to check for outlier values between multiple sets of data providers - `dydx-stark` to sign the input price data with your private STARK key, and send it to the destination endpoint. + - `bitcoin-json-rpc`: composite adapter for querying bitcoin blockchain stats(difficulty, height) according to the existing convention - Added support for metadata in requests. This gives adapters access to the FM on-chain round state. - Moves re-usable test behaviors & testing utils to a new package - `@chainlink/adapter-test-helpers` - Added support for using query string parameters as input to adapters. diff --git a/blockchair/README.md b/blockchair/README.md index d4088ab9aa..cde96a4c8b 100644 --- a/blockchair/README.md +++ b/blockchair/README.md @@ -9,11 +9,8 @@ The adapter takes the following environment variables: ## Input Params -- `endpoint`: The endpoint to use, one of (difficulty|balance). Defaults to `difficulty` - -### Difficulty endpoint - -- `blockchain` or `coin`: The blockchain to get difficulty from +- `blockchain` or `coin`: The blockchain to get stats from +- `endpoint`: The parameter to query for. Default: "difficulty" ### Output diff --git a/blockchair/src/adapter.ts b/blockchair/src/adapter.ts index 0e1cfccccb..a4e301f768 100644 --- a/blockchair/src/adapter.ts +++ b/blockchair/src/adapter.ts @@ -1,7 +1,7 @@ import { Requester, Validator, AdapterError } from '@chainlink/external-adapter' import { ExecuteWithConfig, ExecuteFactory, Config } from '@chainlink/types' import { makeConfig, DEFAULT_ENDPOINT } from './config' -import { difficulty, balance } from './endpoint' +import { stats, balance } from './endpoint' const inputParams = { endpoint: false, @@ -18,8 +18,10 @@ const execute: ExecuteWithConfig = async (request, config) => { const endpoint = validator.validated.data.endpoint || DEFAULT_ENDPOINT switch (endpoint) { - case difficulty.Name: { - return difficulty.execute(request, config) + // might be moved to validator or config + case 'difficulty': + case 'height': { + return stats.execute(request, config) } case balance.Name: { return balance.makeExecute(config)(request) diff --git a/blockchair/src/endpoint/index.ts b/blockchair/src/endpoint/index.ts index f40602eb1d..8d7576c114 100644 --- a/blockchair/src/endpoint/index.ts +++ b/blockchair/src/endpoint/index.ts @@ -1,5 +1,5 @@ export * as balance from './balance' -export * as difficulty from './difficulty' +export * as stats from './stats' export const COIN_KEYS = ['btc', 'dash', 'doge', 'ltc', 'bch'] as const export type CoinType = typeof COIN_KEYS[number] diff --git a/blockchair/src/endpoint/difficulty.ts b/blockchair/src/endpoint/stats.ts similarity index 71% rename from blockchair/src/endpoint/difficulty.ts rename to blockchair/src/endpoint/stats.ts index ab19544719..1441ea68ef 100644 --- a/blockchair/src/endpoint/difficulty.ts +++ b/blockchair/src/endpoint/stats.ts @@ -1,17 +1,25 @@ import { Requester, Validator } from '@chainlink/external-adapter' import { ExecuteWithConfig, Config } from '@chainlink/types' import { COINS } from '.' +import { DEFAULT_ENDPOINT } from '../config' -export const Name = 'difficulty' +export const Name = 'stats' const inputParams = { blockchain: ['blockchain', 'coin'], + endpoint: false, +} + +const convertEndpoint: { [key: string]: string } = { + height: 'blocks', } export const execute: ExecuteWithConfig = async (input, config) => { const validator = new Validator(input, inputParams) if (validator.error) throw validator.error const jobRunID = validator.validated.id + let endpoint = validator.validated.data.endpoint || DEFAULT_ENDPOINT + endpoint = convertEndpoint[endpoint] || endpoint const blockchain = Requester.toVendorName( validator.validated.data.blockchain.toLowerCase(), @@ -22,6 +30,6 @@ export const execute: ExecuteWithConfig = async (input, config) => { const reqConfig = { ...config.api, url } const response = await Requester.request(reqConfig) - response.data.result = Requester.validateResultNumber(response.data, ['data', 'difficulty']) + response.data.result = Requester.validateResultNumber(response.data, ['data', endpoint]) return Requester.success(jobRunID, response) } diff --git a/blockchair/test/stats.test.ts b/blockchair/test/stats.test.ts new file mode 100644 index 0000000000..289a48d969 --- /dev/null +++ b/blockchair/test/stats.test.ts @@ -0,0 +1,90 @@ +import { assert } from 'chai' +import { Requester, AdapterError } from '@chainlink/external-adapter' +import { assertSuccess, assertError } from '@chainlink/adapter-test-helpers' +import { AdapterRequest } from '@chainlink/types' +import { makeExecute } from '../src/adapter' + +describe('stats endpoint', () => { + const jobID = '1' + const execute = makeExecute() + + context('successful calls @integration', () => { + const requests = [ + { + name: 'id not supplied', + testData: { data: { blockchain: 'BTC' } }, + }, + { + name: 'blockchain', + testData: { id: jobID, data: { blockchain: 'BTC' } }, + }, + { + name: 'coin', + testData: { id: jobID, data: { coin: 'BTC' } }, + }, + { + name: 'blockchain difficulty with endpoint', + testData: { id: jobID, data: { blockchain: 'BTC', endpoint: 'difficulty' } }, + }, + { + name: 'coing difficulty with endpoint', + testData: { id: jobID, data: { coin: 'BTC', endpoint: 'difficulty' } }, + }, + { + name: 'blockchain height', + testData: { id: jobID, data: { blockchain: 'BTC', endpoint: 'height' } }, + }, + { + name: 'coin height', + testData: { id: jobID, data: { coin: 'BTC', endpoint: 'height' } }, + }, + ] + + requests.forEach((req) => { + it(`${req.name}`, async () => { + const data = await execute(req.testData as AdapterRequest) + assertSuccess({ expected: 200, actual: data.statusCode }, data, jobID) + assert.isAbove(data.result, 0) + assert.isAbove(data.data.result, 0) + }) + }) + }) + + context('validation error', () => { + const requests = [ + { name: 'empty body', testData: {} }, + { name: 'empty data', testData: { data: {} } }, + ] + + requests.forEach((req) => { + it(`${req.name}`, async () => { + try { + await execute(req.testData as AdapterRequest) + } catch (error) { + const errorResp = Requester.errored(jobID, new AdapterError(error)) + assertError({ expected: 400, actual: errorResp.statusCode }, errorResp, jobID) + } + }) + }) + }) + + context('error calls @integration', () => { + const requests = [ + { + name: 'unknown blockchain', + testData: { id: jobID, data: { blockchain: 'not_real' } }, + }, + ] + + requests.forEach((req) => { + it(`${req.name}`, async () => { + try { + await execute(req.testData as AdapterRequest) + } catch (error) { + const errorResp = Requester.errored(jobID, new AdapterError(error)) + assertError({ expected: 500, actual: errorResp.statusCode }, errorResp, jobID) + } + }) + }) + }) +}) diff --git a/coinapi/test/adapter_test.js b/coinapi/test/adapter_test.js index c709757c12..97ce68e8e7 100644 --- a/coinapi/test/adapter_test.js +++ b/coinapi/test/adapter_test.js @@ -27,6 +27,20 @@ describe('execute', () => { name: 'coin/market lowercase', testData: { id: jobID, data: { coin: 'eth', market: 'usd' } }, }, + { + name: 'BTC testnet difficulty', + testData: { + id: jobID, + data: { blockchain: 'BTC', network: 'testnet', endpoint: 'difficulty' }, + }, + }, + { + name: 'BTC mainnet height', + testData: { + id: jobID, + data: { blockchain: 'BTC', endpoint: 'height' }, + }, + }, ] requests.forEach((req) => { diff --git a/composite/bitcoin-json-rpc/.eslintrc.js b/composite/bitcoin-json-rpc/.eslintrc.js new file mode 100644 index 0000000000..30c00e34a1 --- /dev/null +++ b/composite/bitcoin-json-rpc/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('../../.eslintrc.ts.js'), +} diff --git a/composite/bitcoin-json-rpc/README.md b/composite/bitcoin-json-rpc/README.md new file mode 100644 index 0000000000..73fa8cb4c5 --- /dev/null +++ b/composite/bitcoin-json-rpc/README.md @@ -0,0 +1,86 @@ +# Bitcoin JSON-RPC Blockchain info adapter for Chainlink + +- Takes optional connection to RPC endpoint (set via `RPC_URL` environment variable) + +## Input Params + +Returns blockchain info stats, by calling `"method": "getblockchainfo"`. It relies on `json-rpc` adapter. + +- `endpoint`: The parameter to query for. Default: "difficulty" + +## Output + +```json +{ + "jobRunID": "1", + "data": { + "chain": "main", + "blocks": 412022, + "headers": 665582, + "bestblockhash": "0000000000000000056482e60e14364c82903764eb88aef8fb0b1b60647334be", + "difficulty": 194254820283.444, + "mediantime": 1463406562, + "verificationprogress": 0.2162006436056612, + "initialblockdownload": true, + "chainwork": "0000000000000000000000000000000000000000001973393fcfc0215ecc9726", + "size_on_disk": 4758448869, + "pruned": true, + "pruneheight": 406538, + "automatic_pruning": true, + "prune_target_size": 5242880000, + "softforks": { + "bip34": { + "type": "buried", + "active": true, + "height": 227931 + }, + "bip66": { + "type": "buried", + "active": true, + "height": 363725 + }, + "bip65": { + "type": "buried", + "active": true, + "height": 388381 + }, + "csv": { + "type": "buried", + "active": false, + "height": 419328 + }, + "segwit": { + "type": "buried", + "active": false, + "height": 481824 + } + }, + "warnings": "", + "result": 665582 + }, + "result": 665582, + "statusCode": 200 +} +``` + +## Install + +Install dependencies + +```bash +yarn +``` + +Set the `RPC_URL` environment variable to your client URL. + +## Testing + +Testing is dependent on the type of node you're connecting to. You can set a local environment variable `RPC_URL` to point to an RPC connection. Otherwise, the adapter will default to `"http://localhost:8545"`. + +RPC Address and Port Defaults: + +- BTC: (bitcoind) http://localhost:8332 (btcd) http://localhost:8334 + +```bash +yarn test +``` diff --git a/composite/bitcoin-json-rpc/package.json b/composite/bitcoin-json-rpc/package.json new file mode 100644 index 0000000000..413e3ec141 --- /dev/null +++ b/composite/bitcoin-json-rpc/package.json @@ -0,0 +1,36 @@ +{ + "name": "@chainlink/bitcoin-json-rpc-adapter", + "version": "0.0.1", + "description": "", + "author": "Evangelos Barakos (evangelos@smartcontract.com)", + "license": "MIT", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "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 -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 --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/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": { + "@chainlink/json-rpc-adapter": "^0.0.1" + } +} diff --git a/composite/bitcoin-json-rpc/src/adapter.ts b/composite/bitcoin-json-rpc/src/adapter.ts new file mode 100644 index 0000000000..e3caf0fd35 --- /dev/null +++ b/composite/bitcoin-json-rpc/src/adapter.ts @@ -0,0 +1,38 @@ +import JSONRPC from '@chainlink/json-rpc-adapter' +import { Config, ExecuteWithConfig, ExecuteFactory, AdapterRequest } from '@chainlink/types' +import { Validator, Requester } from '@chainlink/external-adapter' +import { DEFAULT_ENDPOINT, makeConfig } from './config' + +const inputParams = { + endpoint: false, +} + +const convertEndpoint: { [key: string]: string } = { + height: 'headers', +} + +// Export function to integrate with Chainlink node +const execute: ExecuteWithConfig = async (request: AdapterRequest) => { + const validator = new Validator(request, inputParams) + if (validator.error) throw validator.error + + const jobRunID = validator.validated.id + let endpoint = validator.validated.data.endpoint || DEFAULT_ENDPOINT + + const response = await JSONRPC.execute({ + ...request, + data: { ...request.data, method: 'getblockchaininfo' }, + }) + + endpoint = convertEndpoint[endpoint] || endpoint + const result = Requester.validateResultNumber(response.data, ['result', endpoint]) + return Requester.success(jobRunID, { + data: { ...response.data.result, result }, + result, + status: 200, + }) +} + +export const makeExecute: ExecuteFactory = (config) => { + return async (request) => execute(request, config || makeConfig()) +} diff --git a/composite/bitcoin-json-rpc/src/config.ts b/composite/bitcoin-json-rpc/src/config.ts new file mode 100644 index 0000000000..48bea4203d --- /dev/null +++ b/composite/bitcoin-json-rpc/src/config.ts @@ -0,0 +1,7 @@ +import { Requester } from '@chainlink/external-adapter' +import { Config } from '@chainlink/types' + +export const DEFAULT_ENDPOINT = 'difficulty' + +// TODO: needs to setup config for underlying JSON-RPC adapter +export const makeConfig = (prefix?: string): Config => Requester.getDefaultConfig(prefix) diff --git a/composite/bitcoin-json-rpc/src/index.ts b/composite/bitcoin-json-rpc/src/index.ts new file mode 100644 index 0000000000..65e183daef --- /dev/null +++ b/composite/bitcoin-json-rpc/src/index.ts @@ -0,0 +1,6 @@ +import { makeConfig } from './config' +import { makeExecute } from './adapter' +import { expose, util } from '@chainlink/ea-bootstrap' + +const NAME = 'BITCOIN-JSON-RPC' +export = { NAME, makeExecute, makeConfig, ...expose(util.wrapExecute(makeExecute())) } diff --git a/composite/bitcoin-json-rpc/test/adapter.test.ts b/composite/bitcoin-json-rpc/test/adapter.test.ts new file mode 100644 index 0000000000..cadc0f02cd --- /dev/null +++ b/composite/bitcoin-json-rpc/test/adapter.test.ts @@ -0,0 +1,73 @@ +import { assertSuccess, assertError } from '@chainlink/adapter-test-helpers' +import { AdapterRequest } from '@chainlink/types' +import { Requester } from '@chainlink/external-adapter' +import { makeExecute } from '../src/adapter' + +/** + * Running these tests requires a connection to a Bitcoin client. + * Not all supported methods have a test case, just enough to display capability. + */ + +describe('execute', () => { + const jobID = '1' + const execute = makeExecute() + context('successful calls @integration', () => { + const requests = [ + { + name: 'get height with endpoint convention', + testData: { + id: jobID, + data: { endpoint: 'height' }, + }, + }, + { + name: 'get difficulty with blockchain convention and default endpoint', + testData: { + id: jobID, + data: { blockchain: 'BTC' }, + }, + }, + { + name: 'get height with common interface', + testData: { + id: jobID, + data: { blockchain: 'BTC', endpoint: 'height' }, + }, + }, + { + name: 'get difficulty with common interface', + testData: { + id: jobID, + data: { blockchain: 'BTC', endpoint: 'difficulty' }, + }, + }, + ] + + requests.forEach((req) => { + it(`${req.name}`, async () => { + const data = await execute(req.testData as AdapterRequest) + assertSuccess({ expected: 200, actual: data.statusCode }, data, jobID) + }) + }) + }) + + context('error calls @integration', () => { + const requests = [ + { + name: 'endpoint not existing', + testData: { id: jobID, data: { endpoint: 'no_op' } }, + }, + ] + + requests.forEach((req) => { + it(`${req.name}`, async () => { + try { + await execute(req.testData as AdapterRequest) + } catch (error) { + const errorResp = Requester.errored(jobID, error) + assertError({ expected: 500, actual: errorResp.statusCode }, errorResp, jobID) + } + }) + }) + }) +}) diff --git a/composite/bitcoin-json-rpc/tsconfig.json b/composite/bitcoin-json-rpc/tsconfig.json new file mode 100644 index 0000000000..2e4c1b9ec1 --- /dev/null +++ b/composite/bitcoin-json-rpc/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "typeRoots": [ + "../../node_modules/@types", + "../../typings", + "./typings" + ], + "resolveJsonModule": true + }, + "include": [ + "src/**/*", + "src/**/*.json" + ], + "exclude": [ + "dist", + "**/*.spec.ts", + "**/*.test.ts" + ] +} diff --git a/cryptoapis/README.md b/cryptoapis/README.md index ebbab3c006..8b100f2262 100644 --- a/cryptoapis/README.md +++ b/cryptoapis/README.md @@ -9,40 +9,95 @@ The adapter takes the following environment variables: ## Input Params -- `endpoint`: The endpoint to use. Default: "price" +- `endpoint`: The requested data point. One of (`price`|`difficulty`|`height`|`balance`). Defaults: `price`. -### Price endpoint +### Price -- `base`, `from`, or `coin`: The symbol or ID of the coin to query -- `quote`, `to`, or `market`: The symbol or ID of the market to convert to +- `base`, `from`, or `coin`: The symbol or ID of the coin to query (required). +- `quote`, `to`, or `market`: The symbol or ID of the market to convert to (required). -### Difficulty endpoint +### Output + +```json +{ + "jobRunID": "1", + "data": { + "payload": { + "weightedAveragePrice": 36670.69405468086, + "amount": 135.37338203000004, + "timestamp": 1610724372, + "datetime": "2021-01-15T15:26:12+0000", + "baseAsset": "BTC", + "quoteAsset": "USD" + }, + "result": 36670.69405468086 + }, + "result": 36670.69405468086, + "statusCode": 200 +} +``` + +### Difficulty -- `blockchain` or `coin`: The blockchain to get difficulty from -- `network`: The network of the blockchain to get difficulty from. Default: "mainnet" +- `blockchain` or `coin`: The blockchain name (required). +- `network`: The blockchain network name. Default: `mainnet` -## Output +### Output ```json { "jobRunID": "1", "data": { "payload": { - "weightedAveragePrice": 188.02563659478218, - "amount": 2848.4069787899994, - "timestamp": 1587650913, - "datetime": "2020-04-23T14:08:33+0000", - "baseAsset": "ETH", - "quoteAsset": "USD" + "difficulty": 20607418304385.63, + "headers": 666185, + "chain": "main", + "chainWork": "000000000000000000000000000000000000000018255ab714d1a15ffccd987e", + "mediantime": 1610721116, + "blocks": 666185, + "bestBlockHash": "0000000000000000000cc82b0a9a6e290cd13721a1abf88fdebb37fdc927308e", + "currency": "BTC", + "transactions": 606560353, + "verificationProgress": 0.9999930065052965 + }, + "result": 20607418304385.63 + }, + "result": 20607418304385.63, + "statusCode": 200 +} +``` + +### Height + +- `blockchain` or `coin`: The blockchain name (required). +- `network`: The blockchain network name. Default: `mainnet` + +### Output + +```json +{ + "jobRunID": "1", + "data": { + "payload": { + "difficulty": 20607418304385.63, + "headers": 666185, + "chain": "main", + "chainWork": "000000000000000000000000000000000000000018255ab714d1a15ffccd987e", + "mediantime": 1610721116, + "blocks": 666185, + "bestBlockHash": "0000000000000000000cc82b0a9a6e290cd13721a1abf88fdebb37fdc927308e", + "currency": "BTC", + "transactions": 606560353, + "verificationProgress": 0.9999935897991173 }, - "result": 188.02563659478218 + "result": 666185 }, - "result": 188.02563659478218, + "result": 666185, "statusCode": 200 } ``` -### Balance endpoint +### Balance https://docs.cryptoapis.io/rest-apis/blockchain-as-a-service-apis/btc/index#btc-address-info-endpoint diff --git a/cryptoapis/package.json b/cryptoapis/package.json index 34a1a459c8..a095b529b0 100644 --- a/cryptoapis/package.json +++ b/cryptoapis/package.json @@ -11,7 +11,7 @@ "lint:fix": "eslint --ignore-path ../.eslintignore . --ext .js,.jsx,.ts,.tsx --fix", "test": "yarn test:unit && yarn test:integration", "test:unit": "mocha --exit --grep @integration --invert -r ts-node/register 'test/**/*.test.ts'", - "test:integration": "mocha --timeout 6000 --exit --grep @integration -r ts-node/register 'test/**/*.test.ts'", + "test:integration": "mocha --timeout 0 --exit --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" diff --git a/cryptoapis/src/adapter.ts b/cryptoapis/src/adapter.ts index 531b89f19c..4136a726e5 100644 --- a/cryptoapis/src/adapter.ts +++ b/cryptoapis/src/adapter.ts @@ -1,7 +1,7 @@ import { Requester, Validator, AdapterError } from '@chainlink/external-adapter' import { ExecuteWithConfig, ExecuteFactory, Config } from '@chainlink/types' import { makeConfig, DEFAULT_ENDPOINT } from './config' -import { price, difficulty, balance } from './endpoint' +import { price, bc_info, balance } from './endpoint' const inputParams = { endpoint: false, @@ -23,8 +23,9 @@ export const execute: ExecuteWithConfig = async (request, config) => { response = await price.execute(config, request) break } - case difficulty.Name: { - response = await difficulty.execute(config, request) + case 'difficulty': + case 'height': { + response = await bc_info.execute(config, request) break } case balance.Name: { diff --git a/cryptoapis/src/endpoint/difficulty.ts b/cryptoapis/src/endpoint/bc_info.ts similarity index 63% rename from cryptoapis/src/endpoint/difficulty.ts rename to cryptoapis/src/endpoint/bc_info.ts index 5b4ab7f2a7..985de99d24 100644 --- a/cryptoapis/src/endpoint/difficulty.ts +++ b/cryptoapis/src/endpoint/bc_info.ts @@ -1,24 +1,32 @@ import { Requester, Validator } from '@chainlink/external-adapter' import { AdapterRequest, Config } from '@chainlink/types' +import { DEFAULT_ENDPOINT } from '../config' -export const Name = 'difficulty' +export const Name = 'bc_info' -const difficultyParams = { +const statsParams = { blockchain: ['blockchain', 'coin'], + endpoint: false, network: false, } +const convertEndpoint: { [key: string]: string } = { + height: 'headers', +} + export const execute = async (config: Config, request: AdapterRequest) => { - const validator = new Validator(request, difficultyParams) + const validator = new Validator(request, statsParams) if (validator.error) throw validator.error const blockchain = validator.validated.data.blockchain const network = validator.validated.data.network || 'mainnet' + let endpoint = validator.validated.data.endpoint || DEFAULT_ENDPOINT + endpoint = convertEndpoint[endpoint] || endpoint const url = `/v1/bc/${blockchain.toLowerCase()}/${network.toLowerCase()}/info` const reqConfig = { ...config.api, url } const response = await Requester.request(reqConfig) - response.data.result = Requester.validateResultNumber(response.data, ['payload', 'difficulty']) + response.data.result = Requester.validateResultNumber(response.data, ['payload', endpoint]) return response } diff --git a/cryptoapis/src/endpoint/index.ts b/cryptoapis/src/endpoint/index.ts index 8b978122a6..ac0d59e88b 100644 --- a/cryptoapis/src/endpoint/index.ts +++ b/cryptoapis/src/endpoint/index.ts @@ -1,6 +1,6 @@ export * as balance from './balance' export * as price from './price' -export * as difficulty from './difficulty' +export * as bc_info from './bc_info' export const COIN_KEYS = ['btc', 'eth', 'etc', 'bch', 'ltc', 'dash', 'doge', 'btcv', 'zil'] as const export type CoinType = typeof COIN_KEYS[number] diff --git a/cryptoapis/test/difficulty.test.ts b/cryptoapis/test/stats.test.ts similarity index 80% rename from cryptoapis/test/difficulty.test.ts rename to cryptoapis/test/stats.test.ts index fb0359f1b9..f7eb10b9dc 100644 --- a/cryptoapis/test/difficulty.test.ts +++ b/cryptoapis/test/stats.test.ts @@ -4,7 +4,7 @@ import { assertSuccess, assertError } from '@chainlink/adapter-test-helpers' import { AdapterRequest } from '@chainlink/types' import { makeExecute } from '../src/adapter' -describe('difficulty endpoint', () => { +describe('bc_info endpoint', () => { const jobID = '1' const execute = makeExecute() @@ -21,7 +21,21 @@ describe('difficulty endpoint', () => { name: 'BTC testnet difficulty', testData: { id: jobID, - data: { blockchain: 'BTC', network: 'Testnet', endpoint: 'difficulty' }, + data: { blockchain: 'BTC', network: 'testnet', endpoint: 'difficulty' }, + }, + }, + { + name: 'BTC height', + testData: { + id: jobID, + data: { blockchain: 'BTC', endpoint: 'height' }, + }, + }, + { + name: 'BTC testnet height', + testData: { + id: jobID, + data: { blockchain: 'BTC', network: 'testnet', endpoint: 'height' }, }, }, ] diff --git a/cryptoid/.eslintrc.js b/cryptoid/.eslintrc.js new file mode 100644 index 0000000000..11f16f9a15 --- /dev/null +++ b/cryptoid/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('../.eslintrc.ts.js'), +} diff --git a/cryptoid/README.md b/cryptoid/README.md index e785f87aeb..018ea45639 100644 --- a/cryptoid/README.md +++ b/cryptoid/README.md @@ -2,17 +2,18 @@ ## Input Params -- `blockchain` or `coin`: The blockchain to get difficulty from +- `blockchain` or `coin`: The blockchain name (required). +- `endpoint`: The requested data point. One of (`difficulty`|`height`). Defaults: `difficulty`. ## Output ```json { - "jobRunID":"1", - "data":{ - "result":19298087186262.6 - }, - "result":19298087186262.6, - "statusCode":200 + "jobRunID": "1", + "data": { + "result": 19298087186262.6 + }, + "result": 19298087186262.6, + "statusCode": 200 } ``` diff --git a/cryptoid/adapter.js b/cryptoid/adapter.js deleted file mode 100644 index 26f50f070c..0000000000 --- a/cryptoid/adapter.js +++ /dev/null @@ -1,27 +0,0 @@ -const { Requester, Validator } = require('@chainlink/external-adapter') -const { util } = require('@chainlink/ea-bootstrap') - -const customParams = { - blockchain: ['blockchain', 'coin'], -} - -const execute = (input, callback) => { - const validator = new Validator(input, customParams) - if (validator.error) return callback(validator.error.statusCode, validator.errored) - - const jobRunID = validator.validated.id - const blockchain = validator.validated.data.blockchain.toLowerCase() - const url = `https://${blockchain}.cryptoid.info/${blockchain}/api.dws` - const key = util.getRandomRequiredEnv('API_KEY') - const q = 'getdifficulty' - - const params = { key, q } - const config = { url, params } - - Requester.request(config) - .then((response) => ({ ...response, data: { result: response.data } })) - .then((response) => callback(response.status, Requester.success(jobRunID, response))) - .catch((error) => callback(500, Requester.errored(jobRunID, error))) -} - -module.exports.execute = execute diff --git a/cryptoid/index.js b/cryptoid/index.js deleted file mode 100644 index d5dfd92b9f..0000000000 --- a/cryptoid/index.js +++ /dev/null @@ -1,4 +0,0 @@ -const { expose } = require('@chainlink/ea-bootstrap') -const { execute } = require('./adapter') - -module.exports = expose(execute) diff --git a/cryptoid/package.json b/cryptoid/package.json index 37aa646801..8011ea4a05 100644 --- a/cryptoid/package.json +++ b/cryptoid/package.json @@ -1,15 +1,46 @@ { "name": "@chainlink/cryptoid-adapter", "version": "0.0.1", + "description": "Chainlink cryptoid adapter.", + "keywords": [ + "Chainlink", + "LINK", + "blockchain", + "oracle", + "cryptoid" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "repository": { + "url": "https://github.com/smartcontractkit/external-adapters-js", + "type": "git" + }, "license": "MIT", - "main": "index.js", "scripts": { - "server": "node -e 'require(\"./index.js\").server()'", + "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": "yarn _mocha --timeout 0", - "test:unit": "yarn _mocha --grep @integration --invert --timeout 0", - "test:integration": "yarn _mocha --grep @integration --timeout 0" + "test": "mocha --exit --timeout 0 -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 0 --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": {} } diff --git a/cryptoid/src/adapter.ts b/cryptoid/src/adapter.ts new file mode 100644 index 0000000000..0df15ba266 --- /dev/null +++ b/cryptoid/src/adapter.ts @@ -0,0 +1,46 @@ +import { Requester, Validator } from '@chainlink/external-adapter' +import { Config, ExecuteWithConfig, ExecuteFactory } from '@chainlink/types' +import { makeConfig, DEFAULT_ENDPOINT } from './config' + +const customParams = { + blockchain: ['blockchain', 'coin'], + endpoint: false, +} + +const endpointToApiFunctionName: { [key: string]: string } = { + difficulty: 'getdifficulty', + height: 'getblockcount', +} + +export const execute: ExecuteWithConfig = async (request, config) => { + const validator = new Validator(request, customParams) + if (validator.error) throw validator.error + + Requester.logConfig(config) + + const jobRunID = validator.validated.id + const endpoint = validator.validated.data.endpoint || DEFAULT_ENDPOINT + const blockchain = validator.validated.data.blockchain.toLowerCase() + + const key = config.apiKey + const apiFunctionName = endpointToApiFunctionName[endpoint] + const params = { key, q: apiFunctionName } + + const reqConfig = { + ...config.api, + params, + baseURL: config.api.baseURL || `https://${blockchain}.cryptoid.info/${blockchain}/api.dws`, + } + const response = await Requester.request(reqConfig) + const result = response.data + + return Requester.success(jobRunID, { + data: { result }, + result, + status: 200, + }) +} + +export const makeExecute: ExecuteFactory = (config) => { + return async (request) => execute(request, config || makeConfig()) +} diff --git a/cryptoid/src/config.ts b/cryptoid/src/config.ts new file mode 100644 index 0000000000..981d982259 --- /dev/null +++ b/cryptoid/src/config.ts @@ -0,0 +1,6 @@ +import { Requester } from '@chainlink/external-adapter' +import { Config } from '@chainlink/types' + +export const DEFAULT_ENDPOINT = 'difficulty' + +export const makeConfig = (prefix?: string): Config => Requester.getDefaultConfig(prefix) diff --git a/cryptoid/src/index.ts b/cryptoid/src/index.ts new file mode 100644 index 0000000000..51bc23e101 --- /dev/null +++ b/cryptoid/src/index.ts @@ -0,0 +1,7 @@ +import { expose, util } from '@chainlink/ea-bootstrap' +import { makeExecute } from './adapter' +import { makeConfig } from './config' + +const NAME = 'CRYPTO-ID' + +export = { NAME, makeExecute, makeConfig, ...expose(util.wrapExecute(makeExecute())) } diff --git a/blockchair/test/difficulty.test.ts b/cryptoid/test/adapter.test.ts similarity index 79% rename from blockchair/test/difficulty.test.ts rename to cryptoid/test/adapter.test.ts index 46c014294c..9414a1ab57 100644 --- a/blockchair/test/difficulty.test.ts +++ b/cryptoid/test/adapter.test.ts @@ -1,10 +1,10 @@ import { assert } from 'chai' -import { Requester, AdapterError } from '@chainlink/external-adapter' +import { Requester } from '@chainlink/external-adapter' import { assertSuccess, assertError } from '@chainlink/adapter-test-helpers' import { AdapterRequest } from '@chainlink/types' import { makeExecute } from '../src/adapter' -describe('difficulty endpoint', () => { +describe('execute', () => { const jobID = '1' const execute = makeExecute() @@ -22,6 +22,20 @@ describe('difficulty endpoint', () => { name: 'coin', testData: { id: jobID, data: { coin: 'BTC' } }, }, + { + name: 'BTC difficulty', + testData: { + id: jobID, + data: { blockchain: 'BTC' }, + }, + }, + { + name: 'BTC height', + testData: { + id: jobID, + data: { blockchain: 'BTC', endpoint: 'height' }, + }, + }, ] requests.forEach((req) => { @@ -45,7 +59,7 @@ describe('difficulty endpoint', () => { try { await execute(req.testData as AdapterRequest) } catch (error) { - const errorResp = Requester.errored(jobID, new AdapterError(error)) + const errorResp = Requester.errored(jobID, error) assertError({ expected: 400, actual: errorResp.statusCode }, errorResp, jobID) } }) @@ -65,7 +79,7 @@ describe('difficulty endpoint', () => { try { await execute(req.testData as AdapterRequest) } catch (error) { - const errorResp = Requester.errored(jobID, new AdapterError(error)) + const errorResp = Requester.errored(jobID, error) assertError({ expected: 500, actual: errorResp.statusCode }, errorResp, jobID) } }) diff --git a/cryptoid/test/adapter_test.js b/cryptoid/test/adapter_test.js deleted file mode 100644 index 8521770914..0000000000 --- a/cryptoid/test/adapter_test.js +++ /dev/null @@ -1,69 +0,0 @@ -const { assert } = require('chai') -const { assertSuccess, assertError } = require('@chainlink/adapter-test-helpers') -const { execute } = require('../adapter') - -describe('execute', () => { - const jobID = '1' - - context('successful calls @integration', () => { - const requests = [ - { - name: 'id not supplied', - testData: { data: { blockchain: 'BTC' } }, - }, - { - name: 'blockchain', - testData: { id: jobID, data: { blockchain: 'BTC' } }, - }, - { - name: 'coin', - testData: { id: jobID, data: { coin: 'BTC' } }, - }, - ] - - requests.forEach((req) => { - it(`${req.name}`, (done) => { - execute(req.testData, (statusCode, data) => { - assertSuccess({ expected: 200, actual: statusCode }, data, jobID) - assert.isAbove(data.result, 0) - assert.isAbove(data.data.result, 0) - done() - }) - }) - }) - }) - - context('validation error', () => { - const requests = [ - { name: 'empty body', testData: {} }, - { name: 'empty data', testData: { data: {} } }, - ] - - requests.forEach((req) => { - it(`${req.name}`, (done) => { - execute(req.testData, (statusCode, data) => { - assertError({ expected: 400, actual: statusCode }, data, jobID) - done() - }) - }) - }) - }) - - context('error calls @integration', () => { - const requests = [ - { - name: 'unknown blockchain', - testData: { id: jobID, data: { blockchain: 'not_real' } }, - }, - ] - - requests.forEach((req) => { - it(`${req.name}`, (done) => { - execute(req.testData, (statusCode, data) => { - assertError({ expected: 500, actual: statusCode }, data, jobID) - done() - }) - }) - }) - }) -}) diff --git a/cryptoid/tsconfig.json b/cryptoid/tsconfig.json new file mode 100644 index 0000000000..3b4ccf41fa --- /dev/null +++ b/cryptoid/tsconfig.json @@ -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"] +} diff --git a/metalsapi/test/adapter_test.js b/metalsapi/test/adapter_test.js index 0751ac7893..6c40840fc7 100644 --- a/metalsapi/test/adapter_test.js +++ b/metalsapi/test/adapter_test.js @@ -19,6 +19,24 @@ describe('execute', () => { name: 'from/to', testData: { id: jobID, data: { from: 'CHF', to: 'USD' } }, }, + { + name: 'coin', + testData: { id: jobID, data: { coin: 'BTC' } }, + }, + { + name: 'BTC difficulty', + testData: { + id: jobID, + data: { blockchain: 'BTC' }, + }, + }, + { + name: 'BTC height', + testData: { + id: jobID, + data: { blockchain: 'BTC', endpoint: 'height' }, + }, + }, ] requests.forEach((req) => {