Intro to Ravencoin Development – Part 4 – Exchanging Assets

All code described below has been checked into github. Feel free to clone and go along with it as you read.

Welcome to our next journey into ravencoin development. This will be the last one in this intro series, and next i’ll focus on some other cool ravencoin projects including expanding out the library I’ve been building as part of this.

Anyways, I was running out of ideas, so I went to twitter:

So this is what we’ll do. Lets break down the logic of the application:

  1. A transaction comes in
  2. We need to identify what kind of transaction it is (Incoming/Outgoing? RVN? Asset? Fee?)
  3. We then look up if we’re willing to exchange our asset for the one that came in
    1. Or alternatively, have our application set to accept any asset for the asset we’re going to send
  4. Run through our existing logic and send the asset back to the origin address, like we did when we received RVN in the last post.

Just a quick note about what’s changed since the last post

I’ve been focusing a lot on the Ravencoin.ApplicationCore library, and much too much has changed there to post all the code. But the gist of it is:

  • All the classes that don’t say “Custom” are standard node commands. All the logic for we’ve done in the past are now part of “CustomX” classes, where X could be Transactions, Assets, etc. Anything in the Custom classes call the standard classes for data.
  • Logging improvements. You can inject log4net into this library from a web or console application and get full logging through the lifecycle.
  • Documentation. Mostly from the raven core node is now inside the library, so you can hover on a method and get info instead of googling it.
  • Almost fully complete. Most of the out of the box methods are implemented.

OK Lets get into it.

As I said above, its best to download the latest from github for this lesson since the class structure has changed. If you want to follow along with the tutorial, just copy everything over except CustomAssets and CustomTransactions and you should be ok.

We need to identify what kind of transaction it is (Incoming/Outgoing? RVN? Asset? Fee?)

I decided to use enums for this. Create a new Class inside Ravencoin.ApplicationCore/Models. Call it TransactionType.cs

namespace Ravencoin.ApplicationCore.Models
{
    public enum TType
    {
        RVN,
        Asset,
        Fee
    }
    public enum CType
    {
        Receive,
        Send
    }
    public class TransactionType
    {
        public TType transactionTypes { get; set; }

        public CType categoryTypes { get; set; }

    }
}

Now we’re going to use this class to write a method in CustomTransactions.cs to determine if the incoming transaction’s type:

public async Task<TransactionType> GetTransactionType(string txid, ServerConnection serverConnection) {
            Transactions transactions = new Transactions();
            //Get our in-wallet transaction data. Unlike GetPublicTransaction, this will give us details like category, amount, confirmations etc.
            ServerResponse response = await wallet.GetTransaction(serverConnection, txid);
            JObject result = JObject.Parse(response.responseContent);
            JToken resultDetails = result["result"]["details"];
            //Get the data we need to ensure this transaction is something we want to send an asset to 
            //Check to see is this is a payment in RVN or a fee payment or asset transfer transaction
            //Check to see if there are asset details in the transaction to determine if it's an asset transaction
            JToken assetDetails = result["result"]["asset_details"];
            JToken feeDetails = result["result"]["fee"];
            //If it has values in asset_details, it's an asset transaction.
            if (assetDetails.HasValues && feeDetails == null) {
                if (assetDetails[0]["category"].ToString() == "receive") {
                    return new TransactionType { transactionTypes = TType.Asset, categoryTypes = CType.Receive };
                } else { return new TransactionType { transactionTypes = TType.Asset, categoryTypes = CType.Receive }; }
            }
            //If it doesn't have asset_details, but still has result details, its a RVN Transaction
            else if (resultDetails.HasValues) {
                if (resultDetails[0]["category"].ToString() == "receive") { return new TransactionType { transactionTypes = TType.RVN, categoryTypes = CType.Receive }; } else { return new TransactionType { transactionTypes = TType.RVN, categoryTypes = CType.Receive }; }
            }
        // If it doesn't have details at all, it's a fee.
        else {
                return new TransactionType { transactionTypes = TType.Fee };
            }
        }

Over in CustomAssets.cs, we’re going to add a new method underneath ExchangeRvnForAsset. We’ll call this ExchangeAssetForAsset.

using log4net;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using Ravencoin.ApplicationCore.Models;
using System;
using System.Net;
using System.Threading.Tasks;

namespace Ravencoin.ApplicationCore.BusinessLogic
{
    public class CustomAssets
    {
        //Inject Logger
        private static readonly ILog _logger = LogManager.GetLogger(typeof(CustomAssets));
        
