diff --git a/.changeset/thirty-ducks-wait.md b/.changeset/thirty-ducks-wait.md new file mode 100644 index 0000000000..33fe4f935d --- /dev/null +++ b/.changeset/thirty-ducks-wait.md @@ -0,0 +1,5 @@ +--- +'@chainlink/liveart-external-adapter': major +--- + +LiveArt EA initial release diff --git a/.pnp.cjs b/.pnp.cjs index 1a349fa0e2..7f78fd4447 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -666,6 +666,10 @@ const RAW_RUNTIME_STATE = "name": "@chainlink/lition-adapter",\ "reference": "workspace:packages/sources/lition"\ },\ + {\ + "name": "@chainlink/liveart-external-adapter",\ + "reference": "workspace:packages/sources/liveart"\ + },\ {\ "name": "@chainlink/lotus-adapter",\ "reference": "workspace:packages/sources/lotus"\ @@ -1133,6 +1137,7 @@ const RAW_RUNTIME_STATE = ["@chainlink/linear-finance-adapter", ["workspace:packages/composites/linear-finance"]],\ ["@chainlink/linkpool-adapter", ["workspace:packages/sources/linkpool"]],\ ["@chainlink/lition-adapter", ["workspace:packages/sources/lition"]],\ + ["@chainlink/liveart-external-adapter", ["workspace:packages/sources/liveart"]],\ ["@chainlink/llama-guard-adapter", ["workspace:packages/composites/llama-guard"]],\ ["@chainlink/lotus-adapter", ["workspace:packages/sources/lotus"]],\ ["@chainlink/m0-adapter", ["workspace:packages/sources/m0"]],\ @@ -7859,6 +7864,22 @@ const RAW_RUNTIME_STATE = "linkType": "SOFT"\ }]\ ]],\ + ["@chainlink/liveart-external-adapter", [\ + ["workspace:packages/sources/liveart", {\ + "packageLocation": "./packages/sources/liveart/",\ + "packageDependencies": [\ + ["@chainlink/liveart-external-adapter", "workspace:packages/sources/liveart"],\ + ["@chainlink/external-adapter-framework", "npm:2.7.0"],\ + ["@types/jest", "npm:29.5.14"],\ + ["@types/node", "npm:22.14.1"],\ + ["axios", "npm:1.11.0"],\ + ["nock", "npm:13.5.6"],\ + ["tslib", "npm:2.4.1"],\ + ["typescript", "patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5"]\ + ],\ + "linkType": "SOFT"\ + }]\ + ]],\ ["@chainlink/llama-guard-adapter", [\ ["workspace:packages/composites/llama-guard", {\ "packageLocation": "./packages/composites/llama-guard/",\ @@ -19410,6 +19431,16 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ + ["npm:1.11.0", {\ + "packageLocation": "./.yarn/cache/axios-npm-1.11.0-64966324ac-232df4af7a.zip/node_modules/axios/",\ + "packageDependencies": [\ + ["axios", "npm:1.11.0"],\ + ["follow-redirects", "virtual:0c877f1ea627088aa7a1bdfef2b23b74690ff0132006b80a74eda5cd1c7ddf9090e5571b1d9e31fbfe0c1a2d6e4bb2fbebb5c63ff72b896b6cbc80384f357768#npm:1.15.9"],\ + ["form-data", "npm:4.0.4"],\ + ["proxy-from-env", "npm:1.1.0"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:1.9.0", {\ "packageLocation": "./.yarn/cache/axios-npm-1.9.0-007b36cf56-a2f90bba56.zip/node_modules/axios/",\ "packageDependencies": [\ @@ -20744,6 +20775,17 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["call-bind-apply-helpers", [\ + ["npm:1.0.2", {\ + "packageLocation": "./.yarn/cache/call-bind-apply-helpers-npm-1.0.2-3eedbea3bb-00482c1f6a.zip/node_modules/call-bind-apply-helpers/",\ + "packageDependencies": [\ + ["call-bind-apply-helpers", "npm:1.0.2"],\ + ["es-errors", "npm:1.3.0"],\ + ["function-bind", "npm:1.1.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["call-me-maybe", [\ ["npm:1.0.2", {\ "packageLocation": "./.yarn/cache/call-me-maybe-npm-1.0.2-a465269a37-3d375b6f81.zip/node_modules/call-me-maybe/",\ @@ -22949,6 +22991,18 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["dunder-proto", [\ + ["npm:1.0.1", {\ + "packageLocation": "./.yarn/cache/dunder-proto-npm-1.0.1-90eb6829db-5add88a3d6.zip/node_modules/dunder-proto/",\ + "packageDependencies": [\ + ["dunder-proto", "npm:1.0.1"],\ + ["call-bind-apply-helpers", "npm:1.0.2"],\ + ["es-errors", "npm:1.3.0"],\ + ["gopd", "npm:1.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["duplexer3", [\ ["npm:0.1.5", {\ "packageLocation": "./.yarn/cache/duplexer3-npm-0.1.5-343d4ab7e3-e677cb4c48.zip/node_modules/duplexer3/",\ @@ -23346,6 +23400,13 @@ const RAW_RUNTIME_STATE = ["get-intrinsic", "npm:1.2.4"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.0.1", {\ + "packageLocation": "./.yarn/cache/es-define-property-npm-1.0.1-3fc6324f1c-f8dc9e660d.zip/node_modules/es-define-property/",\ + "packageDependencies": [\ + ["es-define-property", "npm:1.0.1"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["es-errors", [\ @@ -23366,6 +23427,29 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["es-object-atoms", [\ + ["npm:1.1.1", {\ + "packageLocation": "./.yarn/cache/es-object-atoms-npm-1.1.1-362d8043c2-54fe77de28.zip/node_modules/es-object-atoms/",\ + "packageDependencies": [\ + ["es-object-atoms", "npm:1.1.1"],\ + ["es-errors", "npm:1.3.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["es-set-tostringtag", [\ + ["npm:2.1.0", {\ + "packageLocation": "./.yarn/cache/es-set-tostringtag-npm-2.1.0-4e55705d3f-86814bf8af.zip/node_modules/es-set-tostringtag/",\ + "packageDependencies": [\ + ["es-set-tostringtag", "npm:2.1.0"],\ + ["es-errors", "npm:1.3.0"],\ + ["get-intrinsic", "npm:1.3.0"],\ + ["has-tostringtag", "npm:1.0.2"],\ + ["hasown", "npm:2.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["es5-ext", [\ ["npm:0.10.64", {\ "packageLocation": "./.yarn/unplugged/es5-ext-npm-0.10.64-c30cdc3d60/node_modules/es5-ext/",\ @@ -25039,6 +25123,18 @@ const RAW_RUNTIME_STATE = ["mime-types", "npm:2.1.35"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:4.0.4", {\ + "packageLocation": "./.yarn/cache/form-data-npm-4.0.4-10eb4ef9c3-a4b62e2193.zip/node_modules/form-data/",\ + "packageDependencies": [\ + ["form-data", "npm:4.0.4"],\ + ["asynckit", "npm:0.4.0"],\ + ["combined-stream", "npm:1.0.8"],\ + ["es-set-tostringtag", "npm:2.1.0"],\ + ["hasown", "npm:2.0.2"],\ + ["mime-types", "npm:2.1.35"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["formdata-polyfill", [\ @@ -25413,6 +25509,23 @@ const RAW_RUNTIME_STATE = ["hasown", "npm:2.0.2"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.3.0", {\ + "packageLocation": "./.yarn/cache/get-intrinsic-npm-1.3.0-35558f27b6-6e9dd920ff.zip/node_modules/get-intrinsic/",\ + "packageDependencies": [\ + ["get-intrinsic", "npm:1.3.0"],\ + ["call-bind-apply-helpers", "npm:1.0.2"],\ + ["es-define-property", "npm:1.0.1"],\ + ["es-errors", "npm:1.3.0"],\ + ["es-object-atoms", "npm:1.1.1"],\ + ["function-bind", "npm:1.1.2"],\ + ["get-proto", "npm:1.0.1"],\ + ["gopd", "npm:1.2.0"],\ + ["has-symbols", "npm:1.1.0"],\ + ["hasown", "npm:2.0.2"],\ + ["math-intrinsics", "npm:1.1.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["get-iterator", [\ @@ -25451,6 +25564,17 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["get-proto", [\ + ["npm:1.0.1", {\ + "packageLocation": "./.yarn/cache/get-proto-npm-1.0.1-4d30bac614-4fc96afdb5.zip/node_modules/get-proto/",\ + "packageDependencies": [\ + ["get-proto", "npm:1.0.1"],\ + ["dunder-proto", "npm:1.0.1"],\ + ["es-object-atoms", "npm:1.1.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["get-starknet-core", [\ ["npm:4.0.0", {\ "packageLocation": "./.yarn/cache/get-starknet-core-npm-4.0.0-e9ea97d844-e1ae3a3b86.zip/node_modules/get-starknet-core/",\ @@ -25773,6 +25897,13 @@ const RAW_RUNTIME_STATE = ["get-intrinsic", "npm:1.2.4"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.2.0", {\ + "packageLocation": "./.yarn/cache/gopd-npm-1.2.0-df89ffa78e-94e296d69f.zip/node_modules/gopd/",\ + "packageDependencies": [\ + ["gopd", "npm:1.2.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["got", [\ @@ -26133,6 +26264,13 @@ const RAW_RUNTIME_STATE = ["has-symbols", "npm:1.0.3"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.1.0", {\ + "packageLocation": "./.yarn/cache/has-symbols-npm-1.1.0-9aa7dc2ac1-959385c986.zip/node_modules/has-symbols/",\ + "packageDependencies": [\ + ["has-symbols", "npm:1.1.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["has-to-string-tag-x", [\ @@ -31575,6 +31713,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["math-intrinsics", [\ + ["npm:1.1.0", {\ + "packageLocation": "./.yarn/cache/math-intrinsics-npm-1.1.0-9204d80e7d-11df2eda46.zip/node_modules/math-intrinsics/",\ + "packageDependencies": [\ + ["math-intrinsics", "npm:1.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["md5.js", [\ ["npm:1.3.5", {\ "packageLocation": "./.yarn/cache/md5.js-npm-1.3.5-130901125a-098494d885.zip/node_modules/md5.js/",\ diff --git a/.yarn/cache/axios-npm-1.11.0-64966324ac-232df4af7a.zip b/.yarn/cache/axios-npm-1.11.0-64966324ac-232df4af7a.zip new file mode 100644 index 0000000000..eb0e14008e Binary files /dev/null and b/.yarn/cache/axios-npm-1.11.0-64966324ac-232df4af7a.zip differ diff --git a/.yarn/cache/call-bind-apply-helpers-npm-1.0.2-3eedbea3bb-00482c1f6a.zip b/.yarn/cache/call-bind-apply-helpers-npm-1.0.2-3eedbea3bb-00482c1f6a.zip new file mode 100644 index 0000000000..b79445617d Binary files /dev/null and b/.yarn/cache/call-bind-apply-helpers-npm-1.0.2-3eedbea3bb-00482c1f6a.zip differ diff --git a/.yarn/cache/dunder-proto-npm-1.0.1-90eb6829db-5add88a3d6.zip b/.yarn/cache/dunder-proto-npm-1.0.1-90eb6829db-5add88a3d6.zip new file mode 100644 index 0000000000..887bf61758 Binary files /dev/null and b/.yarn/cache/dunder-proto-npm-1.0.1-90eb6829db-5add88a3d6.zip differ diff --git a/.yarn/cache/es-define-property-npm-1.0.1-3fc6324f1c-f8dc9e660d.zip b/.yarn/cache/es-define-property-npm-1.0.1-3fc6324f1c-f8dc9e660d.zip new file mode 100644 index 0000000000..ada4508647 Binary files /dev/null and b/.yarn/cache/es-define-property-npm-1.0.1-3fc6324f1c-f8dc9e660d.zip differ diff --git a/.yarn/cache/es-object-atoms-npm-1.1.1-362d8043c2-54fe77de28.zip b/.yarn/cache/es-object-atoms-npm-1.1.1-362d8043c2-54fe77de28.zip new file mode 100644 index 0000000000..9a128723d0 Binary files /dev/null and b/.yarn/cache/es-object-atoms-npm-1.1.1-362d8043c2-54fe77de28.zip differ diff --git a/.yarn/cache/es-set-tostringtag-npm-2.1.0-4e55705d3f-86814bf8af.zip b/.yarn/cache/es-set-tostringtag-npm-2.1.0-4e55705d3f-86814bf8af.zip new file mode 100644 index 0000000000..a22f6f363c Binary files /dev/null and b/.yarn/cache/es-set-tostringtag-npm-2.1.0-4e55705d3f-86814bf8af.zip differ diff --git a/.yarn/cache/form-data-npm-4.0.4-10eb4ef9c3-a4b62e2193.zip b/.yarn/cache/form-data-npm-4.0.4-10eb4ef9c3-a4b62e2193.zip new file mode 100644 index 0000000000..35ffc59e00 Binary files /dev/null and b/.yarn/cache/form-data-npm-4.0.4-10eb4ef9c3-a4b62e2193.zip differ diff --git a/.yarn/cache/get-intrinsic-npm-1.3.0-35558f27b6-6e9dd920ff.zip b/.yarn/cache/get-intrinsic-npm-1.3.0-35558f27b6-6e9dd920ff.zip new file mode 100644 index 0000000000..6f31af4793 Binary files /dev/null and b/.yarn/cache/get-intrinsic-npm-1.3.0-35558f27b6-6e9dd920ff.zip differ diff --git a/.yarn/cache/get-proto-npm-1.0.1-4d30bac614-4fc96afdb5.zip b/.yarn/cache/get-proto-npm-1.0.1-4d30bac614-4fc96afdb5.zip new file mode 100644 index 0000000000..65fbfe076a Binary files /dev/null and b/.yarn/cache/get-proto-npm-1.0.1-4d30bac614-4fc96afdb5.zip differ diff --git a/.yarn/cache/gopd-npm-1.2.0-df89ffa78e-94e296d69f.zip b/.yarn/cache/gopd-npm-1.2.0-df89ffa78e-94e296d69f.zip new file mode 100644 index 0000000000..91834ee743 Binary files /dev/null and b/.yarn/cache/gopd-npm-1.2.0-df89ffa78e-94e296d69f.zip differ diff --git a/.yarn/cache/has-symbols-npm-1.1.0-9aa7dc2ac1-959385c986.zip b/.yarn/cache/has-symbols-npm-1.1.0-9aa7dc2ac1-959385c986.zip new file mode 100644 index 0000000000..529f580165 Binary files /dev/null and b/.yarn/cache/has-symbols-npm-1.1.0-9aa7dc2ac1-959385c986.zip differ diff --git a/.yarn/cache/math-intrinsics-npm-1.1.0-9204d80e7d-11df2eda46.zip b/.yarn/cache/math-intrinsics-npm-1.1.0-9204d80e7d-11df2eda46.zip new file mode 100644 index 0000000000..741535d5de Binary files /dev/null and b/.yarn/cache/math-intrinsics-npm-1.1.0-9204d80e7d-11df2eda46.zip differ diff --git a/packages/sources/liveart/package.json b/packages/sources/liveart/package.json new file mode 100644 index 0000000000..f045388cfe --- /dev/null +++ b/packages/sources/liveart/package.json @@ -0,0 +1,35 @@ +{ + "name": "@chainlink/liveart-external-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.7.0", + "axios": "^1.11.0", + "tslib": "2.4.1" + } +} diff --git a/packages/sources/liveart/src/config/config.ts b/packages/sources/liveart/src/config/config.ts new file mode 100644 index 0000000000..954b71f946 --- /dev/null +++ b/packages/sources/liveart/src/config/config.ts @@ -0,0 +1,16 @@ +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, + default: 'https://artwork-price-oracle-api-dev-ms.liveart.ai', + }, + BEARER_TOKEN: { + description: 'The Bearer token for authentication', + type: 'string', + required: true, + sensitive: true, + }, +}) diff --git a/packages/sources/liveart/src/endpoint/nav.ts b/packages/sources/liveart/src/endpoint/nav.ts new file mode 100644 index 0000000000..2356b0aaf7 --- /dev/null +++ b/packages/sources/liveart/src/endpoint/nav.ts @@ -0,0 +1,26 @@ +import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { InputParameters } from '@chainlink/external-adapter-framework/validation' + +import { httpTransport } from '../transport/transport' + +export const inputParameters = new InputParameters( + { + artwork_id: { + aliases: ['artworkId'], + required: true, + type: 'string', + description: 'The ID of the artwork to fetch', + }, + }, + [ + { + artwork_id: 'banksy', + }, + ], +) + +export const nav = new AdapterEndpoint({ + name: 'nav', + transport: httpTransport, + inputParameters, +}) diff --git a/packages/sources/liveart/src/index.ts b/packages/sources/liveart/src/index.ts new file mode 100644 index 0000000000..4ed53fe40c --- /dev/null +++ b/packages/sources/liveart/src/index.ts @@ -0,0 +1,21 @@ +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: 'LIVEART', + config, + endpoints: [nav], + rateLimiting: { + tiers: { + default: { + rateLimit1m: 60, + }, + }, + }, +}) + +export const server = (): Promise => expose(adapter) diff --git a/packages/sources/liveart/src/transport/transport.ts b/packages/sources/liveart/src/transport/transport.ts new file mode 100644 index 0000000000..270f0409c4 --- /dev/null +++ b/packages/sources/liveart/src/transport/transport.ts @@ -0,0 +1,160 @@ +import { + AdapterSettings, + SettingsDefinitionMap, +} from '@chainlink/external-adapter-framework/config' +import { + HttpTransport, + ProviderRequestConfig, +} from '@chainlink/external-adapter-framework/transports' +import { + ProviderResult, + SingleNumberResultResponse, +} from '@chainlink/external-adapter-framework/util' +import { AdapterError } from '@chainlink/external-adapter-framework/validation/error' +import { TypeFromDefinition } from '@chainlink/external-adapter-framework/validation/input-params' +import { AxiosResponse } from 'axios' +import { config } from '../config/config' +import { inputParameters } from '../endpoint/nav' + +export interface ResponseSchema { + artwork_id: string + current_estimated_price_updated_at: string | null + current_estimated_price: string | null + total_shares: number | null + nav_per_share: string | null + valuation_price_date: string | null + valuation_price: string | null + valuation_method: string | null + success: boolean + message: string | null + 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 + } +} + +/** + * Validates and parse the nav_per_share value from the response + * @param navString Per-share NAV value as a string + * @returns Parsed NAV value as a number + */ +export function parseNavPerShare(navString: string | null): number { + if (navString == null) { + throw new AdapterError({ statusCode: 400, message: 'nav_per_share is null' }) + } + + const nav = Number(navString) + if (isNaN(nav) || !isFinite(nav) || nav < 0) { + throw new AdapterError({ + statusCode: 400, + message: `Invalid nav_per_share value: ${navString}`, + }) + } + + return nav +} + +/** + * Prepare the requests to be sent to the data provider + * @param params list of parameters sent to the adapter + * @param adapterSettings adapter configuration settings containing API base URL and bearer token + */ +export const prepareRequests = ( + params: TypeFromDefinition[], + adapterSettings: AdapterSettings, +): ProviderRequestConfig[] => { + return params.map((param) => { + return { + params: [param], + request: { + baseURL: adapterSettings.API_BASE_URL, + url: `/artwork/${param.artwork_id}/price`, + headers: { + Authorization: `Bearer ${adapterSettings.BEARER_TOKEN}`, + }, + }, + } + }) +} + +/** + * Parse the response from the data provider + * @param params list of parameters sent to the adapter + * @param response the response from the data provider + * @returns an array of provider results + */ +export function parseResponse( + params: TypeFromDefinition[], + response: AxiosResponse, +): ProviderResult[] { + // Check that request went through + if (!response.data) { + return params.map((param) => { + return { + params: param, + response: { + errorMessage: `The data provider failed to respond for artwork_id=${param.artwork_id}`, + statusCode: 502, + }, + } + }) + } + if (response.status !== 200 || !response.data.success) { + return params.map((param) => { + return { + params: param, + response: { + errorMessage: `The data provider failed to return a value for artwork_id=${param.artwork_id}, errorMessage: ${response.data?.message}`, + statusCode: 502, + }, + } + }) + } + + return params.map((param: TypeFromDefinition) => { + try { + if (param.artwork_id !== response.data.artwork_id) { + throw new AdapterError({ + statusCode: 500, + message: `Mismatched artwork_id in response. Expected ${param.artwork_id}, got ${response.data.artwork_id}`, + }) + } + + const nav = parseNavPerShare(response.data.nav_per_share) + return { + params: param, + response: { + result: nav, + data: { + result: nav, + }, + }, + } + } catch (error: Error | unknown) { + return { + params: param, + response: { + errorMessage: `Failed to parse response for artwork_id=${param.artwork_id}. Error: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + statusCode: 502, + }, + } + } + }) +} + +export const httpTransport = new HttpTransport({ + prepareRequests, + parseResponse, +}) diff --git a/packages/sources/liveart/test-payload.json b/packages/sources/liveart/test-payload.json new file mode 100644 index 0000000000..062ce02b02 --- /dev/null +++ b/packages/sources/liveart/test-payload.json @@ -0,0 +1,5 @@ +{ + "requests": [{ + "artwork_id": "banksy" + }] +} \ No newline at end of file diff --git a/packages/sources/liveart/test/integration/__snapshots__/adapter.test.ts.snap b/packages/sources/liveart/test/integration/__snapshots__/adapter.test.ts.snap new file mode 100644 index 0000000000..bfb1344b01 --- /dev/null +++ b/packages/sources/liveart/test/integration/__snapshots__/adapter.test.ts.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LiveArt NAV endpoint should handle upstream bad response 1`] = ` +{ + "errorMessage": "The data provider failed to return a value for artwork_id=dicaprio, errorMessage: Asset ID 'dicaprio' not found", + "statusCode": 502, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; + +exports[`LiveArt NAV endpoint should return success for both valid artwork_id and artworkId 1`] = ` +{ + "data": { + "result": 1000000, + }, + "result": 1000000, + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; + +exports[`LiveArt NAV endpoint should return success for valid artworkId 1`] = ` +{ + "data": { + "result": 1000000, + }, + "result": 1000000, + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; diff --git a/packages/sources/liveart/test/integration/adapter.test.ts b/packages/sources/liveart/test/integration/adapter.test.ts new file mode 100644 index 0000000000..076255c95b --- /dev/null +++ b/packages/sources/liveart/test/integration/adapter.test.ts @@ -0,0 +1,93 @@ +import { + TestAdapter, + setEnvVariables, +} from '@chainlink/external-adapter-framework/util/testing-utils' +import * as nock from 'nock' + +import { TEST_BEARER_TOKEN, TEST_URL } from '../utils/testConfig' +import { mockHappyPathResponseSuccess, mockResponseFailure } from './utils/fixtures' +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.BEARER_TOKEN = TEST_BEARER_TOKEN + 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, + }) + }) + + afterEach(async () => { + setEnvVariables(oldEnv) + nock.cleanAll() + clearTestCache(testAdapter) + }) + + afterAll(async () => { + spy.mockRestore() + await testAdapter.api.close() + }) + + describe('endpoint', () => { + it('should return success for both valid artwork_id', async () => { + const dataInput = { + artwork_id: 'banksy', + endpoint: 'nav', + } + + const expectedValueString = '1000000' + mockHappyPathResponseSuccess(dataInput.artwork_id, expectedValueString) + const response1 = await testAdapter.request(dataInput) + + expect(response1.statusCode).toBe(200) + expect(response1.json()).toMatchSnapshot() + }) + + it('should return success for valid artworkId', async () => { + const dataInput = { + artworkId: 'banksy', + endpoint: 'nav', + } + + const expectedValueString = '1000000' + mockHappyPathResponseSuccess(dataInput.artworkId, expectedValueString) + const response2 = await testAdapter.request(dataInput) + + expect(response2.statusCode).toBe(200) + expect(response2.json()).toMatchSnapshot() + }) + + it('should handle upstream bad response', async () => { + const data = { + artwork_id: 'dicaprio', + endpoint: 'nav', + } + + mockResponseFailure(data.artwork_id) + + const response = await testAdapter.request(data) + const responseJson = response.json() + + expect(responseJson.data).toBeUndefined() + expect(responseJson.result).toBeUndefined() + expect(response.statusCode).toBe(502) + expect(responseJson.errorMessage).toBe( + `The data provider failed to return a value for artwork_id=${data.artwork_id}, errorMessage: Asset ID '${data.artwork_id}' not found`, + ) + expect(response.json()).toMatchSnapshot() + }) + }) +}) diff --git a/packages/sources/liveart/test/integration/utils/fixtures.ts b/packages/sources/liveart/test/integration/utils/fixtures.ts new file mode 100644 index 0000000000..e31a345c0e --- /dev/null +++ b/packages/sources/liveart/test/integration/utils/fixtures.ts @@ -0,0 +1,50 @@ +import nock from 'nock' +import { ErrorResponse, SuccessResponse, TEST_BEARER_TOKEN, TEST_URL } from '../../utils/testConfig' + +export const mockHappyPathResponseSuccess = (artworkId: string, navPerShare: string): nock.Scope => + nock(TEST_URL) + .get(`/artwork/${artworkId}/price`) + .matchHeader('Authorization', `Bearer ${TEST_BEARER_TOKEN}`) + .reply( + 200, + () => ({ + ...SuccessResponse, + artwork_id: `${artworkId}`, + nav_per_share: navPerShare, + }), + [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ], + ) + .persist() + +export const mockResponseFailure = (artworkId: string): nock.Scope => + nock(TEST_URL) + .get(`/artwork/${artworkId}/price`) + .matchHeader('Authorization', `Bearer ${TEST_BEARER_TOKEN}`) + .reply( + 200, + () => ({ + ...ErrorResponse, + artwork_id: `${artworkId}`, + message: `Asset ID '${artworkId}' not found`, + }), + [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ], + ) + .persist() diff --git a/packages/sources/liveart/test/integration/utils/utilFunctions.ts b/packages/sources/liveart/test/integration/utils/utilFunctions.ts new file mode 100644 index 0000000000..42f66f0c3a --- /dev/null +++ b/packages/sources/liveart/test/integration/utils/utilFunctions.ts @@ -0,0 +1,12 @@ +import { TestAdapter } from '@chainlink/external-adapter-framework/util/testing-utils' + +export function clearTestCache(testAdapter: TestAdapter) { + // clear EA cache + const keys = testAdapter.mockCache?.cache.keys() + if (!keys) { + throw new Error('unexpected failure 1') + } + for (const key of keys) { + testAdapter.mockCache?.delete(key) + } +} diff --git a/packages/sources/liveart/test/unit/transport.test.ts b/packages/sources/liveart/test/unit/transport.test.ts new file mode 100644 index 0000000000..e728b87275 --- /dev/null +++ b/packages/sources/liveart/test/unit/transport.test.ts @@ -0,0 +1,172 @@ +import { AxiosResponse } from 'axios' +import { + parseNavPerShare, + parseResponse, + prepareRequests, + ResponseSchema, +} from '../../src/transport/transport' +import { ErrorResponse, SuccessResponse, TEST_BEARER_TOKEN, TEST_URL } from '../utils/testConfig' + +describe('Transport functions', () => { + // Mock adapter settings that match the expected type + const mockAdapterSettings = { + API_BASE_URL: TEST_URL, + BEARER_TOKEN: TEST_BEARER_TOKEN, + } as any + + describe('parseNavPerShare', () => { + it('should parse valid nav_per_share values', () => { + expect(parseNavPerShare('100')).toBe(100) + expect(parseNavPerShare('0')).toBe(0) + expect(parseNavPerShare('3.14')).toBe(3.14) + }) + + it('should throw an error for invalid nav_per_share values', () => { + expect(() => parseNavPerShare(null)).toThrow('nav_per_share is null') + expect(() => parseNavPerShare('invalid')).toThrow('Invalid nav_per_share value: invalid') + expect(() => parseNavPerShare('NaN')).toThrow('Invalid nav_per_share value: NaN') + }) + }) + + describe('prepareRequests', () => { + it('should create a single request for single artwork_id', () => { + const data = { artwork_id: 'TEST' } + const singleParam = [data] + const result = prepareRequests(singleParam, mockAdapterSettings) + + expect(Array.isArray(result)).toBe(true) + expect(result).toHaveLength(1) + + const request = result[0] + expect(request.params).toEqual(singleParam) + expect(request.request.baseURL).toBe(TEST_URL) + expect(request.request.url).toBe(`/artwork/${data.artwork_id}/price`) + expect(request.request.headers?.Authorization).toBe(`Bearer ${TEST_BEARER_TOKEN}`) + }) + + it('should prepare multiple requests correctly', () => { + const params = [{ artwork_id: 'banksy' }, { artwork_id: 'picasso' }, { artwork_id: 'monet' }] + + const result = prepareRequests(params, mockAdapterSettings) + + expect(Array.isArray(result)).toBe(true) + expect(result).toHaveLength(3) + result.forEach((request, index) => { + expect(request.params).toEqual([params[index]]) + expect(request.request.baseURL).toBe(TEST_URL) + expect(request.request.url).toBe(`/artwork/${params[index].artwork_id}/price`) + expect(request.request.headers?.Authorization).toBe(`Bearer ${TEST_BEARER_TOKEN}`) + }) + }) + + it('should handle empty params array', () => { + const params: any[] = [] + + const result = prepareRequests(params, mockAdapterSettings) + + expect(Array.isArray(result)).toBe(true) + expect(result).toHaveLength(0) + }) + }) + + describe('parseResponse', () => { + it('should parse successful response correctly', () => { + const data = { artwork_id: 'banksy' } + const params = [data] + const mockResponseData: ResponseSchema = { + ...SuccessResponse, + artwork_id: 'banksy', + } + + const mockSuccessResponse = { + data: mockResponseData, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + } as AxiosResponse + const result = parseResponse(params, mockSuccessResponse) + const expectedNAV = Number(mockResponseData.nav_per_share) + + expect(result).toHaveLength(1) + expect(result[0].params).toEqual(data) + expect(result[0].response.result).toBe(expectedNAV) + expect(result[0].response.data?.result).toBe(expectedNAV) + expect('errorMessage' in result[0].response).toBe(false) + }) + + it('should handle response with success=false', () => { + const data = { artwork_id: 'TEST_ERROR' } + const params = [data] + const mockResponseData: ResponseSchema = { + ...ErrorResponse, + artwork_id: data.artwork_id, + message: `Asset ID '${data.artwork_id}' not found`, + } + + const mockErrorResponse = { + data: mockResponseData, + status: 404, + statusText: 'Not Found', + headers: {}, + config: {}, + } as AxiosResponse + const result = parseResponse(params, mockErrorResponse) + + expect(result).toHaveLength(1) + expect(result[0].params).toEqual(data) + expect(result[0].response.errorMessage).toBe( + `The data provider failed to return a value for artwork_id=TEST_ERROR, errorMessage: ${mockResponseData.message}`, + ) + }) + + it('should handle response with no data', () => { + const params = [{ artwork_id: 'no-data' }] + const mockResponse = { + data: null as unknown, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + } as AxiosResponse + + const result = parseResponse(params, mockResponse) + + expect(result).toHaveLength(1) + expect(result[0].params).toEqual({ artwork_id: 'no-data' }) + expect('errorMessage' in result[0].response).toBe(true) + if ('errorMessage' in result[0].response) { + expect(result[0].response.errorMessage).toBe( + `The data provider failed to respond for artwork_id=${params[0].artwork_id}`, + ) + } + }) + + it('should handle invalid nav_per_share value', () => { + const data = { artwork_id: 'invalid-nav' } + const params = [data] + const mockResponseData: ResponseSchema = { + ...SuccessResponse, + artwork_id: data.artwork_id, + nav_per_share: 'invalid-number', + } + + const mockResponse = { + data: mockResponseData, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + } as AxiosResponse + const result = parseResponse(params, mockResponse) + + expect(result).toHaveLength(1) + expect(result[0].params).toEqual(data) + expect(result[0].response.result).toBeUndefined() + expect(result[0].response.data).toBeUndefined() + expect(result[0].response.errorMessage).toBe( + `Failed to parse response for artwork_id=${data.artwork_id}. Error: Invalid nav_per_share value: ${mockResponseData.nav_per_share}`, + ) + }) + }) +}) diff --git a/packages/sources/liveart/test/utils/testConfig.ts b/packages/sources/liveart/test/utils/testConfig.ts new file mode 100644 index 0000000000..9e84f7425b --- /dev/null +++ b/packages/sources/liveart/test/utils/testConfig.ts @@ -0,0 +1,30 @@ +export const TEST_URL = 'https://test.liveart.ai' +export const TEST_BEARER_TOKEN = 'TEST_TOKEN' + +export const SuccessResponse = { + artwork_id: '', + current_estimated_price_updated_at: '2025-08-28T14:27:11.345Z', + current_estimated_price: '1000000', + total_shares: 1000, + nav_per_share: '0.5', + valuation_price_date: '2025-08-28', + valuation_price: '10', + valuation_method: 'consignment', + success: true, + message: null, + response_timestamp: '2025-08-28T14:27:11.345Z', +} + +export const ErrorResponse = { + artwork_id: '', + current_estimated_price_updated_at: null, + current_estimated_price: null, + total_shares: null, + nav_per_share: null, + valuation_price_date: null, + valuation_price: null, + valuation_method: null, + success: false, + message: 'Asset ID not found', + response_timestamp: '2025-08-28T14:27:11.345Z', +} diff --git a/packages/sources/liveart/tsconfig.json b/packages/sources/liveart/tsconfig.json new file mode 100644 index 0000000000..f59363fd76 --- /dev/null +++ b/packages/sources/liveart/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*", "src/**/*.json"], + "exclude": ["dist", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/sources/liveart/tsconfig.test.json b/packages/sources/liveart/tsconfig.test.json new file mode 100755 index 0000000000..e3de28cb5c --- /dev/null +++ b/packages/sources/liveart/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*", "**/test", "src/**/*.json"], + "compilerOptions": { + "noEmit": true + } +} diff --git a/packages/tsconfig.json b/packages/tsconfig.json index c9e0640a22..b35c378e32 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -479,6 +479,9 @@ { "path": "./sources/lition" }, + { + "path": "./sources/liveart" + }, { "path": "./sources/lotus" }, diff --git a/packages/tsconfig.test.json b/packages/tsconfig.test.json index e5121d61e4..a439dd1009 100644 --- a/packages/tsconfig.test.json +++ b/packages/tsconfig.test.json @@ -479,6 +479,9 @@ { "path": "./sources/lition/tsconfig.test.json" }, + { + "path": "./sources/liveart/tsconfig.test.json" + }, { "path": "./sources/lotus/tsconfig.test.json" }, diff --git a/yarn.lock b/yarn.lock index fdf1953566..08c514fdab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4820,6 +4820,20 @@ __metadata: languageName: unknown linkType: soft +"@chainlink/liveart-external-adapter@workspace:packages/sources/liveart": + version: 0.0.0-use.local + resolution: "@chainlink/liveart-external-adapter@workspace:packages/sources/liveart" + dependencies: + "@chainlink/external-adapter-framework": "npm:2.7.0" + "@types/jest": "npm:^29.5.14" + "@types/node": "npm:22.14.1" + axios: "npm:^1.11.0" + nock: "npm:13.5.6" + tslib: "npm:2.4.1" + typescript: "npm:5.8.3" + languageName: unknown + linkType: soft + "@chainlink/llama-guard-adapter@workspace:packages/composites/llama-guard": version: 0.0.0-use.local resolution: "@chainlink/llama-guard-adapter@workspace:packages/composites/llama-guard" @@ -14936,6 +14950,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.11.0": + version: 1.11.0 + resolution: "axios@npm:1.11.0" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.4" + proxy-from-env: "npm:^1.1.0" + checksum: 10/232df4af7a4e4e07baa84621b9cc4b0c518a757b4eacc7f635c0eb3642cb98dff347326739f24b891b3b4481b7b838c79a3a0c4819c9fbc1fc40232431b9c5dc + languageName: node + linkType: hard + "babel-jest@npm:^29.7.0": version: 29.7.0 resolution: "babel-jest@npm:29.7.0" @@ -15990,6 +16015,16 @@ __metadata: languageName: node linkType: hard +"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + checksum: 10/00482c1f6aa7cfb30fb1dbeb13873edf81cfac7c29ed67a5957d60635a56b2a4a480f1016ddbdb3395cc37900d46037fb965043a51c5c789ffeab4fc535d18b5 + languageName: node + linkType: hard + "call-bind@npm:^1.0.2, call-bind@npm:^1.0.7": version: 1.0.7 resolution: "call-bind@npm:1.0.7" @@ -17918,6 +17953,17 @@ __metadata: languageName: node linkType: hard +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + gopd: "npm:^1.2.0" + checksum: 10/5add88a3d68d42d6e6130a0cac450b7c2edbe73364bbd2fc334564418569bea97c6943a8fcd70e27130bf32afc236f30982fc4905039b703f23e9e0433c29934 + languageName: node + linkType: hard + "duplexer3@npm:^0.1.4": version: 0.1.5 resolution: "duplexer3@npm:0.1.5" @@ -18282,6 +18328,13 @@ __metadata: languageName: node linkType: hard +"es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 10/f8dc9e660d90919f11084db0a893128f3592b781ce967e4fccfb8f3106cb83e400a4032c559184ec52ee1dbd4b01e7776c7cd0b3327b1961b1a4a7008920fe78 + languageName: node + linkType: hard + "es-errors@npm:^1.3.0": version: 1.3.0 resolution: "es-errors@npm:1.3.0" @@ -18296,6 +18349,27 @@ __metadata: languageName: node linkType: hard +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" + dependencies: + es-errors: "npm:^1.3.0" + checksum: 10/54fe77de288451dae51c37bfbfe3ec86732dc3778f98f3eb3bdb4bf48063b2c0b8f9c93542656986149d08aa5be3204286e2276053d19582b76753f1a2728867 + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.1.0": + version: 2.1.0 + resolution: "es-set-tostringtag@npm:2.1.0" + dependencies: + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.6" + has-tostringtag: "npm:^1.0.2" + hasown: "npm:^2.0.2" + checksum: 10/86814bf8afbcd8966653f731415888019d4bc4aca6b6c354132a7a75bb87566751e320369654a101d23a91c87a85c79b178bcf40332839bd347aff437c4fb65f + languageName: node + linkType: hard + "es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.62, es5-ext@npm:^0.10.63, es5-ext@npm:^0.10.64, es5-ext@npm:~0.10.14": version: 0.10.64 resolution: "es5-ext@npm:0.10.64" @@ -19832,6 +19906,19 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.4": + version: 4.0.4 + resolution: "form-data@npm:4.0.4" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" + mime-types: "npm:^2.1.12" + checksum: 10/a4b62e21932f48702bc468cc26fb276d186e6b07b557e3dd7cc455872bdbb82db7db066844a64ad3cf40eaf3a753c830538183570462d3649fdfd705601cbcfb + languageName: node + linkType: hard + "form-data@npm:~2.3.2": version: 2.3.3 resolution: "form-data@npm:2.3.3" @@ -20217,6 +20304,24 @@ __metadata: languageName: node linkType: hard +"get-intrinsic@npm:^1.2.6": + version: 1.3.0 + resolution: "get-intrinsic@npm:1.3.0" + dependencies: + call-bind-apply-helpers: "npm:^1.0.2" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.1.1" + function-bind: "npm:^1.1.2" + get-proto: "npm:^1.0.1" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + math-intrinsics: "npm:^1.1.0" + checksum: 10/6e9dd920ff054147b6f44cb98104330e87caafae051b6d37b13384a45ba15e71af33c3baeac7cb630a0aaa23142718dcf25b45cfdd86c184c5dcb4e56d953a10 + languageName: node + linkType: hard + "get-iterator@npm:^1.0.2": version: 1.0.2 resolution: "get-iterator@npm:1.0.2" @@ -20245,6 +20350,16 @@ __metadata: languageName: node linkType: hard +"get-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: "npm:^1.0.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10/4fc96afdb58ced9a67558698b91433e6b037aaa6f1493af77498d7c85b141382cf223c0e5946f334fb328ee85dfe6edd06d218eaf09556f4bc4ec6005d7f5f7b + languageName: node + linkType: hard + "get-starknet-core@npm:^4.0.0-next.3": version: 4.0.0 resolution: "get-starknet-core@npm:4.0.0" @@ -20558,6 +20673,13 @@ __metadata: languageName: node linkType: hard +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: 10/94e296d69f92dc1c0768fcfeecfb3855582ab59a7c75e969d5f96ce50c3d201fd86d5a2857c22565764d5bb8a816c7b1e58f133ec318cd56274da36c5e3fb1a1 + languageName: node + linkType: hard + "got@npm:^11.8.2": version: 11.8.6 resolution: "got@npm:11.8.6" @@ -20873,6 +20995,13 @@ __metadata: languageName: node linkType: hard +"has-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "has-symbols@npm:1.1.0" + checksum: 10/959385c98696ebbca51e7534e0dc723ada325efa3475350951363cce216d27373e0259b63edb599f72eb94d6cde8577b4b2375f080b303947e560f85692834fa + languageName: node + linkType: hard + "has-to-string-tag-x@npm:^1.2.0": version: 1.4.1 resolution: "has-to-string-tag-x@npm:1.4.1" @@ -25784,6 +25913,13 @@ __metadata: languageName: node linkType: hard +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 10/11df2eda46d092a6035479632e1ec865b8134bdfc4bd9e571a656f4191525404f13a283a515938c3a8de934dbfd9c09674d9da9fa831e6eb7e22b50b197d2edd + languageName: node + linkType: hard + "md5.js@npm:^1.3.4": version: 1.3.5 resolution: "md5.js@npm:1.3.5"