From a74eeaa79fb15d3263e4bef6a3c72ee9a4111644 Mon Sep 17 00:00:00 2001 From: "ea-generator[bot]" Date: Tue, 24 Feb 2026 02:41:15 +0000 Subject: [PATCH] feat(opdata-4777): generated EA --- .pnp.cjs | 24 +- package.json | 5 + .../kalshi-binary-markets/CHANGELOG.md | 0 .../sources/kalshi-binary-markets/README.md | 3 + .../kalshi-binary-markets/package.json | 43 ++++ .../kalshi-binary-markets/src/config/index.ts | 15 ++ .../src/config/overrides.json | 3 + .../src/endpoint/index.ts | 1 + .../src/endpoint/market.ts | 53 +++++ .../kalshi-binary-markets/src/index.ts | 21 ++ .../src/transport/market.ts | 139 +++++++++++ .../kalshi-binary-markets/test-payload.json | 7 + .../__snapshots__/adapter.test.ts.snap | 194 +++++++++++++++ .../test/integration/adapter.test.ts | 154 ++++++++++++ .../test/integration/fixtures.ts | 90 +++++++ .../test/unit/market.test.ts | 222 ++++++++++++++++++ .../kalshi-binary-markets/tsconfig.json | 9 + .../kalshi-binary-markets/tsconfig.test.json | 7 + packages/tsconfig.json | 3 + packages/tsconfig.test.json | 3 + yarn.lock | 18 ++ 21 files changed, 1013 insertions(+), 1 deletion(-) create mode 100644 packages/sources/kalshi-binary-markets/CHANGELOG.md create mode 100644 packages/sources/kalshi-binary-markets/README.md create mode 100644 packages/sources/kalshi-binary-markets/package.json create mode 100644 packages/sources/kalshi-binary-markets/src/config/index.ts create mode 100644 packages/sources/kalshi-binary-markets/src/config/overrides.json create mode 100644 packages/sources/kalshi-binary-markets/src/endpoint/index.ts create mode 100644 packages/sources/kalshi-binary-markets/src/endpoint/market.ts create mode 100644 packages/sources/kalshi-binary-markets/src/index.ts create mode 100644 packages/sources/kalshi-binary-markets/src/transport/market.ts create mode 100644 packages/sources/kalshi-binary-markets/test-payload.json create mode 100644 packages/sources/kalshi-binary-markets/test/integration/__snapshots__/adapter.test.ts.snap create mode 100644 packages/sources/kalshi-binary-markets/test/integration/adapter.test.ts create mode 100644 packages/sources/kalshi-binary-markets/test/integration/fixtures.ts create mode 100644 packages/sources/kalshi-binary-markets/test/unit/market.test.ts create mode 100644 packages/sources/kalshi-binary-markets/tsconfig.json create mode 100755 packages/sources/kalshi-binary-markets/tsconfig.test.json diff --git a/.pnp.cjs b/.pnp.cjs index eef0fd263a..051d3c8114 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -486,6 +486,10 @@ const RAW_RUNTIME_STATE = "name": "@chainlink/kaiko-state-adapter",\ "reference": "workspace:packages/sources/kaiko-state"\ },\ + {\ + "name": "@chainlink/kalshi-binary-markets-adapter",\ + "reference": "workspace:packages/sources/kalshi-binary-markets"\ + },\ {\ "name": "@chainlink/layer2-sequencer-health-adapter",\ "reference": "workspace:packages/sources/layer2-sequencer-health"\ @@ -871,6 +875,7 @@ const RAW_RUNTIME_STATE = ["@chainlink/json-rpc-adapter", ["workspace:packages/sources/json-rpc"]],\ ["@chainlink/kaiko-adapter", ["workspace:packages/sources/kaiko"]],\ ["@chainlink/kaiko-state-adapter", ["workspace:packages/sources/kaiko-state"]],\ + ["@chainlink/kalshi-binary-markets-adapter", ["workspace:packages/sources/kalshi-binary-markets"]],\ ["@chainlink/layer2-sequencer-health-adapter", ["workspace:packages/sources/layer2-sequencer-health"]],\ ["@chainlink/lcx-adapter", ["workspace:packages/sources/lcx"]],\ ["@chainlink/lido-adapter", ["workspace:packages/sources/lido"]],\ @@ -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"],\ @@ -6783,6 +6788,23 @@ const RAW_RUNTIME_STATE = "linkType": "SOFT"\ }]\ ]],\ + ["@chainlink/kalshi-binary-markets-adapter", [\ + ["workspace:packages/sources/kalshi-binary-markets", {\ + "packageLocation": "./packages/sources/kalshi-binary-markets/",\ + "packageDependencies": [\ + ["@chainlink/kalshi-binary-markets-adapter", "workspace:packages/sources/kalshi-binary-markets"],\ + ["@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"],\ + ["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/layer2-sequencer-health-adapter", [\ ["workspace:packages/sources/layer2-sequencer-health", {\ "packageLocation": "./packages/sources/layer2-sequencer-health/",\ 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/kalshi-binary-markets/CHANGELOG.md b/packages/sources/kalshi-binary-markets/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/sources/kalshi-binary-markets/README.md b/packages/sources/kalshi-binary-markets/README.md new file mode 100644 index 0000000000..91ab1d4455 --- /dev/null +++ b/packages/sources/kalshi-binary-markets/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/kalshi-binary-markets/package.json b/packages/sources/kalshi-binary-markets/package.json new file mode 100644 index 0000000000..7f4fb2f1d2 --- /dev/null +++ b/packages/sources/kalshi-binary-markets/package.json @@ -0,0 +1,43 @@ +{ + "name": "@chainlink/kalshi-binary-markets-adapter", + "version": "0.0.0", + "description": "Chainlink External Adapter for fetching Kalshi Binary Market data (prices, status, settlement)", + "keywords": [ + "Chainlink", + "LINK", + "blockchain", + "oracle", + "kalshi", + "binary-markets" + ], + "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", + "tslib": "2.4.1" + } +} diff --git a/packages/sources/kalshi-binary-markets/src/config/index.ts b/packages/sources/kalshi-binary-markets/src/config/index.ts new file mode 100644 index 0000000000..37cebb87ea --- /dev/null +++ b/packages/sources/kalshi-binary-markets/src/config/index.ts @@ -0,0 +1,15 @@ +import { AdapterConfig } from '@chainlink/external-adapter-framework/config' + +export const config = new AdapterConfig({ + API_KEY: { + description: 'API key for Kalshi', + type: 'string', + required: true, + sensitive: true, + }, + API_ENDPOINT: { + description: 'API endpoint for Kalshi', + type: 'string', + default: 'https://api.kalshi.com/v1', + }, +}) diff --git a/packages/sources/kalshi-binary-markets/src/config/overrides.json b/packages/sources/kalshi-binary-markets/src/config/overrides.json new file mode 100644 index 0000000000..6121143f42 --- /dev/null +++ b/packages/sources/kalshi-binary-markets/src/config/overrides.json @@ -0,0 +1,3 @@ +{ + "kalshi-binary-markets": {} +} diff --git a/packages/sources/kalshi-binary-markets/src/endpoint/index.ts b/packages/sources/kalshi-binary-markets/src/endpoint/index.ts new file mode 100644 index 0000000000..844dfd72cd --- /dev/null +++ b/packages/sources/kalshi-binary-markets/src/endpoint/index.ts @@ -0,0 +1 @@ +export { endpoint as market } from './market' diff --git a/packages/sources/kalshi-binary-markets/src/endpoint/market.ts b/packages/sources/kalshi-binary-markets/src/endpoint/market.ts new file mode 100644 index 0000000000..d5fb602abd --- /dev/null +++ b/packages/sources/kalshi-binary-markets/src/endpoint/market.ts @@ -0,0 +1,53 @@ +import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { InputParameters } from '@chainlink/external-adapter-framework/validation' +import { config } from '../config' +import { httpTransport } from '../transport/market' + +export const inputParameters = new InputParameters( + { + market_ticker: { + required: true, + type: 'string', + description: 'The market ticker symbol', + }, + }, + [ + { + market_ticker: 'KXUSIRATECUTS25MAR', + }, + ], +) + +export interface MarketResult { + market_ticker: string + event_ticker: string + market_status: number + settlement_flag: number + yes_bid_price: number + yes_ask_price: number + no_bid_price: number + no_ask_price: number + yes_mid_price: number + no_mid_price: number + open_interest: number + category: string + close_timestamp: number + updated_at: number +} + +export type BaseEndpointTypes = { + Parameters: typeof inputParameters.definition + Response: { + Result: null + Data: { + result: MarketResult + } + } + Settings: typeof config.settings +} + +export const endpoint = new AdapterEndpoint({ + name: 'market', + transport: httpTransport, + inputParameters, +}) diff --git a/packages/sources/kalshi-binary-markets/src/index.ts b/packages/sources/kalshi-binary-markets/src/index.ts new file mode 100644 index 0000000000..9b3e4ff7f5 --- /dev/null +++ b/packages/sources/kalshi-binary-markets/src/index.ts @@ -0,0 +1,21 @@ +import { expose, ServerInstance } from '@chainlink/external-adapter-framework' +import { Adapter } from '@chainlink/external-adapter-framework/adapter' +import { config } from './config' +import { market } from './endpoint' + +export const adapter = new Adapter({ + defaultEndpoint: market.name, + name: 'KALSHI_BINARY_MARKETS', + config, + endpoints: [market], + rateLimiting: { + tiers: { + default: { + rateLimit1s: 10, + note: 'Kalshi API rate limit', + }, + }, + }, +}) + +export const server = (): Promise => expose(adapter) diff --git a/packages/sources/kalshi-binary-markets/src/transport/market.ts b/packages/sources/kalshi-binary-markets/src/transport/market.ts new file mode 100644 index 0000000000..62f39a5117 --- /dev/null +++ b/packages/sources/kalshi-binary-markets/src/transport/market.ts @@ -0,0 +1,139 @@ +import { HttpTransport } from '@chainlink/external-adapter-framework/transports' +import { BaseEndpointTypes } from '../endpoint/market' + +export interface MarketData { + can_close_early: boolean + category: string + close_time: string + event_ticker: string + expiration_time: string + last_price: number + liquidity: number + market_type: string + no_ask: number + no_bid: number + open_interest: number + status: string + ticker: string + yes_ask: number + yes_bid: number + result: string | null +} + +export interface ResponseSchema { + market: MarketData +} + +type HttpTransportTypes = BaseEndpointTypes & { + Provider: { + RequestBody: never + ResponseBody: ResponseSchema + } +} + +export const convertStatusToCode = (status: string): number => { + switch (status) { + case 'active': + return 0 + case 'closed': + return 1 + case 'settled': + return 2 + default: + return 0 + } +} + +export const convertResultToFlag = (result: string | null): number => { + if (result === null) return 0 + if (result === 'yes') return 1 + if (result === 'no') return 2 + return 0 +} + +export const parseTimestamp = (isoString: string): number => { + return Math.floor(new Date(isoString).getTime() / 1000) +} + +export const calculateMidPrice = (bid: number, ask: number): number => { + return (bid + ask) / 2 +} + +export const buildMarketResult = ( + market: MarketData, + updatedAt: number, +): { + market_ticker: string + event_ticker: string + market_status: number + settlement_flag: number + yes_bid_price: number + yes_ask_price: number + no_bid_price: number + no_ask_price: number + yes_mid_price: number + no_mid_price: number + open_interest: number + category: string + close_timestamp: number + updated_at: number +} => { + return { + market_ticker: market.ticker, + event_ticker: market.event_ticker, + market_status: convertStatusToCode(market.status), + settlement_flag: convertResultToFlag(market.result), + yes_bid_price: market.yes_bid, + yes_ask_price: market.yes_ask, + no_bid_price: market.no_bid, + no_ask_price: market.no_ask, + yes_mid_price: calculateMidPrice(market.yes_bid, market.yes_ask), + no_mid_price: calculateMidPrice(market.no_bid, market.no_ask), + open_interest: market.open_interest, + category: market.category, + close_timestamp: parseTimestamp(market.close_time), + updated_at: updatedAt, + } +} + +export const httpTransport = new HttpTransport({ + prepareRequests: (params, config) => { + return params.map((param) => ({ + params: [param], + request: { + baseURL: config.API_ENDPOINT, + url: `/markets/${param.market_ticker}`, + headers: { + accept: 'application/json', + Authorization: `Bearer ${config.API_KEY}`, + }, + }, + })) + }, + parseResponse: (params, response) => { + if (!response.data?.market) { + return params.map((param) => ({ + params: param, + response: { + errorMessage: `No market data returned for ${param.market_ticker}`, + statusCode: 502, + }, + })) + } + + const market = response.data.market + const updatedAt = Math.floor(Date.now() / 1000) + + return params.map((param) => { + return { + params: param, + response: { + result: null, + data: { + result: buildMarketResult(market, updatedAt), + }, + }, + } + }) + }, +}) diff --git a/packages/sources/kalshi-binary-markets/test-payload.json b/packages/sources/kalshi-binary-markets/test-payload.json new file mode 100644 index 0000000000..50428890b2 --- /dev/null +++ b/packages/sources/kalshi-binary-markets/test-payload.json @@ -0,0 +1,7 @@ +{ + "requests": [ + { + "market_ticker": "KXUSIRATECUTS25MAR" + } + ] +} diff --git a/packages/sources/kalshi-binary-markets/test/integration/__snapshots__/adapter.test.ts.snap b/packages/sources/kalshi-binary-markets/test/integration/__snapshots__/adapter.test.ts.snap new file mode 100644 index 0000000000..63b9528e32 --- /dev/null +++ b/packages/sources/kalshi-binary-markets/test/integration/__snapshots__/adapter.test.ts.snap @@ -0,0 +1,194 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`execute market endpoint happy path should return success for active market 1`] = ` +{ + "data": { + "result": { + "category": "Macro", + "close_timestamp": 1742407200, + "event_ticker": "USIRATECUTS25", + "market_status": 0, + "market_ticker": "KXUSIRATECUTS25MAR", + "no_ask_price": 52, + "no_bid_price": 48, + "no_mid_price": 50, + "open_interest": 104923, + "settlement_flag": 0, + "updated_at": 978347471, + "yes_ask_price": 52, + "yes_bid_price": 48, + "yes_mid_price": 50, + }, + }, + "result": null, + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; + +exports[`execute market endpoint happy path should return success for closed market with yes result 1`] = ` +{ + "data": { + "result": { + "category": "Politics", + "close_timestamp": 1730847600, + "event_ticker": "PRESWIN24", + "market_status": 1, + "market_ticker": "PRESWIN24", + "no_ask_price": 0, + "no_bid_price": 0, + "no_mid_price": 0, + "open_interest": 250000, + "settlement_flag": 1, + "updated_at": 978347471, + "yes_ask_price": 100, + "yes_bid_price": 100, + "yes_mid_price": 100, + }, + }, + "result": null, + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; + +exports[`execute market endpoint happy path should return success for settled market with no result 1`] = ` +{ + "data": { + "result": { + "category": "Economics", + "close_timestamp": 1735689599, + "event_ticker": "GDP2024", + "market_status": 2, + "market_ticker": "GDP2024Q4", + "no_ask_price": 0, + "no_bid_price": 0, + "no_mid_price": 0, + "open_interest": 50000, + "settlement_flag": 2, + "updated_at": 978347471, + "yes_ask_price": 0, + "yes_bid_price": 0, + "yes_mid_price": 0, + }, + }, + "result": null, + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; + +exports[`execute market endpoint happy path should use default endpoint when not specified 1`] = ` +{ + "data": { + "result": { + "category": "Macro", + "close_timestamp": 1742407200, + "event_ticker": "USIRATECUTS25", + "market_status": 0, + "market_ticker": "KXUSIRATECUTS25MAR", + "no_ask_price": 52, + "no_bid_price": 48, + "no_mid_price": 50, + "open_interest": 104923, + "settlement_flag": 0, + "updated_at": 978347471, + "yes_ask_price": 52, + "yes_bid_price": 48, + "yes_mid_price": 50, + }, + }, + "result": null, + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; + +exports[`execute market endpoint upstream failures should handle 401 unauthorized error 1`] = ` +{ + "errorMessage": "Provider request failed with status 401: {"error":"Unauthorized"}", + "statusCode": 502, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; + +exports[`execute market endpoint upstream failures should handle 404 not found error 1`] = ` +{ + "errorMessage": "Provider request failed with status 404: {"error":"Market not found"}", + "statusCode": 502, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; + +exports[`execute market endpoint upstream failures should handle 500 server error 1`] = ` +{ + "errorMessage": "Provider request failed with status 500: {"error":"Internal Server Error"}", + "statusCode": 502, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; + +exports[`execute market endpoint upstream failures should handle empty response from upstream 1`] = ` +{ + "errorMessage": "No market data returned for EMPTY_MARKET", + "statusCode": 502, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; + +exports[`execute market endpoint validation errors should fail on empty request 1`] = ` +{ + "error": { + "message": "[Param: market_ticker] param is required but no value was provided", + "name": "AdapterError", + }, + "status": "errored", + "statusCode": 400, +} +`; + +exports[`execute market endpoint validation errors should fail on invalid endpoint 1`] = ` +{ + "error": { + "message": "Adapter does not have a "invalid_endpoint" endpoint.", + "name": "AdapterError", + }, + "status": "errored", + "statusCode": 404, +} +`; + +exports[`execute market endpoint validation errors should fail on missing market_ticker 1`] = ` +{ + "error": { + "message": "[Param: market_ticker] param is required but no value was provided", + "name": "AdapterError", + }, + "status": "errored", + "statusCode": 400, +} +`; diff --git a/packages/sources/kalshi-binary-markets/test/integration/adapter.test.ts b/packages/sources/kalshi-binary-markets/test/integration/adapter.test.ts new file mode 100644 index 0000000000..4561fffd86 --- /dev/null +++ b/packages/sources/kalshi-binary-markets/test/integration/adapter.test.ts @@ -0,0 +1,154 @@ +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)) + const mockDate = new Date('2001-01-01T11:11:11.111Z') + spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) + process.env.API_KEY = process.env.API_KEY ?? 'test-api-key' + 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('market endpoint', () => { + describe('happy path', () => { + it('should return success for active market', async () => { + const data = { + market_ticker: 'KXUSIRATECUTS25MAR', + endpoint: 'market', + } + mockResponseSuccess() + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + + it('should return success for closed market with yes result', async () => { + const data = { + market_ticker: 'PRESWIN24', + endpoint: 'market', + } + mockResponseSuccess() + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + + it('should return success for settled market with no result', async () => { + const data = { + market_ticker: 'GDP2024Q4', + endpoint: 'market', + } + mockResponseSuccess() + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + + it('should use default endpoint when not specified', async () => { + const data = { + market_ticker: 'KXUSIRATECUTS25MAR', + } + mockResponseSuccess() + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + }) + + describe('validation errors', () => { + it('should fail on empty request', async () => { + const response = await testAdapter.request({}) + expect(response.statusCode).toBe(400) + expect(response.json()).toMatchSnapshot() + }) + + it('should fail on missing market_ticker', async () => { + const response = await testAdapter.request({ + endpoint: 'market', + }) + expect(response.statusCode).toBe(400) + expect(response.json()).toMatchSnapshot() + }) + + it('should fail on invalid endpoint', async () => { + const response = await testAdapter.request({ + market_ticker: 'KXUSIRATECUTS25MAR', + endpoint: 'invalid_endpoint', + }) + expect(response.statusCode).toBe(404) + expect(response.json()).toMatchSnapshot() + }) + }) + + describe('upstream failures', () => { + it('should handle 404 not found error', async () => { + const data = { + market_ticker: 'INVALID_TICKER', + endpoint: 'market', + } + mockResponseSuccess() + await testAdapter.request(data) + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(502) + expect(response.json()).toMatchSnapshot() + }) + + it('should handle empty response from upstream', async () => { + const data = { + market_ticker: 'EMPTY_MARKET', + endpoint: 'market', + } + mockResponseSuccess() + await testAdapter.request(data) + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(502) + expect(response.json()).toMatchSnapshot() + }) + + it('should handle 500 server error', async () => { + const data = { + market_ticker: 'SERVER_ERROR', + endpoint: 'market', + } + mockResponseSuccess() + await testAdapter.request(data) + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(502) + expect(response.json()).toMatchSnapshot() + }) + + it('should handle 401 unauthorized error', async () => { + const data = { + market_ticker: 'UNAUTHORIZED', + endpoint: 'market', + } + mockResponseSuccess() + await testAdapter.request(data) + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(502) + expect(response.json()).toMatchSnapshot() + }) + }) + }) +}) diff --git a/packages/sources/kalshi-binary-markets/test/integration/fixtures.ts b/packages/sources/kalshi-binary-markets/test/integration/fixtures.ts new file mode 100644 index 0000000000..ba321a4eaa --- /dev/null +++ b/packages/sources/kalshi-binary-markets/test/integration/fixtures.ts @@ -0,0 +1,90 @@ +import nock from 'nock' + +const API_ENDPOINT = 'https://api.kalshi.com/v1' + +export const mockMarketResponse = { + market: { + can_close_early: true, + category: 'Macro', + close_time: '2025-03-19T18:00:00Z', + event_ticker: 'USIRATECUTS25', + expiration_time: '2025-03-19T18:00:00Z', + last_price: 50, + liquidity: 1000000, + market_type: 'binary', + no_ask: 52, + no_bid: 48, + open_interest: 104923, + status: 'active', + ticker: 'KXUSIRATECUTS25MAR', + yes_ask: 52, + yes_bid: 48, + result: null, + }, +} + +export const mockClosedMarketYesResponse = { + market: { + can_close_early: false, + category: 'Politics', + close_time: '2024-11-05T23:00:00Z', + event_ticker: 'PRESWIN24', + expiration_time: '2024-11-05T23:00:00Z', + last_price: 100, + liquidity: 0, + market_type: 'binary', + no_ask: 0, + no_bid: 0, + open_interest: 250000, + status: 'closed', + ticker: 'PRESWIN24', + yes_ask: 100, + yes_bid: 100, + result: 'yes', + }, +} + +export const mockSettledMarketNoResponse = { + market: { + can_close_early: false, + category: 'Economics', + close_time: '2024-12-31T23:59:59Z', + event_ticker: 'GDP2024', + expiration_time: '2024-12-31T23:59:59Z', + last_price: 0, + liquidity: 0, + market_type: 'binary', + no_ask: 0, + no_bid: 0, + open_interest: 50000, + status: 'settled', + ticker: 'GDP2024Q4', + yes_ask: 0, + yes_bid: 0, + result: 'no', + }, +} + +export const mockResponseSuccess = (): nock.Scope => + nock(API_ENDPOINT) + .persist() + .get('/markets/KXUSIRATECUTS25MAR') + .reply(200, mockMarketResponse) + .persist() + .get('/markets/PRESWIN24') + .reply(200, mockClosedMarketYesResponse) + .persist() + .get('/markets/GDP2024Q4') + .reply(200, mockSettledMarketNoResponse) + .persist() + .get('/markets/INVALID_TICKER') + .reply(404, { error: 'Market not found' }) + .persist() + .get('/markets/EMPTY_MARKET') + .reply(200, {}) + .persist() + .get('/markets/SERVER_ERROR') + .reply(500, { error: 'Internal Server Error' }) + .persist() + .get('/markets/UNAUTHORIZED') + .reply(401, { error: 'Unauthorized' }) diff --git a/packages/sources/kalshi-binary-markets/test/unit/market.test.ts b/packages/sources/kalshi-binary-markets/test/unit/market.test.ts new file mode 100644 index 0000000000..01334e17f3 --- /dev/null +++ b/packages/sources/kalshi-binary-markets/test/unit/market.test.ts @@ -0,0 +1,222 @@ +import { + buildMarketResult, + calculateMidPrice, + convertResultToFlag, + convertStatusToCode, + MarketData, + parseTimestamp, +} from '../../src/transport/market' + +describe('convertStatusToCode', () => { + it('returns 0 for active status', () => { + expect(convertStatusToCode('active')).toBe(0) + }) + + it('returns 1 for closed status', () => { + expect(convertStatusToCode('closed')).toBe(1) + }) + + it('returns 2 for settled status', () => { + expect(convertStatusToCode('settled')).toBe(2) + }) + + it('returns 0 for unknown status', () => { + expect(convertStatusToCode('unknown')).toBe(0) + }) + + it('returns 0 for empty string', () => { + expect(convertStatusToCode('')).toBe(0) + }) +}) + +describe('convertResultToFlag', () => { + it('returns 0 for null result', () => { + expect(convertResultToFlag(null)).toBe(0) + }) + + it('returns 1 for yes result', () => { + expect(convertResultToFlag('yes')).toBe(1) + }) + + it('returns 2 for no result', () => { + expect(convertResultToFlag('no')).toBe(2) + }) + + it('returns 0 for unknown result string', () => { + expect(convertResultToFlag('maybe')).toBe(0) + }) + + it('returns 0 for empty string', () => { + expect(convertResultToFlag('')).toBe(0) + }) +}) + +describe('parseTimestamp', () => { + it('converts ISO string to Unix timestamp in seconds', () => { + const isoString = '2025-03-19T18:00:00Z' + const expected = Math.floor(new Date(isoString).getTime() / 1000) + expect(parseTimestamp(isoString)).toBe(expected) + }) + + it('handles ISO string with milliseconds', () => { + const isoString = '2024-12-31T23:59:59.999Z' + const expected = Math.floor(new Date(isoString).getTime() / 1000) + expect(parseTimestamp(isoString)).toBe(expected) + }) + + it('returns timestamp for epoch date', () => { + const isoString = '1970-01-01T00:00:00Z' + expect(parseTimestamp(isoString)).toBe(0) + }) +}) + +describe('calculateMidPrice', () => { + it('calculates mid price correctly for equal bid and ask', () => { + expect(calculateMidPrice(50, 50)).toBe(50) + }) + + it('calculates mid price correctly for different bid and ask', () => { + expect(calculateMidPrice(48, 52)).toBe(50) + }) + + it('handles zero values', () => { + expect(calculateMidPrice(0, 0)).toBe(0) + }) + + it('handles decimal values', () => { + expect(calculateMidPrice(45.5, 54.5)).toBe(50) + }) + + it('handles large values', () => { + expect(calculateMidPrice(1000000, 2000000)).toBe(1500000) + }) +}) + +describe('buildMarketResult', () => { + const mockMarketData: MarketData = { + can_close_early: true, + category: 'Macro', + close_time: '2025-03-19T18:00:00Z', + event_ticker: 'USIRATECUTS25', + expiration_time: '2025-03-19T18:00:00Z', + last_price: 50, + liquidity: 1000000, + market_type: 'binary', + no_ask: 52, + no_bid: 48, + open_interest: 104923, + status: 'active', + ticker: 'KXUSIRATECUTS25MAR', + yes_ask: 52, + yes_bid: 48, + result: null, + } + + const fixedUpdatedAt = 1700000000 + + it('correctly maps market_ticker from ticker', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + expect(result.market_ticker).toBe('KXUSIRATECUTS25MAR') + }) + + it('correctly maps event_ticker', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + expect(result.event_ticker).toBe('USIRATECUTS25') + }) + + it('converts status to market_status code', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + expect(result.market_status).toBe(0) // active = 0 + }) + + it('converts null result to settlement_flag 0', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + expect(result.settlement_flag).toBe(0) + }) + + it('maps yes_bid_price correctly', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + expect(result.yes_bid_price).toBe(48) + }) + + it('maps yes_ask_price correctly', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + expect(result.yes_ask_price).toBe(52) + }) + + it('maps no_bid_price correctly', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + expect(result.no_bid_price).toBe(48) + }) + + it('maps no_ask_price correctly', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + expect(result.no_ask_price).toBe(52) + }) + + it('calculates yes_mid_price correctly', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + expect(result.yes_mid_price).toBe(50) // (48 + 52) / 2 + }) + + it('calculates no_mid_price correctly', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + expect(result.no_mid_price).toBe(50) // (48 + 52) / 2 + }) + + it('maps open_interest correctly', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + expect(result.open_interest).toBe(104923) + }) + + it('maps category correctly', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + expect(result.category).toBe('Macro') + }) + + it('parses close_timestamp correctly', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + const expectedTimestamp = Math.floor(new Date('2025-03-19T18:00:00Z').getTime() / 1000) + expect(result.close_timestamp).toBe(expectedTimestamp) + }) + + it('uses provided updated_at value', () => { + const result = buildMarketResult(mockMarketData, fixedUpdatedAt) + expect(result.updated_at).toBe(1700000000) + }) + + it('handles closed market with yes result', () => { + const closedMarket: MarketData = { + ...mockMarketData, + status: 'closed', + result: 'yes', + } + const result = buildMarketResult(closedMarket, fixedUpdatedAt) + expect(result.market_status).toBe(1) // closed = 1 + expect(result.settlement_flag).toBe(1) // yes = 1 + }) + + it('handles settled market with no result', () => { + const settledMarket: MarketData = { + ...mockMarketData, + status: 'settled', + result: 'no', + } + const result = buildMarketResult(settledMarket, fixedUpdatedAt) + expect(result.market_status).toBe(2) // settled = 2 + expect(result.settlement_flag).toBe(2) // no = 2 + }) + + it('handles zero price values', () => { + const zeroMarket: MarketData = { + ...mockMarketData, + yes_bid: 0, + yes_ask: 0, + no_bid: 0, + no_ask: 0, + } + const result = buildMarketResult(zeroMarket, fixedUpdatedAt) + expect(result.yes_mid_price).toBe(0) + expect(result.no_mid_price).toBe(0) + }) +}) diff --git a/packages/sources/kalshi-binary-markets/tsconfig.json b/packages/sources/kalshi-binary-markets/tsconfig.json new file mode 100644 index 0000000000..f59363fd76 --- /dev/null +++ b/packages/sources/kalshi-binary-markets/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/kalshi-binary-markets/tsconfig.test.json b/packages/sources/kalshi-binary-markets/tsconfig.test.json new file mode 100755 index 0000000000..e3de28cb5c --- /dev/null +++ b/packages/sources/kalshi-binary-markets/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..789bcf7458 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -347,6 +347,9 @@ { "path": "./sources/kaiko-state" }, + { + "path": "./sources/kalshi-binary-markets" + }, { "path": "./sources/layer2-sequencer-health" }, diff --git a/packages/tsconfig.test.json b/packages/tsconfig.test.json index 34b47d0d3a..d7a3da2331 100644 --- a/packages/tsconfig.test.json +++ b/packages/tsconfig.test.json @@ -347,6 +347,9 @@ { "path": "./sources/kaiko-state/tsconfig.test.json" }, + { + "path": "./sources/kalshi-binary-markets/tsconfig.test.json" + }, { "path": "./sources/layer2-sequencer-health/tsconfig.test.json" }, diff --git a/yarn.lock b/yarn.lock index 91d0f297f8..3bb2401201 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 @@ -4128,6 +4131,21 @@ __metadata: languageName: unknown linkType: soft +"@chainlink/kalshi-binary-markets-adapter@workspace:packages/sources/kalshi-binary-markets": + version: 0.0.0-use.local + resolution: "@chainlink/kalshi-binary-markets-adapter@workspace:packages/sources/kalshi-binary-markets" + 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" + nock: "npm:13.5.6" + tslib: "npm:2.4.1" + typescript: "npm:5.8.3" + languageName: unknown + linkType: soft + "@chainlink/layer2-sequencer-health-adapter@workspace:packages/sources/layer2-sequencer-health": version: 0.0.0-use.local resolution: "@chainlink/layer2-sequencer-health-adapter@workspace:packages/sources/layer2-sequencer-health"