Skip to content

Commit bc0d130

Browse files
Decibel vault draft
1 parent 4ca2a61 commit bc0d130

11 files changed

Lines changed: 555 additions & 0 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "@chainlink/decibel-vault-adapter",
3+
"version": "0.0.1",
4+
"description": "Chainlink decibel-vault-adapter.",
5+
"keywords": [
6+
"Chainlink",
7+
"LINK",
8+
"blockchain",
9+
"oracle",
10+
"decibel-vault-adapter"
11+
],
12+
"main": "dist/index.js",
13+
"types": "dist/index.d.ts",
14+
"files": [
15+
"dist"
16+
],
17+
"repository": {
18+
"url": "https://github.com/smartcontractkit/external-adapters-js",
19+
"type": "git"
20+
},
21+
"license": "MIT",
22+
"scripts": {
23+
"clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo",
24+
"prepack": "yarn build",
25+
"build": "tsc -b",
26+
"server": "node -e 'require(\"./index.js\").server()'",
27+
"server:dist": "node -e 'require(\"./dist/index.js\").server()'",
28+
"start": "yarn server:dist"
29+
},
30+
"devDependencies": {
31+
"@types/jest": "^29.5.14",
32+
"@types/node": "22.14.1",
33+
"nock": "13.5.6",
34+
"typescript": "5.8.3"
35+
},
36+
"dependencies": {
37+
"@chainlink/external-adapter-framework": "2.11.6",
38+
"tslib": "2.4.1"
39+
}
40+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { AdapterConfig } from '@chainlink/external-adapter-framework/config'
2+
3+
export const config = new AdapterConfig({
4+
APTOS_RPC_URL: {
5+
description: 'The Aptos fullnode REST API URL',
6+
type: 'string',
7+
required: true,
8+
sensitive: false,
9+
},
10+
MODULE_ADDRESS: {
11+
description: 'The Decibel vault module address on Aptos',
12+
type: 'string',
13+
required: true,
14+
default: '0x50ead22afd6ffd9769e3b3d6e0e64a2a350d68e8b102c4e72e33d0b8cfdfdb06',
15+
sensitive: false,
16+
},
17+
BACKGROUND_EXECUTE_MS: {
18+
description:
19+
'The number of milliseconds the background execute loop should sleep before performing the next iteration',
20+
type: 'number',
21+
default: 10_000,
22+
},
23+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { endpoint as sharePrice } from './share-price'
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
2+
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
3+
import { config } from '../config'
4+
import { sharePriceTransport } from '../transport/share-price'
5+
6+
export const inputParameters = new InputParameters(
7+
{
8+
vault_object_id: {
9+
description: 'The Aptos object ID of the Decibel vault to query',
10+
type: 'string',
11+
required: true,
12+
},
13+
output_decimals: {
14+
description: 'Number of decimals to scale the output share price (default 18)',
15+
type: 'number',
16+
required: false,
17+
default: 18,
18+
},
19+
},
20+
[
21+
{
22+
vault_object_id: '0x06ad70a9a4f30349b489791e2f2bcf58363dad30e54a9d2d4095d6213d7a9bf9',
23+
output_decimals: 18,
24+
},
25+
],
26+
)
27+
28+
export type BaseEndpointTypes = {
29+
Parameters: typeof inputParameters.definition
30+
Response: {
31+
Result: number
32+
Data: {
33+
result: number
34+
share_price: string
35+
vault_nav: string
36+
vault_total_shares: string
37+
}
38+
}
39+
Settings: typeof config.settings
40+
}
41+
42+
export const endpoint = new AdapterEndpoint({
43+
name: 'share-price',
44+
transport: sharePriceTransport,
45+
inputParameters,
46+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
2+
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
3+
import { config } from './config'
4+
import { sharePrice } from './endpoint'
5+
6+
export const adapter = new Adapter({
7+
name: 'DECIBEL_VAULT',
8+
defaultEndpoint: sharePrice.name,
9+
config,
10+
endpoints: [sharePrice],
11+
rateLimiting: {
12+
tiers: {
13+
default: {
14+
rateLimit1s: 10,
15+
note: 'Aptos fullnode REST API default rate limit',
16+
},
17+
},
18+
},
19+
})
20+
21+
export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
2+
import { calculateHttpRequestKey } from '@chainlink/external-adapter-framework/cache'
3+
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
4+
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
5+
import {
6+
TimestampedAdapterResponse,
7+
makeLogger,
8+
sleep,
9+
} from '@chainlink/external-adapter-framework/util'
10+
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
11+
import { AdapterError } from '@chainlink/external-adapter-framework/validation/error'
12+
import { BaseEndpointTypes, inputParameters } from '../endpoint/share-price'
13+
14+
const logger = makeLogger('DecibelVaultSharePriceTransport')
15+
16+
type RequestParams = typeof inputParameters.validated
17+
18+
class SharePriceTransport extends SubscriptionTransport<BaseEndpointTypes> {
19+
requester!: Requester
20+
settings!: BaseEndpointTypes['Settings']
21+
endpointName!: string
22+
23+
async initialize(
24+
dependencies: TransportDependencies<BaseEndpointTypes>,
25+
adapterSettings: BaseEndpointTypes['Settings'],
26+
endpointName: string,
27+
transportName: string,
28+
): Promise<void> {
29+
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
30+
this.requester = dependencies.requester
31+
this.settings = adapterSettings
32+
this.endpointName = endpointName
33+
}
34+
35+
getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number {
36+
return adapterSettings.WARMUP_SUBSCRIPTION_TTL
37+
}
38+
39+
async backgroundHandler(context: EndpointContext<BaseEndpointTypes>, entries: RequestParams[]) {
40+
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
41+
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)
42+
}
43+
44+
async handleRequest(param: RequestParams) {
45+
let response: TimestampedAdapterResponse<BaseEndpointTypes['Response']>
46+
try {
47+
response = await this._handleRequest(param)
48+
} catch (e: unknown) {
49+
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred'
50+
logger.error(e, errorMessage)
51+
response = {
52+
statusCode: (e as AdapterError)?.statusCode || 502,
53+
errorMessage,
54+
timestamps: {
55+
providerDataRequestedUnixMs: 0,
56+
providerDataReceivedUnixMs: 0,
57+
providerIndicatedTimeUnixMs: undefined,
58+
},
59+
}
60+
}
61+
62+
await this.responseCache.write(this.name, [{ params: param, response }])
63+
}
64+
65+
async _handleRequest(
66+
param: RequestParams,
67+
): Promise<TimestampedAdapterResponse<BaseEndpointTypes['Response']>> {
68+
const providerDataRequestedUnixMs = Date.now()
69+
const { APTOS_RPC_URL, MODULE_ADDRESS } = this.settings
70+
const { vault_object_id, output_decimals } = param
71+
72+
const navResult = await this.callViewFunction(
73+
APTOS_RPC_URL,
74+
`${MODULE_ADDRESS}::vault::get_vault_net_asset_value`,
75+
[vault_object_id],
76+
)
77+
78+
const sharesResult = await this.callViewFunction(
79+
APTOS_RPC_URL,
80+
`${MODULE_ADDRESS}::vault::get_vault_num_shares`,
81+
[vault_object_id],
82+
)
83+
84+
const nav = BigInt(navResult)
85+
const shares = BigInt(sharesResult)
86+
87+
if (shares === 0n) {
88+
throw new AdapterError({
89+
statusCode: 502,
90+
message: 'INVALID_SHARES: vault total shares is zero',
91+
})
92+
}
93+
94+
let sharePrice: string
95+
if (nav === 0n) {
96+
sharePrice = '0'
97+
} else {
98+
sharePrice = ((nav * 10n ** BigInt(output_decimals)) / shares).toString()
99+
}
100+
101+
return {
102+
data: {
103+
result: Number(sharePrice),
104+
share_price: sharePrice,
105+
vault_nav: navResult,
106+
vault_total_shares: sharesResult,
107+
},
108+
result: Number(sharePrice),
109+
timestamps: {
110+
providerDataRequestedUnixMs,
111+
providerDataReceivedUnixMs: Date.now(),
112+
providerIndicatedTimeUnixMs: undefined,
113+
},
114+
}
115+
}
116+
117+
private async callViewFunction(
118+
rpcUrl: string,
119+
functionSignature: string,
120+
args: string[],
121+
): Promise<string> {
122+
const requestConfig = {
123+
baseURL: rpcUrl,
124+
url: '/view',
125+
method: 'POST' as const,
126+
headers: { 'Content-Type': 'application/json' },
127+
data: {
128+
function: functionSignature,
129+
type_arguments: [],
130+
arguments: args,
131+
},
132+
}
133+
134+
const cacheKey = calculateHttpRequestKey<BaseEndpointTypes>({
135+
context: {
136+
adapterSettings: this.settings,
137+
inputParameters,
138+
endpointName: this.endpointName,
139+
},
140+
data: requestConfig.data,
141+
transportName: this.name,
142+
})
143+
144+
const result = await this.requester.request<string[]>(cacheKey, requestConfig)
145+
146+
if (!Array.isArray(result.response.data) || result.response.data.length === 0) {
147+
throw new AdapterError({
148+
statusCode: 502,
149+
message: `Aptos view function ${functionSignature} returned invalid response: ${JSON.stringify(
150+
result.response.data,
151+
)}`,
152+
})
153+
}
154+
155+
return String(result.response.data[0])
156+
}
157+
}
158+
159+
export const sharePriceTransport = new SharePriceTransport()
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Decibel Vault Adapter share-price endpoint should return error when vault total shares is zero 1`] = `
4+
{
5+
"errorMessage": "INVALID_SHARES: vault total shares is zero",
6+
"statusCode": 502,
7+
"timestamps": {
8+
"providerDataReceivedUnixMs": 0,
9+
"providerDataRequestedUnixMs": 0,
10+
},
11+
}
12+
`;
13+
14+
exports[`Decibel Vault Adapter share-price endpoint should return error when vault_object_id is missing 1`] = `
15+
{
16+
"error": {
17+
"message": "[Param: vault_object_id] param is required but no value was provided",
18+
"name": "AdapterError",
19+
},
20+
"status": "errored",
21+
"statusCode": 400,
22+
}
23+
`;
24+
25+
exports[`Decibel Vault Adapter share-price endpoint should return share_price = 0 when vault NAV is zero 1`] = `
26+
{
27+
"data": {
28+
"result": 0,
29+
"share_price": "0",
30+
"vault_nav": "0",
31+
"vault_total_shares": "1000000",
32+
},
33+
"result": 0,
34+
"statusCode": 200,
35+
"timestamps": {
36+
"providerDataReceivedUnixMs": 1704067200000,
37+
"providerDataRequestedUnixMs": 1704067200000,
38+
},
39+
}
40+
`;
41+
42+
exports[`Decibel Vault Adapter share-price endpoint should return the correct share price for default decimals (18) 1`] = `
43+
{
44+
"data": {
45+
"result": 1056761456089666200,
46+
"share_price": "1056761456089666181",
47+
"vault_nav": "41230251777103",
48+
"vault_total_shares": "39015665777277",
49+
},
50+
"result": 1056761456089666200,
51+
"statusCode": 200,
52+
"timestamps": {
53+
"providerDataReceivedUnixMs": 1704067200000,
54+
"providerDataRequestedUnixMs": 1704067200000,
55+
},
56+
}
57+
`;
58+
59+
exports[`Decibel Vault Adapter share-price endpoint should return the correct share price with custom output_decimals 1`] = `
60+
{
61+
"data": {
62+
"result": 105676145,
63+
"share_price": "105676145",
64+
"vault_nav": "41230251777103",
65+
"vault_total_shares": "39015665777277",
66+
},
67+
"result": 105676145,
68+
"statusCode": 200,
69+
"timestamps": {
70+
"providerDataReceivedUnixMs": 1704067200000,
71+
"providerDataRequestedUnixMs": 1704067200000,
72+
},
73+
}
74+
`;

0 commit comments

Comments
 (0)