Skip to content

Commit 22cfe72

Browse files
authored
Merge branch 'main' into main
2 parents 4f2911e + 5fb38df commit 22cfe72

File tree

18 files changed

+555
-0
lines changed

18 files changed

+555
-0
lines changed

.changeset/thirty-ducks-wait.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/liveart-adapter': major
3+
---
4+
5+
LiveArt EA initial release

.pnp.cjs

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "@chainlink/liveart-adapter",
3+
"version": "0.0.0",
4+
"description": "Chainlink liveart adapter.",
5+
"keywords": [
6+
"Chainlink",
7+
"LINK",
8+
"blockchain",
9+
"oracle",
10+
"liveart"
11+
],
12+
"main": "dist/index.js",
13+
"types": "dist/index.d.ts",
14+
"files": [
15+
"dist"
16+
],
17+
"repository": {
18+
"url": "https://github.com/smartcontractkit/external-adapters-js",
19+
"type": "git"
20+
},
21+
"license": "MIT",
22+
"scripts": {
23+
"clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo",
24+
"prepack": "yarn build",
25+
"build": "tsc -b",
26+
"server": "node -e 'require(\"./index.js\").server()'",
27+
"server:dist": "node -e 'require(\"./dist/index.js\").server()'",
28+
"start": "yarn server:dist"
29+
},
30+
"devDependencies": {
31+
"@types/jest": "^29.5.14",
32+
"@types/node": "22.14.1",
33+
"nock": "13.5.6",
34+
"typescript": "5.8.3"
35+
},
36+
"dependencies": {
37+
"@chainlink/external-adapter-framework": "2.11.0",
38+
"tslib": "2.4.1"
39+
}
40+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { AdapterConfig } from '@chainlink/external-adapter-framework/config'
2+
3+
export const config: AdapterConfig = new AdapterConfig({
4+
API_BASE_URL: {
5+
description: 'The API URL for the LiveArt data provider',
6+
type: 'string',
7+
required: true,
8+
default: 'https://artwork-price-oracle-api-ms.liveart.ai',
9+
},
10+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { endpoint as nav } from './nav'
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
2+
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
3+
4+
import { httpTransport } from '../transport/nav'
5+
6+
export const inputParameters = new InputParameters(
7+
{
8+
assetId: {
9+
required: true,
10+
type: 'string',
11+
description: 'The ID of the artwork asset to fetch',
12+
},
13+
},
14+
[
15+
{
16+
assetId: 'KUSPUM',
17+
},
18+
],
19+
)
20+
21+
export const endpoint = new AdapterEndpoint({
22+
name: 'nav',
23+
transport: httpTransport,
24+
inputParameters,
25+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
2+
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
3+
4+
import { config } from './config'
5+
import { nav } from './endpoint'
6+
7+
export const adapter = new Adapter({
8+
defaultEndpoint: nav.name,
9+
name: 'LIVE_ART',
10+
config,
11+
endpoints: [nav],
12+
rateLimiting: {
13+
tiers: {
14+
default: {
15+
rateLimit1s: 1,
16+
note: 'Setting reasonable default limits',
17+
},
18+
},
19+
},
20+
})
21+
22+
export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { HttpTransport } from '@chainlink/external-adapter-framework/transports/http'
2+
import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util'
3+
import { config } from '../config'
4+
import { inputParameters } from '../endpoint/nav'
5+
6+
export interface ResponseSchema {
7+
asset_id: string
8+
asset_info_category: string
9+
asset_info_creator: string
10+
asset_info_title: string
11+
asset_info_year_created: string
12+
asset_info_description: string
13+
asset_info_url: string
14+
current_estimated_nav_usd: string
15+
current_estimated_nav_updated_at: string
16+
token_total_shares: number
17+
token_current_estimated_nav_per_share_usd: string
18+
offering_price_usd: string
19+
success: boolean
20+
message: string
21+
response_timestamp: string
22+
}
23+
24+
export type BaseEndpointTypes = {
25+
Parameters: typeof inputParameters.definition
26+
Response: SingleNumberResultResponse
27+
Settings: typeof config.settings
28+
}
29+
30+
export type HttpTransportTypes = BaseEndpointTypes & {
31+
Provider: {
32+
RequestBody: never
33+
ResponseBody: ResponseSchema
34+
}
35+
}
36+
37+
export const httpTransport = new HttpTransport<HttpTransportTypes>({
38+
prepareRequests: (params, adapterSettings) => {
39+
return params.map((param) => {
40+
return {
41+
params: [param],
42+
request: {
43+
baseURL: adapterSettings.API_BASE_URL,
44+
url: `/asset/${param.assetId}`,
45+
},
46+
}
47+
})
48+
},
49+
parseResponse: (params, response) => {
50+
return params.map((param) => {
51+
if (param.assetId !== response.data.asset_id) {
52+
return {
53+
params: param,
54+
response: {
55+
errorMessage: `Mismatched asset_id in response. Expected ${param.assetId}, got ${response.data.asset_id}`,
56+
statusCode: 502,
57+
},
58+
}
59+
}
60+
const responseData = response.data
61+
62+
if (!responseData.success)
63+
return {
64+
params: param,
65+
response: {
66+
errorMessage: responseData.message,
67+
statusCode: 502,
68+
},
69+
}
70+
71+
const navString = responseData.token_current_estimated_nav_per_share_usd
72+
const result = parseFloat(navString)
73+
74+
return {
75+
params: param,
76+
response: {
77+
result,
78+
data: {
79+
result,
80+
},
81+
},
82+
}
83+
})
84+
},
85+
})
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`LiveArt NAV endpoints nav framework should handle API 422 bad response 1`] = `
4+
{
5+
"errorMessage": "Provider request failed with status 422: {"detail":[{"type":"enum","loc":["path","asset_id"],"msg":"Input should be 'KUSPUM', 'ROLSUB', 'KOODOG', 'BANGIR', 'MURFLO', 'HOCPOOL' or 'HARPLA'","input":"abcd","ctx":{"expected":"'KUSPUM', 'ROLSUB', 'KOODOG', 'BANGIR', 'MURFLO', 'HOCPOOL' or 'HARPLA'"}}]}",
6+
"statusCode": 502,
7+
"timestamps": {
8+
"providerDataReceivedUnixMs": 978347471111,
9+
"providerDataRequestedUnixMs": 978347471111,
10+
},
11+
}
12+
`;
13+
14+
exports[`LiveArt NAV endpoints nav should handle upstream bad response for unsuccessful request 1`] = `
15+
{
16+
"errorMessage": "Asset ID 'AssetId.ROLSUB' not found",
17+
"statusCode": 502,
18+
"timestamps": {
19+
"providerDataReceivedUnixMs": 978347471111,
20+
"providerDataRequestedUnixMs": 978347471111,
21+
},
22+
}
23+
`;
24+
25+
exports[`LiveArt NAV endpoints nav should return error for invalid assetId 1`] = `
26+
{
27+
"errorMessage": "Mismatched asset_id in response. Expected invalid-asset-id, got KUSPUM",
28+
"statusCode": 502,
29+
"timestamps": {
30+
"providerDataReceivedUnixMs": 978347471111,
31+
"providerDataRequestedUnixMs": 978347471111,
32+
},
33+
}
34+
`;
35+
36+
exports[`LiveArt NAV endpoints nav should return success for valid assetId 1`] = `
37+
{
38+
"data": {
39+
"result": 0.09630117,
40+
},
41+
"result": 0.09630117,
42+
"statusCode": 200,
43+
"timestamps": {
44+
"providerDataReceivedUnixMs": 978347471111,
45+
"providerDataRequestedUnixMs": 978347471111,
46+
},
47+
}
48+
`;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import {
2+
TestAdapter,
3+
setEnvVariables,
4+
} from '@chainlink/external-adapter-framework/util/testing-utils'
5+
import * as nock from 'nock'
6+
7+
import {
8+
mockHappyPathResponseSuccessAsset,
9+
mockResponseApiFailureAsset,
10+
mockResponseFailureAsset,
11+
} from './utils/fixtures'
12+
import { TEST_FAILURE_ASSET_ID, TEST_SUCCESS_ASSET_ID, TEST_URL } from './utils/testConfig'
13+
import { clearTestCache } from './utils/utilFunctions'
14+
15+
describe('LiveArt NAV', () => {
16+
let testAdapter: TestAdapter
17+
let spy: jest.SpyInstance
18+
let oldEnv: NodeJS.ProcessEnv
19+
20+
beforeAll(async () => {
21+
oldEnv = JSON.parse(JSON.stringify(process.env))
22+
// Mock time for request's timestamp
23+
const mockDate = new Date('2001-01-01T11:11:11.111Z')
24+
spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime())
25+
// Set environment variables
26+
process.env.API_BASE_URL = TEST_URL
27+
process.env.BACKGROUND_EXECUTE_MS = '0'
28+
29+
// Create adapter instance only once
30+
const adapter = (await import('../../src')).adapter
31+
adapter.rateLimiting = undefined
32+
testAdapter = await TestAdapter.startWithMockedCache(adapter, {
33+
testAdapter: {} as TestAdapter<never>,
34+
})
35+
})
36+
37+
afterAll(async () => {
38+
clearTestCache(testAdapter)
39+
await testAdapter.api.close()
40+
spy.mockRestore()
41+
setEnvVariables(oldEnv)
42+
nock.restore()
43+
nock.cleanAll()
44+
})
45+
46+
describe('endpoints', () => {
47+
describe('nav', () => {
48+
it('should return success for valid assetId', async () => {
49+
const dataInput = {
50+
assetId: TEST_SUCCESS_ASSET_ID,
51+
endpoint: 'nav',
52+
}
53+
54+
mockHappyPathResponseSuccessAsset(dataInput.assetId)
55+
const response = await testAdapter.request(dataInput)
56+
57+
expect(response.statusCode).toBe(200)
58+
expect(response.json()).toMatchSnapshot()
59+
})
60+
61+
it('should return error for invalid assetId', async () => {
62+
const data = {
63+
assetId: 'invalid-asset-id',
64+
endpoint: 'nav',
65+
}
66+
67+
// Mock for other assetId
68+
mockHappyPathResponseSuccessAsset(data.assetId)
69+
70+
const response = await testAdapter.request(data)
71+
expect(response.statusCode).toBe(502)
72+
expect(response.json()).toMatchSnapshot()
73+
})
74+
75+
it('should handle upstream bad response for unsuccessful request', async () => {
76+
const data = {
77+
assetId: TEST_FAILURE_ASSET_ID,
78+
endpoint: 'nav',
79+
}
80+
81+
mockResponseFailureAsset(data.assetId)
82+
83+
const response = await testAdapter.request(data)
84+
expect(response.statusCode).toBe(502)
85+
expect(response.json()).toMatchSnapshot()
86+
})
87+
88+
it('framework should handle API 422 bad response', async () => {
89+
const data = {
90+
assetId: 'abcd',
91+
endpoint: 'nav',
92+
}
93+
94+
// prep cache
95+
await testAdapter.request(data)
96+
97+
mockResponseApiFailureAsset()
98+
99+
const response = await testAdapter.request(data)
100+
expect(response.statusCode).toBe(502)
101+
expect(response.json()).toMatchSnapshot()
102+
})
103+
})
104+
})
105+
})

0 commit comments

Comments
 (0)