        //Create a ravencore instance to call against
        private readonly Transactions transactions = new Transactions();
        private readonly Utilities utilities = new Utilities();
        private readonly CustomTransactions customtransactions = new CustomTransactions();
        private readonly Wallet wallet = new Wallet();
      
      public async Task<ServerResponse> ExchangeAssetForAsset(string txid, string assetReceiveAddress, string expectedIncomingAsset, string assetToSend, int? multiplier, int minConfirmations, ServerConnection serverConnection)
        {
            _logger?.Info($"TransactionID {txid}: Beginning ExchangeRvnForAsset Function. Transaction ID: {txid}, Asset Receive Address: {assetReceiveAddress}, Asset Received: {expectedIncomingAsset}, Asset To Send: {assetToSend}, Multiplier: {multiplier}, Minimum Confirmations: {minConfirmations}");
            int quantity = new int();
            Transactions transactions = new Transactions();
            Utilities utilities = new Utilities();

            //Get our in-wallet transaction data. Unlike GetPublicTransaction, this will give us details like category, amount, confirmations etc.
            ServerResponse response = await wallet.GetTransaction(serverConnection, txid);
            JObject result = JObject.Parse(response.responseContent);

            if (result != null)
            {
                //Get the data we need to ensure this transaction is something we want to send an asset to 
                //Check to see is this is a payment in RVN or a fee payment or asset transfer transaction
                JToken detailsArray = result["result"]["asset_details"];
                if (detailsArray.HasValues)
                {
                    string category = result["result"]["asset_details"][0]["category"].ToString();
                    string receiveAddress = result["result"]["asset_details"][0]["destination"].ToString();
                    double amountReceived = Double.Parse(result["result"]["asset_details"][0]["amount"].ToString());
                    string txAssetReceived = result["result"]["asset_details"][0]["asset_name"].ToString();
                    int confirmations = Int32.Parse(result["result"]["confirmations"].ToString());

                    //Round down the amount received. Rounding down discourages sending fractional RVN :)
                    int finalAmountReceived = Convert.ToInt32(Math.Floor(amountReceived));

                    //Check if we have a multiplier, and if its higher than one. If the multiplier is set to 100 for example, every 1 RVN Received will get 100 assets in response.
                    if (multiplier > 1 || multiplier != null) { quantity = (int)(finalAmountReceived * multiplier); }

                    //Check to see if assetReceived is set to * (wildcard). If it is we automatically set the assetMatch to true
                    bool assetMatch = false;
                    if (expectedIncomingAsset == "*") {
                        assetMatch = true;
                    } else if (expectedIncomingAsset == txAssetReceived) {
                        assetMatch = true;
                    };


                    //Check to see if we have enough of this asset to send.
                    Assets assets = new Assets();
                    ServerResponse assetBalanceRequest = await assets.ListMyAssets(serverConnection, assetToSend);
                    JObject assetBalanceResponse = JObject.Parse(assetBalanceRequest.responseContent);
                    int assetBalance = Int32.Parse(assetBalanceResponse["result"][assetToSend].ToString());

                    if (assetBalance >= quantity)
                    {
                        // Requirements:
                        // Category must be "receive" aka an icoming transaction
                        // Our expecteded receive address must match the one we're watching for
                        // The total amount of Assets is 1 or more.
                        // Confirmations must be equal or greater to the minimum confirmations we want. 
                        if (category == "receive" && receiveAddress == assetReceiveAddress && finalAmountReceived >= 1 && confirmations >= minConfirmations && assetMatch == true)
                        {
                            //Do a GetPublicTransaction on the txid to grab the vout address
                            ServerResponse getSenderAddress = await customtransactions.GetSenderAddress(serverConnection, txid);

                            if (getSenderAddress != null)
                            {
                                //Validate it's a valid Ravencoin Address  
                                ServerResponse checkIfValid = await utilities.ValidateAddress(getSenderAddress.responseContent, serverConnection);
                                JObject checkIfValidResponse = JObject.Parse(checkIfValid.responseContent);
                                bool isValid = bool.Parse(checkIfValidResponse["result"]["isvalid"].ToString());

                                if (isValid == true)
                                {
                                    ServerResponse sendAsset = await assets.Transfer(serverConnection, assetToSend, quantity, getSenderAddress.responseContent);
                                    sendAsset.errorEx = confirmations.ToString();
                                    return sendAsset;
                                }
                                else
                                {
                                    return new ServerResponse { statusCode = HttpStatusCode.InternalServerError, errorEx = "Invalid sender address" };
                                }
                            }
                            else
                            {
                                return new ServerResponse { statusCode = HttpStatusCode.InternalServerError, errorEx = "Could not find Sender Address" };
                            }
                        }
                        else
                        {
                            return new ServerResponse { statusCode = HttpStatusCode.InternalServerError, errorEx = $"The transaction didn't meet the minimum requirements. Incoming Address: {receiveAddress}, Amount: {finalAmountReceived}, Category: {category}, Confirmationss: { confirmations }" };
                        }
                    }
                    else
                    {
                        return new ServerResponse { statusCode = HttpStatusCode.InternalServerError, errorEx = $"Not enough Asset: {assetToSend} left in wallet." };
                    }
                }
                else
                {
                    return new ServerResponse { statusCode = HttpStatusCode.InternalServerError, errorEx = $"Transaction {txid} has no details section. Likely a fee transaction or an asset transfer transaction." };
                }
            }
            else
            {
                return new ServerResponse { statusCode = HttpStatusCode.InternalServerError, errorEx = $" Could not look up Transaction {txid}" };
            }
        }

        
    }
}

