From 3b1f95f845fb0c5d6f1b4a71557809dbc848d04b Mon Sep 17 00:00:00 2001 From: mayowa Date: Tue, 14 Oct 2025 17:23:41 +0100 Subject: [PATCH 1/5] add canton ea endpoint and transport --- .pnp.cjs | 22 +++++ .../sources/canton-functions/CHANGELOG.md | 0 packages/sources/canton-functions/README.md | 3 + .../sources/canton-functions/package.json | 42 +++++++++ .../canton-functions/src/config/index.ts | 21 +++++ .../src/endpoint/canton-data.ts | 38 ++++++++ .../canton-functions/src/endpoint/index.ts | 1 + .../sources/canton-functions/src/index.ts | 13 +++ .../src/shared/canton-client.ts | 64 +++++++++++++ .../src/transport/canton-data.ts | 93 +++++++++++++++++++ .../canton-functions/test-payload.json | 10 ++ .../canton-query-contracts-response.json | 33 +++++++ .../__snapshots__/canton-data.test.ts.snap | 53 +++++++++++ .../test/integration/adapter.test.ts | 33 +++++++ .../test/integration/canton-data.test.ts | 64 +++++++++++++ .../test/integration/fixtures.ts | 32 +++++++ .../sources/canton-functions/tsconfig.json | 9 ++ .../canton-functions/tsconfig.test.json | 7 ++ yarn.lock | 15 +++ 19 files changed, 553 insertions(+) create mode 100644 packages/sources/canton-functions/CHANGELOG.md create mode 100644 packages/sources/canton-functions/README.md create mode 100644 packages/sources/canton-functions/package.json create mode 100644 packages/sources/canton-functions/src/config/index.ts create mode 100644 packages/sources/canton-functions/src/endpoint/canton-data.ts create mode 100644 packages/sources/canton-functions/src/endpoint/index.ts create mode 100644 packages/sources/canton-functions/src/index.ts create mode 100644 packages/sources/canton-functions/src/shared/canton-client.ts create mode 100644 packages/sources/canton-functions/src/transport/canton-data.ts create mode 100644 packages/sources/canton-functions/test-payload.json create mode 100644 packages/sources/canton-functions/test/fixtures/canton-query-contracts-response.json create mode 100644 packages/sources/canton-functions/test/integration/__snapshots__/canton-data.test.ts.snap create mode 100644 packages/sources/canton-functions/test/integration/adapter.test.ts create mode 100644 packages/sources/canton-functions/test/integration/canton-data.test.ts create mode 100644 packages/sources/canton-functions/test/integration/fixtures.ts create mode 100644 packages/sources/canton-functions/tsconfig.json create mode 100755 packages/sources/canton-functions/tsconfig.test.json diff --git a/.pnp.cjs b/.pnp.cjs index e994dab804..4f373b024b 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -314,6 +314,10 @@ const RAW_RUNTIME_STATE = "name": "@chainlink/cache.gold-adapter",\ "reference": "workspace:packages/sources/cache.gold"\ },\ + {\ + "name": "@chainlink/canton-functions-adapter",\ + "reference": "workspace:packages/sources/canton-functions"\ + },\ {\ "name": "@chainlink/ceffu-adapter",\ "reference": "workspace:packages/sources/ceffu"\ @@ -1005,6 +1009,7 @@ const RAW_RUNTIME_STATE = ["@chainlink/bsol-price-adapter", ["workspace:packages/composites/bsol-price"]],\ ["@chainlink/btc.com-adapter", ["workspace:packages/sources/btc.com"]],\ ["@chainlink/cache.gold-adapter", ["workspace:packages/sources/cache.gold"]],\ + ["@chainlink/canton-functions-adapter", ["workspace:packages/sources/canton-functions"]],\ ["@chainlink/ceffu-adapter", ["workspace:packages/sources/ceffu"]],\ ["@chainlink/celsius-address-list-adapter", ["workspace:packages/sources/celsius-address-list"]],\ ["@chainlink/cfbenchmarks-adapter", ["workspace:packages/sources/cfbenchmarks"]],\ @@ -5785,6 +5790,23 @@ const RAW_RUNTIME_STATE = "linkType": "SOFT"\ }]\ ]],\ + ["@chainlink/canton-functions-adapter", [\ + ["workspace:packages/sources/canton-functions", {\ + "packageLocation": "./packages/sources/canton-functions/",\ + "packageDependencies": [\ + ["@chainlink/canton-functions-adapter", "workspace:packages/sources/canton-functions"],\ + ["@chainlink/external-adapter-framework", "npm:2.7.1"],\ + ["@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/ceffu-adapter", [\ ["workspace:packages/sources/ceffu", {\ "packageLocation": "./packages/sources/ceffu/",\ diff --git a/packages/sources/canton-functions/CHANGELOG.md b/packages/sources/canton-functions/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/sources/canton-functions/README.md b/packages/sources/canton-functions/README.md new file mode 100644 index 0000000000..36e508b35d --- /dev/null +++ b/packages/sources/canton-functions/README.md @@ -0,0 +1,3 @@ +# Chainlink External Adapter for canton-functions + +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 canton-functions`. diff --git a/packages/sources/canton-functions/package.json b/packages/sources/canton-functions/package.json new file mode 100644 index 0000000000..95048c12f9 --- /dev/null +++ b/packages/sources/canton-functions/package.json @@ -0,0 +1,42 @@ +{ + "name": "@chainlink/canton-functions-adapter", + "version": "1.0.0", + "description": "Chainlink canton-functions adapter.", + "keywords": [ + "Chainlink", + "LINK", + "blockchain", + "oracle", + "canton-functions" + ], + "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.7.1", + "tslib": "2.4.1" + } +} diff --git a/packages/sources/canton-functions/src/config/index.ts b/packages/sources/canton-functions/src/config/index.ts new file mode 100644 index 0000000000..ae15e5b5c3 --- /dev/null +++ b/packages/sources/canton-functions/src/config/index.ts @@ -0,0 +1,21 @@ +import { AdapterConfig } from '@chainlink/external-adapter-framework/config' + +export const config = new AdapterConfig({ + JSON_API: { + description: 'The Canton JSON API URL', + type: 'string', + required: true, + }, + AUTH_TOKEN: { + description: 'JWT token for Canton JSON API authentication', + type: 'string', + required: true, + sensitive: true, + }, + BACKGROUND_EXECUTE_MS: { + description: + 'The amount of time the background execute should sleep before performing the next request', + type: 'number', + default: 1_000, + }, +}) diff --git a/packages/sources/canton-functions/src/endpoint/canton-data.ts b/packages/sources/canton-functions/src/endpoint/canton-data.ts new file mode 100644 index 0000000000..46837cd6c1 --- /dev/null +++ b/packages/sources/canton-functions/src/endpoint/canton-data.ts @@ -0,0 +1,38 @@ +import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { InputParameters } from '@chainlink/external-adapter-framework/validation' +import { config } from '../config' +import { cantonDataTransport } from '../transport/canton-data' + +export const inputParameters = new InputParameters( + { + templateId: { + description: 'The template ID to query contracts for (format: packageId:Module:Template)', + type: 'string', + required: true, + }, + }, + [ + { + templateId: 'example-package-id:Main:Asset', + }, + ], +) + +export type BaseEndpointTypes = { + Parameters: typeof inputParameters.definition + Response: { + Data: { + result: string + contracts: any[] + } + Result: string + } + Settings: typeof config.settings +} + +export const endpoint = new AdapterEndpoint({ + name: 'canton-data', + aliases: [], + transport: cantonDataTransport, + inputParameters, +}) diff --git a/packages/sources/canton-functions/src/endpoint/index.ts b/packages/sources/canton-functions/src/endpoint/index.ts new file mode 100644 index 0000000000..f5e858bfe7 --- /dev/null +++ b/packages/sources/canton-functions/src/endpoint/index.ts @@ -0,0 +1 @@ +export { endpoint as cantonData } from './canton-data' diff --git a/packages/sources/canton-functions/src/index.ts b/packages/sources/canton-functions/src/index.ts new file mode 100644 index 0000000000..32f2af7e8b --- /dev/null +++ b/packages/sources/canton-functions/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 { cantonData } from './endpoint' + +export const adapter = new Adapter({ + defaultEndpoint: cantonData.name, + name: 'CANTON_FUNCTIONS', + config, + endpoints: [cantonData], +}) + +export const server = (): Promise => expose(adapter) diff --git a/packages/sources/canton-functions/src/shared/canton-client.ts b/packages/sources/canton-functions/src/shared/canton-client.ts new file mode 100644 index 0000000000..ac703bab91 --- /dev/null +++ b/packages/sources/canton-functions/src/shared/canton-client.ts @@ -0,0 +1,64 @@ +import { Requester } from '@chainlink/external-adapter-framework/util/requester' + +export interface CantonClientConfig { + JSON_API: string + AUTH_TOKEN: string +} + +export interface QueryContractRequest { + templateIds: string[] +} + +export interface Contract { + contractId: string + templateId: string + payload: Record + signatories: string[] + observers: string[] + agreementText: string +} + +export class CantonClient { + private requester: Requester + private config: CantonClientConfig + private static instance: CantonClient + + constructor(requester: Requester, config: CantonClientConfig) { + this.requester = requester + this.config = config + } + + static getInstance(requester: Requester, config: CantonClientConfig): CantonClient { + if (!this.instance) { + this.instance = new CantonClient(requester, config) + } + + return this.instance + } + + /** + * Query contracts by template ID + */ + async queryContracts(request: QueryContractRequest): Promise { + const baseURL = `${this.config.JSON_API}/v1/query` + + const requestConfig = { + method: 'POST', + baseURL, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.config.AUTH_TOKEN}`, + }, + data: request, + } + + const response = await this.requester.request<{ result: Contract[] }>(baseURL, requestConfig) + + //todo: check for other error codes + if (response.response?.status !== 200) { + throw new Error(`Failed to query contracts: ${response.response?.statusText}`) + } + + return response.response.data.result + } +} diff --git a/packages/sources/canton-functions/src/transport/canton-data.ts b/packages/sources/canton-functions/src/transport/canton-data.ts new file mode 100644 index 0000000000..0f03553634 --- /dev/null +++ b/packages/sources/canton-functions/src/transport/canton-data.ts @@ -0,0 +1,93 @@ +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 { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error' +import { BaseEndpointTypes, inputParameters } from '../endpoint/canton-data' +import { CantonClient } from '../shared/canton-client' + +const logger = makeLogger('CantonDataTransport') + +type RequestParams = typeof inputParameters.validated + +export class CantonDataTransport extends SubscriptionTransport { + cantonClient!: CantonClient + + async initialize( + dependencies: TransportDependencies, + adapterSettings: BaseEndpointTypes['Settings'], + endpointName: string, + transportName: string, + ): Promise { + await super.initialize(dependencies, adapterSettings, endpointName, transportName) + this.cantonClient = CantonClient.getInstance(dependencies.requester, { + JSON_API: adapterSettings.JSON_API, + AUTH_TOKEN: adapterSettings.AUTH_TOKEN, + }) + } + + async backgroundHandler(context: EndpointContext, entries: RequestParams[]) { + await Promise.all(entries.map(async (param) => this.handleRequest(param))) + await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS) + } + + async handleRequest(param: RequestParams) { + let response: AdapterResponse + try { + response = await this._handleRequest(param) + } catch (e: unknown) { + const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred' + logger.error(e, errorMessage) + response = { + statusCode: (e as AdapterInputError)?.statusCode || 502, + errorMessage, + timestamps: { + providerDataRequestedUnixMs: 0, + providerDataReceivedUnixMs: 0, + providerIndicatedTimeUnixMs: undefined, + }, + } + } + + await this.responseCache.write(this.name, [{ params: param, response }]) + } + + async _handleRequest( + params: RequestParams, + ): Promise> { + const providerDataRequestedUnixMs = Date.now() + + const contracts = await this.cantonClient.queryContracts({ + templateIds: [params.templateId], + }) + + if (!contracts || contracts.length === 0) { + throw new AdapterInputError({ + message: `No contracts found for template ID '${params.templateId}'`, + statusCode: 404, + }) + } + + const result = JSON.stringify(contracts) + + return { + data: { + result, + contracts, + }, + statusCode: 200, + result, + timestamps: { + providerDataRequestedUnixMs, + providerDataReceivedUnixMs: Date.now(), + providerIndicatedTimeUnixMs: undefined, + }, + } + } + + getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number { + return adapterSettings.WARMUP_SUBSCRIPTION_TTL + } +} + +export const cantonDataTransport = new CantonDataTransport() diff --git a/packages/sources/canton-functions/test-payload.json b/packages/sources/canton-functions/test-payload.json new file mode 100644 index 0000000000..987a88d603 --- /dev/null +++ b/packages/sources/canton-functions/test-payload.json @@ -0,0 +1,10 @@ +{ + "requests": [ + { + "endpoint": "canton-data", + "data": { + "templateId": "example-package-id:Main:Asset" + } + } + ] +} diff --git a/packages/sources/canton-functions/test/fixtures/canton-query-contracts-response.json b/packages/sources/canton-functions/test/fixtures/canton-query-contracts-response.json new file mode 100644 index 0000000000..f3565a16a4 --- /dev/null +++ b/packages/sources/canton-functions/test/fixtures/canton-query-contracts-response.json @@ -0,0 +1,33 @@ +{ + "result": [ + { + "contractId": "00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0", + "templateId": "example-package-id:Main:Asset", + "payload": { + "issuer": "Alice", + "owner": "Bob", + "amount": "1000", + "currency": "USD", + "isin": "US0378331005" + }, + "signatories": ["Alice"], + "observers": ["Bob"], + "agreementText": "Asset transfer agreement" + }, + { + "contractId": "11f2a6c7d9b0a8f5e4c3d2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1", + "templateId": "example-package-id:Main:Asset", + "payload": { + "issuer": "Alice", + "owner": "Charlie", + "amount": "2500", + "currency": "USD", + "isin": "US0378331005" + }, + "signatories": ["Alice"], + "observers": ["Charlie"], + "agreementText": "Asset transfer agreement" + } + ], + "status": 200 +} diff --git a/packages/sources/canton-functions/test/integration/__snapshots__/canton-data.test.ts.snap b/packages/sources/canton-functions/test/integration/__snapshots__/canton-data.test.ts.snap new file mode 100644 index 0000000000..0f1b03f411 --- /dev/null +++ b/packages/sources/canton-functions/test/integration/__snapshots__/canton-data.test.ts.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`execute canton-data endpoint should return success when querying contracts with valid templateId 1`] = ` +{ + "data": { + "contracts": [ + { + "agreementText": "Asset transfer agreement", + "contractId": "00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0", + "observers": [ + "Bob", + ], + "payload": { + "amount": "1000", + "currency": "USD", + "isin": "US0378331005", + "issuer": "Alice", + "owner": "Bob", + }, + "signatories": [ + "Alice", + ], + "templateId": "example-package-id:Main:Asset", + }, + { + "agreementText": "Asset transfer agreement", + "contractId": "11f2a6c7d9b0a8f5e4c3d2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1", + "observers": [ + "Charlie", + ], + "payload": { + "amount": "2500", + "currency": "USD", + "isin": "US0378331005", + "issuer": "Alice", + "owner": "Charlie", + }, + "signatories": [ + "Alice", + ], + "templateId": "example-package-id:Main:Asset", + }, + ], + "result": "[{"contractId":"00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0","templateId":"example-package-id:Main:Asset","payload":{"issuer":"Alice","owner":"Bob","amount":"1000","currency":"USD","isin":"US0378331005"},"signatories":["Alice"],"observers":["Bob"],"agreementText":"Asset transfer agreement"},{"contractId":"11f2a6c7d9b0a8f5e4c3d2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1","templateId":"example-package-id:Main:Asset","payload":{"issuer":"Alice","owner":"Charlie","amount":"2500","currency":"USD","isin":"US0378331005"},"signatories":["Alice"],"observers":["Charlie"],"agreementText":"Asset transfer agreement"}]", + }, + "result": "[{"contractId":"00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0","templateId":"example-package-id:Main:Asset","payload":{"issuer":"Alice","owner":"Bob","amount":"1000","currency":"USD","isin":"US0378331005"},"signatories":["Alice"],"observers":["Bob"],"agreementText":"Asset transfer agreement"},{"contractId":"11f2a6c7d9b0a8f5e4c3d2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1","templateId":"example-package-id:Main:Asset","payload":{"issuer":"Alice","owner":"Charlie","amount":"2500","currency":"USD","isin":"US0378331005"},"signatories":["Alice"],"observers":["Charlie"],"agreementText":"Asset transfer agreement"}]", + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 1760400000000, + "providerDataRequestedUnixMs": 1760400000000, + }, +} +`; diff --git a/packages/sources/canton-functions/test/integration/adapter.test.ts b/packages/sources/canton-functions/test/integration/adapter.test.ts new file mode 100644 index 0000000000..81bae15977 --- /dev/null +++ b/packages/sources/canton-functions/test/integration/adapter.test.ts @@ -0,0 +1,33 @@ +import { + TestAdapter, + setEnvVariables, +} from '@chainlink/external-adapter-framework/util/testing-utils' +import * as nock from 'nock' + +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() + }) +}) diff --git a/packages/sources/canton-functions/test/integration/canton-data.test.ts b/packages/sources/canton-functions/test/integration/canton-data.test.ts new file mode 100644 index 0000000000..caa86ae03f --- /dev/null +++ b/packages/sources/canton-functions/test/integration/canton-data.test.ts @@ -0,0 +1,64 @@ +import { + TestAdapter, + setEnvVariables, +} from '@chainlink/external-adapter-framework/util/testing-utils' +import * as nock from 'nock' +import * as process from 'process' +import { mockCantonApiErrorResponse, mockCantonApiResponse } 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['JSON_API'] = 'http://localhost:7575' + process.env['AUTH_TOKEN'] = 'test-jwt-token' + process.env['BACKGROUND_EXECUTE_MS'] = '1000' + + const mockDate = new Date('2025-10-14T00:00:00.000Z') + 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('canton-data endpoint', () => { + it('should return success when querying contracts with valid templateId', async () => { + const data = { + endpoint: 'canton-data', + templateId: 'example-package-id:Main:Asset', + } + + mockCantonApiResponse() + + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }, 10000) + + it('should handle errors when Canton API returns failure', async () => { + const data = { + endpoint: 'canton-data', + templateId: 'invalid-template-id', + } + + mockCantonApiErrorResponse() + + const response = await testAdapter.request(data) + expect(response.statusCode).toBeGreaterThanOrEqual(400) + }, 10000) + }) +}) diff --git a/packages/sources/canton-functions/test/integration/fixtures.ts b/packages/sources/canton-functions/test/integration/fixtures.ts new file mode 100644 index 0000000000..22984078f6 --- /dev/null +++ b/packages/sources/canton-functions/test/integration/fixtures.ts @@ -0,0 +1,32 @@ +import nock from 'nock' +import mockCantonResponse from '../fixtures/canton-query-contracts-response.json' + +export function mockCantonApiResponse() { + nock('http://localhost:7575', { + reqheaders: { + 'Content-Type': 'application/json', + Authorization: 'Bearer test-jwt-token', + }, + }) + .persist() + .post('/v1/query', { + templateIds: ['example-package-id:Main:Asset'], + }) + .reply(200, mockCantonResponse) +} + +export function mockCantonApiErrorResponse() { + nock('http://localhost:7575', { + reqheaders: { + 'Content-Type': 'application/json', + Authorization: 'Bearer test-jwt-token', + }, + }) + .persist() + .post('/v1/query', { + templateIds: ['invalid-template-id'], + }) + .reply(400, { + error: 'Invalid template ID format', + }) +} diff --git a/packages/sources/canton-functions/tsconfig.json b/packages/sources/canton-functions/tsconfig.json new file mode 100644 index 0000000000..f59363fd76 --- /dev/null +++ b/packages/sources/canton-functions/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/canton-functions/tsconfig.test.json b/packages/sources/canton-functions/tsconfig.test.json new file mode 100755 index 0000000000..e3de28cb5c --- /dev/null +++ b/packages/sources/canton-functions/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*", "**/test", "src/**/*.json"], + "compilerOptions": { + "noEmit": true + } +} diff --git a/yarn.lock b/yarn.lock index 569f56169f..b832338f98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2995,6 +2995,21 @@ __metadata: languageName: unknown linkType: soft +"@chainlink/canton-functions-adapter@workspace:packages/sources/canton-functions": + version: 0.0.0-use.local + resolution: "@chainlink/canton-functions-adapter@workspace:packages/sources/canton-functions" + dependencies: + "@chainlink/external-adapter-framework": "npm:2.7.1" + "@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/ceffu-adapter@workspace:*, @chainlink/ceffu-adapter@workspace:packages/sources/ceffu": version: 0.0.0-use.local resolution: "@chainlink/ceffu-adapter@workspace:packages/sources/ceffu" From cc311674daf1e01759b14ca2b281f9869a4ac2db Mon Sep 17 00:00:00 2001 From: mayowa Date: Tue, 14 Oct 2025 17:31:24 +0100 Subject: [PATCH 2/5] add changeset --- .changeset/fair-geese-beam.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fair-geese-beam.md diff --git a/.changeset/fair-geese-beam.md b/.changeset/fair-geese-beam.md new file mode 100644 index 0000000000..70bc2f1e94 --- /dev/null +++ b/.changeset/fair-geese-beam.md @@ -0,0 +1,5 @@ +--- +'@chainlink/canton-functions-adapter': major +--- + +This EA enables us to read data from Canton participant nodes via the Ledger API From c6b9360aba7342d7c56b38139be872698afdf0e9 Mon Sep 17 00:00:00 2001 From: mayowa Date: Fri, 17 Oct 2025 13:25:49 +0100 Subject: [PATCH 3/5] update contract query and input params --- .../canton-functions/src/config/index.ts | 5 - .../src/endpoint/canton-data.ts | 31 ++++- .../src/shared/canton-client.ts | 111 ++++++++++++++- .../src/transport/canton-data.ts | 68 ++++++++-- .../canton-functions/test-payload.json | 24 +++- .../canton-exercise-choice-response.json | 15 ++ .../fixtures/canton-query-by-id-response.json | 19 +++ .../canton-query-with-filter-response.json | 35 +++++ .../__snapshots__/canton-data.test.ts.snap | 128 +++++++++++++----- .../test/integration/adapter.test.ts | 33 ----- .../test/integration/canton-data.test.ts | 45 ++++-- .../test/integration/fixtures.ts | 87 +++++++++++- 12 files changed, 496 insertions(+), 105 deletions(-) create mode 100644 packages/sources/canton-functions/test/fixtures/canton-exercise-choice-response.json create mode 100644 packages/sources/canton-functions/test/fixtures/canton-query-by-id-response.json create mode 100644 packages/sources/canton-functions/test/fixtures/canton-query-with-filter-response.json delete mode 100644 packages/sources/canton-functions/test/integration/adapter.test.ts diff --git a/packages/sources/canton-functions/src/config/index.ts b/packages/sources/canton-functions/src/config/index.ts index ae15e5b5c3..b7f39ea273 100644 --- a/packages/sources/canton-functions/src/config/index.ts +++ b/packages/sources/canton-functions/src/config/index.ts @@ -1,11 +1,6 @@ import { AdapterConfig } from '@chainlink/external-adapter-framework/config' export const config = new AdapterConfig({ - JSON_API: { - description: 'The Canton JSON API URL', - type: 'string', - required: true, - }, AUTH_TOKEN: { description: 'JWT token for Canton JSON API authentication', type: 'string', diff --git a/packages/sources/canton-functions/src/endpoint/canton-data.ts b/packages/sources/canton-functions/src/endpoint/canton-data.ts index 46837cd6c1..e3eee08d7b 100644 --- a/packages/sources/canton-functions/src/endpoint/canton-data.ts +++ b/packages/sources/canton-functions/src/endpoint/canton-data.ts @@ -5,15 +5,43 @@ import { cantonDataTransport } from '../transport/canton-data' export const inputParameters = new InputParameters( { + url: { + description: 'The Canton JSON API URL', + type: 'string', + required: true, + }, templateId: { description: 'The template ID to query contracts for (format: packageId:Module:Template)', type: 'string', required: true, }, + contractId: { + description: 'The contract ID to exercise choice on', + type: 'string', + required: false, + }, + choice: { + description: 'The non-consuming choice to exercise on the contract', + type: 'string', + required: true, + }, + argument: { + description: 'The argument for the choice (JSON string)', + type: 'string', + required: false, + }, + contractFilter: { + description: 'Filter to query contracts when contractId is not provided (JSON string)', + type: 'string', + required: false, + }, }, [ { + url: 'http://localhost:7575', templateId: 'example-package-id:Main:Asset', + contractId: '00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0', + choice: 'GetValue', }, ], ) @@ -23,7 +51,8 @@ export type BaseEndpointTypes = { Response: { Data: { result: string - contracts: any[] + exerciseResult: any + contract?: any } Result: string } diff --git a/packages/sources/canton-functions/src/shared/canton-client.ts b/packages/sources/canton-functions/src/shared/canton-client.ts index ac703bab91..fbe49b6ffe 100644 --- a/packages/sources/canton-functions/src/shared/canton-client.ts +++ b/packages/sources/canton-functions/src/shared/canton-client.ts @@ -1,12 +1,24 @@ import { Requester } from '@chainlink/external-adapter-framework/util/requester' export interface CantonClientConfig { - JSON_API: string AUTH_TOKEN: string } -export interface QueryContractRequest { +export interface QueryContractByTemplateRequest { templateIds: string[] + filter?: string | Record +} + +export interface QueryContractByIdRequest { + contractId: string + templateId: string +} + +export interface ExerciseChoiceRequest { + contractId: string + templateId: string + choice: string + argument?: string } export interface Contract { @@ -16,6 +28,12 @@ export interface Contract { signatories: string[] observers: string[] agreementText: string + createdAt?: string +} + +export interface ExerciseResult { + exerciseResult: any + events: any[] } export class CantonClient { @@ -37,10 +55,22 @@ export class CantonClient { } /** - * Query contracts by template ID + * Query contracts by template ID with an optional filter */ - async queryContracts(request: QueryContractRequest): Promise { - const baseURL = `${this.config.JSON_API}/v1/query` + async queryContractsByTemplate( + url: string, + request: QueryContractByTemplateRequest, + ): Promise { + const baseURL = `${url}/v1/query` + + const requestData: any = { + templateIds: request.templateIds, + } + + if (request.filter) { + requestData.query = + typeof request.filter === 'string' ? JSON.parse(request.filter) : request.filter + } const requestConfig = { method: 'POST', @@ -49,16 +79,83 @@ export class CantonClient { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.AUTH_TOKEN}`, }, - data: request, + data: requestData, } const response = await this.requester.request<{ result: Contract[] }>(baseURL, requestConfig) - //todo: check for other error codes if (response.response?.status !== 200) { throw new Error(`Failed to query contracts: ${response.response?.statusText}`) } return response.response.data.result } + + /** + * Query contract by template ID and contract ID + */ + async queryContractById( + url: string, + request: QueryContractByIdRequest, + ): Promise { + const baseURL = `${url}/v1/query` + + const requestConfig = { + method: 'POST', + baseURL, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.config.AUTH_TOKEN}`, + }, + data: { + templateIds: [request.templateId], + }, + } + + const response = await this.requester.request<{ result: Contract[] }>(baseURL, requestConfig) + + if (response.response?.status !== 200) { + throw new Error(`Failed to query contracts: ${response.response?.statusText}`) + } + + const contracts = response.response.data.result + const contract = contracts.find((c) => c.contractId === request.contractId) + + return contract || null + } + + /** + * Exercise a non-consuming choice on a contract + */ + async exerciseChoice(url: string, request: ExerciseChoiceRequest): Promise { + const baseURL = `${url}/v1/exercise` + + const requestData: any = { + templateId: request.templateId, + contractId: request.contractId, + choice: request.choice, + } + + if (request.argument) { + requestData.argument = JSON.parse(request.argument) + } + + const requestConfig = { + method: 'POST', + baseURL, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.config.AUTH_TOKEN}`, + }, + data: requestData, + } + + const response = await this.requester.request(baseURL, requestConfig) + + if (response.response?.status !== 200) { + throw new Error(`Failed to exercise choice: ${response.response?.statusText}`) + } + + return response.response.data + } } diff --git a/packages/sources/canton-functions/src/transport/canton-data.ts b/packages/sources/canton-functions/src/transport/canton-data.ts index 0f03553634..a77abaa825 100644 --- a/packages/sources/canton-functions/src/transport/canton-data.ts +++ b/packages/sources/canton-functions/src/transport/canton-data.ts @@ -21,7 +21,6 @@ export class CantonDataTransport extends SubscriptionTransport { await super.initialize(dependencies, adapterSettings, endpointName, transportName) this.cantonClient = CantonClient.getInstance(dependencies.requester, { - JSON_API: adapterSettings.JSON_API, AUTH_TOKEN: adapterSettings.AUTH_TOKEN, }) } @@ -56,24 +55,57 @@ export class CantonDataTransport extends SubscriptionTransport> { const providerDataRequestedUnixMs = Date.now() + const url = params.url + const templateId = params.templateId + const choice = params.choice - const contracts = await this.cantonClient.queryContracts({ - templateIds: [params.templateId], - }) + let contractId: string + let contract: any + + // If contractId is provided, use it directly + if (params.contractId) { + contractId = params.contractId + } else { + // Query contracts using contractFilter + if (!params.contractFilter) { + throw new AdapterInputError({ + message: 'Either contractId or contractFilter must be provided', + statusCode: 400, + }) + } - if (!contracts || contracts.length === 0) { - throw new AdapterInputError({ - message: `No contracts found for template ID '${params.templateId}'`, - statusCode: 404, + const contracts = await this.cantonClient.queryContractsByTemplate(url, { + templateIds: [templateId], + filter: String(params.contractFilter), }) + + if (!contracts || contracts.length === 0) { + throw new AdapterInputError({ + message: `No contracts found for template ID '${templateId}' with the provided filter`, + statusCode: 404, + }) + } + + // Find the latest contract by createdAt + contract = this.findLatestContract(contracts) + contractId = contract.contractId } - const result = JSON.stringify(contracts) + // Exercise the choice on the contract + const exerciseResult = await this.cantonClient.exerciseChoice(url, { + contractId, + templateId, + choice, + argument: params.argument ? String(params.argument) : undefined, + }) + + const result = JSON.stringify(exerciseResult) return { data: { result, - contracts, + exerciseResult, + contract, }, statusCode: 200, result, @@ -85,6 +117,22 @@ export class CantonDataTransport extends SubscriptionTransport { + const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0 + const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0 + return dateB - dateA + })[0] + } + getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number { return adapterSettings.WARMUP_SUBSCRIPTION_TTL } diff --git a/packages/sources/canton-functions/test-payload.json b/packages/sources/canton-functions/test-payload.json index 987a88d603..c772b8e703 100644 --- a/packages/sources/canton-functions/test-payload.json +++ b/packages/sources/canton-functions/test-payload.json @@ -3,7 +3,29 @@ { "endpoint": "canton-data", "data": { - "templateId": "example-package-id:Main:Asset" + "url": "http://localhost:7575", + "templateId": "example-package-id:Main:Asset", + "contractId": "00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0", + "choice": "GetValue" + } + }, + { + "endpoint": "canton-data", + "data": { + "url": "http://localhost:7575", + "templateId": "example-package-id:Main:Asset", + "contractId": "00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0", + "choice": "UpdateValue", + "argument": "{\"newValue\":2000}" + } + }, + { + "endpoint": "canton-data", + "data": { + "url": "http://localhost:7575", + "templateId": "example-package-id:Main:Asset", + "contractFilter": "{\"owner\":\"Bob\"}", + "choice": "GetValue" } } ] diff --git a/packages/sources/canton-functions/test/fixtures/canton-exercise-choice-response.json b/packages/sources/canton-functions/test/fixtures/canton-exercise-choice-response.json new file mode 100644 index 0000000000..8e82505e4e --- /dev/null +++ b/packages/sources/canton-functions/test/fixtures/canton-exercise-choice-response.json @@ -0,0 +1,15 @@ +{ + "exerciseResult": { + "value": "1000", + "currency": "USD" + }, + "events": [ + { + "eventId": "event-123", + "contractId": "00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0", + "templateId": "example-package-id:Main:Asset", + "eventType": "ChoiceExercised" + } + ], + "status": 200 +} diff --git a/packages/sources/canton-functions/test/fixtures/canton-query-by-id-response.json b/packages/sources/canton-functions/test/fixtures/canton-query-by-id-response.json new file mode 100644 index 0000000000..c6c998ef11 --- /dev/null +++ b/packages/sources/canton-functions/test/fixtures/canton-query-by-id-response.json @@ -0,0 +1,19 @@ +{ + "result": [ + { + "contractId": "00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0", + "templateId": "example-package-id:Main:Asset", + "payload": { + "issuer": "Alice", + "owner": "Bob", + "amount": "1000", + "currency": "USD", + "isin": "US0378331005" + }, + "signatories": ["Alice"], + "observers": ["Bob"], + "agreementText": "Asset transfer agreement" + } + ], + "status": 200 +} diff --git a/packages/sources/canton-functions/test/fixtures/canton-query-with-filter-response.json b/packages/sources/canton-functions/test/fixtures/canton-query-with-filter-response.json new file mode 100644 index 0000000000..e57910788d --- /dev/null +++ b/packages/sources/canton-functions/test/fixtures/canton-query-with-filter-response.json @@ -0,0 +1,35 @@ +{ + "result": [ + { + "contractId": "22a3b7c8d0b1a9f6e5c4d3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2", + "templateId": "example-package-id:Main:Asset", + "payload": { + "issuer": "Alice", + "owner": "Bob", + "amount": "1500", + "currency": "USD", + "isin": "US0378331005" + }, + "signatories": ["Alice"], + "observers": ["Bob"], + "agreementText": "Asset transfer agreement", + "createdAt": "2025-10-16T10:00:00Z" + }, + { + "contractId": "33b4c8d9e1c2b0a7f6e5d4c3b2d1e0a9f8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3", + "templateId": "example-package-id:Main:Asset", + "payload": { + "issuer": "Alice", + "owner": "Bob", + "amount": "2000", + "currency": "USD", + "isin": "US0378331005" + }, + "signatories": ["Alice"], + "observers": ["Bob"], + "agreementText": "Asset transfer agreement", + "createdAt": "2025-10-17T12:00:00Z" + } + ], + "status": 200 +} diff --git a/packages/sources/canton-functions/test/integration/__snapshots__/canton-data.test.ts.snap b/packages/sources/canton-functions/test/integration/__snapshots__/canton-data.test.ts.snap index 0f1b03f411..c01bf1af66 100644 --- a/packages/sources/canton-functions/test/integration/__snapshots__/canton-data.test.ts.snap +++ b/packages/sources/canton-functions/test/integration/__snapshots__/canton-data.test.ts.snap @@ -1,49 +1,103 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`execute canton-data endpoint should return success when querying contracts with valid templateId 1`] = ` +exports[`execute canton-data endpoint should query with filter and exercise choice on latest contract 1`] = ` { "data": { - "contracts": [ - { - "agreementText": "Asset transfer agreement", - "contractId": "00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0", - "observers": [ - "Bob", - ], - "payload": { - "amount": "1000", - "currency": "USD", - "isin": "US0378331005", - "issuer": "Alice", - "owner": "Bob", + "contract": { + "agreementText": "Asset transfer agreement", + "contractId": "33b4c8d9e1c2b0a7f6e5d4c3b2d1e0a9f8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3", + "createdAt": "2025-10-17T12:00:00Z", + "observers": [ + "Bob", + ], + "payload": { + "amount": "2000", + "currency": "USD", + "isin": "US0378331005", + "issuer": "Alice", + "owner": "Bob", + }, + "signatories": [ + "Alice", + ], + "templateId": "example-package-id:Main:Asset", + }, + "exerciseResult": { + "events": [ + { + "contractId": "00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0", + "eventId": "event-123", + "eventType": "ChoiceExercised", + "templateId": "example-package-id:Main:Asset", }, - "signatories": [ - "Alice", - ], - "templateId": "example-package-id:Main:Asset", + ], + "exerciseResult": { + "currency": "USD", + "value": "1000", }, - { - "agreementText": "Asset transfer agreement", - "contractId": "11f2a6c7d9b0a8f5e4c3d2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1", - "observers": [ - "Charlie", - ], - "payload": { - "amount": "2500", - "currency": "USD", - "isin": "US0378331005", - "issuer": "Alice", - "owner": "Charlie", + "status": 200, + }, + "result": "{"exerciseResult":{"value":"1000","currency":"USD"},"events":[{"eventId":"event-123","contractId":"00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0","templateId":"example-package-id:Main:Asset","eventType":"ChoiceExercised"}],"status":200}", + }, + "result": "{"exerciseResult":{"value":"1000","currency":"USD"},"events":[{"eventId":"event-123","contractId":"00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0","templateId":"example-package-id:Main:Asset","eventType":"ChoiceExercised"}],"status":200}", + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 1760400000000, + "providerDataRequestedUnixMs": 1760400000000, + }, +} +`; + +exports[`execute canton-data endpoint should return success when exercising a choice with argument 1`] = ` +{ + "data": { + "exerciseResult": { + "events": [ + { + "contractId": "00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0", + "eventId": "event-123", + "eventType": "ChoiceExercised", + "templateId": "example-package-id:Main:Asset", + }, + ], + "exerciseResult": { + "currency": "USD", + "value": "1000", + }, + "status": 200, + }, + "result": "{"exerciseResult":{"value":"1000","currency":"USD"},"events":[{"eventId":"event-123","contractId":"00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0","templateId":"example-package-id:Main:Asset","eventType":"ChoiceExercised"}],"status":200}", + }, + "result": "{"exerciseResult":{"value":"1000","currency":"USD"},"events":[{"eventId":"event-123","contractId":"00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0","templateId":"example-package-id:Main:Asset","eventType":"ChoiceExercised"}],"status":200}", + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 1760400000000, + "providerDataRequestedUnixMs": 1760400000000, + }, +} +`; + +exports[`execute canton-data endpoint should return success when exercising a choice without argument 1`] = ` +{ + "data": { + "exerciseResult": { + "events": [ + { + "contractId": "00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0", + "eventId": "event-123", + "eventType": "ChoiceExercised", + "templateId": "example-package-id:Main:Asset", }, - "signatories": [ - "Alice", - ], - "templateId": "example-package-id:Main:Asset", + ], + "exerciseResult": { + "currency": "USD", + "value": "1000", }, - ], - "result": "[{"contractId":"00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0","templateId":"example-package-id:Main:Asset","payload":{"issuer":"Alice","owner":"Bob","amount":"1000","currency":"USD","isin":"US0378331005"},"signatories":["Alice"],"observers":["Bob"],"agreementText":"Asset transfer agreement"},{"contractId":"11f2a6c7d9b0a8f5e4c3d2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1","templateId":"example-package-id:Main:Asset","payload":{"issuer":"Alice","owner":"Charlie","amount":"2500","currency":"USD","isin":"US0378331005"},"signatories":["Alice"],"observers":["Charlie"],"agreementText":"Asset transfer agreement"}]", + "status": 200, + }, + "result": "{"exerciseResult":{"value":"1000","currency":"USD"},"events":[{"eventId":"event-123","contractId":"00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0","templateId":"example-package-id:Main:Asset","eventType":"ChoiceExercised"}],"status":200}", }, - "result": "[{"contractId":"00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0","templateId":"example-package-id:Main:Asset","payload":{"issuer":"Alice","owner":"Bob","amount":"1000","currency":"USD","isin":"US0378331005"},"signatories":["Alice"],"observers":["Bob"],"agreementText":"Asset transfer agreement"},{"contractId":"11f2a6c7d9b0a8f5e4c3d2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1","templateId":"example-package-id:Main:Asset","payload":{"issuer":"Alice","owner":"Charlie","amount":"2500","currency":"USD","isin":"US0378331005"},"signatories":["Alice"],"observers":["Charlie"],"agreementText":"Asset transfer agreement"}]", + "result": "{"exerciseResult":{"value":"1000","currency":"USD"},"events":[{"eventId":"event-123","contractId":"00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0","templateId":"example-package-id:Main:Asset","eventType":"ChoiceExercised"}],"status":200}", "statusCode": 200, "timestamps": { "providerDataReceivedUnixMs": 1760400000000, diff --git a/packages/sources/canton-functions/test/integration/adapter.test.ts b/packages/sources/canton-functions/test/integration/adapter.test.ts deleted file mode 100644 index 81bae15977..0000000000 --- a/packages/sources/canton-functions/test/integration/adapter.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - TestAdapter, - setEnvVariables, -} from '@chainlink/external-adapter-framework/util/testing-utils' -import * as nock from 'nock' - -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() - }) -}) diff --git a/packages/sources/canton-functions/test/integration/canton-data.test.ts b/packages/sources/canton-functions/test/integration/canton-data.test.ts index caa86ae03f..4d81e7c39a 100644 --- a/packages/sources/canton-functions/test/integration/canton-data.test.ts +++ b/packages/sources/canton-functions/test/integration/canton-data.test.ts @@ -4,7 +4,12 @@ import { } from '@chainlink/external-adapter-framework/util/testing-utils' import * as nock from 'nock' import * as process from 'process' -import { mockCantonApiErrorResponse, mockCantonApiResponse } from './fixtures' +import { + mockCantonApiExerciseChoiceOnLatestContractResponse, + mockCantonApiExerciseChoiceResponse, + mockCantonApiExerciseChoiceWithArgumentResponse, + mockCantonApiQueryWithFilterResponse, +} from './fixtures' describe('execute', () => { let spy: jest.SpyInstance @@ -13,7 +18,6 @@ describe('execute', () => { beforeAll(async () => { oldEnv = JSON.parse(JSON.stringify(process.env)) - process.env['JSON_API'] = 'http://localhost:7575' process.env['AUTH_TOKEN'] = 'test-jwt-token' process.env['BACKGROUND_EXECUTE_MS'] = '1000' @@ -36,29 +40,54 @@ describe('execute', () => { }) describe('canton-data endpoint', () => { - it('should return success when querying contracts with valid templateId', async () => { + it('should return success when exercising a choice without argument', async () => { const data = { endpoint: 'canton-data', + url: 'http://localhost:7575', templateId: 'example-package-id:Main:Asset', + contractId: '00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0', + choice: 'GetValue', } - mockCantonApiResponse() + mockCantonApiExerciseChoiceResponse() const response = await testAdapter.request(data) expect(response.statusCode).toBe(200) expect(response.json()).toMatchSnapshot() }, 10000) - it('should handle errors when Canton API returns failure', async () => { + it('should return success when exercising a choice with argument', async () => { const data = { endpoint: 'canton-data', - templateId: 'invalid-template-id', + url: 'http://localhost:7575', + templateId: 'example-package-id:Main:Asset', + contractId: '00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0', + choice: 'UpdateValue', + argument: JSON.stringify({ newValue: 2000 }), } - mockCantonApiErrorResponse() + mockCantonApiExerciseChoiceWithArgumentResponse() const response = await testAdapter.request(data) - expect(response.statusCode).toBeGreaterThanOrEqual(400) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }, 10000) + + it('should query with filter and exercise choice on latest contract', async () => { + const data = { + endpoint: 'canton-data', + url: 'http://localhost:7575', + templateId: 'example-package-id:Main:Asset', + contractFilter: JSON.stringify({ owner: 'Bob' }), + choice: 'GetValue', + } + + mockCantonApiQueryWithFilterResponse() + mockCantonApiExerciseChoiceOnLatestContractResponse() + + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() }, 10000) }) }) diff --git a/packages/sources/canton-functions/test/integration/fixtures.ts b/packages/sources/canton-functions/test/integration/fixtures.ts index 22984078f6..7626a0b788 100644 --- a/packages/sources/canton-functions/test/integration/fixtures.ts +++ b/packages/sources/canton-functions/test/integration/fixtures.ts @@ -1,7 +1,10 @@ import nock from 'nock' -import mockCantonResponse from '../fixtures/canton-query-contracts-response.json' +import mockCantonExerciseResponse from '../fixtures/canton-exercise-choice-response.json' +import mockCantonQueryByIdResponse from '../fixtures/canton-query-by-id-response.json' +import mockCantonQueryResponse from '../fixtures/canton-query-contracts-response.json' +import mockCantonQueryWithFilterResponse from '../fixtures/canton-query-with-filter-response.json' -export function mockCantonApiResponse() { +export function mockCantonApiQueryByTemplateResponse() { nock('http://localhost:7575', { reqheaders: { 'Content-Type': 'application/json', @@ -12,7 +15,85 @@ export function mockCantonApiResponse() { .post('/v1/query', { templateIds: ['example-package-id:Main:Asset'], }) - .reply(200, mockCantonResponse) + .reply(200, mockCantonQueryResponse) +} + +export function mockCantonApiQueryByIdResponse() { + nock('http://localhost:7575', { + reqheaders: { + 'Content-Type': 'application/json', + Authorization: 'Bearer test-jwt-token', + }, + }) + .persist() + .post('/v1/query', { + templateIds: ['example-package-id:Main:Asset'], + }) + .reply(200, mockCantonQueryByIdResponse) +} + +export function mockCantonApiExerciseChoiceResponse() { + nock('http://localhost:7575', { + reqheaders: { + 'Content-Type': 'application/json', + Authorization: 'Bearer test-jwt-token', + }, + }) + .persist() + .post('/v1/exercise', { + templateId: 'example-package-id:Main:Asset', + contractId: '00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0', + choice: 'GetValue', + }) + .reply(200, mockCantonExerciseResponse) +} + +export function mockCantonApiExerciseChoiceWithArgumentResponse() { + nock('http://localhost:7575', { + reqheaders: { + 'Content-Type': 'application/json', + Authorization: 'Bearer test-jwt-token', + }, + }) + .persist() + .post('/v1/exercise', { + templateId: 'example-package-id:Main:Asset', + contractId: '00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0', + choice: 'UpdateValue', + argument: { newValue: 2000 }, + }) + .reply(200, mockCantonExerciseResponse) +} + +export function mockCantonApiQueryWithFilterResponse() { + nock('http://localhost:7575', { + reqheaders: { + 'Content-Type': 'application/json', + Authorization: 'Bearer test-jwt-token', + }, + }) + .persist() + .post('/v1/query', { + templateIds: ['example-package-id:Main:Asset'], + query: { owner: 'Bob' }, + }) + .reply(200, mockCantonQueryWithFilterResponse) +} + +export function mockCantonApiExerciseChoiceOnLatestContractResponse() { + nock('http://localhost:7575', { + reqheaders: { + 'Content-Type': 'application/json', + Authorization: 'Bearer test-jwt-token', + }, + }) + .persist() + .post('/v1/exercise', { + templateId: 'example-package-id:Main:Asset', + contractId: '33b4c8d9e1c2b0a7f6e5d4c3b2d1e0a9f8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3', + choice: 'GetValue', + }) + .reply(200, mockCantonExerciseResponse) } export function mockCantonApiErrorResponse() { From 63ae031e23606c54913cc50a31ad3616ee50a0cc Mon Sep 17 00:00:00 2001 From: mayowa Date: Thu, 23 Oct 2025 23:51:05 +0100 Subject: [PATCH 4/5] remove unsued functions and refactor code --- .../src/shared/canton-client.ts | 54 ++----------------- .../src/transport/canton-data.ts | 30 ++++------- 2 files changed, 13 insertions(+), 71 deletions(-) diff --git a/packages/sources/canton-functions/src/shared/canton-client.ts b/packages/sources/canton-functions/src/shared/canton-client.ts index fbe49b6ffe..803054fc69 100644 --- a/packages/sources/canton-functions/src/shared/canton-client.ts +++ b/packages/sources/canton-functions/src/shared/canton-client.ts @@ -9,16 +9,11 @@ export interface QueryContractByTemplateRequest { filter?: string | Record } -export interface QueryContractByIdRequest { - contractId: string - templateId: string -} - export interface ExerciseChoiceRequest { contractId: string templateId: string choice: string - argument?: string + argument: Record } export interface Contract { @@ -91,55 +86,12 @@ export class CantonClient { return response.response.data.result } - /** - * Query contract by template ID and contract ID - */ - async queryContractById( - url: string, - request: QueryContractByIdRequest, - ): Promise { - const baseURL = `${url}/v1/query` - - const requestConfig = { - method: 'POST', - baseURL, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.config.AUTH_TOKEN}`, - }, - data: { - templateIds: [request.templateId], - }, - } - - const response = await this.requester.request<{ result: Contract[] }>(baseURL, requestConfig) - - if (response.response?.status !== 200) { - throw new Error(`Failed to query contracts: ${response.response?.statusText}`) - } - - const contracts = response.response.data.result - const contract = contracts.find((c) => c.contractId === request.contractId) - - return contract || null - } - /** * Exercise a non-consuming choice on a contract */ - async exerciseChoice(url: string, request: ExerciseChoiceRequest): Promise { + async exerciseChoice(url: string, payload: ExerciseChoiceRequest): Promise { const baseURL = `${url}/v1/exercise` - const requestData: any = { - templateId: request.templateId, - contractId: request.contractId, - choice: request.choice, - } - - if (request.argument) { - requestData.argument = JSON.parse(request.argument) - } - const requestConfig = { method: 'POST', baseURL, @@ -147,7 +99,7 @@ export class CantonClient { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.AUTH_TOKEN}`, }, - data: requestData, + data: payload, } const response = await this.requester.request(baseURL, requestConfig) diff --git a/packages/sources/canton-functions/src/transport/canton-data.ts b/packages/sources/canton-functions/src/transport/canton-data.ts index a77abaa825..001e0964aa 100644 --- a/packages/sources/canton-functions/src/transport/canton-data.ts +++ b/packages/sources/canton-functions/src/transport/canton-data.ts @@ -4,7 +4,7 @@ import { SubscriptionTransport } from '@chainlink/external-adapter-framework/tra import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util' import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error' import { BaseEndpointTypes, inputParameters } from '../endpoint/canton-data' -import { CantonClient } from '../shared/canton-client' +import { CantonClient, QueryContractByTemplateRequest } from '../shared/canton-client' const logger = makeLogger('CantonDataTransport') @@ -60,24 +60,16 @@ export class CantonDataTransport extends SubscriptionTransport Date: Fri, 24 Oct 2025 00:31:33 +0100 Subject: [PATCH 5/5] update integration tests for new changes --- .../src/transport/canton-data.ts | 1 - .../test/integration/canton-data.test.ts | 90 ++++++++---- .../test/integration/fixtures.ts | 135 +++++++++++------- 3 files changed, 148 insertions(+), 78 deletions(-) diff --git a/packages/sources/canton-functions/src/transport/canton-data.ts b/packages/sources/canton-functions/src/transport/canton-data.ts index 001e0964aa..5bc678f811 100644 --- a/packages/sources/canton-functions/src/transport/canton-data.ts +++ b/packages/sources/canton-functions/src/transport/canton-data.ts @@ -110,7 +110,6 @@ export class CantonDataTransport extends SubscriptionTransport { @@ -39,55 +42,86 @@ describe('execute', () => { spy.mockRestore() }) - describe('canton-data endpoint', () => { - it('should return success when exercising a choice without argument', async () => { + describe('canton-data endpoint (manual test cases)', () => { + // Test case 1 + it('should exercise GetInfo on latest contract (no contractId, no argument, no filter)', async () => { const data = { endpoint: 'canton-data', - url: 'http://localhost:7575', - templateId: 'example-package-id:Main:Asset', - contractId: '00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0', - choice: 'GetValue', + url: 'http://127.0.0.1:7575', + templateId: TEST_TEMPLATE_ID, + choice: 'GetInfo', } - mockCantonApiExerciseChoiceResponse() + mockQueryByTemplateNoFilter() + mockExerciseGetInfoOnLatestFromNoFilter() const response = await testAdapter.request(data) expect(response.statusCode).toBe(200) - expect(response.json()).toMatchSnapshot() + const body = response.json() + expect(body.data.exerciseResult).toBeDefined() + expect(typeof body.data.result).toBe('string') }, 10000) - it('should return success when exercising a choice with argument', async () => { + // Test case 2 + it('should exercise CheckValueAbove with argument on latest contract (no contractId)', async () => { const data = { endpoint: 'canton-data', - url: 'http://localhost:7575', - templateId: 'example-package-id:Main:Asset', - contractId: '00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0', - choice: 'UpdateValue', - argument: JSON.stringify({ newValue: 2000 }), + url: 'http://127.0.0.1:7575', + templateId: TEST_TEMPLATE_ID, + choice: 'CheckValueAbove', + argument: '{"threshold":2000}', } - mockCantonApiExerciseChoiceWithArgumentResponse() + mockQueryByTemplateNoFilter() + mockExerciseCheckValueAboveOnLatestFromNoFilter() const response = await testAdapter.request(data) expect(response.statusCode).toBe(200) - expect(response.json()).toMatchSnapshot() + const body = response.json() + expect(body.data.exerciseResult).toBeDefined() + expect(typeof body.data.result).toBe('string') }, 10000) - it('should query with filter and exercise choice on latest contract', async () => { + // Test case 3 + it('should exercise CheckValueAbove with argument using provided contractID', async () => { const data = { endpoint: 'canton-data', - url: 'http://localhost:7575', - templateId: 'example-package-id:Main:Asset', - contractFilter: JSON.stringify({ owner: 'Bob' }), - choice: 'GetValue', + url: 'http://127.0.0.1:7575', + templateId: TEST_TEMPLATE_ID, + contractId: + '0013102064814a98f87bb076314c6b82bdff97211e6cf8281c654d1b0df9c855e8ca03122027c21329e8823932596af34d936e7e25c69d28a1af118ba599732370266b589f', + choice: 'CheckValueAbove', + argument: '{"threshold":2000}', } - mockCantonApiQueryWithFilterResponse() - mockCantonApiExerciseChoiceOnLatestContractResponse() + mockExerciseCheckValueAboveWithProvidedId() const response = await testAdapter.request(data) expect(response.statusCode).toBe(200) - expect(response.json()).toMatchSnapshot() + const body = response.json() + expect(body.data.exerciseResult).toBeDefined() + expect(typeof body.data.result).toBe('string') + }, 10000) + + // Test case 4 + it('should query with contractFilter and exercise CheckValueAbove on returned contract', async () => { + const data = { + endpoint: 'canton-data', + url: 'http://127.0.0.1:7575', + templateId: TEST_TEMPLATE_ID, + choice: 'CheckValueAbove', + argument: '{"threshold":2000}', + contractFilter: '{"name":"Laptop"}', + } + + mockQueryWithNameFilter() + mockExerciseCheckValueAboveOnFilteredContract() + + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) + const body = response.json() + expect(body.data.exerciseResult).toBeDefined() + expect(typeof body.data.result).toBe('string') }, 10000) }) }) diff --git a/packages/sources/canton-functions/test/integration/fixtures.ts b/packages/sources/canton-functions/test/integration/fixtures.ts index 7626a0b788..d800c56967 100644 --- a/packages/sources/canton-functions/test/integration/fixtures.ts +++ b/packages/sources/canton-functions/test/integration/fixtures.ts @@ -1,11 +1,17 @@ import nock from 'nock' import mockCantonExerciseResponse from '../fixtures/canton-exercise-choice-response.json' -import mockCantonQueryByIdResponse from '../fixtures/canton-query-by-id-response.json' -import mockCantonQueryResponse from '../fixtures/canton-query-contracts-response.json' -import mockCantonQueryWithFilterResponse from '../fixtures/canton-query-with-filter-response.json' -export function mockCantonApiQueryByTemplateResponse() { - nock('http://localhost:7575', { +// New constants from manual test cases +const TEST_URL = 'http://127.0.0.1:7575' +export const TEST_TEMPLATE_ID = + '07722379f6f533cd18ec65a44953e507a032b19fba17302c566ca191b569392f:Main:Asset' + +// Helpers for building simple mock query results +const buildQueryResult = (contracts: any[]) => ({ result: contracts }) + +// Test case 1 and 2: Query by template without filter (returns two contracts, second is latest) +export function mockQueryByTemplateNoFilter() { + nock(TEST_URL, { reqheaders: { 'Content-Type': 'application/json', Authorization: 'Bearer test-jwt-token', @@ -13,13 +19,36 @@ export function mockCantonApiQueryByTemplateResponse() { }) .persist() .post('/v1/query', { - templateIds: ['example-package-id:Main:Asset'], + templateIds: [TEST_TEMPLATE_ID], }) - .reply(200, mockCantonQueryResponse) + .reply( + 200, + buildQueryResult([ + { + contractId: '00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff0001', + templateId: TEST_TEMPLATE_ID, + createdAt: '2025-10-16T12:00:00Z', + payload: { name: 'Phone', value: 1500 }, + signatories: [], + observers: [], + agreementText: 'Agreement A', + }, + { + contractId: 'aabbccddeeff00112233445566778899aabbccddeeff001122334455667788990002', + templateId: TEST_TEMPLATE_ID, + createdAt: '2025-10-17T12:00:00Z', + payload: { name: 'Laptop', value: 2500 }, + signatories: [], + observers: [], + agreementText: 'Agreement B', + }, + ]), + ) } -export function mockCantonApiQueryByIdResponse() { - nock('http://localhost:7575', { +// Test case 4: Query with filter { name: 'Laptop' } +export function mockQueryWithNameFilter() { + nock(TEST_URL, { reqheaders: { 'Content-Type': 'application/json', Authorization: 'Bearer test-jwt-token', @@ -27,13 +56,29 @@ export function mockCantonApiQueryByIdResponse() { }) .persist() .post('/v1/query', { - templateIds: ['example-package-id:Main:Asset'], + templateIds: [TEST_TEMPLATE_ID], + query: { name: 'Laptop' }, }) - .reply(200, mockCantonQueryByIdResponse) + .reply( + 200, + buildQueryResult([ + { + contractId: 'bbccddeeff00112233445566778899aabbccddeeff001122334455667788990003', + templateId: TEST_TEMPLATE_ID, + createdAt: '2025-10-18T12:00:00Z', + payload: { name: 'Laptop', value: 3000 }, + signatories: [], + observers: [], + agreementText: 'Agreement C', + }, + ]), + ) } -export function mockCantonApiExerciseChoiceResponse() { - nock('http://localhost:7575', { +// Exercise mocks +// Test case 1: exercise GetInfo on latest contract from no-filter query (second contract) +export function mockExerciseGetInfoOnLatestFromNoFilter() { + nock(TEST_URL, { reqheaders: { 'Content-Type': 'application/json', Authorization: 'Bearer test-jwt-token', @@ -41,15 +86,17 @@ export function mockCantonApiExerciseChoiceResponse() { }) .persist() .post('/v1/exercise', { - templateId: 'example-package-id:Main:Asset', - contractId: '00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0', - choice: 'GetValue', + templateId: TEST_TEMPLATE_ID, + contractId: 'aabbccddeeff00112233445566778899aabbccddeeff001122334455667788990002', + choice: 'GetInfo', + argument: {}, }) .reply(200, mockCantonExerciseResponse) } -export function mockCantonApiExerciseChoiceWithArgumentResponse() { - nock('http://localhost:7575', { +// Test case 2: exercise CheckValueAbove with argument on latest from no-filter query +export function mockExerciseCheckValueAboveOnLatestFromNoFilter() { + nock(TEST_URL, { reqheaders: { 'Content-Type': 'application/json', Authorization: 'Bearer test-jwt-token', @@ -57,31 +104,17 @@ export function mockCantonApiExerciseChoiceWithArgumentResponse() { }) .persist() .post('/v1/exercise', { - templateId: 'example-package-id:Main:Asset', - contractId: '00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0', - choice: 'UpdateValue', - argument: { newValue: 2000 }, + templateId: TEST_TEMPLATE_ID, + contractId: 'aabbccddeeff00112233445566778899aabbccddeeff001122334455667788990002', + choice: 'CheckValueAbove', + argument: { threshold: 2000 }, }) .reply(200, mockCantonExerciseResponse) } -export function mockCantonApiQueryWithFilterResponse() { - nock('http://localhost:7575', { - reqheaders: { - 'Content-Type': 'application/json', - Authorization: 'Bearer test-jwt-token', - }, - }) - .persist() - .post('/v1/query', { - templateIds: ['example-package-id:Main:Asset'], - query: { owner: 'Bob' }, - }) - .reply(200, mockCantonQueryWithFilterResponse) -} - -export function mockCantonApiExerciseChoiceOnLatestContractResponse() { - nock('http://localhost:7575', { +// Test case 3: exercise CheckValueAbove with provided contract ID +export function mockExerciseCheckValueAboveWithProvidedId() { + nock(TEST_URL, { reqheaders: { 'Content-Type': 'application/json', Authorization: 'Bearer test-jwt-token', @@ -89,25 +122,29 @@ export function mockCantonApiExerciseChoiceOnLatestContractResponse() { }) .persist() .post('/v1/exercise', { - templateId: 'example-package-id:Main:Asset', - contractId: '33b4c8d9e1c2b0a7f6e5d4c3b2d1e0a9f8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3', - choice: 'GetValue', + templateId: TEST_TEMPLATE_ID, + contractId: + '0013102064814a98f87bb076314c6b82bdff97211e6cf8281c654d1b0df9c855e8ca03122027c21329e8823932596af34d936e7e25c69d28a1af118ba599732370266b589f', + choice: 'CheckValueAbove', + argument: { threshold: 2000 }, }) .reply(200, mockCantonExerciseResponse) } -export function mockCantonApiErrorResponse() { - nock('http://localhost:7575', { +// Test case 4: exercise on the filtered contract (only one returned) +export function mockExerciseCheckValueAboveOnFilteredContract() { + nock(TEST_URL, { reqheaders: { 'Content-Type': 'application/json', Authorization: 'Bearer test-jwt-token', }, }) .persist() - .post('/v1/query', { - templateIds: ['invalid-template-id'], - }) - .reply(400, { - error: 'Invalid template ID format', + .post('/v1/exercise', { + templateId: TEST_TEMPLATE_ID, + contractId: 'bbccddeeff00112233445566778899aabbccddeeff001122334455667788990003', + choice: 'CheckValueAbove', + argument: { threshold: 2000 }, }) + .reply(200, mockCantonExerciseResponse) }