Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5c5c573
feature/CM-1099 Add prototype basic nav for liveArt
markhoangcll Aug 28, 2025
a64af31
feature/CM-1099 Update package json
markhoangcll Aug 28, 2025
ba0ef5c
feature/CM-1099 Fix the tests and add extra validations on the payloa…
markhoangcll Sep 4, 2025
e4bb691
CM-1099 Make changes according to some suggestions made in PR review
markhoangcll Sep 23, 2025
372f03f
CM-1099 Add more changes after PR review
markhoangcll Sep 29, 2025
05d634b
CM-1099 Add missing changes to address the PR comments
markhoangcll Sep 30, 2025
2cc1535
CM-1099 remove unnecessary config type, add error message in api resp…
markhoangcll Sep 30, 2025
4ad49a9
CM-1099 Fix param type for prepare and parse transport functions
markhoangcll Sep 30, 2025
7e6998c
Merge branch 'main' of github.com:smartcontractkit/external-adapters-…
markhoangcll Sep 30, 2025
a775786
CM-1120 add missing fields in the package root tsconfigs, restore for…
markhoangcll Oct 1, 2025
3406afd
CM-1099 restore ftse formatted files
markhoangcll Oct 1, 2025
41a1c4c
Merge branch 'main' into feature/CM-1099_liveart_ea_nav
markhoangcll Oct 2, 2025
ff5e8bf
Update .changeset/thirty-ducks-wait.md
mmcallister-cll Oct 3, 2025
e2c52cb
Update packages/sources/liveart/src/index.ts
mmcallister-cll Oct 3, 2025
2fae932
CM-1099 Change endpoint and transport definition to match the Jira sp…
markhoangcll Oct 21, 2025
3233279
Merge branch 'feature/CM-1099_liveart_ea_nav' of github.com:smartcont…
markhoangcll Oct 21, 2025
45dd724
Merge branch 'main' of github.com:smartcontractkit/external-adapters-…
markhoangcll Oct 21, 2025
d1d79e1
CM-1099 Restore accidental changes
markhoangcll Oct 21, 2025
a9a3faf
CM-1099 Restore accidental changes for this covid-tracker
markhoangcll Oct 21, 2025
731f1d7
CM-1099 Remove unneeded imports
markhoangcll Oct 21, 2025
90b6795
CM-1099 Add tests WIP
markhoangcll Oct 22, 2025
26d82b9
CM-1099 Update adapter to return nav_per_share value
markhoangcll Oct 29, 2025
b38beb2
CM-1099 Update package.json
markhoangcll Oct 29, 2025
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: 5 additions & 0 deletions .changeset/thirty-ducks-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/liveart-external-adapter': major
---

LiveArt EA initial release
20 changes: 20 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions packages/sources/liveart/package.json
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"
}
}
9 changes: 9 additions & 0 deletions packages/sources/liveart/src/config/config.ts
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,
},
})
25 changes: 25 additions & 0 deletions packages/sources/liveart/src/endpoint/nav.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider calling this endpoint nav (update both the filename and the name field in the AdapterEndpoint) to be in line with other nav EAs.

Copy link
Author

Choose a reason for hiding this comment

The 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,
})
14 changes: 14 additions & 0 deletions packages/sources/liveart/src/index.ts
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)
86 changes: 86 additions & 0 deletions packages/sources/liveart/src/transport/nav.ts
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>) => {
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,
},
}
`;
74 changes: 74 additions & 0 deletions packages/sources/liveart/test/integration/adapter.test.ts
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()
})
})
})
})
37 changes: 37 additions & 0 deletions packages/sources/liveart/test/integration/utils/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import nock from 'nock'
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',
],
)
Loading
Loading