-
Notifications
You must be signed in to change notification settings - Fork 325
Add Canton EA to read from Canton participant node #4103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3b1f95f
cc31167
c6b9360
63ae031
d652378
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@chainlink/canton-functions-adapter': major | ||
| --- | ||
|
|
||
| This EA enables us to read data from Canton participant nodes via the Ledger API |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { AdapterConfig } from '@chainlink/external-adapter-framework/config' | ||
|
|
||
| export const config = new AdapterConfig({ | ||
| 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, | ||
| }, | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| 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( | ||
| { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As noted: these should be ENV variables specified in the implementing project, most of these are fixed and do not need to be in the request |
||
| 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', | ||
| }, | ||
| ], | ||
| ) | ||
|
|
||
| export type BaseEndpointTypes = { | ||
| Parameters: typeof inputParameters.definition | ||
| Response: { | ||
| Data: { | ||
| result: string | ||
| exerciseResult: any | ||
| contract?: any | ||
| } | ||
| Result: string | ||
| } | ||
| Settings: typeof config.settings | ||
| } | ||
|
|
||
| export const endpoint = new AdapterEndpoint({ | ||
| name: 'canton-data', | ||
| aliases: [], | ||
| transport: cantonDataTransport, | ||
| inputParameters, | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { endpoint as cantonData } from './canton-data' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Base contract should not have an instance, instance should be configured within implementing contracts as discussed |
||
| defaultEndpoint: cantonData.name, | ||
| name: 'CANTON_FUNCTIONS', | ||
| config, | ||
| endpoints: [cantonData], | ||
| }) | ||
|
|
||
| export const server = (): Promise<ServerInstance | undefined> => expose(adapter) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import { Requester } from '@chainlink/external-adapter-framework/util/requester' | ||
|
|
||
| export interface CantonClientConfig { | ||
| AUTH_TOKEN: string | ||
| } | ||
|
|
||
| export interface QueryContractByTemplateRequest { | ||
| templateIds: string[] | ||
| filter?: string | Record<string, any> | ||
| } | ||
|
|
||
| export interface ExerciseChoiceRequest { | ||
| contractId: string | ||
| templateId: string | ||
| choice: string | ||
| argument: Record<string, any> | ||
| } | ||
|
|
||
| export interface Contract { | ||
| contractId: string | ||
| templateId: string | ||
| payload: Record<string, any> | ||
| signatories: string[] | ||
| observers: string[] | ||
| agreementText: string | ||
| createdAt?: string | ||
| } | ||
|
|
||
| export interface ExerciseResult { | ||
| exerciseResult: any | ||
| events: any[] | ||
| } | ||
|
|
||
| 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 with an optional filter | ||
| */ | ||
| async queryContractsByTemplate( | ||
| url: string, | ||
| request: QueryContractByTemplateRequest, | ||
| ): Promise<Contract[]> { | ||
| const baseURL = `${url}/v1/query` | ||
|
|
||
| const requestData: any = { | ||
| templateIds: request.templateIds, | ||
| } | ||
|
|
||
| if (request.filter) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can either
|
||
| requestData.query = | ||
| typeof request.filter === 'string' ? JSON.parse(request.filter) : request.filter | ||
| } | ||
|
|
||
| const requestConfig = { | ||
| method: 'POST', | ||
| baseURL, | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Bearer ${this.config.AUTH_TOKEN}`, | ||
| }, | ||
| data: requestData, | ||
| } | ||
|
|
||
| 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}`) | ||
| } | ||
|
|
||
| return response.response.data.result | ||
| } | ||
|
|
||
| /** | ||
| * Exercise a non-consuming choice on a contract | ||
| */ | ||
| async exerciseChoice(url: string, payload: ExerciseChoiceRequest): Promise<ExerciseResult> { | ||
| const baseURL = `${url}/v1/exercise` | ||
|
|
||
| const requestConfig = { | ||
| method: 'POST', | ||
| baseURL, | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Bearer ${this.config.AUTH_TOKEN}`, | ||
| }, | ||
| data: payload, | ||
| } | ||
|
|
||
| const response = await this.requester.request<ExerciseResult>(baseURL, requestConfig) | ||
|
|
||
| if (response.response?.status !== 200) { | ||
| throw new Error(`Failed to exercise choice: ${response.response?.statusText}`) | ||
| } | ||
|
|
||
| return response.response.data | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to override the default here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we can always export a different BACKGROUND_EXECUTE_MS env variable. But if we want to do that programmatically, we might need to create in input param for it.