Yes, and yes as far as I can tell. Sending an empty array as the product id list did nothing, although I’ve only tried the ‘user’ and ‘ticker’ channels.
Here’s the bit I use that handles the subscriptions. You can see the timestampAndSign()
function returns the message, signed message, timestamp, etc in the object that is then sent as JSON.
// SIGNATURE
// generate a signature using CryptoJS
function sign(str, secret) {
const hash = CryptoJS.HmacSHA256(str, secret);
return hash.toString();
}
// create the signed message
function timestampAndSign(message, channel, products = []) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const strToSign = `${timestamp}${channel}${products.join(',')}`;
const sig = sign(strToSign, secret);
return { ...message, signature: sig, timestamp: timestamp };
}
// SUBSCRIPTIONS
// list of products to subscribe to
const products = ['BTC-USD', 'ETH-USD'];
// send a subscribe message
function subscribeToProducts(products, channelName, ws) {
// console.log('products: %s', products.join(','));
const message = {
type: 'subscribe',
channel: channelName,
api_key: key,
product_ids: products,
user_id: '',
};
// sign the message
const subscribeMsg = timestampAndSign(message, channelName, products);
ws.send(JSON.stringify(subscribeMsg));
}
Here
function startWebsocket(key, secret) {
// open a socket
open();
// don't start ws if no key or secret
if (!secret?.length || !key?.length) {
throw new Error('websocket connection to coinbase is missing mandatory environment variable(s)');
}
// SIGNATURE
// generate a signature using CryptoJS
function sign(str, secret) {
const hash = CryptoJS.HmacSHA256(str, secret);
return hash.toString();
}
// create the signed message
function timestampAndSign(message, channel, products = []) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const strToSign = `${timestamp}${channel}${products.join(',')}`;
const sig = sign(strToSign, secret);
return { ...message, signature: sig, timestamp: timestamp };
}
// SUBSCRIPTIONS
const products = ['BTC-USD', 'ETH-USD'];
// send a subscribe message
function subscribeToProducts(products, channelName, ws) {
// console.log('products: %s', products.join(','));
const message = {
type: 'subscribe',
channel: channelName,
api_key: key,
product_ids: products,
user_id: '',
};
// sign the message
const subscribeMsg = timestampAndSign(message, channelName, products);
ws.send(JSON.stringify(subscribeMsg));
}
// send an unsubscribe message
function unsubscribeToProducts(products, channelName, ws) {
const message = {
type: 'unsubscribe',
channel: channelName,
api_key: key,
product_ids: products,
};
const subscribeMsg = timestampAndSign(message, channelName, products);
ws.send(JSON.stringify(subscribeMsg));
}
function open() {
console.log('OPENING');
// the base URL of the API
const WS_API_URL = 'wss://advanced-trade-ws.coinbase.com';
// create the websocket. everything else inside the open() function is specific to this socket
let ws = new WebSocket(WS_API_URL);
ws.on('open', function () {
console.log('Socket open!');
subscribeToProducts(products, 'ticker', ws);
subscribeToProducts(products, 'user', ws);
});
ws.on('close', function () {
console.log('Socket was closed. Reopens in 1 sec');
// always reopen the socket if it closes
setTimeout(() => {
open();
}, 1000);
});
ws.on('error', (error) => {
console.log(error, 'error on ws connection');
});
ws.on('message', function (data) {
const parsedData = JSON.parse(data);
// if (parsedData.channel) {
// console.log(parsedData.channel, '<--channel from ws', parsedData, '<-- data from message event');
// }
if (parsedData.events) {
parsedData.events.forEach(event => {
if (event.tickers) {
// console.log(event, event.type, 'event from ws');
handleTickers(event.tickers);
} else if (event.type === 'snapshot') {
handleSnapshot(event);
} else if (event.type === 'update' && event.orders) {
handleOrdersUpdate(event);
} else {
console.log(event, event.type, 'event from ws');
}
});
}
});
// destroy connection if timeout
function timer() {
clearTimeout(this.pingTimeout);
// Use `WebSocket#terminate()`, which immediately destroys the connection,
// instead of `WebSocket#close()`, which waits for the close timer.
// Delay should be equal to the interval at which your server
// sends out pings plus a conservative assumption of the latency.
this.pingTimeout = setTimeout(() => {
console.log('ending socket after timeout');
this.terminate();
}, 10000);
}
// start timer when socket opens
ws.on('open', timer);
// each message will reset the timer
// ticker channel sends many messages so it will be sufficient to reset the timer
ws.on('message', timer);
// clear the timer if socket closes
ws.on('close', function clear() {
clearTimeout(this.pingTimeout);
});
}
// EVENT HANDLERS
function handleSnapshot(snapshot) {
console.log(snapshot, '<-- snapshot');
// first snapshot should be a list of active orders
// devs have said these come in groups of 25 or less?
}
async function handleOrdersUpdate(update) {
// do something with the array of orders
console.log(update.orders, '<-- orders update');
// update should look like this:
// {
// type: 'update',
// orders: [
// {
// order_id: 'xxxxxxxxxxx',
// client_order_id: 'xxxxxxxxxxx',
// cumulative_quantity: '0.00029411',
// leaves_quantity: '0',
// avg_price: '16554.17',
// total_fees: '0.01217186734675',
// status: 'FILLED'
// }
// ]
// }
}
function handleTickers(tickers) {
tickers.forEach(ticker => {
// do something with each ticker
console.log(ticker, '<-- ticker')
// ticker should look like this
// {
// type: 'ticker',
// product_id: 'BTC-USD',
// price: '17044.29',
// volume_24_h: '39981.56397272',
// low_24_h: '16407.01',
// high_24_h: '17148.31',
// low_52_w: '15460',
// high_52_w: '59118.84',
// price_percent_chg_24_h: '3.49888784916769'
// }
});
}
}
is the rest of the javascript code if you want the context. It will open the socket, subscribe to whatever channels, and destroy + reopen it if the connection drops which I’ve noticed tends to happen every few hours.