Skip to content

Commit

Permalink
Add Coinbase Prime adapter (#3176)
Browse files Browse the repository at this point in the history
* Added Coinbase Prime adapter

* Updated error handling
  • Loading branch information
amit-momin committed Feb 5, 2024
1 parent 143d923 commit cb3e120
Show file tree
Hide file tree
Showing 22 changed files with 481 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/pretty-balloons-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/coinbase-prime-adapter': major
---

Initial version of the adapter
36 changes: 36 additions & 0 deletions .pnp.cjs

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

Binary file not shown.
Binary file not shown.
Empty file.
3 changes: 3 additions & 0 deletions packages/sources/coinbase-prime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Chainlink External Adapter for coinbase-prime

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 coinbase-prime`.
42 changes: 42 additions & 0 deletions packages/sources/coinbase-prime/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@chainlink/coinbase-prime-adapter",
"version": "0.0.0",
"description": "Chainlink coinbase-prime adapter.",
"keywords": [
"Chainlink",
"LINK",
"blockchain",
"oracle",
"coinbase-prime"
],
"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": "27.5.2",
"@types/node": "16.11.51",
"nock": "13.2.9",
"typescript": "5.0.4"
},
"dependencies": {
"@chainlink/external-adapter-framework": "0.33.2",
"@types/crypto-js": "4.2.2",
"crypto-js": "4.2.0",
"tslib": "2.4.1"
}
}
27 changes: 27 additions & 0 deletions packages/sources/coinbase-prime/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AdapterConfig } from '@chainlink/external-adapter-framework/config'

export const config = new AdapterConfig({
API_ENDPOINT: {
description: 'The HTTP URL to retrieve data from',
type: 'string',
default: 'https://api.prime.coinbase.com',
},
ACCESS_KEY: {
description: 'The API key for Coinbase Prime auth',
type: 'string',
required: true,
sensitive: true,
},
PASSPHRASE: {
description: 'The passphrase for Coinbase Prime auth',
type: 'string',
required: true,
sensitive: true,
},
SIGNING_KEY: {
description: 'The signing key for Coinbase Prime auth',
type: 'string',
required: true,
sensitive: true,
},
})
45 changes: 45 additions & 0 deletions packages/sources/coinbase-prime/src/endpoint/balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util'
import { config } from '../config'
import { httpTransport } from '../transport/balance'

export const inputParameters = new InputParameters(
{
portfolio: {
required: true,
type: 'string',
description: 'The portfolio ID to query the balance of',
},
symbol: {
required: true,
type: 'string',
description: 'The symbol to return the balance for',
},
type: {
type: 'string',
description: 'The balance type to return',
default: 'total',
options: ['total', 'vault', 'trading'],
},
},
[
{
portfolio: 'abcd1234-123a-1234-ab12-12a34bcd56e7',
symbol: 'BTC',
type: 'total',
},
],
)

export type BaseEndpointTypes = {
Parameters: typeof inputParameters.definition
Response: SingleNumberResultResponse
Settings: typeof config.settings
}

export const endpoint = new AdapterEndpoint({
name: 'balance',
transport: httpTransport,
inputParameters,
})
1 change: 1 addition & 0 deletions packages/sources/coinbase-prime/src/endpoint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { endpoint as balance } from './balance'
21 changes: 21 additions & 0 deletions packages/sources/coinbase-prime/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 { balance } from './endpoint'

export const adapter = new Adapter({
defaultEndpoint: balance.name,
name: 'COINBASE-PRIME',
config,
endpoints: [balance],
rateLimiting: {
tiers: {
default: {
rateLimit1s: 25,
note: 'Using the most restrictive rate limit. Docs: IP address at 100 requests per second (rps). Portfolio ID at 25 rps with a burst of 50 rps.',
},
},
},
})

export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
119 changes: 119 additions & 0 deletions packages/sources/coinbase-prime/src/transport/balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { HttpTransport } from '@chainlink/external-adapter-framework/transports'
import { BaseEndpointTypes } from '../endpoint/balance'
import { sign } from './utils'

export interface ResponseSchema {
balances: {
symbol: string
amount: string
holds: string
bonded_amount: string
reserved_amount: string
unbonding_amount: string
unvested_amount: string
pending_rewards_amount: string
past_rewards_amount: string
bondable_amount: string
withdrawable_amount: string
fiat_amount: string
}[]
type: string
trading_balances: {
total: string // Returns total in fiat amount
holds: string
}
vault_balances: {
total: string // Returns total in fiat amount
holds: string
}
}

export type HttpTransportTypes = BaseEndpointTypes & {
Provider: {
RequestBody: never
ResponseBody: ResponseSchema
}
}
export const httpTransport = new HttpTransport<HttpTransportTypes>({
prepareRequests: (params, config) => {
return params.map((param) => {
const timestamp = Math.floor(Date.now() / 1000)
const method = 'GET'
const path = `/v1/portfolios/${param.portfolio}/balances`
const message = `${timestamp}${method}${path}`
const signature = sign(message, config.SIGNING_KEY)
return {
params: [param],
request: {
baseURL: config.API_ENDPOINT,
url: path,
headers: {
'X-CB-ACCESS-KEY': config.ACCESS_KEY,
'X-CB-ACCESS-PASSPHRASE': config.PASSPHRASE,
'X-CB-ACCESS-SIGNATURE': signature,
'X-CB-ACCESS-TIMESTAMP': timestamp,
'Content-Type': 'application/json',
},
params: {
symbols: param.symbol.toUpperCase(),
balance_type: `${param.type.toUpperCase()}_BALANCES`,
},
},
}
})
},
parseResponse: (params, response) => {
return params.map((param) => {
if (!response.data) {
return {
params: param,
response: {
errorMessage: `The data provider did not return data for Portfolio: ${param.portfolio}, Balance Type: ${param.type}, Symbol: ${param.symbol}`,
statusCode: 502,
},
}
}

if (!response.data.balances) {
return {
params: param,
response: {
errorMessage: `The data provider response does not contain a balances list for Portfolio: ${param.portfolio}, Balance Type: ${param.type}, Symbol: ${param.symbol}`,
statusCode: 502,
},
}
}

// The adapter only supports querying one asset at a time so the balances list should only contain 1 element
if (response.data.balances.length !== 1) {
return {
params: param,
response: {
errorMessage: `The data provider response does not contain exactly one element in the balances list for Portfolio: ${param.portfolio}, Balance Type: ${param.type}, Symbol: ${param.symbol}`,
statusCode: 502,
},
}
}

const result = Number(response.data.balances[0].amount)
if (isNaN(result)) {
return {
params: param,
response: {
errorMessage: `The data provider returned non-numeric balance: ${response.data.balances[0].amount}`,
statusCode: 502,
},
}
}
return {
params: param,
response: {
result: result,
data: {
result: result,
},
},
}
})
},
})
6 changes: 6 additions & 0 deletions packages/sources/coinbase-prime/src/transport/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import CryptoJS from 'crypto-js'

export const sign = (str: string, secret: string) => {
const hash = CryptoJS.HmacSHA256(str, secret)
return hash.toString(CryptoJS.enc.Base64)
}
3 changes: 3 additions & 0 deletions packages/sources/coinbase-prime/test-payload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"requests": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`execute balance endpoint should return success 1`] = `
{
"data": {
"result": 100,
},
"result": 100,
"statusCode": 200,
"timestamps": {
"providerDataReceivedUnixMs": 1652198967193,
"providerDataRequestedUnixMs": 1652198967193,
},
}
`;

0 comments on commit cb3e120

Please sign in to comment.