Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e7337ba
add canton ea endpoint and transport
cl-mayowa Oct 14, 2025
18bd27c
add changeset
cl-mayowa Oct 14, 2025
c742a59
update contract query and input params
cl-mayowa Oct 17, 2025
511d86e
remove unsued functions and refactor code
cl-mayowa Oct 23, 2025
7740b18
update integration tests for new changes
cl-mayowa Oct 23, 2025
56c9ac7
refactor adapter settings and tests
cl-mayowa Nov 4, 2025
406352a
add custom return handler with tests
cl-mayowa Nov 4, 2025
21b971f
refactor canton to be extensible and added tests
cl-mayowa Nov 12, 2025
af69997
update dependencies lock
cl-mayowa Dec 5, 2025
75afbec
remove outdated snapshots
cl-mayowa Dec 19, 2025
31b221c
update external adapter framework version
cl-mayowa Dec 19, 2025
2372d13
remove useless read me
cl-mayowa Dec 19, 2025
acb7400
Merge branch 'main' into DS-1113-Canton-EA
app-token-issuer-data-feeds[bot] Dec 20, 2025
ba364e6
Merge branch 'main' into DS-1113-Canton-EA
app-token-issuer-data-feeds[bot] Dec 24, 2025
bf71600
Merge branch 'main' into DS-1113-Canton-EA
app-token-issuer-data-feeds[bot] Dec 24, 2025
f9a90a7
Merge branch 'main' into DS-1113-Canton-EA
app-token-issuer-data-feeds[bot] Dec 24, 2025
cb56ec8
Merge branch 'main' into DS-1113-Canton-EA
app-token-issuer-data-feeds[bot] Dec 25, 2025
5fecd29
Merge branch 'main' into DS-1113-Canton-EA
app-token-issuer-data-feeds[bot] Dec 30, 2025
a77e0c9
Merge branch 'main' into DS-1113-Canton-EA
app-token-issuer-data-feeds[bot] Dec 30, 2025
66e56f3
Merge branch 'main' into DS-1113-Canton-EA
app-token-issuer-data-feeds[bot] Dec 31, 2025
9f040d4
Merge branch 'main' into DS-1113-Canton-EA
app-token-issuer-data-feeds[bot] Dec 31, 2025
da9910d
Merge branch 'main' into DS-1113-Canton-EA
app-token-issuer-data-feeds[bot] Dec 31, 2025
6894d6f
Merge branch 'main' into DS-1113-Canton-EA
app-token-issuer-data-feeds[bot] Dec 31, 2025
7d7eca5
Merge branch 'main' into DS-1113-Canton-EA
app-token-issuer-data-feeds[bot] Jan 5, 2026
eb0f8a0
Merge branch 'main' into DS-1113-Canton-EA
app-token-issuer-data-feeds[bot] Jan 5, 2026
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
10 changes: 10 additions & 0 deletions .editorconfig
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure what these new toplevel files are, maybe add them to the gitignore file.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true

[*.{js,json,yml}]
charset = utf-8
indent_style = space
indent_size = 2
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated
1,492 changes: 924 additions & 568 deletions .pnp.cjs

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed .yarn/cache/gopd-npm-1.0.1-10c1d0b534-5fbc7ad57b.zip
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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",
Copy link
Contributor

Choose a reason for hiding this comment

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

0.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.11.4",
"tslib": "2.4.1"
}
}
41 changes: 41 additions & 0 deletions packages/sources/canton-functions/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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,
},
URL: {
description: 'The Canton JSON API URL',
type: 'string',
required: true,
},
TEMPLATE_ID: {
description: 'The template ID to query contracts for (format: packageId:Module:Template)',
type: 'string',
required: true,
},
CHOICE: {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this/are these env vars unique to each contract or to the chain itself?

Reason i'm asking is if we intend to reuse this EA for multiple separate contract calls, you'd either want these to be variable & prefixed with a selector name or set in the request params rather than a single env var here.

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,
},
CONTRACT_FILTER: {
description: 'Filter to query contracts when contractId is not provided (JSON string)',
type: 'string',
required: false,
},
})
39 changes: 39 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,39 @@
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(
{
contractId: {
description: 'The contract ID to exercise choice on',
type: 'string',
required: false,
},
},
[
{
contractId: '00e1f5c6d8b9a7f4e3c2d1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0',
},
],
)

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: [],
Copy link
Contributor

Choose a reason for hiding this comment

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

can remove this line

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'
24 changes: 24 additions & 0 deletions packages/sources/canton-functions/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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<ServerInstance | undefined> => expose(adapter)

// Export types and utilities for secondary adapters
export { BaseEndpointTypes, inputParameters } from './endpoint/canton-data'
export type {
Contract,
ExerciseChoiceRequest,
ExerciseResponse,
QueryContractByTemplateRequest,
} from './shared/canton-client'
export { CantonDataTransport, ResultHandler } from './transport/canton-data'
export { config as cantonConfig }
126 changes: 126 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,126 @@
import { Requester } from '@chainlink/external-adapter-framework/util/requester'

export interface CantonClientConfig {
AUTH_TOKEN: string
URL: 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 {
Copy link
Contributor

@mmcallister-cll mmcallister-cll Dec 19, 2025

Choose a reason for hiding this comment

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

is this true for every contract?

contractId: string
templateId: string
payload: Record<string, any>
signatories: string[]
observers: string[]
agreementText: string
createdAt?: string
}

export interface ExerciseResult {
completionOffset: string
events: any[]
exerciseResult: any
}

export interface ExerciseResponse {
result: any
status: number
}

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(request: QueryContractByTemplateRequest): Promise<Contract[]> {
const baseURL = `${this.config.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',
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}`)
}

const contracts = response.response.data.result

// When a filter is provided, it should return exactly one contract
if (request.filter && contracts.length > 1) {
throw new Error(
`Filter query returned ${contracts.length} contracts, but expected exactly 1. `,
)
}

return contracts
}

/**
* Exercise a non-consuming choice on a contract
*/
async exerciseChoice(payload: ExerciseChoiceRequest): Promise<ExerciseResult> {
const baseURL = `${this.config.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<ExerciseResponse>(baseURL, requestConfig)

if (response.response?.status !== 200) {
throw new Error(`Failed to exercise choice: ${response.response?.statusText}`)
}

return response.response.data.result
}
}
Loading
Loading