Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fair-geese-beam.md
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
22 changes: 22 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.
3 changes: 3 additions & 0 deletions packages/sources/canton-functions/README.md
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`.
42 changes: 42 additions & 0 deletions packages/sources/canton-functions/package.json
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"
}
}
16 changes: 16 additions & 0 deletions packages/sources/canton-functions/src/config/index.ts
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: {
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

description:
'The amount of time the background execute should sleep before performing the next request',
type: 'number',
default: 1_000,
},
})
67 changes: 67 additions & 0 deletions packages/sources/canton-functions/src/endpoint/canton-data.ts
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(
{
Copy link
Collaborator

Choose a reason for hiding this comment

The 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,
})
1 change: 1 addition & 0 deletions packages/sources/canton-functions/src/endpoint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { endpoint as cantonData } from './canton-data'
13 changes: 13 additions & 0 deletions packages/sources/canton-functions/src/index.ts
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({
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
113 changes: 113 additions & 0 deletions packages/sources/canton-functions/src/shared/canton-client.ts
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) {
Copy link
Collaborator

@Fletch153 Fletch153 Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can either

  • Return an error if duplicates are found
  • We could also return the full response (all the contracts) and pass this down to the implementing project to handle the return values. This might be preferable

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
}
}
Loading
Loading