Click here to Skip to main content
15,885,985 members
Articles / Programming Languages / C#

Creating a Real-time Cryptocurrency Websocket API [Part 1]

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
4 Dec 2017CPOL6 min read 27.6K   684   11   9
We do not want to keep asking for information. Let's create an API to send push messages regarding the current cryptocurrency stock market.

Introduction

The world is changing. We want information as soon as possible without having to keep asking for it. Websockets allow us to do just that.

In this guide, you will learn how to create an API that listens to a websocket and pushes that information to subscribers.

I am going to be implementing Coinigy's API which provides real-time feeds for trade history, orderbook data, and blockchain alerts. In this example, we will only be looking at trade information.

The code for this article can be downloaded here.

This article is the first part in the series.

Part 2 can be found here: Implement a Websocket API with Owin and SignalR [Part 2].

Prerequisites

  • Visual Studio 2015 or 2017 with Framework 4.6.1
  • A Coinigy API key and secret

Table of Contents

  1. Getting your API Key
  2. Using the code
    • Step 01 - Create a Class Library
    • Step 02 - Install Nuget packages
    • Step 03 - Create a Websocket class
    • Step 04 - Authenticate
    • Step 05 - Subscribe to trade channel
    • Step 06 - Parse messages
    • Step 07 - Deserialize trade response
    • Step 08 - Connect to the socket
    • Step 09 - Testing
  3. Final words

Getting Your API Key

Note that this article can be easily adapted to use any other API.

Once you are logged in to Coinigy, go the Settings > My Account > Coinigy API and click on the "Generate New Key" button. This will create a new key and secret you will use within the API.

Keep your credentials safe!

Using the Code

Step 01 - Create a Class Library

Open Visual Studio and go to File > New > Project and select Class Library.

Ensure your framework targets .NET Framework 4.6.1.

Give your project a name (e.g. Coinigy.API) and click "OK".

Step 02 - Install Nuget Packages

In the package manager console, install the following packages using these commands:

Install-Package PureSocketCluster

Step 03 - Create a Websocket Class

We need to be able to authenticate with our socket.

Add a "Models" folder to your project (right-click your project, Add > New Folder).

This folder will contain all our models. We can now add an ApiCredentials model under this folder:

C#
public class ApiCredentials
{
    [JsonProperty("apiKey")]
    public string ApiKey { get; set; }

    [JsonProperty("apiSecret")]
    public string ApiSecret { get; set; }
}

Rename your "Class1.cs" file to "Websocket.cs".

Add the following two fields to your Websocket class:

C#
private readonly PureSocketClusterSocket socket;
private readonly ApiCredentials credentials;

Add a constructor to the Websocket class:

C#
public Websocket(ApiCredentials credentials)
{
    this.credentials = credentials;

    this.socket = new PureSocketClusterSocket("wss://sc-02.coinigy.com/socketcluster/");
}

The constructor allows the user to pass in their credentials and establishes a connection to Coingiy's websocket URI.

Step 04 - Authenticate

The websocket will send information to us but we first need to subscribe to these events. We first need to authenticate with the socket. We will start receiving information from the socket after the authentication succeeds. To authenticate with the webservice, we need to send the "auth" command along with our credentials. The "auth" command can only be called once the socket has opened. Therefore, we need to subscribe to the "OnOpened"-event. This is how we subscribe to the event:

C#
this.socket.OnOpened += On_Opened;

And the function looks like this:

C#
private void On_Opened()
{
    socket.Emit("auth", this.credentials, ack: (string name, object error, object data) =>
    {
        // We can now start listening to trade information
    });
}

Step 05 - Subscribe to Trade Channel

To subscribe to a trade channel, we can add the following function to our Websocket-class:

C#
public bool SubscribeToTradeChannel(string exchange, string primaryCurrency,
string secondaryCurrency) => this.socket.Subscribe
($"TRADE-{exchange}--{primaryCurrency}--{secondaryCurrency}");

