WAXCANNER, a tool for tracking NFT purchases and sales

Categories Blog, Guild, WAXPosted on
accountant working

The NFT ecosystem is growing at an unstoppable rate and more and more people are interested in participating. It is a fast-moving market and the developer community is putting more effort into creating consumer products than into creating management and administration services.

When the time comes to audit, many collectors and dealers find it difficult to keep track of their purchases and sales in detail.

Another problem they encounter is the conversion of the cryptocurrency used for purchases and sales, in our case WAXP, to its corresponding FIAT value. We can keep a manual control and write down, in each transaction, the corresponding value, but that forces us to be excessively vigilant of our operations. Day and night!

What if we could generate a list with the movements we need to manage with the corresponding FIAT value at the time of each transaction?

We have also faced this problem and we have definitely rolled up our sleeves to make a solution available to everyone.

For now, the solution we offer is more of a tutorial aimed at those with JavaScript programming skills. Our intention is to offer a universal and free tool suitable for all members of the WAX community as soon as we can.

GitHub code: https://github.com/3dkrender/waxcanner

The Oracle

The first thing we are going to need is an external oracle that can provide us with the price information of a cryptocurrency at a given time and in a reliable way.

For our exercise, we have proposed the use of the Bittrex exchange API. Specifically, we will use this call from its library:


GET /markets/{marketSymbol}/candles/{candleType}/{candleInterval}/historical/{year}/{month}/{day}

You can access the information on this call here:


https://bittrex.github.io/api/v3#operation--markets--marketSymbol--candles--candleType---candleInterval--historical--year---month---day--get

With this call, we can retrieve a candle reading of a token at a given time.

We have selected the 5-minute candles and, if we do the relevant calculations, we will know that in a day (24 hours) we will have 288 5-minute candles, so this call will return an array of 288 candles.

We will only need one candle; the one whose time is closest to the time of the transaction, so we will calculate the minutes elapsed in the day and by dividing by 5 we will get the closest candle.

Note: This Bittrex API call will return an error if the requested date is today, so we will limit the readings to the day before the current one.


The response of the call will be an object of this type:

[
  {
	"startsAt": "string (date-time)",
	"open": "number (double)",
	"high": "number (double)",
	"low": "number (double)",
	"close": "number (double)",
	"volume": "number (double)",
	"quoteVolume": "number (double)"
  }
]

We need “close” value for our purpose. That’s the code to get it:

const get_ticker = async (ticker, date) => {
  try {
    let minutes = Math.floor((parseInt(date.substr(11,2)) * 60 + parseInt(date.substr(14,2))) / 5);
    const url = `https://api.bittrex.com/v3/markets/${ticker}/candles/trade/minute_5/historical/${date.substr(0,4)}/${date.substr(5,2)}/${date.substr(8,2)}`
    const response = await fetch(url);
    const result = await response.json();
    return result[minutes];
  } catch (error) {
    throw error;    
  }
}

Querying the WAX history

To query the WAX Blockchain historical records we are going to request HTML GET requests to a public API that has a complete historical record, such as those offered by the WAX Guilds through their Hyperion services.

For our exercise we will select our guild’s public API, 3DK Render, with this call (you can use the URL of any other WAX Blockchain public API):


https://apiwax.3dkrender.com/v2/docs/index.html#/history/get_v2_history_get_actions

We will make the call with the “fetch” function as we did in the case of the Bittrex API.

This time the call will be different as it will need a series of arguments that we will provide thanks to a JSON object:

{
	‘limit’: número de acciones a leer (recomendado entre 500 y 1000),
	'account': nombre de la cuenta a consultar,
	'sort': orden de los resultados (ascendente/descendete),
	'after': fecha de incio de la consulta,
	'simple': modo de salida de datos (true),
	'skip': número de acciones a saltar antes de empezar a leer (por defecto 0)
  }

The more actions we get on each read, the fewer reads we have to make against the public API, but keep in mind that these APIs usually have a configured maximum.

