-
Couldn't load subscription status.
- Fork 324
CM-1099 LiveArt External Adapter #4018
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
base: main
Are you sure you want to change the base?
Changes from all commits
5c5c573
a64af31
ba0ef5c
e4bb691
372f03f
05d634b
2cc1535
4ad49a9
7e6998c
a775786
3406afd
41a1c4c
ff5e8bf
e2c52cb
2fae932
3233279
45dd724
d1d79e1
a9a3faf
731f1d7
90b6795
26d82b9
b38beb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@chainlink/liveart-external-adapter': major | ||
| --- | ||
|
|
||
| LiveArt EA initial release |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| { | ||
| "name": "@chainlink/liveart-adapter", | ||
| "version": "1.0.0", | ||
| "description": "LiveArt NAV external adapter for Chainlink", | ||
| "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": { | ||
| "test": "echo \"Error: no test specified\" && exit 1", | ||
| "clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo", | ||
| "prepack": "yarn build", | ||
| "build": "tsc -b", | ||
| "server": "node -e 'require(\"./src/index.ts\").server()'", | ||
| "server:dist": "node -e 'require(\"./dist/index.js\").server()'", | ||
| "start": "yarn server:dist" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/jest": "^29.5.14", | ||
| "@types/node": "22.14.1", | ||
| "nock": "13.5.6", | ||
| "typescript": "5.8.3" | ||
| }, | ||
| "dependencies": { | ||
| "@chainlink/external-adapter-framework": "2.8.0", | ||
| "tslib": "2.4.1" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { AdapterConfig } from '@chainlink/external-adapter-framework/config' | ||
|
|
||
| export const config: AdapterConfig = new AdapterConfig({ | ||
| API_BASE_URL: { | ||
| description: 'The API URL for the LiveArt data provider', | ||
| type: 'string', | ||
| required: true, | ||
| }, | ||
| }) |
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consider calling this endpoint There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but its not a nav? ticket says to return all the data from the endpoint? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter' | ||
| import { InputParameters } from '@chainlink/external-adapter-framework/validation' | ||
|
|
||
| import { httpTransport } from '../transport/nav' | ||
|
|
||
| export const inputParameters = new InputParameters( | ||
| { | ||
| asset_id: { | ||
| required: true, | ||
| type: 'string', | ||
| description: 'The ID of the artwork asset to fetch', | ||
| }, | ||
| }, | ||
| [ | ||
| { | ||
| asset_id: 'KUSPUM', | ||
| }, | ||
| ], | ||
| ) | ||
|
|
||
| export const nav = new AdapterEndpoint({ | ||
| name: 'nav', | ||
| transport: httpTransport, | ||
| inputParameters, | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { expose, ServerInstance } from '@chainlink/external-adapter-framework' | ||
| import { Adapter } from '@chainlink/external-adapter-framework/adapter' | ||
|
|
||
| import { config } from './config/config' | ||
| import { nav } from './endpoint/nav' | ||
|
|
||
| export const adapter = new Adapter({ | ||
| defaultEndpoint: nav.name, | ||
| name: 'LIVE_ART', | ||
| config, | ||
| endpoints: [nav], | ||
| }) | ||
|
|
||
| export const server = (): Promise<ServerInstance | undefined> => expose(adapter) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { HttpTransport } from '@chainlink/external-adapter-framework/transports/http' | ||
| import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util' | ||
| import { TypeFromDefinition } from '@chainlink/external-adapter-framework/validation/input-params' | ||
| import { config } from '../config/config' | ||
| import { inputParameters } from '../endpoint/nav' | ||
|
|
||
| export interface ResponseSchema { | ||
| asset_id: string | ||
| asset_info_category: string | ||
| asset_info_creator: string | ||
| asset_info_title: string | ||
| asset_info_year_created: string | ||
| asset_info_description: string | ||
| asset_info_url: string | ||
| current_estimated_nav_usd: string | ||
| current_estimated_nav_updated_at: string | ||
| token_total_shares: number | ||
| token_current_estimated_nav_per_share_usd: string | ||
| offering_price_usd: string | ||
| success: boolean | ||
| message: string | ||
| response_timestamp: string | ||
| } | ||
|
|
||
| export type BaseEndpointTypes = { | ||
| Parameters: typeof inputParameters.definition | ||
| Response: SingleNumberResultResponse | ||
| Settings: typeof config.settings | ||
| } | ||
|
|
||
| export type HttpTransportTypes = BaseEndpointTypes & { | ||
| Provider: { | ||
| RequestBody: never | ||
| ResponseBody: ResponseSchema | ||
| } | ||
| } | ||
|
|
||
| export const httpTransport = new HttpTransport<HttpTransportTypes>({ | ||
| prepareRequests: (params, adapterSettings) => { | ||
| return params.map((param) => { | ||
| return { | ||
| params: [param], | ||
| request: { | ||
| baseURL: adapterSettings.API_BASE_URL, | ||
| url: `/asset/${param.asset_id}`, | ||
| }, | ||
| } | ||
| }) | ||
| }, | ||
| parseResponse: (params, response) => { | ||
| return params.map((param: TypeFromDefinition<typeof inputParameters.definition>) => { | ||
markhoangcll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (param.asset_id !== response.data.asset_id) { | ||
| return { | ||
| params: param, | ||
| response: { | ||
| errorMessage: `Mismatched asset_id in response. Expected ${param.asset_id}, got ${response.data.asset_id}`, | ||
| statusCode: 502, | ||
| }, | ||
| } | ||
| } | ||
| const responseData = response.data | ||
|
|
||
| if (!responseData.success) | ||
| return { | ||
| params: param, | ||
| response: { | ||
| errorMessage: responseData.message, | ||
| statusCode: 502, | ||
| }, | ||
| } | ||
|
|
||
| const navString = responseData.token_current_estimated_nav_per_share_usd | ||
| const nav = parseFloat(navString) | ||
|
|
||
| return { | ||
| params: param, | ||
| response: { | ||
| result: nav, | ||
| data: { | ||
| result: nav, | ||
| }, | ||
| }, | ||
| } | ||
| }) | ||
| }, | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
|
||
| exports[`LiveArt NAV endpoints nav should handle upstream bad response for asset_id 1`] = ` | ||
| { | ||
| "errorMessage": "Asset ID 'AssetId.ROLSUB' not found", | ||
| "statusCode": 502, | ||
| "timestamps": { | ||
| "providerDataReceivedUnixMs": 978347471111, | ||
| "providerDataRequestedUnixMs": 978347471111, | ||
| }, | ||
| } | ||
| `; | ||
|
|
||
| exports[`LiveArt NAV endpoints nav should return success for valid asset_id 1`] = ` | ||
| { | ||
| "data": { | ||
| "result": 0.09630117, | ||
| }, | ||
| "result": 0.09630117, | ||
| "statusCode": 200, | ||
| "timestamps": { | ||
| "providerDataReceivedUnixMs": 978347471111, | ||
| "providerDataRequestedUnixMs": 978347471111, | ||
| }, | ||
| } | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import { | ||
| TestAdapter, | ||
| setEnvVariables, | ||
| } from '@chainlink/external-adapter-framework/util/testing-utils' | ||
| import * as nock from 'nock' | ||
|
|
||
| import { nav } from '../../src/endpoint/nav' | ||
| import { mockHappyPathResponseSuccessAsset, mockResponseFailureAsset } from './utils/fixtures' | ||
| import { TEST_FAILURE_ASSET_ID, TEST_SUCCESS_ASSET_ID, TEST_URL } from './utils/testConfig' | ||
| import { clearTestCache } from './utils/utilFunctions' | ||
|
|
||
| describe('LiveArt NAV', () => { | ||
| let testAdapter: TestAdapter | ||
| let spy: jest.SpyInstance | ||
| let oldEnv: NodeJS.ProcessEnv | ||
|
|
||
| beforeAll(async () => { | ||
| oldEnv = JSON.parse(JSON.stringify(process.env)) | ||
| // Mock time for request's timestamp | ||
| const mockDate = new Date('2001-01-01T11:11:11.111Z') | ||
| spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) | ||
| // Set environment variables | ||
| process.env.API_BASE_URL = TEST_URL | ||
|
|
||
| // Create adapter instance only once | ||
| const adapter = (await import('../../src')).adapter | ||
| adapter.rateLimiting = undefined | ||
| testAdapter = await TestAdapter.startWithMockedCache(adapter, { | ||
| testAdapter: {} as TestAdapter<never>, | ||
| }) | ||
| }) | ||
|
|
||
| afterEach(async () => { | ||
| setEnvVariables(oldEnv) | ||
| nock.cleanAll() | ||
| clearTestCache(testAdapter) | ||
| }) | ||
|
|
||
| afterAll(async () => { | ||
| spy.mockRestore() | ||
| await testAdapter.api.close() | ||
| }) | ||
|
|
||
| describe('endpoints', () => { | ||
| describe('nav', () => { | ||
| it('should return success for valid asset_id', async () => { | ||
| const dataInput = { | ||
| asset_id: TEST_SUCCESS_ASSET_ID, | ||
| endpoint: nav.name, | ||
| } | ||
|
|
||
| mockHappyPathResponseSuccessAsset(dataInput.asset_id) | ||
| const response = await testAdapter.request(dataInput) | ||
|
|
||
| expect(response.statusCode).toBe(200) | ||
| expect(response.json()).toMatchSnapshot() | ||
| }) | ||
|
|
||
| it('should handle upstream bad response for asset_id', async () => { | ||
| const data = { | ||
| asset_id: TEST_FAILURE_ASSET_ID, | ||
| endpoint: nav.name, | ||
| } | ||
|
|
||
| mockResponseFailureAsset(data.asset_id) | ||
|
|
||
| const response = await testAdapter.request(data) | ||
| const responseJson = response.json() | ||
| expect(responseJson.statusCode).toBe(502) | ||
| expect(responseJson).toMatchSnapshot() | ||
| }) | ||
| }) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import nock from 'nock' | ||
markhoangcll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| import { ErrorResponseAsset, SuccessResponseAsset, TEST_URL } from './testConfig' | ||
|
|
||
| export const mockHappyPathResponseSuccessAsset = (assetId: string): nock.Scope => | ||
| nock(TEST_URL) | ||
| .get(`/asset/${assetId}`) | ||
| .reply(200, () => SuccessResponseAsset, [ | ||
| 'Content-Type', | ||
| 'application/json', | ||
| 'Connection', | ||
| 'close', | ||
| 'Vary', | ||
| 'Accept-Encoding', | ||
| 'Vary', | ||
| 'Origin', | ||
| ]) | ||
|
|
||
| export const mockResponseFailureAsset = (assetId: string): nock.Scope => | ||
| nock(TEST_URL) | ||
| .get(`/asset/${assetId}`) | ||
| .reply( | ||
| 200, | ||
| () => ({ | ||
| ...ErrorResponseAsset, | ||
| asset_id: `${assetId}`, | ||
| }), | ||
| [ | ||
| 'Content-Type', | ||
| 'application/json', | ||
| 'Connection', | ||
| 'close', | ||
| 'Vary', | ||
| 'Accept-Encoding', | ||
| 'Vary', | ||
| 'Origin', | ||
| ], | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.