Skip to content

Merc6933 GMCI EA - Data Link Feed #3919

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

Open
wants to merge 43 commits into
base: main
Choose a base branch
from

Conversation

Subarna-Singh
Copy link
Contributor

@Subarna-Singh Subarna-Singh commented Jun 25, 2025

Closes #MERC6933

Description

GMCI EA

  • to get price value and rebalnce status of GMCI30 Index
  • uses websocket transportation
  • EA subscribes to two types of messages
  • The EA pricesses the incoming messages and stores them in the cache. When data of oth types of messages are available in the cache, the EA combines them to form the EA response.

(https://smartcontract-it.atlassian.net/browse/MERC-6933)

Changes

  • Create New Adapter for GMCI EA
  • Override buildConnectionHandlers to allow cacheing price and rebalance_status response, and combine the responses to compose the EA response.

Steps to Test

yarn test packeages/source/gmci

Quality Assurance

  • If a new adapter was made, or an existing one was modified so that its environment variables have changed, update the relevant infra-k8s configuration file.
  • If a new adapter was made, or an existing one was modified so that its environment variables have changed, update the relevant adapter-secrets configuration file or update the soak testing blacklist.
  • [ x] If a new adapter was made, or a new endpoint was added, update the test-payload.json file with relevant requests.
  • The branch naming follows git flow (feature/x, chore/x, release/x, hotfix/x, fix/x) or is created from Jira.
  • This is related to a maximum of one Jira story or GitHub issue.
  • Types are safe (avoid TypeScript/TSLint features like any and disable, instead use more specific types).
  • All code changes have 100% unit and integration test coverage. If testing is not applicable or too difficult to justify doing, the reasoning should be documented explicitly in the PR.

Copy link

changeset-bot bot commented Jun 25, 2025

🦋 Changeset detected

Latest commit: 81abf28

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@chainlink/gmci-adapter Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@@ -0,0 +1,5 @@
---
'@chainlink/gmci-adapter': minor
Copy link
Contributor

Choose a reason for hiding this comment

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

major?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FIXED

Copy link
Contributor

Choose a reason for hiding this comment

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

Just in case you don't know. You can just edit these changeset files. You don't need to run the command again. Because now you have 2 changeset files which seems unnecessary for a single PR.
Can you remove one of them and make sure the other one is correct?

@mmcallister-cll mmcallister-cll requested a review from a team July 9, 2025 23:15
},
},
}
transport.price_cache.set(item.symbol, result, 90000)
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is async, there is no guarantee that this result is in the cache by the time you call processMessage.
You should probably just move the entire body here inside processMessage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

rebalance_status_cache = new LocalCache<CachedRebalanceResponse>(10)
}

export async function processMessage(symbols: Array<string>, transport: GMCIWebsocketTransport) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you make this a method on GMCIWebsocketTransport.
It's a bit unfortunate that this function is using transport as a global variable.
If we move it in as a message, it has a cleaner access to everything inside the transport.
Then we still refer to the global transport where the method is called, but at least then it's a single isolated reference.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

expect(message.args).toContain('rebalance_status.gmci30')
expect(message.args.length).toBe(2)
})
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Also add unit tests for the transport itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

Choose a reason for hiding this comment

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

Did you remove it again?

@Subarna-Singh Subarna-Singh requested a review from dskloetc July 16, 2025 12:12
status: 'open',
}

transport.price_cache.set(
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you mock the websocket connection and get this into the cache by sending the corresponding message over the mock websocket?

{
params: { index: 'GMCI30', type: 'price' },
response: {
result: 184,
Copy link
Contributor

Choose a reason for hiding this comment

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

declare variables for the values (price, symbol, timestamps, etc.) so that it's clear that the values you start with are the values you end up with.


expect(mockWrite).toHaveBeenCalledTimes(2)
const [callArg] = mockWrite.mock.calls[0]
expect(callArg).toBe(transport.name)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you know expect(mockWrite).toHaveBeenNthCalledWith(...)?
Then you don't need to reach into the inner workings of the mock.

expect(result.response.data.status).toBe('open')
expect(result.response.data.result).toBe(184)
expect(result.response.timestamps.providerIndicatedTimeUnixMs).toBe(1752494400000)
expect(result.response.timestamps.providerDataReceivedUnixMs).toBe(now)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's better to expect the value of the entire argument, rather than expect specific individual fields.
That way you know you're not missing something you didn't think of or which was added later.

expect(result.response.timestamps.providerDataReceivedUnixMs).toBe(now)
})

it('skips writing to cache if any data is missing', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Also add a test where rebalance status is set but price is not.

providerDataStreamEstablishedUnixMs: transport.providerDataStreamEstablished,
providerDataReceivedUnixMs: now,
providerIndicatedTimeUnixMs:
price_data.response.timestamps.providerIndicatedTimeUnixMs,
Copy link
Contributor

Choose a reason for hiding this comment

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

What is conceptually the difference between providerDataReceivedUnixMs and providerIndicatedTimeUnixMs?
And why do we use the price_data timestamp as opposed to the rebalance_data timestamp for providerIndicatedTimeUnixMs?

Would it make sense to use the maximum of both timestamps for providerDataReceivedUnixMs, rather than creating a new now timestamp?

transport.rebalance_status_cache.get(symbol),
])

