From 26532bcbf8de8fd3aa63868da838fe98b9d3ae47 Mon Sep 17 00:00:00 2001 From: Milk Date: Mon, 10 Nov 2025 11:05:34 -0600 Subject: [PATCH 1/3] generated link template, modified adapter type --- .pnp.cjs | 20 ++++++ .../basic-link-price-source/CHANGELOG.md | 0 .../sources/basic-link-price-source/README.md | 3 + .../basic-link-price-source/package.json | 40 ++++++++++++ .../src/config/index.ts | 16 +++++ .../src/config/overrides.json | 3 + .../src/endpoint/index.ts | 2 + .../src/endpoint/link-eth.ts | 31 +++++++++ .../src/endpoint/link-usdc.ts | 19 ++++++ .../basic-link-price-source/src/index.ts | 15 +++++ .../src/transport/link-eth.ts | 62 ++++++++++++++++++ .../src/transport/link-usdc.ts | 62 ++++++++++++++++++ .../basic-link-price-source/test-payload.json | 6 ++ .../test/integration/adapter.test.ts | 64 +++++++++++++++++++ .../test/integration/fixtures.ts | 22 +++++++ .../basic-link-price-source/tsconfig.json | 15 +++++ .../tsconfig.test.json | 7 ++ .../bravenewcoin/src/config/limits.json | 15 ++++- packages/sources/bravenewcoin/tsconfig.json | 10 ++- ty-notes.md | 18 ++++++ yarn.lock | 13 ++++ 21 files changed, 441 insertions(+), 2 deletions(-) create mode 100644 packages/sources/basic-link-price-source/CHANGELOG.md create mode 100644 packages/sources/basic-link-price-source/README.md create mode 100644 packages/sources/basic-link-price-source/package.json create mode 100644 packages/sources/basic-link-price-source/src/config/index.ts create mode 100644 packages/sources/basic-link-price-source/src/config/overrides.json create mode 100644 packages/sources/basic-link-price-source/src/endpoint/index.ts create mode 100644 packages/sources/basic-link-price-source/src/endpoint/link-eth.ts create mode 100644 packages/sources/basic-link-price-source/src/endpoint/link-usdc.ts create mode 100644 packages/sources/basic-link-price-source/src/index.ts create mode 100644 packages/sources/basic-link-price-source/src/transport/link-eth.ts create mode 100644 packages/sources/basic-link-price-source/src/transport/link-usdc.ts create mode 100644 packages/sources/basic-link-price-source/test-payload.json create mode 100644 packages/sources/basic-link-price-source/test/integration/adapter.test.ts create mode 100644 packages/sources/basic-link-price-source/test/integration/fixtures.ts create mode 100644 packages/sources/basic-link-price-source/tsconfig.json create mode 100755 packages/sources/basic-link-price-source/tsconfig.test.json create mode 100644 ty-notes.md diff --git a/.pnp.cjs b/.pnp.cjs index e64ad1baed..eaed4e26c1 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -250,6 +250,10 @@ const RAW_RUNTIME_STATE = "name": "@chainlink/bank-frick-adapter",\ "reference": "workspace:packages/sources/bank-frick"\ },\ + {\ + "name": "@chainlink/basic-link-price-source-adapter",\ + "reference": "workspace:packages/sources/basic-link-price-source"\ + },\ {\ "name": "@chainlink/bea-adapter",\ "reference": "workspace:packages/sources/bea"\ @@ -996,6 +1000,7 @@ const RAW_RUNTIME_STATE = ["@chainlink/avalanche-platform-adapter", ["workspace:packages/sources/avalanche-platform"]],\ ["@chainlink/backed-fi-adapter", ["workspace:packages/sources/backed-fi"]],\ ["@chainlink/bank-frick-adapter", ["workspace:packages/sources/bank-frick"]],\ + ["@chainlink/basic-link-price-source-adapter", ["workspace:packages/sources/basic-link-price-source"]],\ ["@chainlink/bea-adapter", ["workspace:packages/sources/bea"]],\ ["@chainlink/binance-adapter", ["workspace:packages/sources/binance"]],\ ["@chainlink/bitcoin-json-rpc-adapter", ["workspace:packages/composites/bitcoin-json-rpc"]],\ @@ -5493,6 +5498,21 @@ const RAW_RUNTIME_STATE = "linkType": "SOFT"\ }]\ ]],\ + ["@chainlink/basic-link-price-source-adapter", [\ + ["workspace:packages/sources/basic-link-price-source", {\ + "packageLocation": "./packages/sources/basic-link-price-source/",\ + "packageDependencies": [\ + ["@chainlink/basic-link-price-source-adapter", "workspace:packages/sources/basic-link-price-source"],\ + ["@chainlink/external-adapter-framework", "npm:2.8.0"],\ + ["@types/jest", "npm:29.5.14"],\ + ["@types/node", "npm:22.14.1"],\ + ["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/bea-adapter", [\ ["workspace:packages/sources/bea", {\ "packageLocation": "./packages/sources/bea/",\ diff --git a/packages/sources/basic-link-price-source/CHANGELOG.md b/packages/sources/basic-link-price-source/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/sources/basic-link-price-source/README.md b/packages/sources/basic-link-price-source/README.md new file mode 100644 index 0000000000..00c1ea990f --- /dev/null +++ b/packages/sources/basic-link-price-source/README.md @@ -0,0 +1,3 @@ +# Chainlink External Adapter for basic-link-price-source + +This README will be generated automatically when code is merged to `main`. If you would like to generate a preview of the README, please run `yarn generate:readme basic-link-price-source`. diff --git a/packages/sources/basic-link-price-source/package.json b/packages/sources/basic-link-price-source/package.json new file mode 100644 index 0000000000..14bab25190 --- /dev/null +++ b/packages/sources/basic-link-price-source/package.json @@ -0,0 +1,40 @@ +{ + "name": "@chainlink/basic-link-price-source-adapter", + "version": "0.0.0", + "description": "Chainlink basic-link-price-source adapter.", + "keywords": [ + "Chainlink", + "LINK", + "blockchain", + "oracle", + "basic-link-price-source" + ], + "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": { + "clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo", + "prepack": "yarn build", + "build": "tsc -b", + "server": "node -e 'require(\"./index.js\").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" + } +} diff --git a/packages/sources/basic-link-price-source/src/config/index.ts b/packages/sources/basic-link-price-source/src/config/index.ts new file mode 100644 index 0000000000..343db289c2 --- /dev/null +++ b/packages/sources/basic-link-price-source/src/config/index.ts @@ -0,0 +1,16 @@ +import { AdapterConfig } from '@chainlink/external-adapter-framework/config' + +export const config = new AdapterConfig({ + ETH_RPC_URL: { + description: 'RPC URL for Ethereum Mainnet', + type: 'string', + required: true, + }, + ARB_RPC_URL: { + description: 'RPC URL for Arbitrum One', + type: 'string', + required: true, + }, + BACKGROUND_EXECUTE_MS: AdapterConfig.DEFAULT_BACKGROUND_EXECUTE_MS_WS, + // Remove API_ENDPOINT if not using off-chain +}) diff --git a/packages/sources/basic-link-price-source/src/config/overrides.json b/packages/sources/basic-link-price-source/src/config/overrides.json new file mode 100644 index 0000000000..890bd3cab2 --- /dev/null +++ b/packages/sources/basic-link-price-source/src/config/overrides.json @@ -0,0 +1,3 @@ +{ + "basic-link-price-source": {} +} diff --git a/packages/sources/basic-link-price-source/src/endpoint/index.ts b/packages/sources/basic-link-price-source/src/endpoint/index.ts new file mode 100644 index 0000000000..fc317ca872 --- /dev/null +++ b/packages/sources/basic-link-price-source/src/endpoint/index.ts @@ -0,0 +1,2 @@ +export { endpoint as linkEth } from './link-eth' +export { endpoint as linkUsdc } from './link-usdc' diff --git a/packages/sources/basic-link-price-source/src/endpoint/link-eth.ts b/packages/sources/basic-link-price-source/src/endpoint/link-eth.ts new file mode 100644 index 0000000000..803ebee416 --- /dev/null +++ b/packages/sources/basic-link-price-source/src/endpoint/link-eth.ts @@ -0,0 +1,31 @@ +import { CryptoPriceEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { config } from '../config' +import { linkEthTransport } from '../transport/link-eth' + +export const endpoint = new CryptoPriceEndpoint({ + name: 'link-eth', + aliases: ['crypto', 'price'], // Optional: already added by CryptoPriceEndpoint + transport: linkEthTransport, + inputParameters: { + base: { + type: 'string', + description: 'The base currency (e.g., LINK)', + required: true, + default: 'LINK', + }, + quote: { + type: 'string', + description: 'The quote currency (e.g., ETH)', + required: true, + default: 'ETH', + }, + chain: { + type: 'string', + description: 'Blockchain to query', + required: false, + default: 'ethereum', + options: ['ethereum', 'arbitrum'], + }, + }, + config, +}) diff --git a/packages/sources/basic-link-price-source/src/endpoint/link-usdc.ts b/packages/sources/basic-link-price-source/src/endpoint/link-usdc.ts new file mode 100644 index 0000000000..e9a2ad9f76 --- /dev/null +++ b/packages/sources/basic-link-price-source/src/endpoint/link-usdc.ts @@ -0,0 +1,19 @@ +import { CryptoPriceEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { config } from '../config' +import { linkUsdcTransport } from '../transport/link-usdc' + +export const endpoint = new CryptoPriceEndpoint({ + name: 'link-usdc', + transport: linkUsdcTransport, + inputParameters: { + base: { type: 'string', required: true, default: 'LINK' }, + quote: { type: 'string', required: true, default: 'USDC' }, + chain: { + type: 'string', + required: false, + default: 'ethereum', + options: ['ethereum', 'arbitrum'], + }, + }, + config, +}) diff --git a/packages/sources/basic-link-price-source/src/index.ts b/packages/sources/basic-link-price-source/src/index.ts new file mode 100644 index 0000000000..40433ecb61 --- /dev/null +++ b/packages/sources/basic-link-price-source/src/index.ts @@ -0,0 +1,15 @@ +import { expose, ServerInstance } from '@chainlink/external-adapter-framework' +import { PriceAdapter } from '@chainlink/external-adapter-framework/adapter' // Use PriceAdapter instead +import { config } from './config' +import { linkEth, linkUsdc } from './endpoint' // Ensure this exports the endpoints correctly (e.g., via index.ts or direct imports) + +export const adapter = new PriceAdapter({ + // Switch to PriceAdapter + defaultEndpoint: linkEth.name, + name: 'BASIC_LINK-PRICE-SOURCE', + config, + endpoints: [linkEth, linkUsdc], + // includes: [...] // Optional: Add if you have an includes.json for inverse pairs (e.g., ETH/LINK as 1 / LINK/ETH) +}) + +export const server = (): Promise => expose(adapter) diff --git a/packages/sources/basic-link-price-source/src/transport/link-eth.ts b/packages/sources/basic-link-price-source/src/transport/link-eth.ts new file mode 100644 index 0000000000..c59712cddf --- /dev/null +++ b/packages/sources/basic-link-price-source/src/transport/link-eth.ts @@ -0,0 +1,62 @@ +import { HttpTransport } from '@chainlink/external-adapter-framework/transports' +import { BaseEndpointTypes } from '../endpoint/link-eth' + +export interface ResponseSchema { + [key: string]: { + price: number + errorMessage?: string + } +} + +export type HttpTransportTypes = BaseEndpointTypes & { + Provider: { + RequestBody: never + ResponseBody: ResponseSchema + } +} +export const httpTransport = new HttpTransport({ + prepareRequests: (params, config) => { + return params.map((param) => { + return { + params: [param], + request: { + baseURL: config.API_ENDPOINT, + url: '/cryptocurrency/price', + headers: { + X_API_KEY: config.API_KEY, + }, + params: { + symbol: param.base.toUpperCase(), + convert: param.quote.toUpperCase(), + }, + }, + } + }) + }, + parseResponse: (params, response) => { + if (!response.data) { + return params.map((param) => { + return { + params: param, + response: { + errorMessage: `The data provider didn't return any value for ${param.base}/${param.quote}`, + statusCode: 502, + }, + } + }) + } + + return params.map((param) => { + const result = response.data[param.base.toUpperCase()].price + return { + params: param, + response: { + result, + data: { + result, + }, + }, + } + }) + }, +}) diff --git a/packages/sources/basic-link-price-source/src/transport/link-usdc.ts b/packages/sources/basic-link-price-source/src/transport/link-usdc.ts new file mode 100644 index 0000000000..76c1e32b16 --- /dev/null +++ b/packages/sources/basic-link-price-source/src/transport/link-usdc.ts @@ -0,0 +1,62 @@ +import { HttpTransport } from '@chainlink/external-adapter-framework/transports' +import { BaseEndpointTypes } from '../endpoint/link-usdc' + +export interface ResponseSchema { + [key: string]: { + price: number + errorMessage?: string + } +} + +export type HttpTransportTypes = BaseEndpointTypes & { + Provider: { + RequestBody: never + ResponseBody: ResponseSchema + } +} +export const httpTransport = new HttpTransport({ + prepareRequests: (params, config) => { + return params.map((param) => { + return { + params: [param], + request: { + baseURL: config.API_ENDPOINT, + url: '/cryptocurrency/price', + headers: { + X_API_KEY: config.API_KEY, + }, + params: { + symbol: param.base.toUpperCase(), + convert: param.quote.toUpperCase(), + }, + }, + } + }) + }, + parseResponse: (params, response) => { + if (!response.data) { + return params.map((param) => { + return { + params: param, + response: { + errorMessage: `The data provider didn't return any value for ${param.base}/${param.quote}`, + statusCode: 502, + }, + } + }) + } + + return params.map((param) => { + const result = response.data[param.base.toUpperCase()].price + return { + params: param, + response: { + result, + data: { + result, + }, + }, + } + }) + }, +}) diff --git a/packages/sources/basic-link-price-source/test-payload.json b/packages/sources/basic-link-price-source/test-payload.json new file mode 100644 index 0000000000..7930bcfa20 --- /dev/null +++ b/packages/sources/basic-link-price-source/test-payload.json @@ -0,0 +1,6 @@ +{ + "requests": [{ + "from": "BTC", + "to": "USD" + }] +} diff --git a/packages/sources/basic-link-price-source/test/integration/adapter.test.ts b/packages/sources/basic-link-price-source/test/integration/adapter.test.ts new file mode 100644 index 0000000000..b0c959ddce --- /dev/null +++ b/packages/sources/basic-link-price-source/test/integration/adapter.test.ts @@ -0,0 +1,64 @@ +import { + TestAdapter, + setEnvVariables, +} from '@chainlink/external-adapter-framework/util/testing-utils' +import * as nock from 'nock' +import { mockResponseSuccess } from './fixtures' + +describe('execute', () => { + let spy: jest.SpyInstance + let testAdapter: TestAdapter + let oldEnv: NodeJS.ProcessEnv + + beforeAll(async () => { + oldEnv = JSON.parse(JSON.stringify(process.env)) + process.env.API_KEY = process.env.API_KEY ?? 'fake-api-key' + + const mockDate = new Date('2001-01-01T11:11:11.111Z') + spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) + + const adapter = (await import('./../../src')).adapter + adapter.rateLimiting = undefined + testAdapter = await TestAdapter.startWithMockedCache(adapter, { + testAdapter: {} as TestAdapter, + }) + }) + + afterAll(async () => { + setEnvVariables(oldEnv) + await testAdapter.api.close() + nock.restore() + nock.cleanAll() + spy.mockRestore() + }) + + describe('link-eth endpoint', () => { + it('should return success', async () => { + const data = { + base: 'ETH', + quote: 'USD', + endpoint: 'link-eth', + transport: 'rest', + } + mockResponseSuccess() + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + }) + + describe('link-usdc endpoint', () => { + it('should return success', async () => { + const data = { + base: 'ETH', + quote: 'USD', + endpoint: 'link-usdc', + transport: 'rest', + } + mockResponseSuccess() + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + }) +}) diff --git a/packages/sources/basic-link-price-source/test/integration/fixtures.ts b/packages/sources/basic-link-price-source/test/integration/fixtures.ts new file mode 100644 index 0000000000..3b7e58e409 --- /dev/null +++ b/packages/sources/basic-link-price-source/test/integration/fixtures.ts @@ -0,0 +1,22 @@ +import nock from 'nock' + +export const mockResponseSuccess = (): nock.Scope => + nock('https://dataproviderapi.com', { + encodedQueryParams: true, + }) + .get('/cryptocurrency/price') + .query({ + symbol: 'ETH', + convert: 'USD', + }) + .reply(200, () => ({ ETH: { price: 10000 } }), [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ]) + .persist() diff --git a/packages/sources/basic-link-price-source/tsconfig.json b/packages/sources/basic-link-price-source/tsconfig.json new file mode 100644 index 0000000000..99dd195f56 --- /dev/null +++ b/packages/sources/basic-link-price-source/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "baseUrl": "../../", + "paths": { + "@chainlink/ea-bootstrap": ["core/bootstrap/src/index.ts"], + "@chainlink/ea-test-helpers": ["core/test-helpers/src/index.ts"] + } + }, + "include": ["src/**/*", "src/**/*.json"], + "exclude": ["dist", "**/*.spec.ts", "**/*.test.ts"], + "references": [{ "path": "../../core/test-helpers" }, { "path": "../../core/bootstrap" }] +} diff --git a/packages/sources/basic-link-price-source/tsconfig.test.json b/packages/sources/basic-link-price-source/tsconfig.test.json new file mode 100755 index 0000000000..e3de28cb5c --- /dev/null +++ b/packages/sources/basic-link-price-source/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*", "**/test", "src/**/*.json"], + "compilerOptions": { + "noEmit": true + } +} diff --git a/packages/sources/bravenewcoin/src/config/limits.json b/packages/sources/bravenewcoin/src/config/limits.json index 665183d257..210eaefc30 100644 --- a/packages/sources/bravenewcoin/src/config/limits.json +++ b/packages/sources/bravenewcoin/src/config/limits.json @@ -1,4 +1,17 @@ { - "http": {}, + "http": { + "free": { + "rateLimit1s": 1, + "rateLimit1m": 5, + "rateLimit1h": 50, + "note": "Based on 600/month free limit (~1 every few minutes avg, but allow small bursts)" + }, + "pro": { + "rateLimit1s": 5, + "rateLimit1m": 100, + "rateLimit1h": 1000, + "note": "For paid BNC plans with higher quotas" + } + }, "ws": {} } diff --git a/packages/sources/bravenewcoin/tsconfig.json b/packages/sources/bravenewcoin/tsconfig.json index a3fd261528..4ae9a25d69 100644 --- a/packages/sources/bravenewcoin/tsconfig.json +++ b/packages/sources/bravenewcoin/tsconfig.json @@ -2,7 +2,15 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist", - "rootDir": "src" + "rootDir": "src", + "baseUrl": "../../", + "paths": { + "@chainlink/ea-bootstrap": ["core/bootstrap/src/index.ts"], + "@chainlink/ea-test-helpers": ["core/test-helpers/src/index.ts"], + "@chainlink/external-adapter-framework": [ + "../../.yarn/cache/@chainlink-external-adapter-framework-npm-*.zip/node_modules/@chainlink/external-adapter-framework" + ] // Adjust the exact zip name if needed (find via `yarn info @chainlink/external-adapter-framework`) + } }, "include": ["src/**/*", "src/**/*.json"], "exclude": ["dist", "**/*.spec.ts", "**/*.test.ts"], diff --git a/ty-notes.md b/ty-notes.md new file mode 100644 index 0000000000..2d8ad539c4 --- /dev/null +++ b/ty-notes.md @@ -0,0 +1,18 @@ +# creating my first external adapter + +below i'll be detailing my experience while implementing a basic chainlink price feed adapter in the external-adapter-js repo. + +## generating the adapter + +1. called yarn source basic-link-price-source + +- + +- after running script log output told me to run this: + yo ./.yarn/cache/node_modules/@chainlink/external-adapter-framework/generator-adapter/generators/app/index.js packages/sources && yarn ne + w tsconfig + +- for adapters use this library to interface with oracles, found it useful to reference which type of call i should use : https://github.com/smartcontractkit/ea-framework-js/tree/main + +- switched adapter logic to crypto endpoint for specfic task +- diff --git a/yarn.lock b/yarn.lock index a8d9d0423c..632506b60f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2727,6 +2727,19 @@ __metadata: languageName: unknown linkType: soft +"@chainlink/basic-link-price-source-adapter@workspace:packages/sources/basic-link-price-source": + version: 0.0.0-use.local + resolution: "@chainlink/basic-link-price-source-adapter@workspace:packages/sources/basic-link-price-source" + dependencies: + "@chainlink/external-adapter-framework": "npm:2.8.0" + "@types/jest": "npm:^29.5.14" + "@types/node": "npm:22.14.1" + nock: "npm:13.5.6" + tslib: "npm:2.4.1" + typescript: "npm:5.8.3" + languageName: unknown + linkType: soft + "@chainlink/bea-adapter@workspace:packages/sources/bea": version: 0.0.0-use.local resolution: "@chainlink/bea-adapter@workspace:packages/sources/bea" From 30bff5041e718d9457603de48895d11e15c4fe92 Mon Sep 17 00:00:00 2001 From: Milk Date: Mon, 10 Nov 2025 16:06:22 -0600 Subject: [PATCH 2/3] link-eth and link-usdc prices working --- .pnp.cjs | 1 + .../sources/basic-link-price-source/README.md | 22 +++- .../basic-link-price-source/package.json | 1 + .../src/config/index.ts | 14 +-- .../src/endpoint/link-eth.ts | 41 +++++-- .../src/endpoint/link-usdc.ts | 47 ++++++-- .../basic-link-price-source/src/index.ts | 4 +- .../src/transport/link-eth.ts | 77 ++++++++---- .../src/transport/link-usdc.ts | 79 ++++++++----- .../basic-link-price-source/test-payload.json | 9 +- .../test/integration/adapter.test.ts | 64 ---------- .../test/integration/fixtures.ts | 111 +++++++++++++++--- ty-notes.md | 2 +- yarn.lock | 1 + 14 files changed, 301 insertions(+), 172 deletions(-) diff --git a/.pnp.cjs b/.pnp.cjs index eaed4e26c1..d613511dfd 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -5506,6 +5506,7 @@ const RAW_RUNTIME_STATE = ["@chainlink/external-adapter-framework", "npm:2.8.0"],\ ["@types/jest", "npm:29.5.14"],\ ["@types/node", "npm:22.14.1"],\ + ["ethers", "npm:6.15.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"]\ diff --git a/packages/sources/basic-link-price-source/README.md b/packages/sources/basic-link-price-source/README.md index 00c1ea990f..65e42a872a 100644 --- a/packages/sources/basic-link-price-source/README.md +++ b/packages/sources/basic-link-price-source/README.md @@ -1,3 +1,21 @@ -# Chainlink External Adapter for basic-link-price-source +# Chainlink External Adapter for basic-link-price-source to understand mono repo -This README will be generated automatically when code is merged to `main`. If you would like to generate a preview of the README, please run `yarn generate:readme basic-link-price-source`. +below i'll be detailing my experience while implementing a basic chainlink price feed adapter in the external-adapter-js repo. + +## generating the adapter + +- called yarn source basic-link-price-source + +- + +- after running script log output told me to run this: + yo ./.yarn/cache/node_modules/@chainlink/external-adapter-framework/generator-adapter/generators/app/index.js packages/sources && yarn ne + w tsconfig + +- for adapters use this library to interface with oracles, found it useful to reference which type of call i should use : https://github.com/smartcontractkit/ea-framework-js/tree/main + +- switched adapter logic to crypto endpoint for specfic task +- seems like lsp information does not read the cached libraries while using my editor (lazy vim) +- had to hard code the description to a specfic string "The symbol of symbols of the currency to query" or else server:dist would fail. TODO look at how other adapters manage this. + +- run test on specfic adapter export adapter=basic-link-price-source; yarn test $adapter/test/integration diff --git a/packages/sources/basic-link-price-source/package.json b/packages/sources/basic-link-price-source/package.json index 14bab25190..03fb673ac0 100644 --- a/packages/sources/basic-link-price-source/package.json +++ b/packages/sources/basic-link-price-source/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@chainlink/external-adapter-framework": "2.8.0", + "ethers": "^6.15.0", "tslib": "2.4.1" } } diff --git a/packages/sources/basic-link-price-source/src/config/index.ts b/packages/sources/basic-link-price-source/src/config/index.ts index 343db289c2..6cafa076e6 100644 --- a/packages/sources/basic-link-price-source/src/config/index.ts +++ b/packages/sources/basic-link-price-source/src/config/index.ts @@ -1,16 +1,14 @@ import { AdapterConfig } from '@chainlink/external-adapter-framework/config' export const config = new AdapterConfig({ - ETH_RPC_URL: { - description: 'RPC URL for Ethereum Mainnet', + RPC_URL_ETHEREUM: { type: 'string', - required: true, + description: 'Ethereum RPC URL', + default: 'https://ethereum-rpc.publicnode.com', }, - ARB_RPC_URL: { - description: 'RPC URL for Arbitrum One', + RPC_URL_ARBITRUM: { type: 'string', - required: true, + description: 'Arbitrum RPC URL', + default: 'https://arbitrum-one-rpc.publicnode.com', }, - BACKGROUND_EXECUTE_MS: AdapterConfig.DEFAULT_BACKGROUND_EXECUTE_MS_WS, - // Remove API_ENDPOINT if not using off-chain }) diff --git a/packages/sources/basic-link-price-source/src/endpoint/link-eth.ts b/packages/sources/basic-link-price-source/src/endpoint/link-eth.ts index 803ebee416..427a59c88d 100644 --- a/packages/sources/basic-link-price-source/src/endpoint/link-eth.ts +++ b/packages/sources/basic-link-price-source/src/endpoint/link-eth.ts @@ -1,23 +1,24 @@ -import { CryptoPriceEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { PriceEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util' +import { InputParameters } from '@chainlink/external-adapter-framework/validation' import { config } from '../config' import { linkEthTransport } from '../transport/link-eth' -export const endpoint = new CryptoPriceEndpoint({ - name: 'link-eth', - aliases: ['crypto', 'price'], // Optional: already added by CryptoPriceEndpoint - transport: linkEthTransport, - inputParameters: { +export const inputParameters = new InputParameters( + { base: { + aliases: ['from', 'coin'], type: 'string', - description: 'The base currency (e.g., LINK)', - required: true, + description: 'The symbol of symbols of the currency to query', default: 'LINK', + required: false, }, quote: { + aliases: ['to', 'market'], type: 'string', - description: 'The quote currency (e.g., ETH)', - required: true, + description: 'The symbol of the currency to convert to', default: 'ETH', + required: false, }, chain: { type: 'string', @@ -27,5 +28,23 @@ export const endpoint = new CryptoPriceEndpoint({ options: ['ethereum', 'arbitrum'], }, }, - config, + [ + { + base: 'LINK', + quote: 'ETH', + chain: 'ethereum', + }, + ], +) + +export type BaseEndpointTypes = { + Parameters: typeof inputParameters.definition + Response: SingleNumberResultResponse + Settings: typeof config.settings +} + +export const endpoint = new PriceEndpoint({ + name: 'link-eth', + transport: linkEthTransport, + inputParameters, }) diff --git a/packages/sources/basic-link-price-source/src/endpoint/link-usdc.ts b/packages/sources/basic-link-price-source/src/endpoint/link-usdc.ts index e9a2ad9f76..c72e198e6a 100644 --- a/packages/sources/basic-link-price-source/src/endpoint/link-usdc.ts +++ b/packages/sources/basic-link-price-source/src/endpoint/link-usdc.ts @@ -1,19 +1,50 @@ -import { CryptoPriceEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { PriceEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util' +import { InputParameters } from '@chainlink/external-adapter-framework/validation' import { config } from '../config' import { linkUsdcTransport } from '../transport/link-usdc' -export const endpoint = new CryptoPriceEndpoint({ - name: 'link-usdc', - transport: linkUsdcTransport, - inputParameters: { - base: { type: 'string', required: true, default: 'LINK' }, - quote: { type: 'string', required: true, default: 'USDC' }, +export const inputParameters = new InputParameters( + { + base: { + aliases: ['from', 'coin'], + type: 'string', + description: 'The symbol of symbols of the currency to query', + default: 'LINK', + required: false, + }, + quote: { + aliases: ['to', 'market'], + type: 'string', + description: 'The symbol of the currency to convert to', + default: 'USDC', + required: false, + }, chain: { type: 'string', + description: 'Blockchain to query', required: false, default: 'ethereum', options: ['ethereum', 'arbitrum'], }, }, - config, + [ + { + base: 'LINK', + quote: 'USDC', + chain: 'ethereum', + }, + ], +) + +export type BaseEndpointTypes = { + Parameters: typeof inputParameters.definition + Response: SingleNumberResultResponse + Settings: typeof config.settings +} + +export const endpoint = new PriceEndpoint({ + name: 'link-usdc', + transport: linkUsdcTransport, + inputParameters, }) diff --git a/packages/sources/basic-link-price-source/src/index.ts b/packages/sources/basic-link-price-source/src/index.ts index 40433ecb61..d63815a3ff 100644 --- a/packages/sources/basic-link-price-source/src/index.ts +++ b/packages/sources/basic-link-price-source/src/index.ts @@ -5,10 +5,10 @@ import { linkEth, linkUsdc } from './endpoint' // Ensure this exports the endpoi export const adapter = new PriceAdapter({ // Switch to PriceAdapter - defaultEndpoint: linkEth.name, + defaultEndpoint: linkUsdc.name, name: 'BASIC_LINK-PRICE-SOURCE', config, - endpoints: [linkEth, linkUsdc], + endpoints: [linkUsdc, linkEth], // includes: [...] // Optional: Add if you have an includes.json for inverse pairs (e.g., ETH/LINK as 1 / LINK/ETH) }) diff --git a/packages/sources/basic-link-price-source/src/transport/link-eth.ts b/packages/sources/basic-link-price-source/src/transport/link-eth.ts index c59712cddf..37e2fa76e2 100644 --- a/packages/sources/basic-link-price-source/src/transport/link-eth.ts +++ b/packages/sources/basic-link-price-source/src/transport/link-eth.ts @@ -1,60 +1,85 @@ import { HttpTransport } from '@chainlink/external-adapter-framework/transports' +import { formatUnits, toBigInt } from 'ethers' import { BaseEndpointTypes } from '../endpoint/link-eth' export interface ResponseSchema { - [key: string]: { - price: number - errorMessage?: string - } + jsonrpc: string + id: number + result: string } export type HttpTransportTypes = BaseEndpointTypes & { Provider: { - RequestBody: never + RequestBody: { + jsonrpc: string + method: string + params: [{ to: string; data: string }, string] + id: number + } ResponseBody: ResponseSchema } } -export const httpTransport = new HttpTransport({ + +export const linkEthTransport = new HttpTransport({ prepareRequests: (params, config) => { return params.map((param) => { + let rpcUrl: string + let poolAddress: string + switch (String(param.chain).toLowerCase()) { + case 'ethereum': + rpcUrl = config.RPC_URL_ETHEREUM + poolAddress = '0xa6cc3c2531fdaa6ae1a3ca84c2855806728693e8' // LINK/WETH 0.3% + break + case 'arbitrum': + rpcUrl = config.RPC_URL_ARBITRUM + poolAddress = '0x468b88941e7cc0b88c1869d68ab6b570bcef62ff' // WETH/LINK 0.3% + break + default: + throw new Error(`Unsupported chain: ${param.chain}`) + } return { params: [param], request: { - baseURL: config.API_ENDPOINT, - url: '/cryptocurrency/price', - headers: { - X_API_KEY: config.API_KEY, - }, - params: { - symbol: param.base.toUpperCase(), - convert: param.quote.toUpperCase(), + baseURL: rpcUrl, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: { + jsonrpc: '2.0', + method: 'eth_call', + params: [{ to: poolAddress, data: '0x3850c7bd' }, 'latest'], + id: 1, }, }, } }) }, parseResponse: (params, response) => { - if (!response.data) { - return params.map((param) => { + return params.map((param) => { + if (!response.data.result) { return { params: param, response: { - errorMessage: `The data provider didn't return any value for ${param.base}/${param.quote}`, + errorMessage: `No data from RPC for ${param.base}/${param.quote} on ${param.chain}`, statusCode: 502, }, } - }) - } - - return params.map((param) => { - const result = response.data[param.base.toUpperCase()].price + } + const sqrtPriceX96 = toBigInt(response.data.result.slice(0, 66)) + const Q192 = 2n ** 192n + let priceBI: bigint + if (String(param.chain).toLowerCase() === 'ethereum') { + // token0 = LINK (18), token1 = WETH (18) → scale by 10^18 to preserve precision + priceBI = (sqrtPriceX96 ** 2n * 10n ** 18n) / Q192 + } else { + // token0 = WETH (18), token1 = LINK (18) → inverted, scale by 10^18 + priceBI = (Q192 * 10n ** 18n) / sqrtPriceX96 ** 2n + } + const priceNumber = Number(formatUnits(priceBI, 18)) return { params: param, response: { - result, - data: { - result, - }, + result: priceNumber, + data: { result: priceNumber }, }, } }) diff --git a/packages/sources/basic-link-price-source/src/transport/link-usdc.ts b/packages/sources/basic-link-price-source/src/transport/link-usdc.ts index 76c1e32b16..d5fa474955 100644 --- a/packages/sources/basic-link-price-source/src/transport/link-usdc.ts +++ b/packages/sources/basic-link-price-source/src/transport/link-usdc.ts @@ -1,60 +1,83 @@ import { HttpTransport } from '@chainlink/external-adapter-framework/transports' +import { formatUnits, toBigInt } from 'ethers' import { BaseEndpointTypes } from '../endpoint/link-usdc' - export interface ResponseSchema { - [key: string]: { - price: number - errorMessage?: string - } + jsonrpc: string + id: number + result: string } - export type HttpTransportTypes = BaseEndpointTypes & { Provider: { - RequestBody: never + RequestBody: { + jsonrpc: string + method: string + params: [{ to: string; data: string }, string] + id: number + } ResponseBody: ResponseSchema } } -export const httpTransport = new HttpTransport({ +export const linkUsdcTransport = new HttpTransport({ prepareRequests: (params, config) => { return params.map((param) => { + let rpcUrl: string + let poolAddress: string + switch (String(param.chain).toLowerCase()) { + case 'ethereum': + rpcUrl = config.RPC_URL_ETHEREUM + poolAddress = '0xfad57d2039c21811c8f2b5d5b65308aa99d31559' // LINK/USDC 0.3% + break + case 'arbitrum': + rpcUrl = config.RPC_URL_ARBITRUM + poolAddress = '0xbbe36e6f0331c6a36ab44bc8421e28e1a1871c1e' // USDC/LINK 0.3% + break + default: + throw new Error(`Unsupported chain: ${param.chain}`) + } return { params: [param], request: { - baseURL: config.API_ENDPOINT, - url: '/cryptocurrency/price', - headers: { - X_API_KEY: config.API_KEY, - }, - params: { - symbol: param.base.toUpperCase(), - convert: param.quote.toUpperCase(), + baseURL: rpcUrl, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: { + jsonrpc: '2.0', + method: 'eth_call', + params: [{ to: poolAddress, data: '0x3850c7bd' }, 'latest'], + id: 1, }, }, } }) }, parseResponse: (params, response) => { - if (!response.data) { - return params.map((param) => { + console.log(response) + return params.map((param) => { + if (!response.data.result) { return { params: param, response: { - errorMessage: `The data provider didn't return any value for ${param.base}/${param.quote}`, + errorMessage: `No data from RPC for ${param.base}/${param.quote} on ${param.chain}`, statusCode: 502, }, } - }) - } - - return params.map((param) => { - const result = response.data[param.base.toUpperCase()].price + } + const sqrtPriceX96 = toBigInt(response.data.result.slice(0, 66)) + const Q192 = 2n ** 192n + let priceBI: bigint + if (String(param.chain).toLowerCase() === 'ethereum') { + // token0 = LINK (18), token1 = USDC (6) → scale by 10^30 to preserve precision + priceBI = (sqrtPriceX96 ** 2n * 10n ** 30n) / Q192 + } else { + // token0 = USDC (6), token1 = LINK (18) → inverted, scale by 10^30 + priceBI = (Q192 * 10n ** 30n) / sqrtPriceX96 ** 2n + } + const priceNumber = Number(formatUnits(priceBI, 18)) return { params: param, response: { - result, - data: { - result, - }, + result: priceNumber, + data: { result: priceNumber }, }, } }) diff --git a/packages/sources/basic-link-price-source/test-payload.json b/packages/sources/basic-link-price-source/test-payload.json index 7930bcfa20..9a3518ba32 100644 --- a/packages/sources/basic-link-price-source/test-payload.json +++ b/packages/sources/basic-link-price-source/test-payload.json @@ -1,6 +1,7 @@ { - "requests": [{ - "from": "BTC", - "to": "USD" - }] + "requests": [{ + "base": "LINK", + "quote": "USDC", + "chain": "ethereum" + }] } diff --git a/packages/sources/basic-link-price-source/test/integration/adapter.test.ts b/packages/sources/basic-link-price-source/test/integration/adapter.test.ts index b0c959ddce..e69de29bb2 100644 --- a/packages/sources/basic-link-price-source/test/integration/adapter.test.ts +++ b/packages/sources/basic-link-price-source/test/integration/adapter.test.ts @@ -1,64 +0,0 @@ -import { - TestAdapter, - setEnvVariables, -} from '@chainlink/external-adapter-framework/util/testing-utils' -import * as nock from 'nock' -import { mockResponseSuccess } from './fixtures' - -describe('execute', () => { - let spy: jest.SpyInstance - let testAdapter: TestAdapter - let oldEnv: NodeJS.ProcessEnv - - beforeAll(async () => { - oldEnv = JSON.parse(JSON.stringify(process.env)) - process.env.API_KEY = process.env.API_KEY ?? 'fake-api-key' - - const mockDate = new Date('2001-01-01T11:11:11.111Z') - spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) - - const adapter = (await import('./../../src')).adapter - adapter.rateLimiting = undefined - testAdapter = await TestAdapter.startWithMockedCache(adapter, { - testAdapter: {} as TestAdapter, - }) - }) - - afterAll(async () => { - setEnvVariables(oldEnv) - await testAdapter.api.close() - nock.restore() - nock.cleanAll() - spy.mockRestore() - }) - - describe('link-eth endpoint', () => { - it('should return success', async () => { - const data = { - base: 'ETH', - quote: 'USD', - endpoint: 'link-eth', - transport: 'rest', - } - mockResponseSuccess() - const response = await testAdapter.request(data) - expect(response.statusCode).toBe(200) - expect(response.json()).toMatchSnapshot() - }) - }) - - describe('link-usdc endpoint', () => { - it('should return success', async () => { - const data = { - base: 'ETH', - quote: 'USD', - endpoint: 'link-usdc', - transport: 'rest', - } - mockResponseSuccess() - const response = await testAdapter.request(data) - expect(response.statusCode).toBe(200) - expect(response.json()).toMatchSnapshot() - }) - }) -}) diff --git a/packages/sources/basic-link-price-source/test/integration/fixtures.ts b/packages/sources/basic-link-price-source/test/integration/fixtures.ts index 3b7e58e409..1ecabc0634 100644 --- a/packages/sources/basic-link-price-source/test/integration/fixtures.ts +++ b/packages/sources/basic-link-price-source/test/integration/fixtures.ts @@ -1,22 +1,97 @@ import nock from 'nock' -export const mockResponseSuccess = (): nock.Scope => - nock('https://dataproviderapi.com', { - encodedQueryParams: true, - }) - .get('/cryptocurrency/price') - .query({ - symbol: 'ETH', - convert: 'USD', +export const mockLinkUsdcEthereumSuccess = (): nock.Scope => + nock('https://rpc.ankr.com/eth') + .matchHeader('content-type', 'application/json') + .matchHeader('user-agent', 'axios/1.12.2') + .matchHeader('accept', 'application/json, text/plain, */*') + .matchHeader('content-length', '136') + .post( + '/', + (body) => + body.jsonrpc === '2.0' && + body.method === 'eth_call' && + body.params[0].to === '0xfad57d2039c21811c8f2b5d5b65308aa99d31559' && + body.params[0].data === '0x3850c7bd' && + body.params[1] === 'latest' && + body.id === 1, + ) + .reply(200, { + jsonrpc: '2.0', + id: 1, + result: + '0x0000000000000000000000000350de10ebf9f28aa4ced00000000000000000000000000000000000000000000000000000000000000000000000000000000000', // Sample for ~$10 USDC/LINK (adjust if needed for exact test) + }) + .persist() + +export const mockLinkUsdcArbitrumSuccess = (): nock.Scope => + nock('https://rpc.ankr.com/arbitrum') + .matchHeader('content-type', 'application/json') + .matchHeader('user-agent', 'axios/1.12.2') + .matchHeader('accept', 'application/json, text/plain, */*') + .matchHeader('content-length', '136') + .post( + '/', + (body) => + body.jsonrpc === '2.0' && + body.method === 'eth_call' && + body.params[0].to === '0xbbe36e6f0331c6a36ab44bc8421e28e1a1871c1e' && + body.params[0].data === '0x3850c7bd' && + body.params[1] === 'latest' && + body.id === 1, + ) + .reply(200, { + jsonrpc: '2.0', + id: 1, + result: + '0x00000000000000000000000004d343c419adf31bc1ca5a1caeef7800000000000000000000000000000000000000000000000000000000000000000000000000', // Sample for ~$10 USDC/LINK + }) + .persist() + +export const mockLinkEthEthereumSuccess = (): nock.Scope => + nock('https://rpc.ankr.com/eth') + .matchHeader('content-type', 'application/json') + .matchHeader('user-agent', 'axios/1.12.2') + .matchHeader('accept', 'application/json, text/plain, */*') + .matchHeader('content-length', '136') + .post( + '/', + (body) => + body.jsonrpc === '2.0' && + body.method === 'eth_call' && + body.params[0].to === '0xa6cc3c2531fdaa6ae1a3ca84c2855806728693e8' && + body.params[0].data === '0x3850c7bd' && + body.params[1] === 'latest' && + body.id === 1, + ) + .reply(200, { + jsonrpc: '2.0', + id: 1, + result: + '0x00000000000000000000000000e058df72d36c1e19be909fa0000000000000000000000000000000000000000000000000000000000000000000000000000000', // Sample for ~0.003 ETH/LINK + }) + .persist() + +export const mockLinkEthArbitrumSuccess = (): nock.Scope => + nock('https://rpc.ankr.com/arbitrum') + .matchHeader('content-type', 'application/json') + .matchHeader('user-agent', 'axios/1.12.2') + .matchHeader('accept', 'application/json, text/plain, */*') + .matchHeader('content-length', '136') + .post( + '/', + (body) => + body.jsonrpc === '2.0' && + body.method === 'eth_call' && + body.params[0].to === '0x468b88941e7cc0b88c1869d68ab6b570bcef62ff' && + body.params[0].data === '0x3850c7bd' && + body.params[1] === 'latest' && + body.id === 1, + ) + .reply(200, { + jsonrpc: '2.0', + id: 1, + result: + '0x0000000000000000000000000d4b3a8b0e8e2a0b6f1a7b0c00000000000000000000000000000000000000000000000000000000000000000000000000000000', // Sample for ~0.003 ETH/LINK }) - .reply(200, () => ({ ETH: { price: 10000 } }), [ - 'Content-Type', - 'application/json', - 'Connection', - 'close', - 'Vary', - 'Accept-Encoding', - 'Vary', - 'Origin', - ]) .persist() diff --git a/ty-notes.md b/ty-notes.md index 2d8ad539c4..9c0ce0bd50 100644 --- a/ty-notes.md +++ b/ty-notes.md @@ -15,4 +15,4 @@ below i'll be detailing my experience while implementing a basic chainlink price - for adapters use this library to interface with oracles, found it useful to reference which type of call i should use : https://github.com/smartcontractkit/ea-framework-js/tree/main - switched adapter logic to crypto endpoint for specfic task -- +- run test on specfic adapter export adapter=basic-link-price-source; yarn test $adapter/test/integration diff --git a/yarn.lock b/yarn.lock index 632506b60f..91a952e5a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2734,6 +2734,7 @@ __metadata: "@chainlink/external-adapter-framework": "npm:2.8.0" "@types/jest": "npm:^29.5.14" "@types/node": "npm:22.14.1" + ethers: "npm:^6.15.0" nock: "npm:13.5.6" tslib: "npm:2.4.1" typescript: "npm:5.8.3" From d792a290736ea0dfec3711a701bfa30a624b0241 Mon Sep 17 00:00:00 2001 From: Milk Date: Mon, 10 Nov 2025 16:26:57 -0600 Subject: [PATCH 3/3] formalized readme --- .../sources/basic-link-price-source/README.md | 109 +++++++++++++++--- 1 file changed, 96 insertions(+), 13 deletions(-) diff --git a/packages/sources/basic-link-price-source/README.md b/packages/sources/basic-link-price-source/README.md index 65e42a872a..a9c9f0b837 100644 --- a/packages/sources/basic-link-price-source/README.md +++ b/packages/sources/basic-link-price-source/README.md @@ -1,21 +1,104 @@ -# Chainlink External Adapter for basic-link-price-source to understand mono repo +# Chainlink External Adapter: Basic LINK Price Source -below i'll be detailing my experience while implementing a basic chainlink price feed adapter in the external-adapter-js repo. +This project documents my hands-on experience developing a basic Chainlink price feed adapter within the `external-adapters-js` monorepo. The primary goal was to familiarize myself with the monorepo structure, Chainlink's External Adapter framework, and demonstrate my ability to onboard independently without external assistance. By building this adapter from scratch, I aimed to showcase rapid learning, problem-solving, and proficiency in TypeScript based blockchain development skills directly aligned with roles at Chainlink Labs. -## generating the adapter +The adapter fetches LINK prices against USDC and ETH from Uniswap V3 pools on Ethereum and Arbitrum, using direct RPC calls for efficiency and decentralization. This exercise not only deepened my understanding of source adapters but also prepared me to contribute to more complex integrations like composites and targets. -- called yarn source basic-link-price-source +## Adapter Generation and Development Notes -- +- Initiated the adapter using `yarn new source basic-link-price-source` to scaffold the basic structure. +- Followed the generator prompt by running: + `yo ./.yarn/cache/node_modules/@chainlink/external-adapter-framework/generator-adapter/generators/app/index.js packages/sources && yarn new tsconfig` +- Leveraged the External Adapter Framework (https://github.com/smartcontractkit/ea-framework-js/tree/main) for oracle interactions, referencing endpoint types (e.g., crypto/price) to ensure compatibility. +- Adapted the logic to a custom crypto price endpoint tailored to LINK-specific queries. +- Encountered an LSP (Language Server Protocol) issue where cached libraries weren't recognized in my editor (Neo Vim)—**TODO: Investigate deeper into LSP configuration for monorepo environments.** +- Temporarily hardcoded parameter descriptions (e.g., "The symbol of symbols of the currency to query") to resolve runtime failures during `yarn server:dist`; **TODO: Review how other adapters handle dynamic descriptions.** +- For testing individual adapters: `export adapter=basic-link-price-source; yarn test $adapter/test/integration`—**TODO: Implement integration tests for this adapter.** -- after running script log output told me to run this: - yo ./.yarn/cache/node_modules/@chainlink/external-adapter-framework/generator-adapter/generators/app/index.js packages/sources && yarn ne - w tsconfig +### General Thoughts on the Repository -- for adapters use this library to interface with oracles, found it useful to reference which type of call i should use : https://github.com/smartcontractkit/ea-framework-js/tree/main +The CONTRIBUTING.md is straightforward and well-organized, providing clear guidelines for setup, PR processes, and best practices. Having access to numerous existing adapters within the monorepo was invaluable, allowing me to reference real world examples for transports, endpoints, and configurations without needing external documentation. This structure facilitated quick iteration and independent problem-solving, highlighting the repo's design for scalability and collaboration. -- switched adapter logic to crypto endpoint for specfic task -- seems like lsp information does not read the cached libraries while using my editor (lazy vim) -- had to hard code the description to a specfic string "The symbol of symbols of the currency to query" or else server:dist would fail. TODO look at how other adapters manage this. +## Building and Running the Server -- run test on specfic adapter export adapter=basic-link-price-source; yarn test $adapter/test/integration +To build and run the adapter: + +- Navigate to the adapter's root directory (`packages/sources/basic-link-price-source`). +- Run `yarn build` to compile the TypeScript code. +- Then, start the server with `yarn server:dist` (runs on localhost:8080 by default; override RPC URLs via environment variables if needed, e.g., `RPC_URL_ETHEREUM=https://ethereum-rpc.publicnode.com yarn server:dist`). + +## Calling the Adapter + +Call the endpoints locally using cURL (assuming the server is running on localhost:8080). These commands query `link-usdc` and `link-eth` on Ethereum and Arbitrum, leveraging default base/quote values for simplicity. + +1. **link-usdc on ethereum**: + + ```bash + curl -X POST http://localhost:8080/ \ + -H 'Content-Type: application/json' \ + -d '{ + "id": "1", + "data": { + "endpoint": "link-usdc", + "base": "LINK", + "quote": "USDC", + "chain": "ethereum" + } + }' + ``` + +2. **link-usdc on arbitrum**: + + ```bash + curl -X POST http://localhost:8080/ \ + -H 'Content-Type: application/json' \ + -d '{ + "id": "1", + "data": { + "endpoint": "link-usdc", + "base": "LINK", + "quote": "USDC", + "chain": "arbitrum" + } + }' + ``` + +3. **link-eth on ethereum**: + + ```bash + curl -X POST http://localhost:8080/ \ + -H 'Content-Type: application/json' \ + -d '{ + "id": "1", + "data": { + "endpoint": "link-eth", + "base": "LINK", + "quote": "ETH", + "chain": "ethereum" + } + }' + ``` + +4. **link-eth on arbitrum**: + ```bash + curl -X POST http://localhost:8080/ \ + -H 'Content-Type: application/json' \ + -d '{ + "id": "1", + "data": { + "endpoint": "link-eth", + "base": "LINK", + "quote": "ETH", + "chain": "arbitrum" + } + }' + ``` + +## Proposed Enhancements + +To extend this project and demonstrate broader expertise in Chainlink's ecosystem: + +- **Composite Adapter for Automated Selling**: Build a composite adapter that chains this source adapter to fetch LINK prices, then triggers a target adapter to execute a sell order if the price exceeds predefined thresholds (e.g., x or y). This could be tested on Sepolia testnet, showcasing integration of source, composite, and target adapters for automated trading logic. +- **Cross-Chain Transfer Integration**: Develop an additional composite adapter that, post-sell, transfers the resulting tokens from Ethereum Sepolia to Arbitrum Sepolia using Chainlink CCIP (Cross-Chain Interoperability Protocol). This enhancement would highlight familiarity with testnets, CCIP for secure cross-chain operations, and end-to-end adapter composition for real-world DeFi workflows. + +This project underscores my technical curious and self-driven approach to as a long time Fan of Chainlink and experienced engineer