diff --git a/.pnp.cjs b/.pnp.cjs index e64ad1baed..d613511dfd 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,22 @@ 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"],\ + ["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"]\ + ],\ + "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..a9c9f0b837 --- /dev/null +++ b/packages/sources/basic-link-price-source/README.md @@ -0,0 +1,104 @@ +# Chainlink External Adapter: Basic LINK Price Source + +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. + +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. + +## 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.** + +### General Thoughts on the Repository + +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. + +## Building and Running the Server + +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 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..03fb673ac0 --- /dev/null +++ b/packages/sources/basic-link-price-source/package.json @@ -0,0 +1,41 @@ +{ + "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", + "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 new file mode 100644 index 0000000000..6cafa076e6 --- /dev/null +++ b/packages/sources/basic-link-price-source/src/config/index.ts @@ -0,0 +1,14 @@ +import { AdapterConfig } from '@chainlink/external-adapter-framework/config' + +export const config = new AdapterConfig({ + RPC_URL_ETHEREUM: { + type: 'string', + description: 'Ethereum RPC URL', + default: 'https://ethereum-rpc.publicnode.com', + }, + RPC_URL_ARBITRUM: { + type: 'string', + description: 'Arbitrum RPC URL', + default: 'https://arbitrum-one-rpc.publicnode.com', + }, +}) 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..427a59c88d --- /dev/null +++ b/packages/sources/basic-link-price-source/src/endpoint/link-eth.ts @@ -0,0 +1,50 @@ +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 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: 'ETH', + required: false, + }, + chain: { + type: 'string', + description: 'Blockchain to query', + required: false, + default: 'ethereum', + options: ['ethereum', 'arbitrum'], + }, + }, + [ + { + 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 new file mode 100644 index 0000000000..c72e198e6a --- /dev/null +++ b/packages/sources/basic-link-price-source/src/endpoint/link-usdc.ts @@ -0,0 +1,50 @@ +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 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'], + }, + }, + [ + { + 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 new file mode 100644 index 0000000000..d63815a3ff --- /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: linkUsdc.name, + name: 'BASIC_LINK-PRICE-SOURCE', + config, + endpoints: [linkUsdc, linkEth], + // 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..37e2fa76e2 --- /dev/null +++ b/packages/sources/basic-link-price-source/src/transport/link-eth.ts @@ -0,0 +1,87 @@ +import { HttpTransport } from '@chainlink/external-adapter-framework/transports' +import { formatUnits, toBigInt } from 'ethers' +import { BaseEndpointTypes } from '../endpoint/link-eth' + +export interface ResponseSchema { + jsonrpc: string + id: number + result: string +} + +export type HttpTransportTypes = BaseEndpointTypes & { + Provider: { + RequestBody: { + jsonrpc: string + method: string + params: [{ to: string; data: string }, string] + id: number + } + ResponseBody: ResponseSchema + } +} + +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: 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) => { + return params.map((param) => { + if (!response.data.result) { + return { + params: param, + response: { + errorMessage: `No data from RPC for ${param.base}/${param.quote} on ${param.chain}`, + statusCode: 502, + }, + } + } + 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: 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 new file mode 100644 index 0000000000..d5fa474955 --- /dev/null +++ b/packages/sources/basic-link-price-source/src/transport/link-usdc.ts @@ -0,0 +1,85 @@ +import { HttpTransport } from '@chainlink/external-adapter-framework/transports' +import { formatUnits, toBigInt } from 'ethers' +import { BaseEndpointTypes } from '../endpoint/link-usdc' +export interface ResponseSchema { + jsonrpc: string + id: number + result: string +} +export type HttpTransportTypes = BaseEndpointTypes & { + Provider: { + RequestBody: { + jsonrpc: string + method: string + params: [{ to: string; data: string }, string] + id: number + } + ResponseBody: ResponseSchema + } +} +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: 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) => { + console.log(response) + return params.map((param) => { + if (!response.data.result) { + return { + params: param, + response: { + errorMessage: `No data from RPC for ${param.base}/${param.quote} on ${param.chain}`, + statusCode: 502, + }, + } + } + 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: 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 new file mode 100644 index 0000000000..9a3518ba32 --- /dev/null +++ b/packages/sources/basic-link-price-source/test-payload.json @@ -0,0 +1,7 @@ +{ + "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 new file mode 100644 index 0000000000..e69de29bb2 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..1ecabc0634 --- /dev/null +++ b/packages/sources/basic-link-price-source/test/integration/fixtures.ts @@ -0,0 +1,97 @@ +import nock from 'nock' + +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 + }) + .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..9c0ce0bd50 --- /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 +- 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 a8d9d0423c..91a952e5a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2727,6 +2727,20 @@ __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" + ethers: "npm:^6.15.0" + 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"