We must also take in mind that these public APIs are protected by anti-spam systems to avoid abuse, so we will have to force a small pause between reads if we want to avoid being kicked out for submitting too many requests in a short period of time.

The process of reading records will be through a loop that will end according to certain conditions

Natural ending:

  • No more actions. The number of actions read is less than the limit of actions to be read.
  • Current date. If the read date is today’s date we do not read any more.


Abnormal ending:

  • Read error

In each cycle we will make a request for actions to the API with the indicated parameters

response’ contains the result of the request and we must convert it to a JSON object to extract the [‘simple_actions’] element if it has one.

response = await fetch(API + new URLSearchParams(values))
response = await response.json();
if (response['simple_actions'] == undefined) {
    	console.log('Something went wrong when trying to read. Check the format of the input data.');
    	return false;
}
actions = response['simple_actions'];

For each block of actions we have obtained, we will have to perform a loop to evaluate each of the actions.

For each action:

  • Update the date of the next action to read.
  • Filter the actions we need and avoid duplicates.
  • Format the output log and
  • Publish the log to the console and/or file

Note: The cyclic process is based on the date of the next action. It may happen that a transaction has more actions than our limit, so if we don’t take this into account, we can fall into an infinite loop repeating over and over again the reading of the same actions. To avoid this, we will check the number of actions read and the dates of the last and the first one to see if it is necessary to skip the next reading.

Action filtering and data extraction

Actions are JSON objects whose content will vary depending on the type of action. For our case, a typical action would look like this:

{
  "block": 148348950,
  "timestamp": "2021-10-30T21:00:14.000",
  "contract": "eosio.token",
  "action": "transfer",
  "actors": "atomicmarket@active",
  "notified": "eosio.token,atomicmarket,3vlau.wam",
  "transaction_id": "42672fe5bda200856bfe347f3846d0506b5ffde839da6634492942fdead74de3",
  "data": {
    "from": "atomicmarket",
    "to": "3vlau.wam",
    "amount": 6.49576905,
    "symbol": "WAX",
    "memo": "AtomicMarket Sale Payout - ID #39632816",
    "quantity": "6.49576905 WAX"
  }
}

We can perform the filtering by consulting some parameters that we can always find in the actions, such as the name of the action (‘transfer‘) or the smart contract that contains the action (‘eosio.token‘). It is also advisable that we filter transactions with those accounts belonging to the markets with which we work.

const markets = ['atomicmarket', 'atomicdropsx', 'neftyblocksd'];
...
if (markets.indexOf(action['data']['from']) !== -1) { ... }

Calculating prices

For each of the actions we will have two data that we will use to calculate the value of the transaction in USD and EUR (or in the currency of our choice):

acction[‘timestamp’]
action[‘data’][‘amount’]

timestamp‘ is the date and time of the transaction and will be one of the parameters with which we will call our function to read the Bittrex API, as we saw before.

let usdteur = await get_ticker("USDT-EUR", action['timestamp']);
let waxpusdt = await get_ticker("WAXP-USDT", action['timestamp']);

The results

Once we have filtered the actions by smart contract and action name and obtained the corresponding values, we can format the output record for our CSV with the data we want.

dataReg = action['timestamp'] + ',' + action['data']['from'] 
  + ',' + action['data']['to'] + ',' 
  + Number.parseFloat(action['data']['amount']).toFixed(4) 
  + ',WAX' + ',' 
  + Number.parseFloat(waxpusdt.close *   action['data']['amount']).toFixed(4) 
  + ',USD' + ',' 
  + Number.parseFloat((waxpusdt.close * action['data']['amount']) * usdteur.close).toFixed(4) 
  + ',EUR,,,,,' + ',' + ',' 
  + action['data']['memo'] + ',' 
  + action['transaction_id'];

Gaps have been intentionally left to separate purchases from sales into columns.

Get code from GitHub:

https://github.com/3dkrender/waxcanner

We hope this tutorial can be of help to you and we thank you for your support with your vote for WAX Block Producer.

Click for more info.

Leave a Reply

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