Skip to content

Commit

Permalink
Merge pull request #47 from tiagosiebler/primeimpl
Browse files Browse the repository at this point in the history
feat(): pseudo-implement prime auth for ws
  • Loading branch information
tiagosiebler authored Sep 26, 2024
2 parents 360ac20 + 120884f commit 66acfa3
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 8 deletions.
65 changes: 57 additions & 8 deletions src/WebsocketClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import {
isCBExchangeWSEvent,
isCBExchangeWSRequestOperation,
isCBINTXWSRequestOperation,
isCBPrimeWSRequestOperation,
} from './lib/websocket/typeGuards.js';
import {
getCBExchangeWSSign,
getCBInternationalWSSign,
getCBPrimeWSSign,
getMergedCBExchangeWSRequestOperations,
getMergedCBINTXRequestOperations,
MessageEventLike,
Expand All @@ -31,6 +33,8 @@ import {
WsInternationalAuthenticatedRequestOperation,
WsInternationalRequestOperation,
WsOperation,
WsPrimeAuthenticatedRequestOperation,
WsPrimeRequestOperation,
} from './types/websockets/requests.js';
import {
WsAPITopicRequestParamMap,
Expand Down Expand Up @@ -468,6 +472,7 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
return wsRequestEvent;
}
case WS_KEY_MAP.internationalMarketData: {
// In case there's ever more operation types than "subscribe" and "unsubscribe"
if (!['subscribe', 'unsubscribe'].includes(operation)) {
throw new Error(
`Unhandled request operation type for CB International WS: "${operation}"`,
Expand All @@ -484,8 +489,15 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {

return wsRequestEvent;
}
// case WS_KEY_MAP.primeMarketData: {
// }
case WS_KEY_MAP.primeMarketData: {
const wsRequestEvent: WsPrimeRequestOperation<WsTopic> = {
type: operation,
channel: topicRequest.topic,
...topicRequest.payload,
};

return wsRequestEvent;
}
default: {
throw new Error(
`Not implemented for "${wsKey}" yet - if you need Prime or INTX, please get in touch.`,
Expand Down Expand Up @@ -646,11 +658,6 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
// We're over the max topics per request limit. Break into batches.
const finalOperations: string[] = [];
for (const operationEvent of operationEvents) {
if (!isPrivateChannel) {
finalOperations.push(JSON.stringify(operationEvent));
continue;
}

if (!apiKey || !apiSecret || !apiPassphrase) {
throw new Error(
`One or more of apiKey, apiSecret and/or apiPassphrase are missing. These must be provided to use private channels.`,
Expand Down Expand Up @@ -681,12 +688,54 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
return finalOperations;
}
case WS_KEY_MAP.primeMarketData: {
if (
!operationEvents.every((evt) =>
isCBPrimeWSRequestOperation(evt, wsKey),
)
) {
// Don't expect this to ever happen, but just to please typescript...
throw new Error(
`Unexpected request schema for exchange WS request builder`,
);
}
if (!apiKey || !apiSecret || !apiPassphrase) {
throw new Error(
`One or more of apiKey, apiSecret and/or apiPassphrase are missing. These must be provided to use private channels.`,
);
}

const finalOperations: string[] = [];
for (const operationEvent of operationEvents) {
const { sign, timestampInSeconds } = await getCBPrimeWSSign({
channelName: operationEvent.channel,
svcAccountId: operationEvent.svcAccountId,
portfolioId: operationEvent.portfolio_id,
apiKey,
apiSecret,
product_ids: operationEvent.product_ids,
});

const wsRequestEventWithSign: WsPrimeAuthenticatedRequestOperation<WsTopic> =
{
...operationEvent,
api_key_id: apiKey,
access_key: apiSecret,
passphrase: apiPassphrase,
signature: sign,
timestamp: timestampInSeconds,
};

finalOperations.push(JSON.stringify(wsRequestEventWithSign));
}

throw new Error(
'CB Prime is not fully implemented yet - awaiting test environment... if you need this, please get in touch.',
);
return finalOperations;
}
default: {
throw new Error(`Not implemented for "${wsKey}" yet`);
throw neverGuard(wsKey, `Not implemented for "${wsKey}" yet`);
// throw new Error(`Not implemented for "${wsKey}" yet`);
}
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/lib/websocket/typeGuards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import {
WsExchangeRequestOperation,
WsInternationalRequestOperation,
WsPrimeRequestOperation,
} from '../../types/websockets/requests.js';
import { WS_KEY_MAP, WsKey } from './websocket-util.js';

Expand Down Expand Up @@ -105,3 +106,27 @@ export function isCBINTXWSRequestOperation<TWSTopic extends string = string>(

return wsKey === WS_KEY_MAP.internationalMarketData;
}

/**
* Silly type guard for the structure of events being sent to the server
* (e.g. when subscribing to a topic)
*/
export function isCBPrimeWSRequestOperation<TWSTopic extends string = string>(
evt: unknown,
wsKey: WsKey,
): evt is WsPrimeRequestOperation<TWSTopic> {
if (!isDefinedObject(evt)) {
return false;
}

const looselyTypedEvent = evt as WsPrimeRequestOperation<TWSTopic>;

if (
typeof looselyTypedEvent.type !== 'string' ||
typeof looselyTypedEvent.channel !== 'string'
) {
return false;
}

return wsKey === WS_KEY_MAP.internationalMarketData;
}
38 changes: 38 additions & 0 deletions src/lib/websocket/websocket-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,41 @@ export async function getCBInternationalWSSign(

return { sign, timestampInSeconds };
}

/**
* Return sign used to authenticate CB Prime WS requests
*/
export async function getCBPrimeWSSign(params: {
channelName: string;
svcAccountId: string;
portfolioId: string;
apiKey: string;
apiSecret: string;
product_ids?: string[];
}): Promise<{
timestampInSeconds: string;
sign: string;
}> {
const timestampInMs = Date.now();
const timestampInSeconds = (timestampInMs / 1000).toFixed(0);

// channelName + accessKey + svcAccountId + timestamp + portfolioId + products
const signInput = [
params.channelName,
params.apiKey,
params.svcAccountId,
timestampInSeconds,
params.portfolioId,
params.product_ids ? params.product_ids.join('') : '',
].join('');

const sign = await signMessage(
signInput,
params.apiSecret,
'base64',
'SHA-256',
'base64:web',
);

return { sign, timestampInSeconds };
}
28 changes: 28 additions & 0 deletions src/types/websockets/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,31 @@ export type WsInternationalAuthenticatedRequestOperation<
passphrase: string;
signature: string;
};

/**
* Public subscribe/unsubscribe requests for the Coinbase Prime product group
*/
export interface WsPrimeRequestOperation<TWSTopic extends string = string> {
type: WsOperation;
channel: TWSTopic;
// these should be provided as a payload with the request, else auth will fail
svcAccountId: string;
portfolio_id: string;
product_ids: string[];
}

/**
* Private (authenticated) subscribe/unsubscribe requests for the Coinbase Prime product group
*
* - https://docs.cdp.coinbase.com/prime/docs/websocket-feed#signing-messages
* - https://docs.cdp.coinbase.com/prime/docs/websocket-channels
*/
export type WsPrimeAuthenticatedRequestOperation<
TWSTopic extends string = string,
> = WsPrimeRequestOperation<TWSTopic> & {
access_key: string;
api_key_id: string;
passphrase: string;
signature: string;
timestamp: string;
};

0 comments on commit 66acfa3

Please sign in to comment.