Notice on line 54 we’re checking for a * on the expected incoming asset. This is if we want to accept any asset in exchange for our asset, unless specifically configured in the next part.

That’s it for the class library. Lets move over to the Ravencoin.AssetSender console application.

Since the Ravencore wallet is going to run this application every time a transaction comes in, we need to write some logic right in the front here to determine if we’re going to accept RVN and send the asset, or accept and asset and send the asset.

using log4net;
using log4net.Config;
using Ravencoin.ApplicationCore.BusinessLogic;
using Ravencoin.ApplicationCore.Models;
using System;
using System.Threading.Tasks;
using System.Configuration;

namespace Ravencoin.AssetSender
{
   class Program
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(Program));
        static async Task Main(string[] args)
        {
            XmlConfigurator.Configure();

            ServerConnection serverConnection = new ServerConnection{
                host = ConfigurationManager.AppSettings["host"],
                port = Int32.Parse(ConfigurationManager.AppSettings["port"]),
                username = ConfigurationManager.AppSettings["username"],
                password = ConfigurationManager.AppSettings["password"]
        };

            //Pull values from the config file
            string rvnListenAddress = ConfigurationManager.AppSettings["rvnListenAddress"];
            string assetListenAddress = ConfigurationManager.AppSettings["assetListenAddress"];
            string assetToSend = ConfigurationManager.AppSettings["assetToSend"];
            int multiplier = Int32.Parse(ConfigurationManager.AppSettings["multiplier"]);
            string expectedIncomingAsset = ConfigurationManager.AppSettings["expectedIncomingAsset"];
            int assetMultiplier = Int32.Parse(ConfigurationManager.AppSettings["assetMultiplier"]);

            try {
                //Get the txid from the args that the ravencoin node will supply when a wallet transaction comes in.
                if (args.Length > 0){
                    string txid = args[0].ToString();
                    log.Info($"Incoming transaction detected from {txid}");

                    //Look up & Log the transaction Type
                    Assets assets = new Assets();
                    CustomAssets cassets = new CustomAssets();
                    CustomTransactions ctransactions = new CustomTransactions();

                    TransactionType ttype = await ctransactions.GetTransactionType(txid, serverConnection);
                    log.Info($"TXID: {txid}. Transaction Type: {ttype.transactionTypes.ToString()}, Category: {ttype.categoryTypes.ToString()}");

                    //Deliver assets depending on the transaction type.
                    if (ttype.transactionTypes == TType.RVN && ttype.categoryTypes == CType.Receive) {
                        log.Info($"TXID: {txid}. Beginning ExchangeRvnForAsset method.");
                        //Send the details to the ExchangeRvnForAsset function in ApplicationCore.
                        ServerResponse response = await cassets.ExchangeRvnForAsset(txid, rvnListenAddress, assetToSend, multiplier, 1, serverConnection);

                        //Check if everything worked, and log the transaction ID of the Asset send. Otherwise, log the error.
                        if (response.statusCode == System.Net.HttpStatusCode.OK) {
                            log.Info($"Asset delivered successfully. Resulting transaction id = {response.responseContent}");
                            log.Info($"Confirmations: {response.errorEx}");
                        } else {
                            log.Error($"Error: {response.errorEx}");
                        }
                    } else if (ttype.transactionTypes == TType.Asset && ttype.categoryTypes == CType.Receive) {
                        log.Info($"TXID: {txid}. Beginning ExchangeAssetForAsset method.");
                        //Send the details to the ExchangeAssetForAsset function in ApplicationCore
                        ServerResponse response = await cassets.ExchangeAssetForAsset(txid, assetListenAddress, expectedIncomingAsset, assetToSend, assetMultiplier, 1, serverConnection);

                        //Check if everything worked, and log the transaction ID of the Asset send. Otherwise, log the error.
                        if (response.statusCode == System.Net.HttpStatusCode.OK) {
                            log.Info($"Asset delivered successfully. Resulting transaction id = {response.responseContent}");
                        } else {
                            log.Error($"Error: {response.errorEx}");
                        }
                    }    
                }
                else{
                    log.Error("No transaction ID passed in arguments");
                }
            }
            catch (Exception ex){
                log.Error(ex.Message);
            }
        }
    }
}

