Skip to content
Draft
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/large-cycles-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/mobula-state-adapter': major
---

dedicated pair subscription method using ids instead of symbols and firehose
46 changes: 46 additions & 0 deletions packages/sources/mobula-state/src/config/includes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[
{
"from": "EZETH",
"to": "USD",
"includes": [
{
"from": "102478632",
"to": "USD",
"inverse": false
}
]
},
{
"from": "EZETH",
"to": "ETH",
"includes": [
{
"from": "102478632",
"to": "100004304",
"inverse": false
}
]
},
{
"from": "GHO",
"to": "USD",
"includes": [
{
"from": "2921",
"to": "USD",
"inverse": false
}
]
},
{
"from": "ETH",
"to": "USD",
"includes": [
{
"from": "100004304",
"to": "USD",
"inverse": false
}
]
}
]
12 changes: 6 additions & 6 deletions packages/sources/mobula-state/src/endpoint/price.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
import { CryptoPriceEndpoint } from '@chainlink/external-adapter-framework/adapter'
import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util'
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
import { config } from '../config'
import { wsTransport } from '../transport/price'

export const inputParameters = new InputParameters(
{
base: {
aliases: ['from', 'coin', 'symbol', 'market'],
aliases: ['from', 'coin', 'symbol'],
required: true,
type: 'string',
description: 'The symbol of symbols of the currency to query',
},
quote: {
aliases: ['to', 'convert'],
aliases: ['to', 'market'],
required: true,
type: 'string',
description: 'The symbol of the currency to convert to',
},
},
[
{
base: 'ETH',
base: 'EZETH',
quote: 'USD',
},
],
Expand All @@ -33,7 +33,7 @@ export type BaseEndpointTypes = {
Settings: typeof config.settings
}

export const endpoint = new AdapterEndpoint({
export const endpoint = new CryptoPriceEndpoint({
name: 'price',
aliases: ['state', 'crypto'],
transport: wsTransport,
Expand Down
6 changes: 4 additions & 2 deletions packages/sources/mobula-state/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
import { PriceAdapter } from '@chainlink/external-adapter-framework/adapter'
import { config } from './config'
import includes from './config/includes.json'
import { fundingRate, price } from './endpoint'

export const adapter = new Adapter({
export const adapter = new PriceAdapter({
defaultEndpoint: price.name,
name: 'MOBULA_STATE',
config,
endpoints: [price, fundingRate],
includes,
})

export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
116 changes: 81 additions & 35 deletions packages/sources/mobula-state/src/transport/price.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { WebSocketTransport } from '@chainlink/external-adapter-framework/transports'
import { WebsocketReverseMappingTransport } from '@chainlink/external-adapter-framework/transports'
import { ProviderResult } from '@chainlink/external-adapter-framework/util'
import { BaseEndpointTypes } from '../endpoint/price'

export interface WSResponse {
Expand All @@ -9,51 +10,96 @@ export interface WSResponse {
volume24h: number
baseSymbol: string
quoteSymbol: string
baseID: string
quoteID: string
}

export type WsTransportTypes = BaseEndpointTypes & {
Provider: {
WsMessage: WSResponse
}
}
export const wsTransport = new WebSocketTransport<WsTransportTypes>({
url: (context) => context.adapterSettings.WS_API_ENDPOINT,
handlers: {
open: (connection, context) => {
connection.send(
JSON.stringify({ type: 'feed', authorization: context.adapterSettings.API_KEY }),
)
},
message(message) {
if (!message.price) {

// Get asset ID from the resolved symbol (after framework includes are applied)
const getAssetId = (symbol: string): number => {
const parsed = Number.parseInt(symbol, 10)

if (!Number.isNaN(parsed)) {
return parsed
}

throw new Error(
`Unable to resolve asset ID for symbol: ${symbol}. Please ensure includes.json is configured for this symbol.`,
)
}

// Map quote symbols to IDs - USD doesn't need quote_id, others do
const getQuoteId = (quote: string): number | undefined => {
if (quote.toUpperCase() === 'USD') {
return undefined // USD works without quote_id
}

// Check if quote is already a number (asset ID)
const parsed = Number.parseInt(quote, 10)
if (!Number.isNaN(parsed)) {
return parsed
}

// For now, if it's not USD and not a number, we need includes.json mapping
// The framework should have applied includes mapping by this point
throw new Error(
`Unable to resolve quote ID for symbol: ${quote}. Please ensure includes.json is configured for this quote currency.`,
)
}

export const wsTransport: WebsocketReverseMappingTransport<WsTransportTypes, string> =
new WebsocketReverseMappingTransport<WsTransportTypes, string>({
url: (context) => context.adapterSettings.WS_API_ENDPOINT,
handlers: {
message: (message): ProviderResult<WsTransportTypes>[] => {
if (!message.price) {
return []
}

// Get original user params using reverse mapping with baseID only
const params = wsTransport.getReverseMapping(message.baseID)
if (!params) {
return []
}

return [
{
params: {
base: message.baseSymbol,
quote: message.quoteSymbol,
},
params, // Use exact original params user sent
response: {
errorMessage: 'No price in message',
statusCode: 500,
},
},
]
}

return [
{
params: { base: message.baseSymbol, quote: message.quoteSymbol },
response: {
result: message.price,
data: {
result: message.price,
},
timestamps: {
providerIndicatedTimeUnixMs: message.timestamp,
data: { result: message.price },
timestamps: { providerIndicatedTimeUnixMs: message.timestamp },
},
},
},
]
]
},
},
builders: {
subscribeMessage: (params, context) => {
const assetId = getAssetId(params.base)
const quoteId = getQuoteId(params.quote)

// Store mapping: baseID -> original user params
wsTransport.setReverseMapping(String(assetId), params)

const subscribeMsg: any = {
type: 'feed',
authorization: context.adapterSettings.API_KEY,
kind: 'asset_ids',
asset_ids: [assetId],
}

if (quoteId !== undefined) {
subscribeMsg.quote_id = quoteId
}

return subscribeMsg
},
unsubscribeMessage: () => undefined,
},
},
})
})
Loading