This function will be called after we have authenticated. We need to let the user know when authentication has completed so let's add an event:

C#
public event ClientIsReady OnClientReady;

public delegate void ClientIsReady();

And invoke it when authentication has succeeded within our Emit-callback:

C#
private void On_Opened()
{
    socket.Emit("auth", this.credentials, ack: (string name, object error, object data) =>
    {
        OnClientReady?.Invoke();
    });
}

Now the user can subscribe to the OnClientReady-event and start subscribing to trade channels once it gets invoked.

Note: Coinigy's channel names need to be in the following format: METHOD-EXCHANGECODE--PRIMARYCURRENCY--SECONDARYCURRENCY

Step 06 - Parse Messages

Now we are subscribed but we still need to create functionality to receive messages being sent to us from the socket. We need to subscribe to the OnMessage-event:

C#
this.socket.OnMessage += On_Message;

And the function looks like this:

C#
private void On_Message(string message)
{
    // Do something with the message received
}

Every message we receive from the socket will be invoked here. We still need to determine what type of message the server has sent us since it can return different types of messsages. To determine the type, we can add the following function:

C#
private static string GetRequestType(JObject jObj)
{
    string requestType = string.Empty;
    var channelName = jObj["data"]["channel"].ToString();

    Guid guid;
    if (!Guid.TryParse(channelName, out guid))
        return channelName.Substring(0, channelName.IndexOf('-'));

    Guid channelGuid;
    requestType = channelName;
    if (Guid.TryParse(channelName, out channelGuid))
        if (channelGuid.ToString().ToLower() == channelName.ToLower())
            requestType = jObj["data"]["data"]["MessageType"].ToString();

    return requestType;
}

When we receive a message, we first need to check if it is a "publish" message. Once the message has been verified, we can parse it into a JObject and send it to our GetRequestType-function to determine the type of request. Add the following logic to the On_Message-function:

C#
string PUBLISH_REGEX = @"^{""event""*.:*.""#publish""";

// Determine if message is a publish message using regex
var m = Regex.Match(message, PUBLISH_REGEX);
if (!m.Success) return;

// If so, parse the string
var jObj = JObject.Parse(message);

// Retrieve the channel's name
string channelName = jObj["data"]["channel"].ToString();

// Determine request type
string requestType = GetRequestType(jObj);
if (string.IsNullOrEmpty(requestType))
    return;

Once we know what the request type is, we need to parse the message to that type and send it to the user. There are different message types so let's add an enum for that:

C#
public enum MessageType
 {
     TradeData,
     OrderData,
     NewsData,
     BlockData,
     FavoriteData,
     NewMarket,
     NotificationData,
     Unknown
 }

And a function to determine what the message type is based on the request type:

C#
private static MessageType GetMessageType(string requestType)
{
    switch (requestType.ToUpper())
    {
        case "ORDER":
            return MessageType.OrderData;
        case "TRADE":
            return MessageType.TradeData;
        case "BLOCK":
            return MessageType.BlockData;
        case "FAVORITE":
            return MessageType.FavoriteData;
        case "NOTIFICATION":
            return MessageType.NotificationData;
        case "NEWS":
            return MessageType.NewsData;
        case "NEWMARKET":
            return MessageType.NewMarket;
        default:
            return MessageType.Unknown;
    }
}

This function takes the request type as parameter and returns our message type. Once we determine the message type, we can use a switch statement to parse and return the data. Update the OnMessage-function:

C#
private void On_Message(string message)
{
    // Previous code that determines the request type and channel name...

    InvokeMessageReceived(channelName, requestType, message);
}

And add the InvokeMessageReceived-function:

C#
private void InvokeMessageReceived(string channelName, string requestType, string message)
{
    // Determine the message type using the function we previously created
    MessageType messageType = GetMessageType(requestType);
    switch (messageType)
    {
        case MessageType.TradeData:
            // Parse the channel name
            var tradeMarketInfo = MarketInfo.ParseMarketInfo(channelName);

            // Deserialize the string to a TradeResponse-entity
            var trade = Helper.ToEntity<TradeResponse>(message);

            // Invoke an event to let the subscribers know we have received trade information
            OnTradeMessage?.Invoke(tradeMarketInfo.Exchange, tradeMarketInfo.PrimaryCurrency,
            tradeMarketInfo.SecondaryCurrency, trade.TradeData.Trade);
            break;

            // Other cases for each MessageType...
    }
}

We still need to add a few functions to parse the message into a trade data entity. Let's create a class with a function which takes a channel name as parameter and returns a MarketInfo-entity (we can add this class into our Models-folder).

C#
internal class MarketInfo
{
    internal string Exchange { get; set; }
    internal string PrimaryCurrency { get; set; }
    internal string SecondaryCurrency { get; set; }

    // A function to parse a string and return our MarketInfo
    internal static MarketInfo ParseMarketInfo(string data)
    {
        var str = data.Replace("--", "-");
        var strArr = str.Split('-');
        return new MarketInfo() { Exchange = strArr[1], PrimaryCurrency = strArr[2],
                                  SecondaryCurrency = strArr[3] };
    }
}

Now we know what the exchange is and what our primary/secondary currencies are. We still need to parse our trade info using the message. Let's create a TradeResponse-entity that will contain all that info:

C#
public class TradeResponse
{
    [JsonProperty("data")]
    public TradeData TradeData { get; set; }

    [JsonProperty("event")]
    public string Event { get; set; }
}

public class TradeData
{
    [JsonProperty("channel")]
    public string Channel { get; set; }

    [JsonProperty("data")]
    public TradeItem Trade { get; set; }
}

public class TradeItem
{
    [JsonProperty("label")]
    public string Label { get; set; }

    [JsonProperty("quantity")]
    public decimal Quantity { get; set; }

    [JsonProperty("exchId")]
    public long ExchId { get; set; }

    [JsonProperty("channel")]
    public string Channel { get; set; }

    [JsonProperty("exchange")]
    public string Exchange { get; set; }

    [JsonProperty("marketid")]
    public long Marketid { get; set; }

    [JsonProperty("market_history_id")]
    public long MarketHistoryId { get; set; }

    [JsonProperty("price")]
    public decimal Price { get; set; }

    [JsonProperty("time_local")]
    public string TimeLocal { get; set; }

    [JsonProperty("total")]
    public decimal Total { get; set; }

    [JsonProperty("time")]
    public string Time { get; set; }

    [JsonProperty("timestamp")]
    public string Timestamp { get; set; }

    [JsonProperty("tradeid")]
    public string TradeId { get; set; }

    [JsonProperty("type")]
    public string Type { get; set; }
}

Step 07 - Deserialize Trade Response

We still need to deserialize our message into a TradeResponse. Let's add a Helper class with a generic "ToEntity" function that takes a string as parameter and returns an entity:

C#
internal static class Helper
{
    internal static T ToEntity<T>(string data)
    {
        return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(data);
    }
}

Now we have deserialized our trade market data and need to let the user know. Let's add an event the user can subscribe to:

C#
public event TradeMessage OnTradeMessage;

public delegate void TradeMessage(string exchange, string primaryCurrency,
                                  string secondaryCurreny, TradeItem trade);

Step 08 - Connect to the Socket

The last function we need to add is to allow the user to connect to the socket with the following code:

C#
public bool Connect()
{
    return this.socket.Connect();
}

Step 09 - Testing

We are now ready to receive trade messages. Let's test our implementation with a unit test project. Right-click on the solution and select Add > New Project > Test > Unit Test Project and give it a name (i.e., Coinigy.API.Tests).

We need to create a class to test our trade message functionality. Add a new "WebsocketTests" class to the test project:

C#
[TestClass]
public class WebsocketTests
{
    private static readonly ManualResetEvent resetEvent = new ManualResetEvent(false);
    private static Websocket socket;

    [TestMethod]
    public void Subscribe_And_Listen()
    {
        // Initialize an instance of our socket
        socket = new Websocket(new ApiCredentials
        {
            ApiKey = "[YOUR-API-KEY]",
            ApiSecret = "[YOUR-API-SECRET]"
        });

        // Subscribe to OnClientReady-event so we know when we can subscribe to trade channels
        socket.OnClientReady += Socket_OnClientReady;

        // Subscribe to the OnTradeMessage-event so we can receive trade messages
        socket.OnTradeMessage += Socket_OnTradeMessage;

        // Finally we can connect to our socket and wait for incoming messages
        socket.Connect();

        // Forces the methods not to exit
        resetEvent.WaitOne();
    }

    private void WriteLog(string message)
    {
        Debug.WriteLine($"{DateTime.UtcNow}: {message}");
    }

    private void Socket_OnTradeMessage(string exchange, string primaryCurrency,
                                       string secondaryCurrency, Models.TradeItem trade)
    {
        WriteLog($"Received new trade for {exchange} market
                 {primaryCurrency}/{secondaryCurrency} price {trade.Price}");
    }

    private void Socket_OnClientReady()
    {
        // Subscribe to a new trade channel
        socket.SubscribeToTradeChannel("BMEX", "XBT", "USD");
    }
}

We need to add a reference to our API. Right-click References under the unit test project and select "Add Reference" > "Project" and select the API project. Finally, we need to install one last package into our unit test project. Run the following command:

"Install-Package PureSocketCluster"

When we run our test (Ctrl+R, Ctrl+A), we should start seeing trade messages in our Output window:

02-Dec-17 8:04:09 PM: Received new trade for BMEX market XBT/USD price 10939.5
02-Dec-17 8:04:09 PM: Received new trade for BMEX market XBT/USD price 10939.5
02-Dec-17 8:04:09 PM: Received new trade for BMEX market XBT/USD price 10940
02-Dec-17 8:04:10 PM: Received new trade for BMEX market XBT/USD price 10940

Final Words

We can now receive live trade data from the Coinigy API. We can even take it a step further and implement this API into a web application which shows us some statistical information. That will be our next project. Thanks for reading!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer BeingIT
South Africa South Africa
I am a software developer that loves creating new things, solving solutions and reading about the latest technologies. I am currently working on MVC projects, creating .NET APIs, deploying applications, writing SQL queries, debugging Windows Services, developing Xamarin mobile applications and working with a bunch of great people. Outside of work I am a brother and father.

Comments and Discussions

 
QuestionPureSocketClusterSocket requires 2 parameters Pin
Member 1228244616-Oct-18 0:24
Member 1228244616-Oct-18 0:24 
QuestionNot getting any repsones from Coinigy Pin
Member 1363863123-Jan-18 6:27
Member 1363863123-Jan-18 6:27 
AnswerRe: Not getting any repsones from Coinigy Pin
Pierre Nortje25-Jan-18 7:57
professionalPierre Nortje25-Jan-18 7:57 
QuestionFantastic! but... Pin
Member 1357251511-Jan-18 9:15
Member 1357251511-Jan-18 9:15 
AnswerRe: Fantastic! but... Pin
Member 1357251511-Jan-18 9:21
Member 1357251511-Jan-18 9:21 
GeneralRe: Fantastic! but... Pin
Pierre Nortje17-Jan-18 6:17
professionalPierre Nortje17-Jan-18 6:17 
QuestionThank you Pin
mou78665-Dec-17 8:00
mou78665-Dec-17 8:00 
QuestionJust for you to know Pin
Nelek3-Dec-17 0:26
protectorNelek3-Dec-17 0:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.