if (cached_price_data != undefined && cached_rebalance_data != undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If either of them is undefined, return early.
Then you avoid the extra indentation in the rest of the method.


export interface WsResponse {
success: boolean
data: Array<PriceMessage | RebalanceMessage>
Copy link
Contributor

Choose a reason for hiding this comment

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

We never mix types of message in a single array, right?
If so data: Array<PriceMessage> | Array<RebalanceMessage> would be more accurate.
Or with the following, the compiler could even deduce the type of array from the value of topic:

interface WsPriceResponse {
  success: boolean
  data: Array<PriceMessage>
  topic: 'price'
}

interface WsRebalanceResponse {
  success: boolean
  data: Array<RebalanceMessage>
  topic: 'rebalance_status'
}

export type WsResponse = WsPriceResponse | WsPriceResponse

}

export interface CachedPriceResponse {
params: {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see where these params are used.

response: {
result: number
data: {
result: number
Copy link
Contributor

Choose a reason for hiding this comment

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

This is just an intermediate data object. We don't need to put the same information multiple times in it, right?

…balance_status message - Wintermute does not support rebalance_status
…tractkit/external-adapters-js into MERC6933-GMCI-EA-Data_Link_Feed

Merge remote branch changes
@Subarna-Singh Subarna-Singh requested a review from dskloetc July 17, 2025 09:04
@Subarna-Singh
Copy link
Contributor Author

@dskloetc I have a question.
For the WS_API_ENDPOINT, do we keep a default? Since the same EA can now be used for GMCI and Wintermute.

description: 'WS endpoint for GMCI Data Provider',
type: 'string',
// default: 'wss://api.gmci.co/private',
default: 'wss://generic-alb-ty.api.wintermute-rfq.xyz:15494/private',
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 for testing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#3919 (comment)

Thats my question. We should remove the defaultaccording to me. Forces us to set endpoint for GMCI and wintermute separately.

Copy link
Contributor

Choose a reason for hiding this comment

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

If there is no obvious single default then it makes sense not to have a default at all.

if (message.topic === 'price') {
for (const item of message.data as PriceMessage[]) {
results.push({
params: { index: item.symbol },
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we call it index if it's the same as symbol? Would it make sense to call it symbol everywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup. Makes sense.

@@ -121,44 +160,7 @@ export const options: WebSocketTransportConfig<WsTransportTypes> = {
return
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see a log.

symbol: item.symbol,
},
timestamps: {
providerIndicatedTimeUnixMs: convertTimetoUnixMs(item.last_updated),
Copy link
Contributor

Choose a reason for hiding this comment

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

Weren't there multiple different timestamps before? How do we decide which timestamps to provide?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The others get written here.
Since we had write to the cache ourselves, we have to this bit of work in the previous version.

const results = []

if (message.topic === 'price') {
for (const item of message.data as PriceMessage[]) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think if we do this, we can avoid a cast here.

expect(message.args).toContain('rebalance_status.gmci30')
expect(message.args.length).toBe(2)
})
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you remove it again?

@Subarna-Singh Subarna-Singh requested a review from dskloetc July 18, 2025 08:17

| Required? | Name | Aliases | Description | Type | Options | Default | Depends On | Not Valid With |
| :-------: | :---: | :-----: | :---------: | :----: | :-----: | :-----: | :--------: | :------------: |
| ✅ | index | | Index name | string | | | | |
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you recreate the README now that the name of the parameter is changed?

expect(results?.[0].params.symbol).toBe('GMCI30')
expect(results?.[1].params.symbol).toBe('GML2')
})
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a test that rebalance messages don't cause problems?

@Subarna-Singh Subarna-Singh requested a review from dskloetc July 18, 2025 09:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants