From 2bef6086a2b23f239c9519dd1bb32a69a7f2d44f Mon Sep 17 00:00:00 2001 From: mayowa Date: Thu, 28 Aug 2025 14:13:26 +0100 Subject: [PATCH 1/9] copper-ea wip --- packages/sources/copper/CHANGELOG.md | 0 packages/sources/copper/README.md | 3 + packages/sources/copper/package.json | 42 +++++ packages/sources/copper/src/config/index.ts | 26 +++ .../sources/copper/src/config/overrides.json | 3 + packages/sources/copper/src/endpoint/index.ts | 1 + .../sources/copper/src/endpoint/solstice.ts | 49 ++++++ packages/sources/copper/src/index.ts | 13 ++ .../sources/copper/src/transport/solstice.ts | 128 ++++++++++++++ .../sources/copper/src/transport/types.ts | 161 +++++++++++++++++ .../sources/copper/src/transport/utils.ts | 165 ++++++++++++++++++ packages/sources/copper/test-payload.json | 6 + .../copper/test/integration/adapter.test.ts | 48 +++++ .../copper/test/integration/fixtures.ts | 22 +++ packages/sources/copper/tsconfig.json | 9 + packages/sources/copper/tsconfig.test.json | 7 + 16 files changed, 683 insertions(+) create mode 100644 packages/sources/copper/CHANGELOG.md create mode 100644 packages/sources/copper/README.md create mode 100644 packages/sources/copper/package.json create mode 100644 packages/sources/copper/src/config/index.ts create mode 100644 packages/sources/copper/src/config/overrides.json create mode 100644 packages/sources/copper/src/endpoint/index.ts create mode 100644 packages/sources/copper/src/endpoint/solstice.ts create mode 100644 packages/sources/copper/src/index.ts create mode 100644 packages/sources/copper/src/transport/solstice.ts create mode 100644 packages/sources/copper/src/transport/types.ts create mode 100644 packages/sources/copper/src/transport/utils.ts create mode 100644 packages/sources/copper/test-payload.json create mode 100644 packages/sources/copper/test/integration/adapter.test.ts create mode 100644 packages/sources/copper/test/integration/fixtures.ts create mode 100644 packages/sources/copper/tsconfig.json create mode 100755 packages/sources/copper/tsconfig.test.json diff --git a/packages/sources/copper/CHANGELOG.md b/packages/sources/copper/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/sources/copper/README.md b/packages/sources/copper/README.md new file mode 100644 index 0000000000..9a2a730361 --- /dev/null +++ b/packages/sources/copper/README.md @@ -0,0 +1,3 @@ +# Chainlink External Adapter for copper + +This README will be generated automatically when code is merged to `main`. If you would like to generate a preview of the README, please run `yarn generate:readme copper`. diff --git a/packages/sources/copper/package.json b/packages/sources/copper/package.json new file mode 100644 index 0000000000..f6e05b6ce8 --- /dev/null +++ b/packages/sources/copper/package.json @@ -0,0 +1,42 @@ +{ + "name": "@chainlink/copper-adapter", + "version": "0.0.0", + "description": "Chainlink copper adapter.", + "keywords": [ + "Chainlink", + "LINK", + "blockchain", + "oracle", + "copper" + ], + "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.7.0", + "@types/crypto-js": "4.2.2", + "crypto-js": "4.2.0", + "tslib": "2.4.1" + } +} diff --git a/packages/sources/copper/src/config/index.ts b/packages/sources/copper/src/config/index.ts new file mode 100644 index 0000000000..f715daf7e0 --- /dev/null +++ b/packages/sources/copper/src/config/index.ts @@ -0,0 +1,26 @@ +import { AdapterConfig } from '@chainlink/external-adapter-framework/config' + +export const config = new AdapterConfig({ + API_KEY: { + description: 'An API key for Data Provider - Copper', + type: 'string', + required: true, + sensitive: true, + }, + API_SECRET: { + description: 'An API secret for Data Provider - Copper', + type: 'string', + required: true, + sensitive: true, + }, + API_ENDPOINT: { + description: 'An API endpoint for Data Provider - Copper', + type: 'string', + default: 'https://api.copper.co', + }, + BACKGROUND_EXECUTE_MS: { + description: 'Background execute time in milliseconds', + type: 'number', + default: 1000, + }, +}) diff --git a/packages/sources/copper/src/config/overrides.json b/packages/sources/copper/src/config/overrides.json new file mode 100644 index 0000000000..47b32ad4de --- /dev/null +++ b/packages/sources/copper/src/config/overrides.json @@ -0,0 +1,3 @@ +{ + "copper": {} +} diff --git a/packages/sources/copper/src/endpoint/index.ts b/packages/sources/copper/src/endpoint/index.ts new file mode 100644 index 0000000000..702f030314 --- /dev/null +++ b/packages/sources/copper/src/endpoint/index.ts @@ -0,0 +1 @@ +export { endpoint as solstice } from './solstice' diff --git a/packages/sources/copper/src/endpoint/solstice.ts b/packages/sources/copper/src/endpoint/solstice.ts new file mode 100644 index 0000000000..b98c045802 --- /dev/null +++ b/packages/sources/copper/src/endpoint/solstice.ts @@ -0,0 +1,49 @@ +import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { InputParameters } from '@chainlink/external-adapter-framework/validation' +import { config } from '../config' +import { solsticeTransport } from '../transport/solstice' + +export const inputParameters = new InputParameters( + { + portfolioId: { + required: true, + type: 'string', + description: 'portfolioId for Solstice on Copper', + }, + currencies: { + required: true, + array: true, + type: 'string', + description: 'currencies for Solstice on Copper', + }, + }, + [ + { + portfolioId: '1134710216', + currencies: ['BTC', 'ETH', 'SOL', 'LTC', 'NEAR', 'USDC', 'USDT'], + }, + ], +) + +export type BaseEndpointTypes = { + Parameters: typeof inputParameters.definition + Response: { + Result: string + Data: { + result: string + decimals: number + exchangeBalances: string[] + rate: { + value: string + decimal: number + } + } + } + Settings: typeof config.settings +} + +export const endpoint = new AdapterEndpoint({ + name: 'solstice', + transport: solsticeTransport, + inputParameters, +}) diff --git a/packages/sources/copper/src/index.ts b/packages/sources/copper/src/index.ts new file mode 100644 index 0000000000..685d506932 --- /dev/null +++ b/packages/sources/copper/src/index.ts @@ -0,0 +1,13 @@ +import { expose, ServerInstance } from '@chainlink/external-adapter-framework' +import { Adapter } from '@chainlink/external-adapter-framework/adapter' +import { config } from './config' +import { solstice } from './endpoint' + +export const adapter = new Adapter({ + defaultEndpoint: solstice.name, + name: 'COPPER', + config, + endpoints: [solstice], +}) + +export const server = (): Promise => expose(adapter) diff --git a/packages/sources/copper/src/transport/solstice.ts b/packages/sources/copper/src/transport/solstice.ts new file mode 100644 index 0000000000..5c8f2f2308 --- /dev/null +++ b/packages/sources/copper/src/transport/solstice.ts @@ -0,0 +1,128 @@ +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 { AdapterResponse, 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/solstice' + +// import { getAssetPositions } from './mirrorX' +// import { btcToUSD } from './btcUSD' +// import { ethers } from 'ethers' +// import { Decimal } from 'decimal.js' +import { getWallets } from './utils' + +const logger = makeLogger('Solstice') + +type RequestParams = typeof inputParameters.validated + +export class SolsticeTransport extends SubscriptionTransport { + baseUrl!: string + apiKey!: string + apiSecret!: string + requester!: Requester + // provider!: ethers.JsonRpcProvider + + async initialize( + dependencies: TransportDependencies, + adapterSettings: BaseEndpointTypes['Settings'], + endpointName: string, + transportName: string, + ): Promise { + await super.initialize(dependencies, adapterSettings, endpointName, transportName) + this.requester = dependencies.requester + this.baseUrl = adapterSettings.API_ENDPOINT + this.apiKey = adapterSettings.API_KEY + this.apiSecret = adapterSettings.API_SECRET + // this.provider = new ethers.JsonRpcProvider( + // adapterSettings.ARBITRUM_RPC_URL, + // adapterSettings.ARBITRUM_RPC_CHAIN_ID, + // ) + } + async backgroundHandler(context: EndpointContext, 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: AdapterResponse + try { + response = await this._handleRequest(param) + } catch (e) { + 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> { + const providerDataRequestedUnixMs = Date.now() + + const [wallets /*activeStakes, pendingStakes, outstandingStakes*/] = await Promise.all([ + getWallets( + param.portfolioId, + param.currencies, + this.baseUrl, + this.apiKey, + this.apiSecret, + this.requester, + ), + // getActiveStakes(param.portfolioId, param.currencies, this.baseUrl, this.apiKey, this.apiSecret, this.requester), + // getPendingStakes(param.portfolioId, param.currencies, this.baseUrl, this.apiKey, this.apiSecret, this.requester), + // getOutstandingStakes(param.portfolioId, param.currencies, this.baseUrl, this.apiKey, this.apiSecret, this.requester), + ]) + + console.log(wallets /*activeStakes, pendingStakes, outstandingStakes*/) + + // const [asset, rate] = await Promise.all([ + // getAssetPositions( + // param.addresses.flatMap((a) => a.address), + // this.url, + // this.proxy, + // this.apiKey, + // this.privateKey, + // this.requester, + // ), + // btcToUSD(this.provider, param.btcUsdContract), + // ]) + + const result = 1 + // BigInt(asset.sum.mul(new Decimal(10).pow(rate.decimal * 2)).toFixed(0)) / rate.value + + return { + data: { + result: String(result), + decimals: 0, //rate.decimal, + exchangeBalances: [], //asset.exchangeBalances, + rate: { + value: '', //String(rate.value), + decimal: 0, //rate.decimal, + }, + }, + statusCode: 200, + result: String(result), + timestamps: { + providerDataRequestedUnixMs, + providerDataReceivedUnixMs: Date.now(), + providerIndicatedTimeUnixMs: undefined, + }, + } + } + + getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number { + return adapterSettings.WARMUP_SUBSCRIPTION_TTL + } +} + +export const solsticeTransport = new SolsticeTransport() diff --git a/packages/sources/copper/src/transport/types.ts b/packages/sources/copper/src/transport/types.ts new file mode 100644 index 0000000000..d189de38e5 --- /dev/null +++ b/packages/sources/copper/src/transport/types.ts @@ -0,0 +1,161 @@ +export interface WalletsEndpointResponse { + wallets: { + available: string + balance: string + createdAt: string + currency: string + extra: { + externalAccountId: string + } + locked: string + mainCurrency: string + organizationId: string + portfolioId: string + portfolioType: 'custody' + reserve: string + stakeBalance: string + totalBalance: string + updatedAt: string + walletId: string + }[] +} + +export interface ActiveStakesEndpointResponse { + activeStakes: { + activeStake: { + activeStakedAmount: string + claimableRewardsAmount: string + endTimeSeconds: string + pool: { + extra: { + netuid: string + } + poolId: string + poolName: string + totalBonded: string + } + pools: [ + { + extra: { + netuid: string + } + poolId: string + poolName: string + totalBonded: string + }, + ] + readyToRedelegate: true + readyToUndelegateStakingPermissions: true + readyToUnstake: true + readyToUnstakeAt: string + requireChillToUnstake: true + rewardRate: string + rewardsAutoRestake: true + rewardsRequireClaim: true + stakeAddress: string + warning: { + code: string + message: string + } + } + activeStakeId: string + createdAt: string + currency: string + depositTargetId: string + mainCurrency: string + organizationId: string + portfolioId: string + updatedAt: string + }[] +} + +export interface PendingStakesEndpointResponse { + pendingStakes: { + createdAt: string + currency: string + depositTargetId: string + mainCurrency: string + organizationId: string + pendingStake: { + canBeRebonded: true + canBeUnstaked: true + originPool: { + extra: { + netuid: string + } + poolId: string + poolName: string + totalBonded: string + } + pendingAmount: string + pendingEndsAt: string + pendingStakeStatus: 'unbonding' + pool: { + extra: { + netuid: string + } + poolId: string + poolName: string + totalBonded: string + } + pools: [ + { + extra: { + netuid: string + } + poolId: string + poolName: string + totalBonded: string + }, + ] + requireChillToUnstake: true + stakeAddress: string + warning: { + code: string + message: string + } + } + pendingStakeId: string + portfolioId: string + updatedAt: string + }[] +} + +export interface OutstandingStakesEndpointResponse { + outstandingStakes: { + createdAt: string + currency: string + depositTargetId: string + mainCurrency: string + organizationId: string + outstandingOperation: { + canCreatePool: true + claimResourceId: string + outstandingAmount: string + outstandingOperationType: 'claim-reward' + pool: { + extra: { + netuid: string + } + poolId: string + poolName: string + totalBonded: string + } + pools: [ + { + extra: { + netuid: string + } + poolId: string + poolName: string + totalBonded: string + }, + ] + rewardResourceIds: [string] + stakeAddress: string + } + outstandingStakeId: string + portfolioId: string + updatedAt: string + }[] +} diff --git a/packages/sources/copper/src/transport/utils.ts b/packages/sources/copper/src/transport/utils.ts new file mode 100644 index 0000000000..50841300da --- /dev/null +++ b/packages/sources/copper/src/transport/utils.ts @@ -0,0 +1,165 @@ +// import CryptoJS from 'crypto-js' +import { Requester } from '@chainlink/external-adapter-framework/util/requester' +import crypto from 'crypto' +import { + ActiveStakesEndpointResponse, + OutstandingStakesEndpointResponse, + PendingStakesEndpointResponse, + WalletsEndpointResponse, +} from './types' +// import { RequesterResult } from '@chainlink/external-adapter-framework/util/requester' +// import { HttpsProxyAgent } from 'https-proxy-agent' + +export const getWallets = async ( + portfolioId: string, + currencies: string[], + baseUrl: string, + apiKey: string, + apiSecret: string, + requester: Requester, +): Promise => { + const params = { + portfolioId, + currencies, + } + + const requestConfig = { + baseURL: baseUrl, + url: '/platform/wallets', + method: 'GET', + headers: generateRequestHeaders('GET', '/platform/wallets', '', apiKey, apiSecret), + params, + } + + const response = await requester.request( + JSON.stringify(requestConfig), + requestConfig, + ) + + return response.response.data +} + +export const getActiveStakes = async ( + portfolioId: string, + currencies: string[], + baseUrl: string, + apiKey: string, + apiSecret: string, + requester: Requester, +): Promise => { + const params = { + portfolioId, + currencies, + } + + const requestConfig = { + baseURL: baseUrl, + url: '/platform/active-stakes', + method: 'GET', + headers: generateRequestHeaders('GET', '/platform/active-stakes', '', apiKey, apiSecret), + params, + } + + const response = await requester.request( + JSON.stringify(requestConfig), + requestConfig, + ) + + return response.response.data +} + +export const getPendingStakes = async ( + portfolioId: string, + currencies: string[], + baseUrl: string, + apiKey: string, + apiSecret: string, + requester: Requester, +): Promise => { + const params = { + portfolioId, + currencies, + } + + const requestConfig = { + baseURL: baseUrl, + url: '/platform/pending-stakes', + method: 'GET', + headers: generateRequestHeaders('GET', '/platform/pending-stakes', '', apiKey, apiSecret), + params, + } + + const response = await requester.request( + JSON.stringify(requestConfig), + requestConfig, + ) + + return response.response.data +} + +export const getOutstandingStakes = async ( + portfolioId: string, + currencies: string[], + baseUrl: string, + apiKey: string, + apiSecret: string, + requester: Requester, +): Promise => { + const params = { + portfolioId, + currencies, + } + + const requestConfig = { + baseURL: baseUrl, + url: '/platform/outstanding-stakes', + method: 'GET', + headers: generateRequestHeaders('GET', '/platform/outstanding-stakes', '', apiKey, apiSecret), + params, + } + + const response = await requester.request( + JSON.stringify(requestConfig), + requestConfig, + ) + + return response.response.data +} + +const generateRequestHeaders = ( + method: string, + path: string, + body = '', + apiKey: string, + apiSecret: string, +): any => { + console.log(apiKey, apiSecret) + const timestamp = '1756380173981' //Date.now().toString(); + const signature = generateSignature(timestamp, method, path, body, apiSecret) + console.log('signature:', signature) + + return { + Authorization: `ApiKey ${apiKey}`, + 'X-Signature': signature, + 'X-Timestamp': timestamp, + } +} + +const generateSignature = ( + timestamp: string, + method: string, + path: string, + body = '', + apiSecret: string, +): string => { + // console.log(timestamp, method, path, body, apiSecret) + const preHash = timestamp + method.toUpperCase() + path + body + console.log(preHash) + return crypto.createHmac('sha256', apiSecret).update(preHash).digest('hex') +} + +// return { +// 'X-COPPER-API-KEY': apiKey, +// 'X-COPPER-SIGNATURE': signature, +// 'X-COPPER-TIMESTAMP': timestamp, +// }; diff --git a/packages/sources/copper/test-payload.json b/packages/sources/copper/test-payload.json new file mode 100644 index 0000000000..7930bcfa20 --- /dev/null +++ b/packages/sources/copper/test-payload.json @@ -0,0 +1,6 @@ +{ + "requests": [{ + "from": "BTC", + "to": "USD" + }] +} diff --git a/packages/sources/copper/test/integration/adapter.test.ts b/packages/sources/copper/test/integration/adapter.test.ts new file mode 100644 index 0000000000..eeb575ef86 --- /dev/null +++ b/packages/sources/copper/test/integration/adapter.test.ts @@ -0,0 +1,48 @@ +import { + TestAdapter, + setEnvVariables, +} from '@chainlink/external-adapter-framework/util/testing-utils' +import * as nock from 'nock' + +describe('execute', () => { + let spy: jest.SpyInstance + let testAdapter: TestAdapter + let oldEnv: NodeJS.ProcessEnv + + beforeAll(async () => { + oldEnv = JSON.parse(JSON.stringify(process.env)) + process.env.API_KEY = 'LvGtQu5MjWdWTWb1sEHiDYgbrCe4vWzvAC9kBfaC7Gf7Z3HCSZbhcd4EpsR7KSqe89e8' //process.env.API_KEY ?? 'fake-api-key' + process.env.API_SECRET = + '0SYnWsVGacK1Dx37UXtNT1AiJO8Pa4P0CWQT4HYLwiRuXOQsPvoamuXGZ9e1LYO8PqdjuWjTsoWDsoQmNsaQZJK8Qr9W5imJAIf7BYb2z4K4JcdIlLQNQWofCYCzc5Hs' //process.env.API_SECRET ?? 'fake-api-secret' + + //const mockDate = new Date('2001-01-01T11:11:11.111Z') + //spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) + + const adapter = (await import('./../../src')).adapter + adapter.rateLimiting = undefined + testAdapter = await TestAdapter.startWithMockedCache(adapter, { + testAdapter: {} as TestAdapter, + }) + }) + + afterAll(async () => { + setEnvVariables(oldEnv) + await testAdapter.api.close() + nock.restore() + nock.cleanAll() + //spy.mockRestore() + }) + + describe('solstice endpoint', () => { + it('should return success', async () => { + const data = { + portfolioId: 'cme0yn5cu00743b6uvbqj9ysn', + currencies: ['BTC', 'ETH', 'SOL', 'LTC', 'NEAR', 'USDC', 'USDT'], + } + // mockResponseSuccess() + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + }) +}) diff --git a/packages/sources/copper/test/integration/fixtures.ts b/packages/sources/copper/test/integration/fixtures.ts new file mode 100644 index 0000000000..3b7e58e409 --- /dev/null +++ b/packages/sources/copper/test/integration/fixtures.ts @@ -0,0 +1,22 @@ +import nock from 'nock' + +export const mockResponseSuccess = (): nock.Scope => + nock('https://dataproviderapi.com', { + encodedQueryParams: true, + }) + .get('/cryptocurrency/price') + .query({ + symbol: 'ETH', + convert: 'USD', + }) + .reply(200, () => ({ ETH: { price: 10000 } }), [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + 'Vary', + 'Accept-Encoding', + 'Vary', + 'Origin', + ]) + .persist() diff --git a/packages/sources/copper/tsconfig.json b/packages/sources/copper/tsconfig.json new file mode 100644 index 0000000000..f59363fd76 --- /dev/null +++ b/packages/sources/copper/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*", "src/**/*.json"], + "exclude": ["dist", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/sources/copper/tsconfig.test.json b/packages/sources/copper/tsconfig.test.json new file mode 100755 index 0000000000..e3de28cb5c --- /dev/null +++ b/packages/sources/copper/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*", "**/test", "src/**/*.json"], + "compilerOptions": { + "noEmit": true + } +} From 22c781a120acba018b6db1301cf1750f91d32e58 Mon Sep 17 00:00:00 2001 From: mayowa Date: Wed, 3 Sep 2025 15:19:49 +0100 Subject: [PATCH 2/9] wip: add copper ea --- .pnp.cjs | 22 ++++++++ .../sources/copper/src/transport/solstice.ts | 45 +++++++++------- .../sources/copper/src/transport/types.ts | 54 +++++++++---------- .../sources/copper/src/transport/utils.ts | 48 ++++++++++------- .../copper/test/integration/adapter.test.ts | 14 ++--- packages/tsconfig.json | 3 ++ packages/tsconfig.test.json | 3 ++ yarn.lock | 15 ++++++ 8 files changed, 130 insertions(+), 74 deletions(-) diff --git a/.pnp.cjs b/.pnp.cjs index 953315d600..184cc64632 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -398,6 +398,10 @@ const RAW_RUNTIME_STATE = "name": "@chainlink/coinranking-adapter",\ "reference": "workspace:packages/sources/coinranking"\ },\ + {\ + "name": "@chainlink/copper-adapter",\ + "reference": "workspace:packages/sources/copper"\ + },\ {\ "name": "@chainlink/covid-tracker-adapter",\ "reference": "workspace:packages/sources/covid-tracker"\ @@ -1051,6 +1055,7 @@ const RAW_RUNTIME_STATE = ["@chainlink/coinpaprika-adapter", ["workspace:packages/sources/coinpaprika"]],\ ["@chainlink/coinranking-adapter", ["workspace:packages/sources/coinranking"]],\ ["@chainlink/conflux-adapter", ["workspace:packages/targets/conflux"]],\ + ["@chainlink/copper-adapter", ["workspace:packages/sources/copper"]],\ ["@chainlink/covid-tracker-adapter", ["workspace:packages/sources/covid-tracker"]],\ ["@chainlink/cryptex-adapter", ["workspace:packages/sources/cryptex"]],\ ["@chainlink/crypto-volatility-index-adapter", ["workspace:packages/composites/crypto-volatility-index"]],\ @@ -6149,6 +6154,23 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@chainlink/copper-adapter", [\ + ["workspace:packages/sources/copper", {\ + "packageLocation": "./packages/sources/copper/",\ + "packageDependencies": [\ + ["@chainlink/copper-adapter", "workspace:packages/sources/copper"],\ + ["@chainlink/external-adapter-framework", "npm:2.7.0"],\ + ["@types/crypto-js", "npm:4.2.2"],\ + ["@types/jest", "npm:29.5.14"],\ + ["@types/node", "npm:22.14.1"],\ + ["crypto-js", "npm:4.2.0"],\ + ["nock", "npm:13.5.6"],\ + ["tslib", "npm:2.4.1"],\ + ["typescript", "patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5"]\ + ],\ + "linkType": "SOFT"\ + }]\ + ]],\ ["@chainlink/covid-tracker-adapter", [\ ["workspace:packages/sources/covid-tracker", {\ "packageLocation": "./packages/sources/covid-tracker/",\ diff --git a/packages/sources/copper/src/transport/solstice.ts b/packages/sources/copper/src/transport/solstice.ts index 5c8f2f2308..33fad6dc03 100644 --- a/packages/sources/copper/src/transport/solstice.ts +++ b/packages/sources/copper/src/transport/solstice.ts @@ -10,7 +10,7 @@ import { BaseEndpointTypes, inputParameters } from '../endpoint/solstice' // import { btcToUSD } from './btcUSD' // import { ethers } from 'ethers' // import { Decimal } from 'decimal.js' -import { getWallets } from './utils' +import { getActiveStakes, getOutstandingStakes, getPendingStakes, getWallets } from './utils' const logger = makeLogger('Solstice') @@ -69,7 +69,7 @@ export class SolsticeTransport extends SubscriptionTransport ): Promise> { const providerDataRequestedUnixMs = Date.now() - const [wallets /*activeStakes, pendingStakes, outstandingStakes*/] = await Promise.all([ + const [wallets, activeStakes, pendingStakes, outstandingStakes] = await Promise.all([ getWallets( param.portfolioId, param.currencies, @@ -78,24 +78,33 @@ export class SolsticeTransport extends SubscriptionTransport this.apiSecret, this.requester, ), - // getActiveStakes(param.portfolioId, param.currencies, this.baseUrl, this.apiKey, this.apiSecret, this.requester), - // getPendingStakes(param.portfolioId, param.currencies, this.baseUrl, this.apiKey, this.apiSecret, this.requester), - // getOutstandingStakes(param.portfolioId, param.currencies, this.baseUrl, this.apiKey, this.apiSecret, this.requester), + getActiveStakes( + param.portfolioId, + param.currencies, + this.baseUrl, + this.apiKey, + this.apiSecret, + this.requester, + ), + getPendingStakes( + param.portfolioId, + param.currencies, + this.baseUrl, + this.apiKey, + this.apiSecret, + this.requester, + ), + getOutstandingStakes( + param.portfolioId, + param.currencies, + this.baseUrl, + this.apiKey, + this.apiSecret, + this.requester, + ), ]) - console.log(wallets /*activeStakes, pendingStakes, outstandingStakes*/) - - // const [asset, rate] = await Promise.all([ - // getAssetPositions( - // param.addresses.flatMap((a) => a.address), - // this.url, - // this.proxy, - // this.apiKey, - // this.privateKey, - // this.requester, - // ), - // btcToUSD(this.provider, param.btcUsdContract), - // ]) + console.log(wallets, activeStakes.activeStakes, pendingStakes, outstandingStakes) const result = 1 // BigInt(asset.sum.mul(new Decimal(10).pow(rate.decimal * 2)).toFixed(0)) / rate.value diff --git a/packages/sources/copper/src/transport/types.ts b/packages/sources/copper/src/transport/types.ts index d189de38e5..a94f6dc59a 100644 --- a/packages/sources/copper/src/transport/types.ts +++ b/packages/sources/copper/src/transport/types.ts @@ -34,16 +34,14 @@ export interface ActiveStakesEndpointResponse { poolName: string totalBonded: string } - pools: [ - { - extra: { - netuid: string - } - poolId: string - poolName: string - totalBonded: string - }, - ] + pools: { + extra: { + netuid: string + } + poolId: string + poolName: string + totalBonded: string + }[] readyToRedelegate: true readyToUndelegateStakingPermissions: true readyToUnstake: true @@ -98,16 +96,14 @@ export interface PendingStakesEndpointResponse { poolName: string totalBonded: string } - pools: [ - { - extra: { - netuid: string - } - poolId: string - poolName: string - totalBonded: string - }, - ] + pools: { + extra: { + netuid: string + } + poolId: string + poolName: string + totalBonded: string + }[] requireChillToUnstake: true stakeAddress: string warning: { @@ -141,16 +137,14 @@ export interface OutstandingStakesEndpointResponse { poolName: string totalBonded: string } - pools: [ - { - extra: { - netuid: string - } - poolId: string - poolName: string - totalBonded: string - }, - ] + pools: { + extra: { + netuid: string + } + poolId: string + poolName: string + totalBonded: string + }[] rewardResourceIds: [string] stakeAddress: string } diff --git a/packages/sources/copper/src/transport/utils.ts b/packages/sources/copper/src/transport/utils.ts index 50841300da..27824f897a 100644 --- a/packages/sources/copper/src/transport/utils.ts +++ b/packages/sources/copper/src/transport/utils.ts @@ -7,8 +7,6 @@ import { PendingStakesEndpointResponse, WalletsEndpointResponse, } from './types' -// import { RequesterResult } from '@chainlink/external-adapter-framework/util/requester' -// import { HttpsProxyAgent } from 'https-proxy-agent' export const getWallets = async ( portfolioId: string, @@ -28,7 +26,7 @@ export const getWallets = async ( url: '/platform/wallets', method: 'GET', headers: generateRequestHeaders('GET', '/platform/wallets', '', apiKey, apiSecret), - params, + //params, NOTE: this was commented out because adding param to the request fails on Copper API - that was what I observed } const response = await requester.request( @@ -54,10 +52,16 @@ export const getActiveStakes = async ( const requestConfig = { baseURL: baseUrl, - url: '/platform/active-stakes', + url: '/platform/staking/active-stakes', method: 'GET', - headers: generateRequestHeaders('GET', '/platform/active-stakes', '', apiKey, apiSecret), - params, + headers: generateRequestHeaders( + 'GET', + '/platform/staking/active-stakes', + '', + apiKey, + apiSecret, + ), + // params,NOTE: this was commented out because adding param to the request fails on Copper API - that was what I observed } const response = await requester.request( @@ -83,10 +87,16 @@ export const getPendingStakes = async ( const requestConfig = { baseURL: baseUrl, - url: '/platform/pending-stakes', + url: '/platform/staking/pending-stakes', method: 'GET', - headers: generateRequestHeaders('GET', '/platform/pending-stakes', '', apiKey, apiSecret), - params, + headers: generateRequestHeaders( + 'GET', + '/platform/staking/pending-stakes', + '', + apiKey, + apiSecret, + ), + // params, NOTE: this was commented out because adding param to the request fails on Copper API - that was what I observed } const response = await requester.request( @@ -112,10 +122,16 @@ export const getOutstandingStakes = async ( const requestConfig = { baseURL: baseUrl, - url: '/platform/outstanding-stakes', + url: '/platform/staking/outstanding-stakes', method: 'GET', - headers: generateRequestHeaders('GET', '/platform/outstanding-stakes', '', apiKey, apiSecret), - params, + headers: generateRequestHeaders( + 'GET', + '/platform/staking/outstanding-stakes', + '', + apiKey, + apiSecret, + ), + // params, NOTE: this was commented out because adding param to the request fails on Copper API - that was what I observed } const response = await requester.request( @@ -134,7 +150,7 @@ const generateRequestHeaders = ( apiSecret: string, ): any => { console.log(apiKey, apiSecret) - const timestamp = '1756380173981' //Date.now().toString(); + const timestamp = Date.now().toString() const signature = generateSignature(timestamp, method, path, body, apiSecret) console.log('signature:', signature) @@ -157,9 +173,3 @@ const generateSignature = ( console.log(preHash) return crypto.createHmac('sha256', apiSecret).update(preHash).digest('hex') } - -// return { -// 'X-COPPER-API-KEY': apiKey, -// 'X-COPPER-SIGNATURE': signature, -// 'X-COPPER-TIMESTAMP': timestamp, -// }; diff --git a/packages/sources/copper/test/integration/adapter.test.ts b/packages/sources/copper/test/integration/adapter.test.ts index eeb575ef86..f3ba88b730 100644 --- a/packages/sources/copper/test/integration/adapter.test.ts +++ b/packages/sources/copper/test/integration/adapter.test.ts @@ -3,6 +3,7 @@ import { setEnvVariables, } from '@chainlink/external-adapter-framework/util/testing-utils' import * as nock from 'nock' +import { mockResponseSuccess } from './fixtures' describe('execute', () => { let spy: jest.SpyInstance @@ -11,12 +12,11 @@ describe('execute', () => { beforeAll(async () => { oldEnv = JSON.parse(JSON.stringify(process.env)) - process.env.API_KEY = 'LvGtQu5MjWdWTWb1sEHiDYgbrCe4vWzvAC9kBfaC7Gf7Z3HCSZbhcd4EpsR7KSqe89e8' //process.env.API_KEY ?? 'fake-api-key' - process.env.API_SECRET = - '0SYnWsVGacK1Dx37UXtNT1AiJO8Pa4P0CWQT4HYLwiRuXOQsPvoamuXGZ9e1LYO8PqdjuWjTsoWDsoQmNsaQZJK8Qr9W5imJAIf7BYb2z4K4JcdIlLQNQWofCYCzc5Hs' //process.env.API_SECRET ?? 'fake-api-secret' + process.env.API_KEY = process.env.API_KEY ?? 'fake-api-key' + process.env.API_SECRET = process.env.API_SECRET ?? 'fake-api-secret' - //const mockDate = new Date('2001-01-01T11:11:11.111Z') - //spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) + const mockDate = new Date('2001-01-01T11:11:11.111Z') + spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) const adapter = (await import('./../../src')).adapter adapter.rateLimiting = undefined @@ -30,7 +30,7 @@ describe('execute', () => { await testAdapter.api.close() nock.restore() nock.cleanAll() - //spy.mockRestore() + spy.mockRestore() }) describe('solstice endpoint', () => { @@ -39,7 +39,7 @@ describe('execute', () => { portfolioId: 'cme0yn5cu00743b6uvbqj9ysn', currencies: ['BTC', 'ETH', 'SOL', 'LTC', 'NEAR', 'USDC', 'USDT'], } - // mockResponseSuccess() + mockResponseSuccess() const response = await testAdapter.request(data) expect(response.statusCode).toBe(200) expect(response.json()).toMatchSnapshot() diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 83eca7c02a..94da59a7e9 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -278,6 +278,9 @@ { "path": "./sources/coinranking" }, + { + "path": "./sources/copper" + }, { "path": "./sources/covid-tracker" }, diff --git a/packages/tsconfig.test.json b/packages/tsconfig.test.json index c5580b12b0..d75451ccae 100644 --- a/packages/tsconfig.test.json +++ b/packages/tsconfig.test.json @@ -278,6 +278,9 @@ { "path": "./sources/coinranking/tsconfig.test.json" }, + { + "path": "./sources/copper/tsconfig.test.json" + }, { "path": "./sources/covid-tracker/tsconfig.test.json" }, diff --git a/yarn.lock b/yarn.lock index e5b8d8878c..b0b19b0c44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3274,6 +3274,21 @@ __metadata: languageName: node linkType: hard +"@chainlink/copper-adapter@workspace:packages/sources/copper": + version: 0.0.0-use.local + resolution: "@chainlink/copper-adapter@workspace:packages/sources/copper" + dependencies: + "@chainlink/external-adapter-framework": "npm:2.7.0" + "@types/crypto-js": "npm:4.2.2" + "@types/jest": "npm:^29.5.14" + "@types/node": "npm:22.14.1" + crypto-js: "npm:4.2.0" + nock: "npm:13.5.6" + tslib: "npm:2.4.1" + typescript: "npm:5.8.3" + languageName: unknown + linkType: soft + "@chainlink/covid-tracker-adapter@workspace:packages/sources/covid-tracker": version: 0.0.0-use.local resolution: "@chainlink/covid-tracker-adapter@workspace:packages/sources/covid-tracker" From 0724ef1b40af980faa64bdcf3636f4086ead0add Mon Sep 17 00:00:00 2001 From: Subarna-Singh Date: Mon, 8 Sep 2025 18:48:17 +0530 Subject: [PATCH 3/9] Adding Changeset --- .changeset/mighty-panthers-carry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mighty-panthers-carry.md diff --git a/.changeset/mighty-panthers-carry.md b/.changeset/mighty-panthers-carry.md new file mode 100644 index 0000000000..a40635d452 --- /dev/null +++ b/.changeset/mighty-panthers-carry.md @@ -0,0 +1,5 @@ +--- +'@chainlink/copper-adapter': major +--- + +Copper EA From cd4ef9e47786963d1b6c99b366b0a6fde9f62d68 Mon Sep 17 00:00:00 2001 From: Subarna-Singh Date: Mon, 8 Sep 2025 18:50:50 +0530 Subject: [PATCH 4/9] Delete unused files --- .../sources/copper/src/transport/types.ts | 155 ------------------ 1 file changed, 155 deletions(-) delete mode 100644 packages/sources/copper/src/transport/types.ts diff --git a/packages/sources/copper/src/transport/types.ts b/packages/sources/copper/src/transport/types.ts deleted file mode 100644 index a94f6dc59a..0000000000 --- a/packages/sources/copper/src/transport/types.ts +++ /dev/null @@ -1,155 +0,0 @@ -export interface WalletsEndpointResponse { - wallets: { - available: string - balance: string - createdAt: string - currency: string - extra: { - externalAccountId: string - } - locked: string - mainCurrency: string - organizationId: string - portfolioId: string - portfolioType: 'custody' - reserve: string - stakeBalance: string - totalBalance: string - updatedAt: string - walletId: string - }[] -} - -export interface ActiveStakesEndpointResponse { - activeStakes: { - activeStake: { - activeStakedAmount: string - claimableRewardsAmount: string - endTimeSeconds: string - pool: { - extra: { - netuid: string - } - poolId: string - poolName: string - totalBonded: string - } - pools: { - extra: { - netuid: string - } - poolId: string - poolName: string - totalBonded: string - }[] - readyToRedelegate: true - readyToUndelegateStakingPermissions: true - readyToUnstake: true - readyToUnstakeAt: string - requireChillToUnstake: true - rewardRate: string - rewardsAutoRestake: true - rewardsRequireClaim: true - stakeAddress: string - warning: { - code: string - message: string - } - } - activeStakeId: string - createdAt: string - currency: string - depositTargetId: string - mainCurrency: string - organizationId: string - portfolioId: string - updatedAt: string - }[] -} - -export interface PendingStakesEndpointResponse { - pendingStakes: { - createdAt: string - currency: string - depositTargetId: string - mainCurrency: string - organizationId: string - pendingStake: { - canBeRebonded: true - canBeUnstaked: true - originPool: { - extra: { - netuid: string - } - poolId: string - poolName: string - totalBonded: string - } - pendingAmount: string - pendingEndsAt: string - pendingStakeStatus: 'unbonding' - pool: { - extra: { - netuid: string - } - poolId: string - poolName: string - totalBonded: string - } - pools: { - extra: { - netuid: string - } - poolId: string - poolName: string - totalBonded: string - }[] - requireChillToUnstake: true - stakeAddress: string - warning: { - code: string - message: string - } - } - pendingStakeId: string - portfolioId: string - updatedAt: string - }[] -} - -export interface OutstandingStakesEndpointResponse { - outstandingStakes: { - createdAt: string - currency: string - depositTargetId: string - mainCurrency: string - organizationId: string - outstandingOperation: { - canCreatePool: true - claimResourceId: string - outstandingAmount: string - outstandingOperationType: 'claim-reward' - pool: { - extra: { - netuid: string - } - poolId: string - poolName: string - totalBonded: string - } - pools: { - extra: { - netuid: string - } - poolId: string - poolName: string - totalBonded: string - }[] - rewardResourceIds: [string] - stakeAddress: string - } - outstandingStakeId: string - portfolioId: string - updatedAt: string - }[] -} From 038c5710981c460b110501abd02f2a80e9fcfb14 Mon Sep 17 00:00:00 2001 From: Subarna-Singh Date: Mon, 8 Sep 2025 19:39:05 +0530 Subject: [PATCH 5/9] FIX: lint error --- packages/sources/copper/src/transport/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sources/copper/src/transport/utils.ts b/packages/sources/copper/src/transport/utils.ts index dfa48604dc..b691746192 100644 --- a/packages/sources/copper/src/transport/utils.ts +++ b/packages/sources/copper/src/transport/utils.ts @@ -11,7 +11,7 @@ export type OraclePriceType = { export function signRequest( method: string, path: string, - body: string = '', + body = '', apiKey: string, apiSecret: string, params: Record = {}, From 4bce6b4b6e82113d14926f5f881b2c34506cdf3f Mon Sep 17 00:00:00 2001 From: Subarna-Singh Date: Mon, 8 Sep 2025 19:41:34 +0530 Subject: [PATCH 6/9] update test-payload --- packages/sources/copper/test-payload.json | 56 ++++++++++++----------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/sources/copper/test-payload.json b/packages/sources/copper/test-payload.json index 743096ef1e..68ea8fbd05 100644 --- a/packages/sources/copper/test-payload.json +++ b/packages/sources/copper/test-payload.json @@ -1,30 +1,34 @@ { - "endpoint": "wallets", - "priceOracles": [ + "requests": [ { - "token": "ETH", - "contractAddress": "0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612", - "chainId": "42161" - }, - { - "token": "SOL", - "contractAddress": "0x24ceA4b8ce57cdA5058b924B9B9987992450590c", - "chainId": "42161" - }, - { - "token": "USDC", - "contractAddress": "0x50834F3163758fcC1Df9973b6e91f0F0F0434aD3", - "chainId": "42161" - }, - { - "token": "USDT", - "contractAddress": "0x3f3f5dF88dC9F13eac63DF89EC16ef6e7E25DdE7", - "chainId": "42161" - }, - { - "token": "USTB", - "contractAddress": "0x289B5036cd942e619E1Ee48670F98d214E745AAC", - "chainId": "1" + "endpoint": "wallets", + "priceOracles": [ + { + "token": "ETH", + "contractAddress": "0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612", + "chainId": "42161" + }, + { + "token": "SOL", + "contractAddress": "0x24ceA4b8ce57cdA5058b924B9B9987992450590c", + "chainId": "42161" + }, + { + "token": "USDC", + "contractAddress": "0x50834F3163758fcC1Df9973b6e91f0F0F0434aD3", + "chainId": "42161" + }, + { + "token": "USDT", + "contractAddress": "0x3f3f5dF88dC9F13eac63DF89EC16ef6e7E25DdE7", + "chainId": "42161" + }, + { + "token": "USTB", + "contractAddress": "0x289B5036cd942e619E1Ee48670F98d214E745AAC", + "chainId": "1" + } + ] } ] - } +} From 160d659b63a13415385628e2b945773360a23aed Mon Sep 17 00:00:00 2001 From: Subarna-Singh Date: Tue, 9 Sep 2025 10:06:19 +0530 Subject: [PATCH 7/9] REWORK: review work --- .../sources/copper/src/transport/utils.ts | 15 ---- .../sources/copper/src/transport/wallets.ts | 45 +++++------- .../sources/copper/test/unit/adapter.test.ts | 70 +++++++++++++++++++ .../sources/copper/test/unit/utils.test.ts | 35 ++++++++++ 4 files changed, 123 insertions(+), 42 deletions(-) create mode 100644 packages/sources/copper/test/unit/adapter.test.ts create mode 100644 packages/sources/copper/test/unit/utils.test.ts diff --git a/packages/sources/copper/src/transport/utils.ts b/packages/sources/copper/src/transport/utils.ts index b691746192..7bced4e593 100644 --- a/packages/sources/copper/src/transport/utils.ts +++ b/packages/sources/copper/src/transport/utils.ts @@ -39,21 +39,6 @@ function buildQuery(params: Record): string { .join('&') } -export async function getPrice( - address: string, - provider: ethers.providers.JsonRpcProvider, -): Promise { - const contract = new ethers.Contract(address, EACAggregatorProxy, provider) - const [[_, answer], decimal] = await Promise.all([ - contract.latestRoundData(), - contract.decimals(), - ]) - return { - value: answer, - decimal: Number(decimal), - } -} - export function toBigIntBalance(balance: string, decimals: number): bigint { const [whole, frac = ''] = balance.split('.') const fracPadded = frac.padEnd(decimals, '0') diff --git a/packages/sources/copper/src/transport/wallets.ts b/packages/sources/copper/src/transport/wallets.ts index a1de7476c3..71d99abdd3 100644 --- a/packages/sources/copper/src/transport/wallets.ts +++ b/packages/sources/copper/src/transport/wallets.ts @@ -17,18 +17,18 @@ type RequestParams = typeof inputParameters.validated const RESULT_DECIMALS = 18 const path = '/platform/wallets' -// type PriceOraclesTypes = { -// token: string -// contractAddress: string -// chainId: string -// } - type RequestContext = { groupedProviders: { [chainId: string]: GroupedProvider } } +type PriceOraclesTypes = { + token: string + contractAddress: string + chainId: string +} + export interface WalletResponseSchema { walletId: string portfolioId: string @@ -47,8 +47,6 @@ export class WalletsTransport extends SubscriptionTransport { ethProvider!: ethers.providers.JsonRpcProvider arbProvider!: ethers.providers.JsonRpcProvider - // private providers: Record = {} - async initialize( dependencies: TransportDependencies, adapterSettings: BaseEndpointTypes['Settings'], @@ -77,18 +75,6 @@ export class WalletsTransport extends SubscriptionTransport { Number(adapterSettings.ETHEREUM_RPC_CHAIN_ID), ) } - - // if (adapterSettings.ETHEREUM_RPC_URL) { - // this.providers["ETHEREUM"] = new ethers.providers.JsonRpcProvider(adapterSettings.ETHEREUM_RPC_URL) - // } else { - // logger.warn("Environment variable ETHEREUM_RPC_URL is missing") - // } - - // if (adapterSettings.ARBITRUM_RPC_URL) { - // this.providers["ARBITRUM"] = new ethers.providers.JsonRpcProvider(adapterSettings.ARBITRUM_RPC_URL) - // } else { - // logger.warn("Environment variable ARBITRUM_RPC_URL is missing") - // } } async backgroundHandler(context: EndpointContext, entries: RequestParams[]) { @@ -250,13 +236,18 @@ export class WalletsTransport extends SubscriptionTransport { ): Promise> { const results: Record = {} - for (const oracle of param.priceOracles) { - const { token, contractAddress, chainId } = oracle - const groupedProvider = this.getGroupedProvider(context, chainId, requestContext) - const priceOracleContract = groupedProvider.createPriceOracleContract(contractAddress) - const oraclePriceUSD: OraclePriceType = await priceOracleContract.getRateFromLatestRoundData() - results[token] = oraclePriceUSD - } + await Promise.all( + param.priceOracles.map(async (oracle: PriceOraclesTypes) => { + const { token, contractAddress, chainId } = oracle + const groupedProvider = this.getGroupedProvider(context, chainId, requestContext) + const priceOracleContract = groupedProvider.createPriceOracleContract(contractAddress) + + const oraclePriceUSD: OraclePriceType = + await priceOracleContract.getRateFromLatestRoundData() + + results[token] = oraclePriceUSD + }), + ) return results } diff --git a/packages/sources/copper/test/unit/adapter.test.ts b/packages/sources/copper/test/unit/adapter.test.ts new file mode 100644 index 0000000000..c66e5381c9 --- /dev/null +++ b/packages/sources/copper/test/unit/adapter.test.ts @@ -0,0 +1,70 @@ +import { OraclePriceType, toBigIntBalance } from '../../src/transport/utils' +import { WalletsTransport } from '../../src/transport/wallets' + +const RESULT_DECIMALS = 18 + +describe('WalletsTransport.computeUsdValue', () => { + let transport: WalletsTransport + + beforeEach(() => { + transport = new WalletsTransport() + }) + + it('scales 6-decimal price correctly', async () => { + const balances = { + USDC: { value: 1000n * 10n ** 18n, decimals: RESULT_DECIMALS }, // 1000 USDC + } + const prices: Record = { + USDC: { value: BigInt(1_000_000), decimal: 6 }, // 1.0 + } + + const result = await transport.computeUsdValue(balances, prices) + expect(result.USDC.value).toBe(1000n * 10n ** 18n) + }) + + it('scales 8-decimal price correctly', async () => { + const balances = { + BTC: { value: 2n * 10n ** 18n, decimals: RESULT_DECIMALS }, // 2 BTC + } + const prices: Record = { + BTC: { value: BigInt(50_000_000_000_000), decimal: 8 }, // 500,000 + } + + const result = await transport.computeUsdValue(balances, prices) + expect(result.BTC.value).toBe(1_000_000n * 10n ** 18n) // 2 * 500k + }) + + it('handles 18-decimal price correctly', async () => { + const balances = { + ETH: { value: 1n * 10n ** 18n, decimals: RESULT_DECIMALS }, // 1 ETH + } + const prices: Record = { + ETH: { value: 2000n * 10n ** 18n, decimal: 18 }, // 2000 + } + + const result = await transport.computeUsdValue(balances, prices) + expect(result.ETH.value).toBe(2000n * 10n ** 18n) + }) +}) + +describe('WalletsTransport.getAggregatedWalletBalance', () => { + let transport: WalletsTransport + + beforeEach(() => { + transport = new WalletsTransport() + jest.spyOn(transport, 'getWalletBalance').mockResolvedValue({ + wallets: [ + { currency: 'ETH', totalBalance: '1' }, + { currency: 'ETH', totalBalance: '2' }, + { currency: 'USDC', totalBalance: '100' }, + ], + } as any) + }) + + it('aggregates balances by currency', async () => { + const balances = await transport.getAggregatedWalletBalance({} as any, {} as any) + + expect(balances.ETH.value).toBe(toBigIntBalance('3', RESULT_DECIMALS)) + expect(balances.USDC.value).toBe(toBigIntBalance('100', RESULT_DECIMALS)) + }) +}) diff --git a/packages/sources/copper/test/unit/utils.test.ts b/packages/sources/copper/test/unit/utils.test.ts new file mode 100644 index 0000000000..4f0b670e6b --- /dev/null +++ b/packages/sources/copper/test/unit/utils.test.ts @@ -0,0 +1,35 @@ +import { signRequest, toBigIntBalance, toEvenHex } from '../../src/transport/utils' + +describe('utils', () => { + describe('toBigIntBalance', () => { + it('converts decimal string to bigint', () => { + expect(toBigIntBalance('1', 18)).toBe(10n ** 18n) + expect(toBigIntBalance('0.5', 18)).toBe(5n * 10n ** 17n) + }) + + it('handles zero and small decimals', () => { + expect(toBigIntBalance('0', 18)).toBe(0n) + expect(toBigIntBalance('0.000001', 18)).toBe(1000000000000n) + }) + }) + + describe('toEvenHex', () => { + it('pads odd hex strings', () => { + expect(toEvenHex(15n)).toBe('0x0f') + }) + it('returns even hex unchanged', () => { + expect(toEvenHex(16n)).toBe('0x10') + }) + it('handles zero', () => { + expect(toEvenHex(0n)).toBe('0x00') + }) + }) + + describe('signRequest', () => { + it('creates deterministic signature', () => { + const sig1 = signRequest('GET', '/wallets', '', 'apiKey', 'secret', { foo: 'bar' }) + const sig2 = signRequest('GET', '/wallets', '', 'apiKey', 'secret', { foo: 'bar' }) + expect(sig1).toEqual(sig2) + }) + }) +}) From fc4447237f9a6d676c01ea0fac2cc544f617893e Mon Sep 17 00:00:00 2001 From: Subarna-Singh Date: Tue, 9 Sep 2025 10:07:58 +0530 Subject: [PATCH 8/9] REWORK: review work --- packages/sources/copper/src/transport/utils.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/sources/copper/src/transport/utils.ts b/packages/sources/copper/src/transport/utils.ts index 7bced4e593..3fa7234697 100644 --- a/packages/sources/copper/src/transport/utils.ts +++ b/packages/sources/copper/src/transport/utils.ts @@ -1,5 +1,4 @@ import { GroupRunner } from '@chainlink/external-adapter-framework/util/group-runner' -import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error' import crypto from 'crypto' import { ethers } from 'ethers' import EACAggregatorProxy from '../config/EACAggregatorProxy.json' @@ -96,15 +95,3 @@ export class GroupedPriceOracleContract { } } } - -export const getNetworkEnvVar = (network: string, suffix: string): string => { - const envVarName = `${network.toUpperCase()}${suffix}` - const envVar = process.env[envVarName] - if (!envVar) { - throw new AdapterInputError({ - statusCode: 400, - message: `Environment variable ${envVarName} is missing`, - }) - } - return envVar -} From 0eff9cde0ac999b16f6ab8805a94e4a6657b19f9 Mon Sep 17 00:00:00 2001 From: Subarna-Singh Date: Tue, 9 Sep 2025 10:21:35 +0530 Subject: [PATCH 9/9] ADD: aggregate balances to result for debugging --- packages/sources/copper/src/endpoint/wallets.ts | 1 + packages/sources/copper/src/transport/wallets.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/sources/copper/src/endpoint/wallets.ts b/packages/sources/copper/src/endpoint/wallets.ts index 0eadd52850..6ae9c6c1d9 100644 --- a/packages/sources/copper/src/endpoint/wallets.ts +++ b/packages/sources/copper/src/endpoint/wallets.ts @@ -69,6 +69,7 @@ export type BaseEndpointTypes = { totalUsdValue: string decimal: number totalUsdValueInHex: string + balances: Record } } Settings: typeof config.settings diff --git a/packages/sources/copper/src/transport/wallets.ts b/packages/sources/copper/src/transport/wallets.ts index 71d99abdd3..366801a0c3 100644 --- a/packages/sources/copper/src/transport/wallets.ts +++ b/packages/sources/copper/src/transport/wallets.ts @@ -120,11 +120,22 @@ export class WalletsTransport extends SubscriptionTransport { const totalUsdValue = Object.values(usdValues).reduce((acc, val) => acc + val.value, 0n) const totalUsdValueInHex = toEvenHex(totalUsdValue) + const stringifiedBalances = Object.fromEntries( + Object.entries(balances).map(([token, { value, decimals }]) => [ + token, + { + value: value.toString(), + decimals, + }, + ]), + ) + return { data: { totalUsdValue: totalUsdValue.toString(), decimal: RESULT_DECIMALS, totalUsdValueInHex: totalUsdValueInHex, + balances: stringifiedBalances, }, statusCode: 200, result: totalUsdValueInHex,