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/sharp-beans-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/decibel-vault-adapter': major
---

Adds first release of Decibel Vault EA
20 changes: 20 additions & 0 deletions .pnp.cjs

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

40 changes: 40 additions & 0 deletions packages/sources/decibel-vault/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@chainlink/decibel-vault-adapter",
"version": "0.0.1",
"description": "Chainlink decibel-vault-adapter.",
"keywords": [
"Chainlink",
"LINK",
"blockchain",
"oracle",
"decibel-vault"
],
"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": {
"@types/jest": "^29.5.14",
"@types/node": "22.14.1",
"nock": "13.5.6",
"typescript": "5.8.3"
},
"dependencies": {
"@chainlink/external-adapter-framework": "2.13.1",
"tslib": "2.4.1"
}
}
23 changes: 23 additions & 0 deletions packages/sources/decibel-vault/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { AdapterConfig } from '@chainlink/external-adapter-framework/config'

export const config = new AdapterConfig({
APTOS_RPC_URL: {
description: 'The Aptos fullnode REST API URL',
type: 'string',
required: true,
sensitive: false,
},
DECIBEL_VAULT_MODULE_ADDRESS: {
description: 'The Decibel vault module address on Aptos',
type: 'string',
required: true,
default: '0x50ead22afd6ffd9769e3b3d6e0e64a2a350d68e8b102c4e72e33d0b8cfdfdb06',
sensitive: false,
},
BACKGROUND_EXECUTE_MS: {
description:
'The number of milliseconds the background execute loop should sleep before performing the next iteration',
type: 'number',
default: 10_000,
},
})
1 change: 1 addition & 0 deletions packages/sources/decibel-vault/src/endpoint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { endpoint as sharePrice } from './share-price'
46 changes: 46 additions & 0 deletions packages/sources/decibel-vault/src/endpoint/share-price.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
import { config } from '../config'
import { sharePriceTransport } from '../transport/share-price'

export const inputParameters = new InputParameters(
{
vault_object_id: {
description: 'The Aptos object ID of the Decibel vault to query',
type: 'string',
required: true,
},
output_decimals: {
description: 'Number of decimals to scale the output share price (default 18)',
type: 'number',
required: false,
default: 18,
},
},
[
{
vault_object_id: '0x06ad70a9a4f30349b489791e2f2bcf58363dad30e54a9d2d4095d6213d7a9bf9',
output_decimals: 18,
},
],
)

export type BaseEndpointTypes = {
Parameters: typeof inputParameters.definition
Response: {
Result: string | null
Data: {
result: string
share_price: string
vault_nav: string
vault_total_shares: string
}
}
Settings: typeof config.settings
}

export const endpoint = new AdapterEndpoint({
name: 'share-price',
transport: sharePriceTransport,
inputParameters,
})
21 changes: 21 additions & 0 deletions packages/sources/decibel-vault/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
import { config } from './config'
import { sharePrice } from './endpoint'

export const adapter = new Adapter({
name: 'DECIBEL_VAULT',
defaultEndpoint: sharePrice.name,
config,
endpoints: [sharePrice],
rateLimiting: {
tiers: {
default: {
rateLimit1s: 10,
note: 'Aptos fullnode REST API default rate limit',
},
},
},
})

export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
144 changes: 144 additions & 0 deletions packages/sources/decibel-vault/src/transport/share-price.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
import {
TimestampedAdapterResponse,
makeLogger,
sleep,
} from '@chainlink/external-adapter-framework/util'
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
import { AdapterError } from '@chainlink/external-adapter-framework/validation/error'
import { BaseEndpointTypes, inputParameters } from '../endpoint/share-price'

const logger = makeLogger('DecibelVaultSharePriceTransport')

type RequestParams = typeof inputParameters.validated

class SharePriceTransport extends SubscriptionTransport<BaseEndpointTypes> {
requester!: Requester
settings!: BaseEndpointTypes['Settings']

async initialize(
dependencies: TransportDependencies<BaseEndpointTypes>,
adapterSettings: BaseEndpointTypes['Settings'],
endpointName: string,
transportName: string,
): Promise<void> {
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
this.requester = dependencies.requester
this.settings = adapterSettings
}

getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number {
return adapterSettings.WARMUP_SUBSCRIPTION_TTL
}

async backgroundHandler(context: EndpointContext<BaseEndpointTypes>, entries: RequestParams[]) {
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)
}

async handleRequest(param: RequestParams) {
let response: TimestampedAdapterResponse<BaseEndpointTypes['Response']>
try {
response = await this._handleRequest(param)
} catch (e: unknown) {
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred'
logger.error(e, errorMessage)
response = {
statusCode: (e as AdapterError)?.statusCode || 502,
errorMessage,
timestamps: {
providerDataRequestedUnixMs: 0,
providerDataReceivedUnixMs: 0,
providerIndicatedTimeUnixMs: undefined,
},
}
}

await this.responseCache.write(this.name, [{ params: param, response }])
}

async _handleRequest(
param: RequestParams,
): Promise<TimestampedAdapterResponse<BaseEndpointTypes['Response']>> {
const providerDataRequestedUnixMs = Date.now()
const { APTOS_RPC_URL, DECIBEL_VAULT_MODULE_ADDRESS } = this.settings
const { vault_object_id, output_decimals } = param

const navResult = await this.callViewFunction(
APTOS_RPC_URL,
`${DECIBEL_VAULT_MODULE_ADDRESS}::vault::get_vault_net_asset_value`,
[vault_object_id],
)

const sharesResult = await this.callViewFunction(
APTOS_RPC_URL,
`${DECIBEL_VAULT_MODULE_ADDRESS}::vault::get_vault_num_shares`,
[vault_object_id],
)

const nav = BigInt(navResult)
const shares = BigInt(sharesResult)

if (shares === 0n) {
throw new AdapterError({
statusCode: 502,
message: 'INVALID_SHARES: vault total shares is zero',
})
}

const sharePrice = ((nav * 10n ** BigInt(output_decimals)) / shares).toString()

return {
data: {
result: sharePrice,
share_price: sharePrice,
vault_nav: navResult,
vault_total_shares: sharesResult,
},
result: sharePrice,
timestamps: {
providerDataRequestedUnixMs,
providerDataReceivedUnixMs: Date.now(),
providerIndicatedTimeUnixMs: undefined,
},
}
}

private async callViewFunction(
rpcUrl: string,
functionSignature: string,
args: string[],
): Promise<string> {
const requestConfig = {
baseURL: rpcUrl,
url: '/view',
method: 'POST' as const,
headers: { 'Content-Type': 'application/json' },
data: {
function: functionSignature,
type_arguments: [],
arguments: args,
},
}

const result = await this.requester.request<string[]>(
JSON.stringify(requestConfig),
requestConfig,
)

if (!Array.isArray(result.response.data) || result.response.data.length === 0) {
throw new AdapterError({
statusCode: 502,
message: `Aptos view function ${functionSignature} returned invalid response: ${JSON.stringify(
result.response.data,
)}`,
})
}

return String(result.response.data[0])
}
}

export const sharePriceTransport = new SharePriceTransport()
Loading
Loading