diff --git a/.pnp.cjs b/.pnp.cjs index eef0fd263a..4ee8e2dc60 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -758,6 +758,10 @@ const RAW_RUNTIME_STATE = "name": "@chainlink/wrapped-adapter",\ "reference": "workspace:packages/sources/wrapped"\ },\ + {\ + "name": "@chainlink/xusd-usd-exchange-rate-adapter",\ + "reference": "workspace:packages/sources/xusd-usd-exchange-rate"\ + },\ {\ "name": "@chainlink/dydx-stark-adapter",\ "reference": "workspace:packages/targets/dydx-stark"\ @@ -958,7 +962,8 @@ const RAW_RUNTIME_STATE = ["@chainlink/wintermute-adapter", ["workspace:packages/sources/wintermute"]],\ ["@chainlink/wisdomtree-adapter", ["workspace:packages/sources/wisdomtree"]],\ ["@chainlink/wrapped-adapter", ["workspace:packages/sources/wrapped"]],\ - ["@chainlink/xsushi-price-adapter", ["workspace:packages/composites/xsushi-price"]]\ + ["@chainlink/xsushi-price-adapter", ["workspace:packages/composites/xsushi-price"]],\ + ["@chainlink/xusd-usd-exchange-rate-adapter", ["workspace:packages/sources/xusd-usd-exchange-rate"]]\ ],\ "fallbackPool": [\ ],\ @@ -6153,7 +6158,7 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }],\ ["npm:2.9.0", {\ - "packageLocation": "./.yarn/cache/@chainlink-external-adapter-framework-npm-2.9.0-664e8a533b-36152824af.zip/node_modules/@chainlink/external-adapter-framework/",\ + "packageLocation": "./.yarn/unplugged/@chainlink-external-adapter-framework-npm-2.9.0-664e8a533b/node_modules/@chainlink/external-adapter-framework/",\ "packageDependencies": [\ ["@chainlink/external-adapter-framework", "npm:2.9.0"],\ ["ajv", "npm:8.17.1"],\ @@ -8371,6 +8376,24 @@ const RAW_RUNTIME_STATE = "linkType": "SOFT"\ }]\ ]],\ + ["@chainlink/xusd-usd-exchange-rate-adapter", [\ + ["workspace:packages/sources/xusd-usd-exchange-rate", {\ + "packageLocation": "./packages/sources/xusd-usd-exchange-rate/",\ + "packageDependencies": [\ + ["@chainlink/xusd-usd-exchange-rate-adapter", "workspace:packages/sources/xusd-usd-exchange-rate"],\ + ["@chainlink/external-adapter-framework", "npm:2.11.4"],\ + ["@sinonjs/fake-timers", "npm:9.1.2"],\ + ["@types/jest", "npm:29.5.14"],\ + ["@types/node", "npm:22.14.1"],\ + ["@types/sinonjs__fake-timers", "npm:8.1.5"],\ + ["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"\ + }]\ + ]],\ ["@changesets/apply-release-plan", [\ ["npm:7.0.8", {\ "packageLocation": "./.yarn/cache/@changesets-apply-release-plan-npm-7.0.8-e5b7eb2ce3-ab7dcf8759.zip/node_modules/@changesets/apply-release-plan/",\ diff --git a/package.json b/package.json index ed3d28f320..cdd6f919ca 100644 --- a/package.json +++ b/package.json @@ -77,5 +77,10 @@ "resolutions": { "ethereum-cryptography@^1.1.2": "patch:ethereum-cryptography@npm%3A1.1.2#./.yarn/patches/ethereum-cryptography-npm-1.1.2-c16cfd7e8a.patch", "ethereum-cryptography@^1.0.3": "patch:ethereum-cryptography@npm%3A1.1.2#./.yarn/patches/ethereum-cryptography-npm-1.1.2-c16cfd7e8a.patch" + }, + "dependenciesMeta": { + "@chainlink/external-adapter-framework@2.9.0": { + "unplugged": true + } } } diff --git a/packages/sources/xusd-usd-exchange-rate/CHANGELOG.md b/packages/sources/xusd-usd-exchange-rate/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/sources/xusd-usd-exchange-rate/README.md b/packages/sources/xusd-usd-exchange-rate/README.md new file mode 100644 index 0000000000..91ab1d4455 --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/README.md @@ -0,0 +1,3 @@ +# Chainlink External Adapter for example-adapter + +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 example-adapter`. diff --git a/packages/sources/xusd-usd-exchange-rate/package.json b/packages/sources/xusd-usd-exchange-rate/package.json new file mode 100644 index 0000000000..2c09b55a02 --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/package.json @@ -0,0 +1,43 @@ +{ + "name": "@chainlink/xusd-usd-exchange-rate-adapter", + "version": "0.0.0", + "description": "External Adapter for fetching the round value from the XUSD contract on Ethereum for XUSD-USD exchange rate", + "keywords": [ + "Chainlink", + "LINK", + "blockchain", + "oracle", + "xusd-usd-exchange-rate" + ], + "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": { + "@sinonjs/fake-timers": "9.1.2", + "@types/jest": "^29.5.14", + "@types/node": "22.14.1", + "@types/sinonjs__fake-timers": "8.1.5", + "nock": "13.5.6", + "typescript": "5.8.3" + }, + "dependencies": { + "@chainlink/external-adapter-framework": "2.11.4", + "ethers": "^6.13.2", + "tslib": "2.4.1" + } +} diff --git a/packages/sources/xusd-usd-exchange-rate/src/config/index.ts b/packages/sources/xusd-usd-exchange-rate/src/config/index.ts new file mode 100644 index 0000000000..7f1447b8aa --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/src/config/index.ts @@ -0,0 +1,20 @@ +import { AdapterConfig } from '@chainlink/external-adapter-framework/config' + +export const config = new AdapterConfig({ + ETHEREUM_RPC_URL: { + description: 'Ethereum JSON-RPC endpoint URL', + type: 'string', + required: true, + }, + ETHEREUM_CHAIN_ID: { + description: 'The chain id to connect to', + type: 'number', + default: 1, + }, + BACKGROUND_EXECUTE_MS: { + description: + 'The amount of time the background execute should sleep before performing the next request', + type: 'number', + default: 10_000, + }, +}) diff --git a/packages/sources/xusd-usd-exchange-rate/src/endpoint/index.ts b/packages/sources/xusd-usd-exchange-rate/src/endpoint/index.ts new file mode 100644 index 0000000000..e2920fd0c6 --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/src/endpoint/index.ts @@ -0,0 +1 @@ +export { endpoint as roundEndpoint } from './round' diff --git a/packages/sources/xusd-usd-exchange-rate/src/endpoint/round.ts b/packages/sources/xusd-usd-exchange-rate/src/endpoint/round.ts new file mode 100644 index 0000000000..d7cb2cb0e4 --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/src/endpoint/round.ts @@ -0,0 +1,20 @@ +import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { EmptyInputParameters } from '@chainlink/external-adapter-framework/validation/input-params' +import { config } from '../config' +import { roundTransport } from '../transport/round' + +export type BaseEndpointTypes = { + Parameters: EmptyInputParameters + Response: { + Data: { + result: string + } + Result: string + } + Settings: typeof config.settings +} + +export const endpoint = new AdapterEndpoint({ + name: 'round', + transport: roundTransport, +}) diff --git a/packages/sources/xusd-usd-exchange-rate/src/index.ts b/packages/sources/xusd-usd-exchange-rate/src/index.ts new file mode 100644 index 0000000000..975d25bb0c --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/src/index.ts @@ -0,0 +1,13 @@ +import { expose, ServerInstance } from '@chainlink/external-adapter-framework' +import { Adapter } from '@chainlink/external-adapter-framework/adapter' +import { config } from './config' +import { roundEndpoint } from './endpoint' + +export const adapter = new Adapter({ + defaultEndpoint: roundEndpoint.name, + name: 'XUSD_USD_EXCHANGE_RATE', + config, + endpoints: [roundEndpoint], +}) + +export const server = (): Promise => expose(adapter) diff --git a/packages/sources/xusd-usd-exchange-rate/src/transport/round.ts b/packages/sources/xusd-usd-exchange-rate/src/transport/round.ts new file mode 100644 index 0000000000..6699e75b9e --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/src/transport/round.ts @@ -0,0 +1,92 @@ +import { EndpointContext } from '@chainlink/external-adapter-framework/adapter' +import { TransportDependencies } from '@chainlink/external-adapter-framework/transports' +import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription' +import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util' +import { ethers } from 'ethers' +import { BaseEndpointTypes } from '../endpoint/round' + +const logger = makeLogger('XUSD USD Exchange Rate') + +const XUSD_CONTRACT_ADDRESS = '0xE2Fc85BfB48C4cF147921fBE110cf92Ef9f26F94' +const ROUND_FUNCTION_SELECTOR = '0x146ca531' + +export type RoundTransportTypes = BaseEndpointTypes + +export function hexToDecimalString(resultHex: string): string { + return BigInt(resultHex).toString() +} + +export class RoundTransport extends SubscriptionTransport { + provider!: ethers.JsonRpcProvider + + async initialize( + dependencies: TransportDependencies, + adapterSettings: RoundTransportTypes['Settings'], + endpointName: string, + transportName: string, + ): Promise { + await super.initialize(dependencies, adapterSettings, endpointName, transportName) + this.provider = new ethers.JsonRpcProvider( + adapterSettings.ETHEREUM_RPC_URL, + adapterSettings.ETHEREUM_CHAIN_ID, + ) + } + + async backgroundHandler( + context: EndpointContext, + _entries: RoundTransportTypes['Parameters'][], + ): Promise { + await this.handleRequest() + await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS) + } + + async handleRequest(): Promise { + let response: AdapterResponse + try { + response = await this._handleRequest() + } catch (e) { + logger.error(e) + const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred' + response = { + statusCode: 502, + errorMessage, + timestamps: { + providerDataRequestedUnixMs: 0, + providerDataReceivedUnixMs: 0, + providerIndicatedTimeUnixMs: undefined, + }, + } + } + await this.responseCache.write(this.name, [{ params: {}, response }]) + } + + async _handleRequest(): Promise> { + const providerDataRequestedUnixMs = Date.now() + + const resultHex = await this.provider.call({ + to: XUSD_CONTRACT_ADDRESS, + data: ROUND_FUNCTION_SELECTOR, + }) + + const result = hexToDecimalString(resultHex) + + return { + data: { + result, + }, + statusCode: 200, + result, + timestamps: { + providerDataRequestedUnixMs, + providerDataReceivedUnixMs: Date.now(), + providerIndicatedTimeUnixMs: undefined, + }, + } + } + + getSubscriptionTtlFromConfig(adapterSettings: RoundTransportTypes['Settings']): number { + return adapterSettings.WARMUP_SUBSCRIPTION_TTL + } +} + +export const roundTransport = new RoundTransport() diff --git a/packages/sources/xusd-usd-exchange-rate/test-payload.json b/packages/sources/xusd-usd-exchange-rate/test-payload.json new file mode 100644 index 0000000000..0eab58d52f --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/test-payload.json @@ -0,0 +1,7 @@ +{ + "requests": [ + { + "endpoint": "round" + } + ] +} diff --git a/packages/sources/xusd-usd-exchange-rate/test/integration/__snapshots__/adapter.test.ts.snap b/packages/sources/xusd-usd-exchange-rate/test/integration/__snapshots__/adapter.test.ts.snap new file mode 100644 index 0000000000..34d5fbe402 --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/test/integration/__snapshots__/adapter.test.ts.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`execute round endpoint happy path should return success for default endpoint (no endpoint specified) 1`] = ` +{ + "data": { + "result": "1000000000000000000", + }, + "result": "1000000000000000000", + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; + +exports[`execute round endpoint happy path should return success for round endpoint 1`] = ` +{ + "data": { + "result": "1000000000000000000", + }, + "result": "1000000000000000000", + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; + +exports[`execute round endpoint upstream failures should handle RPC endpoint failure with 502 1`] = ` +{ + "errorMessage": "server response 500 (request={ }, response={ }, error=null, info={ "requestUrl": "[ETHEREUM_RPC_URL REDACTED]", "responseBody": "{\\"error\\":\\"Internal Server Error\\"}", "responseStatus": "500 " }, code=SERVER_ERROR, version=6.15.0)", + "statusCode": 502, + "timestamps": { + "providerDataReceivedUnixMs": 0, + "providerDataRequestedUnixMs": 0, + }, +} +`; + +exports[`execute round endpoint upstream failures should handle contract execution revert 1`] = ` +{ + "errorMessage": "missing revert data (action="call", data=null, reason=null, transaction={ "data": "0x146ca531", "to": "0xE2Fc85BfB48C4cF147921fBE110cf92Ef9f26F94" }, invocation=null, revert=null, code=CALL_EXCEPTION, version=6.15.0)", + "statusCode": 502, + "timestamps": { + "providerDataReceivedUnixMs": 0, + "providerDataRequestedUnixMs": 0, + }, +} +`; diff --git a/packages/sources/xusd-usd-exchange-rate/test/integration/adapter.test.ts b/packages/sources/xusd-usd-exchange-rate/test/integration/adapter.test.ts new file mode 100644 index 0000000000..8f162cc958 --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/test/integration/adapter.test.ts @@ -0,0 +1,117 @@ +import { + TestAdapter, + setEnvVariables, +} from '@chainlink/external-adapter-framework/util/testing-utils' +import * as nock from 'nock' +import { + MOCK_ROUND_VALUE_DECIMAL, + MOCK_ROUND_VALUE_HEX, + mockEthereumRpcContractError, + mockEthereumRpcContractErrorSingle, + mockEthereumRpcFailure, + mockEthereumRpcSingleRequest, + mockEthereumRpcSuccess, +} 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.ETHEREUM_RPC_URL = process.env.ETHEREUM_RPC_URL ?? 'http://localhost:8545' + process.env.ETHEREUM_CHAIN_ID = process.env.ETHEREUM_CHAIN_ID ?? '1' + process.env.BACKGROUND_EXECUTE_MS = process.env.BACKGROUND_EXECUTE_MS ?? '0' + + 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, + }) + }) + + afterEach(() => { + nock.cleanAll() + + // Clear the EA cache between tests + const keys = testAdapter.mockCache?.cache.keys() + if (keys) { + for (const key of keys) { + testAdapter.mockCache?.delete(key) + } + } + }) + + afterAll(async () => { + setEnvVariables(oldEnv) + await testAdapter.api.close() + nock.restore() + nock.cleanAll() + spy.mockRestore() + }) + + describe('round endpoint', () => { + describe('happy path', () => { + it('should return success for round endpoint', async () => { + const data = { endpoint: 'round' } + mockEthereumRpcSuccess(MOCK_ROUND_VALUE_HEX) + mockEthereumRpcSingleRequest(MOCK_ROUND_VALUE_HEX) + + // First call triggers background execution + await testAdapter.request(data) + // Second call retrieves cached result + const response = await testAdapter.request(data) + + expect(response.statusCode).toBe(200) + const json = response.json() + expect(json.data.result).toBe(MOCK_ROUND_VALUE_DECIMAL) + expect(json.result).toBe(MOCK_ROUND_VALUE_DECIMAL) + expect(response.json()).toMatchSnapshot() + }) + + it('should return success for default endpoint (no endpoint specified)', async () => { + const data = {} + mockEthereumRpcSuccess(MOCK_ROUND_VALUE_HEX) + mockEthereumRpcSingleRequest(MOCK_ROUND_VALUE_HEX) + + await testAdapter.request(data) + const response = await testAdapter.request(data) + + expect(response.statusCode).toBe(200) + const json = response.json() + expect(json.data.result).toBe(MOCK_ROUND_VALUE_DECIMAL) + expect(json.result).toBe(MOCK_ROUND_VALUE_DECIMAL) + expect(response.json()).toMatchSnapshot() + }) + }) + + describe('upstream failures', () => { + it('should handle RPC endpoint failure with 502', async () => { + const data = { endpoint: 'round' } + mockEthereumRpcFailure() + + await testAdapter.request(data) + const response = await testAdapter.request(data) + + expect(response.statusCode).toBe(502) + expect(response.json()).toMatchSnapshot() + }) + + it('should handle contract execution revert', async () => { + const data = { endpoint: 'round' } + mockEthereumRpcContractError() + mockEthereumRpcContractErrorSingle() + + await testAdapter.request(data) + const response = await testAdapter.request(data) + + expect(response.statusCode).toBe(502) + expect(response.json()).toMatchSnapshot() + }) + }) + }) +}) diff --git a/packages/sources/xusd-usd-exchange-rate/test/integration/fixtures.ts b/packages/sources/xusd-usd-exchange-rate/test/integration/fixtures.ts new file mode 100644 index 0000000000..0d8c7dbc34 --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/test/integration/fixtures.ts @@ -0,0 +1,170 @@ +import nock from 'nock' + +type JsonRpcPayload = { + id: number + method: string + params: Array<{ to: string; data: string }> + jsonrpc: '2.0' +} + +export const XUSD_CONTRACT_ADDRESS = '0xE2Fc85BfB48C4cF147921fBE110cf92Ef9f26F94' +export const ROUND_FUNCTION_SELECTOR = '0x146ca531' + +// Sample round value in hex (represents 1e18 - a realistic exchange rate value) +export const MOCK_ROUND_VALUE_HEX = + '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000' +export const MOCK_ROUND_VALUE_DECIMAL = '1000000000000000000' + +const bigintToEthRpcResult = (value: bigint): string => { + return '0x' + value.toString(16).padStart(64, '0') +} + +export const mockEthereumRpcSuccess = (roundValueHex: string = MOCK_ROUND_VALUE_HEX): nock.Scope => + nock('http://localhost:8545', {}) + .post('/', (body: any) => Array.isArray(body)) + .reply( + 200, + (_uri, requestBody: JsonRpcPayload[]) => { + return requestBody.map((request: JsonRpcPayload) => { + if (request.method === 'eth_chainId') { + return { + jsonrpc: '2.0', + id: request.id, + result: bigintToEthRpcResult(1n), + } + } else if (request.method === 'eth_call') { + const [{ to, data }] = request.params + if ( + to.toLowerCase() === XUSD_CONTRACT_ADDRESS.toLowerCase() && + data === ROUND_FUNCTION_SELECTOR + ) { + return { + jsonrpc: '2.0', + id: request.id, + result: roundValueHex, + } + } + } + console.log('Unmocked Ethereum RPC request:', JSON.stringify(request, null, 2)) + return { + jsonrpc: '2.0', + id: request.id, + result: '', + } + }) + }, + ['Content-Type', 'application/json', 'Connection', 'close'], + ) + .persist() + +export const mockEthereumRpcSingleRequest = ( + roundValueHex: string = MOCK_ROUND_VALUE_HEX, +): nock.Scope => + nock('http://localhost:8545', {}) + .post('/', (body: any) => !Array.isArray(body)) + .reply( + 200, + (_uri, requestBody: JsonRpcPayload) => { + const request = requestBody + if (request.method === 'eth_chainId') { + return { + jsonrpc: '2.0', + id: request.id, + result: bigintToEthRpcResult(1n), + } + } else if (request.method === 'eth_call') { + const [{ to, data }] = request.params + if ( + to.toLowerCase() === XUSD_CONTRACT_ADDRESS.toLowerCase() && + data === ROUND_FUNCTION_SELECTOR + ) { + return { + jsonrpc: '2.0', + id: request.id, + result: roundValueHex, + } + } + } + console.log('Unmocked single Ethereum RPC request:', JSON.stringify(request, null, 2)) + return { + jsonrpc: '2.0', + id: request.id, + result: '', + } + }, + ['Content-Type', 'application/json', 'Connection', 'close'], + ) + .persist() + +export const mockEthereumRpcFailure = (): nock.Scope => + nock('http://localhost:8545', {}) + .post('/') + .reply(500, { error: 'Internal Server Error' }) + .persist() + +export const mockEthereumRpcContractError = (): nock.Scope => + nock('http://localhost:8545', {}) + .post('/', (body: any) => Array.isArray(body)) + .reply( + 200, + (_uri, requestBody: JsonRpcPayload[]) => { + return requestBody.map((request: JsonRpcPayload) => { + if (request.method === 'eth_chainId') { + return { + jsonrpc: '2.0', + id: request.id, + result: bigintToEthRpcResult(1n), + } + } else if (request.method === 'eth_call') { + return { + jsonrpc: '2.0', + id: request.id, + error: { + code: -32000, + message: 'execution reverted', + }, + } + } + return { + jsonrpc: '2.0', + id: request.id, + result: '', + } + }) + }, + ['Content-Type', 'application/json', 'Connection', 'close'], + ) + .persist() + +export const mockEthereumRpcContractErrorSingle = (): nock.Scope => + nock('http://localhost:8545', {}) + .post('/', (body: any) => !Array.isArray(body)) + .reply( + 200, + (_uri, requestBody: JsonRpcPayload) => { + const request = requestBody + if (request.method === 'eth_chainId') { + return { + jsonrpc: '2.0', + id: request.id, + result: bigintToEthRpcResult(1n), + } + } else if (request.method === 'eth_call') { + return { + jsonrpc: '2.0', + id: request.id, + error: { + code: -32000, + message: 'execution reverted', + }, + } + } + return { + jsonrpc: '2.0', + id: request.id, + result: '', + } + }, + ['Content-Type', 'application/json', 'Connection', 'close'], + ) + .persist() diff --git a/packages/sources/xusd-usd-exchange-rate/test/unit/round.test.ts b/packages/sources/xusd-usd-exchange-rate/test/unit/round.test.ts new file mode 100644 index 0000000000..96628af3da --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/test/unit/round.test.ts @@ -0,0 +1,46 @@ +import { hexToDecimalString } from '../../src/transport/round' + +describe('hexToDecimalString', () => { + it('converts zero hex to "0"', () => { + const result = hexToDecimalString('0x0') + expect(result).toBe('0') + }) + + it('converts 64-character padded zero to "0"', () => { + const result = hexToDecimalString( + '0x0000000000000000000000000000000000000000000000000000000000000000', + ) + expect(result).toBe('0') + }) + + it('converts 1e18 hex to decimal string', () => { + const result = hexToDecimalString( + '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000', + ) + expect(result).toBe('1000000000000000000') + }) + + it('converts small hex value to decimal string', () => { + const result = hexToDecimalString('0x1') + expect(result).toBe('1') + }) + + it('converts hex without leading zeros', () => { + const result = hexToDecimalString('0xff') + expect(result).toBe('255') + }) + + it('converts large uint256 value', () => { + const result = hexToDecimalString( + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + ) + expect(result).toBe( + '115792089237316195423570985008687907853269984665640564039457584007913129639935', + ) + }) + + it('converts mid-range value correctly', () => { + const result = hexToDecimalString('0x64') + expect(result).toBe('100') + }) +}) diff --git a/packages/sources/xusd-usd-exchange-rate/tsconfig.json b/packages/sources/xusd-usd-exchange-rate/tsconfig.json new file mode 100644 index 0000000000..f59363fd76 --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/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/xusd-usd-exchange-rate/tsconfig.test.json b/packages/sources/xusd-usd-exchange-rate/tsconfig.test.json new file mode 100755 index 0000000000..e3de28cb5c --- /dev/null +++ b/packages/sources/xusd-usd-exchange-rate/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 fdfd8590b0..ea0e858c3c 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -551,6 +551,9 @@ { "path": "./sources/wrapped" }, + { + "path": "./sources/xusd-usd-exchange-rate" + }, { "path": "./targets/dydx-stark" }, diff --git a/packages/tsconfig.test.json b/packages/tsconfig.test.json index 34b47d0d3a..118fd8f3f8 100644 --- a/packages/tsconfig.test.json +++ b/packages/tsconfig.test.json @@ -551,6 +551,9 @@ { "path": "./sources/wrapped/tsconfig.test.json" }, + { + "path": "./sources/xusd-usd-exchange-rate/tsconfig.test.json" + }, { "path": "./targets/dydx-stark/tsconfig.test.json" }, diff --git a/yarn.lock b/yarn.lock index 91d0f297f8..77e4aa281c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3613,6 +3613,9 @@ __metadata: ts-node: "npm:10.9.2" typescript: "npm:5.8.3" yo: "npm:4.3.1" + dependenciesMeta: + "@chainlink/external-adapter-framework@2.9.0": + unplugged: true languageName: unknown linkType: soft @@ -5540,6 +5543,22 @@ __metadata: languageName: unknown linkType: soft +"@chainlink/xusd-usd-exchange-rate-adapter@workspace:packages/sources/xusd-usd-exchange-rate": + version: 0.0.0-use.local + resolution: "@chainlink/xusd-usd-exchange-rate-adapter@workspace:packages/sources/xusd-usd-exchange-rate" + dependencies: + "@chainlink/external-adapter-framework": "npm:2.11.4" + "@sinonjs/fake-timers": "npm:9.1.2" + "@types/jest": "npm:^29.5.14" + "@types/node": "npm:22.14.1" + "@types/sinonjs__fake-timers": "npm:8.1.5" + ethers: "npm:^6.13.2" + nock: "npm:13.5.6" + tslib: "npm:2.4.1" + typescript: "npm:5.8.3" + languageName: unknown + linkType: soft + "@changesets/apply-release-plan@npm:^7.0.8": version: 7.0.8 resolution: "@changesets/apply-release-plan@npm:7.0.8"