You can see on line 48 we’re taking action if the incoming transaction is RVN, and on line 60 if the incoming transaction is an asset.

I also wanted to make sure to have separate configuration options for exchanging assets instead of RVN. For example, I set my multiplier to 1 instead of 100 like we had when we were receiving RVN. Why? Because I had the expected asset received set to a wildcard (any asset, including the BULLSH token I own). So if i set the multiplier to 100, someone could send back my own assets on a loop until they get all my assets.

If you want to receive a specific asset, TRASH for example – you can just replace the * in expectedIncomingAsset to TRASH, and it will only take action if that asset is received. That said, any assets that arent TRASH will just sit in your wallet and the person who sent it will get nothing.

See the app.config below. You could technically expand on this and load many asset pairs into a database and take specific actions on each instead of hard coding it into a config like I have.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<configSections>
		<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
	</configSections>
	<log4net>
		<!-- In log4net, output destinations are known as appenders Roll the file when it reaches 1MB -->
		<appender name="LogFileAppender" type="log4net.Appender.RollingFileAppender">
			<!-- Specify which file to write to -->
			<param name="File" value="RavenCoin_AssetSender.log"/>
			<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
			<appendToFile value="true" />
			<rollingStyle value="Size" />
			<!-- How many log files should we keep? -->
			<maxSizeRollBackups value="2" />
			<!-- Roll to a new file when current one hits 1MB -->
			<maximumFileSize value="1MB" />
			<staticLogFileName value="true" />
			<!-- The format of each line in the log -->
			<layout type="log4net.Layout.PatternLayout">
				<param name="ConversionPattern" value="%d [%t] %-5p %c %m%n"/>
			</layout>
		</appender>
		<!-- Set root logger level to INFO and appender to LogFileAppender -->
		<root>
			<level value="INFO" />
			<appender-ref ref="LogFileAppender" />
		</root>
	</log4net>
	<appSettings>
		<add key ="host" value="127.0.0.1"/>
		<add key ="port" value="8766"/>
		<add key ="username" value="cryptobullsh"/>
		<add key ="password" value="testestest111111111"/>
		<add key="rvnListenAddress" value ="yourRavencoinIncomingAddress"/>
		<add key ="assetToSend" value="BULLSH"/>
		<add key ="multiplier" value="100"/>
		<add key ="assetListenAddress" value="yourRavencoinIncomingAddress"/>
		<add key ="expectedIncomingAsset" value="*"/>
		<add key ="assetMultiplier" value="1"/>
	</appSettings>
</configuration>

Now we’re ready to publish our application. Follow the steps in the previous blog post on publishing, and you should be set!

Here it is working:

2021-09-06 19:45:40,329 [1] INFO  Ravencoin.AssetSender.Program Incoming transaction detected from 06dca213ec0b49fd2b396f273c3505bfba3053086cd01e09cab8522722d36237
2021-09-06 19:45:40,615 [4] INFO  Ravencoin.AssetSender.Program TXID: 06dca213ec0b49fd2b396f273c3505bfba3053086cd01e09cab8522722d36237. Transaction Type: Asset, Category: Receive
2021-09-06 19:45:40,616 [4] INFO  Ravencoin.AssetSender.Program TXID: 06dca213ec0b49fd2b396f273c3505bfba3053086cd01e09cab8522722d36237. Beginning ExchangeAssetForAsset method.
2021-09-06 19:45:41,110 [5] INFO  Ravencoin.AssetSender.Program Asset delivered successfully. Resulting transaction id = {"result":["ad8cf09dea1bbf9f355a06f17a52bc8fe0881ba3955ce57d1540d2eeb1f49271"],"error":null,"id":"0"}

Hope this helps someone out there! Thanks as always for reading this far. Feel free to contact me on Twitter with questions.

Did I help? If you’re feeling generous, buy me a coffee by sending RVN to RHEH92NguBjaxXQPsM1bedkqqTXKr9EZcM

Follow me on social media:

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>