-
Notifications
You must be signed in to change notification settings - Fork 323
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 1 commit
3b1f95f
cc31167
c6b9360
63ae031
d652378
20ad77c
75522c3
30f8250
76f845e
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 |
|---|---|---|
| @@ -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<string, any> | ||
| } | ||
|
|
||
| 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<Contract[]> { | ||
| const baseURL = `${this.config.JSON_API}/v1/query` | ||
| 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', | ||
|
|
@@ -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<Contract | null> { | ||
| 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<ExerciseResult> { | ||
| 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<ExerciseResult>(baseURL, requestConfig) | ||
|
|
||
| if (response.response?.status !== 200) { | ||
| throw new Error(`Failed to exercise choice: ${response.response?.statusText}`) | ||
| } | ||
|
|
||
| return response.response.data | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,6 @@ export class CantonDataTransport extends SubscriptionTransport<BaseEndpointTypes | |
| ): Promise<void> { | ||
| 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<BaseEndpointTypes | |
| params: RequestParams, | ||
| ): Promise<AdapterResponse<BaseEndpointTypes['Response']>> { | ||
| 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<BaseEndpointTypes | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Find the latest contract by createdAt date | ||
| */ | ||
| private findLatestContract(contracts: any[]): any { | ||
| if (contracts.length === 1) { | ||
| return contracts[0] | ||
| } | ||
|
|
||
| // Sort by createdAt in descending order (latest first) | ||
| return contracts.sort((a, b) => { | ||
|
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. Does Canton not have an API to return the latest contract? And also perhaps even filter by flag ACTIVE as i mentioned above 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. Canton does not have the API for that. The only thing that comes close to this is the |
||
| 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 | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
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.
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