Intro
In this advanced tutorial, we will create a MiniDapp that:
- Uses service.js to create a background service
- Records data from the blockchain
- Stores the data in a lightweight SQL database
- Analyses the recorded data to display statistics about the chain
Step 1 - Config file & icon
- Create a new folder for your wallet MiniDapp, call it ministats.
- Inside that folder, create your dapp.conf. file as below
{
"name": "Mini Stats",
"icon": "favicon.ico",
"version": "1.0",
"description": "A MiniDapp dashboard.",
}
Choose an icon for your MiniDapp and save it in your ministats folder
Step 2 - Ministats interface
Let's create a webpage to display the following statistics:
- Latest block speed
- Average block speed for the last 10, 100, 500 and 1000 blocks
- Latest block difficulty (HEX)
- Current Chain weight (hashes)
- Current Cascade weight (hashes)
- Total Chain weight
Ensure you have the latest mds.js downloaded and save it in your mywallet folder.
Create an index.html file containing the following code:
<head>
<!-- The MINIMA MDS JavaScript Library -->
<script type="text/javascript" src="mds.js"></script>
<!-- The MiniStats JS File -->
<script type="text/javascript" src="ministats.js"></script>
<!-- The ChartsJS import -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js"></script>
<!-- The ChartsJS JS File -->
<script type="text/javascript" src="charts.js"></script>
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<title>Mini Stats</title>
</head>
<body>
<!-- HTML Elements for MiniStats -->
<h1>MiniStats</h1>
<h2>Block info</h2>
<p>Current block: <span id="current-block"></span></p>
<p>Current block time: <span id="current-block-time"></span></p>
<p>Current block speed: <span id="block-speed"></span></p>
<p>Current block difficulty: <span id="block-difficulty"></span></p>
<p>Current block hash: <span id="block-hash"></span></p>
<br />
<h2>Chain info</h2>
<p>Current Chain weight: <span id="chain-weight"></span></p>
<p>Current Cascade weight: <span id="cascade-weight"></span></p>
<p>Total Chain weight: <span id="total-weight"></span></p>
<br />
<h2>Speed statistics</h2>
<p>Block Speeds:</p>
<ul>
<li>Previous 10 blocks:</li>
<p>Average speed: <span id="previous-ten"></span></p>
<br />
<p>Speed</p>
<canvas id="myChart" style="width: 100%; max-width: 700px"></canvas>
<p>Blocks</p>
<br />
<li>Previous 100 blocks:</li>
<p>Average speed: <span id="previous-hundred"></span></p>
<br />
<p>Speed</p>
<canvas id="myChart2" style="width: 100%; max-width: 700px"></canvas>
<p>Blocks</p>
<br />
<li>Previous 1000 blocks:</li>
<p>Average speed: <span id="previous-thousand"></span></p>
<br />
<p>Speed</p>
<canvas id="myChart3" style="width: 100%; max-width: 700px"></canvas>
<p>Blocks</p>
</ul>
<script type="text/javascript">
MDS.init(function (msg) {
if (msg.event === "inited") {
// Create the metrics DB table on initialization
// Display the metrics table on the page
displayMetrics()
// Display the charts on the page
displayCharts(function () {
MDS.log("Previous ten blocks displayed")
})
} else if (msg.event === "NEWBLOCK") {
// Refresh the metrics table on the page
displayMetrics()
// Refresh the charts on the page
window.location.reload()
}
})
// Function to display the metrics table on the page
function displayMetrics() {
MDS.cmd("status", function (msg) {
const currentBlockNumber = msg.response.chain.block
const currentBlockTime = msg.response.chain.time
const currentBlockSpeed = msg.response.chain.speed
const currentBlockDifficulty = msg.response.chain.difficulty
const currentBlockChainweight = msg.response.chain.weight
const currentBlockCascadeWeight = msg.response.chain.cascade.weight
const currentBlockHash = msg.response.chain.hash
const totalWeight = msg.response.weight
document.getElementById("current-block").innerText = currentBlockNumber
document.getElementById("block-speed").innerText =
(1 / currentBlockSpeed).toFixed(1) + " s/block"
document.getElementById("block-difficulty").innerText =
currentBlockDifficulty
document.getElementById("chain-weight").innerText =
currentBlockChainweight
document.getElementById("cascade-weight").innerText =
currentBlockCascadeWeight
document.getElementById("current-block-time").innerText =
currentBlockTime
document.getElementById("block-hash").innerText = currentBlockHash
document.getElementById("total-weight").innerText = totalWeight
})
}
</script>
</body>
save the file as index.html in your ministats folder.
Step 3 - Back end (service.js)
We will now create our service.js file which will listen to the chain and
record data about the latest block with every NEWBLOCK event.
Initialise MDS and create a sql table to record the following data:
- Block number
- Block time and date
- Block hash
- Block Difficulty
- Chain weight
- Cascade weight
Steps:
Create a service.js file containing the following code:
MDS.load("ministats.js")
MDS.init(function (msg) {
if (msg.event == "inited") {
createMetricsTable(function () {
MDS.log("Metrics table created")
})
} else if (msg.event == "NEWBLOCK") {
saveNewBlock(msg)
MDS.log("New block event saved to metrics table")
}
})
Create a ministats.js file for your SQL operations and JavaScript functions and insert the following code:
// Description: This function is used to save block metrics to a database
function saveNewBlock(msg) {
// Get block number from message response from NEWBLOCK event
const blockNumber = parseInt(msg.data.txpow.header.block);
// Check if block is already saved
isBlockSaved(blockNumber, function (saved) {
// If block is not saved, build metric row and save it to the DB
if (!saved) {
buildMetricRow(blockNumber);
}
});
}
// Description: This function is used to create a metrics table in the database
function createMetricsTable(callback) {
// SQL query to create metrics table
var initsql =
"CREATE TABLE IF NOT EXISTS `metrics` ( " +
" `id` bigint auto_increment, " +
" `blockTime` varchar(160) NOT NULL, " +
" `blockHash` varchar(160) NOT NULL, " +
" `blockDifficulty` varchar(160) NOT NULL, " +
" `blockNumber` int NOT NULL, " +
" `blockSpeed` float NOT NULL, " +
" `chainWeight` varchar(160) NOT NULL, " +
" `cascadeWeight` varchar(160) NOT NULL " +
" )";
// Call SQL from MDS API to create metrics table in the database
MDS.sql(initsql, function (res) {
MDS.log("Creating metrics table");
if (!res.status) {
MDS.log("Error creating metrics table " + res.message);
}
if (callback) {
callback(res);
}
});
}
// Description: This function is used to build a metric row for a block
function buildMetricRow(blockNumber) {
// Call MDS API to get status of node
MDS.cmd("status", function (msg) {
// Get response from message
const response = msg.response.chain;
// Build metric object with block metrics
const metric = {
time: response.time,
blockSpeed: response.speed,
chainweight: response.weight,
difficulty: response.difficulty,
cascadeWeight: response.cascade.weight,
blockNumber: blockNumber,
blockHash: response.hash,
};
// Store metric in the database
storeMetric(metric);
});
}
// Description: This function is used to store a metric in the database
function storeMetric(metric) {
const MAX_ROWS_IN_TABLE = 4320; // 4k rows is about 1 day (at 20s per block)
// SQL query to insert metric into metrics table
const INSERT =
"INSERT INTO metrics (blockTime, blockHash, cascadeWeight, blockSpeed, chainWeight, blockDifficulty, blockNumber) VALUES (";
const SQL =
INSERT +
"'" +
metric.time +
"', " +
"'" +
metric.blockHash +
"', " +
metric.cascadeWeight +
", " +
metric.blockSpeed +
", " +
"'" +
metric.chainweight +
"'," +
"'" +
metric.difficulty +
"'," +
metric.blockNumber +
")";
// Call MDS API to insert metric into metrics table
MDS.sql(SQL, function (res) {
if (res.status == true) {
MDS.log("metric row added success");
} else {
MDS.log("metric row added failed");
}
});
// Call MDS API to clip metrics table to a certain number of rows
MDS.sql(clipMetricsTable(MAX_ROWS_IN_TABLE), function (res) {
if (res.status == true) {
}
});
}
// Description: This function is used to clip the metrics table to a certain number of rows
function clipMetricsTable(numberOfRowsToKeep) {
return (
"DELETE FROM METRICS " +
"WHERE id NOT IN (" +
"SELECT id " +
"FROM METRICS " +
"ORDER BY blockNumber DESC " +
"LIMIT " +
numberOfRowsToKeep +
")"
);
}
// Description: This function is used to delete the metrics table
function deleteTable() {
const DELETE = "DROP TABLE metrics";
MDS.sql(DELETE, function (res) {
if (res.status == true) {
MDS.log("table deleted");
} else {
MDS.log("table delete failed");
}
});
}
// Description: This function is used to check if a block is already saved in the database
function isBlockSaved(blockNumber, callback) {
const SELECT = "SELECT * FROM metrics WHERE blockNumber = " + blockNumber;
MDS.sql(SELECT, function (res) {
if (res.status == true) {
if (res.rows.length > 0) {
callback(true);
} else {
callback(false);
}
}
});
}
Save the file as service.js and ministats.js in your ministats folder.
Step 4 - Charts
We will now create a chart.js file to display the block speed statistics in a graphical format.
Create a charts.js file containing the following code:
function displayCharts(callback) {
// Select previous 10 blocks query
const SELECT = "SELECT * FROM metrics ORDER BY blockNumber DESC LIMIT 10"
// Select previous 10 blocks from DB
MDS.sql(SELECT, function (res) {
if (res.status == true) {
// Get rows from response
const rows = res.rows
// Initialize total speed, xdata, and ydata
let totalSpeed = 0
let xdata = []
let ydata = []
// Iterate through rows
rows.forEach((row) => {
// Add block speed to total speed
totalSpeed += parseFloat(row.BLOCKSPEED)
// Push time and speed to respective arrays for chart
xdata.push(row.BLOCKNUMBER)
// Get the blockspeed by dividing 1 by BLOCKSPEED
ydata.push(parseFloat(1 / row.BLOCKSPEED))
})
// Calculate average speed
const averageSpeed = totalSpeed / rows.length
// Check if there are at least 10 rows
if (rows.length >= 10) {
// Update previous ten blocks average speed
document.getElementById("previous-ten").innerText =
(1 / averageSpeed).toFixed(1) + " s/block"
// Create chart for block speed
new Chart("myChart", {
type: "line",
data: {
labels: xdata.reverse(),
datasets: [
{
fill: false,
lineTension: 0,
backgroundColor: "rgba(0,0,255,1.0)",
borderColor: "rgba(0,0,255,0.1)",
data: ydata.reverse(),
},
],
},
options: {
legend: { display: false },
scales: {
yAxes: [
{
ticks: {
// Display 1/BLOCKSPEED for better visualization
callback: function (value) {
return value.toFixed(1) + " s/block"
},
},
},
],
},
},
})
// If there are less than 10 rows
} else {
// Display N/A for previous ten blocks average speed
document.getElementById("previous-ten").innerText =
"N/A (less than 10 blocks)"
// Hide chart
document.getElementById("myChart").style.display = "none"
}
}
})
// Select previous 100 blocks query
const SELECT100 = "SELECT * FROM metrics ORDER BY blockNumber DESC LIMIT 100"
// Select previous 100 blocks from DB
MDS.sql(SELECT100, function (res) {
if (res.status == true) {
// Get rows from response
const rows = res.rows
// Initialize total speed, xdata, ydata and ydataDifficultyChart variables
let totalSpeed = 0
let xdata = []
let ydata = []
// Iterate through rows
rows.forEach((row) => {
// Add block speed to total speed
totalSpeed += parseFloat(row.BLOCKSPEED)
// Push time and speed to respective arrays for chart
xdata.push(row.BLOCKNUMBER)
// Get the blockspeed by dividing 1 by BLOCKSPEED
ydata.push(parseFloat(1 / row.BLOCKSPEED))
})
// Calculate average speed
const averageSpeed = totalSpeed / rows.length
// Check if there are at least 100 rows
if (rows.length >= 100) {
// Update previous 100 blocks average speed and difficulty
document.getElementById("previous-hundred").innerText =
(1 / averageSpeed).toFixed(1) + " s/block"
// Create chart for block speed
new Chart("myChart2", {
type: "line",
data: {
labels: xdata.reverse(),
datasets: [
{
fill: false,
lineTension: 0,
backgroundColor: "rgba(0,0,255,1.0)",
borderColor: "rgba(0,0,255,0.1)",
data: ydata.reverse(),
},
],
},
options: {
legend: { display: false },
scales: {
yAxes: [
{
ticks: {
// display 1/BLOCKSPEED for better visualization
callback: function (value) {
return value.toFixed(1) + " s/block"
},
},
},
],
},
},
})
} else {
// Display N/A for previous ten blocks average speed
document.getElementById("previous-hundred").innerText =
"N/A (less than 100 blocks)"
// Hide chart
document.getElementById("myChart2").style.display = "none"
}
}
})
// Select previous 1000 blocks query
const SELECT1000 =
"SELECT * FROM metrics ORDER BY blockNumber DESC LIMIT 1000"
// Select previous 1000 blocks from DB
MDS.sql(SELECT1000, function (res) {
if (res.status == true) {
// Get rows from response
const rows = res.rows
// Initialize total speed, xdata, ydata
let totalSpeed = 0
let xdata = []
let ydata = []
// Iterate through rows
rows.forEach((row) => {
// Add block speed to total speed
totalSpeed += parseFloat(row.BLOCKSPEED)
// Push time and speed to respective arrays for chart
xdata.push(row.BLOCKNUMBER)
// Get the blockspeed by dividing 1 by BLOCKSPEED
ydata.push(parseFloat(1 / row.BLOCKSPEED))
})
// Calculate average speed
const averageSpeed = totalSpeed / rows.length
// Check if there are at least 100 rows
if (rows.length >= 1000) {
// Update previous 100 blocks average speed
document.getElementById("previous-thousand").innerText =
(1 / averageSpeed).toFixed(1) + " s/block"
// Create chart for block speed
new Chart("myChart3", {
type: "line",
data: {
labels: xdata.reverse(),
datasets: [
{
fill: false,
lineTension: 0,
backgroundColor: "rgba(0,0,255,1.0)",
borderColor: "rgba(0,0,255,0.1)",
data: ydata.reverse(),
},
],
},
options: {
legend: { display: false },
scales: {
yAxes: [
{
ticks: {
// display 1/BLOCKSPEED for better visualization
callback: function (value) {
return value.toFixed(1) + " s/block"
},
},
},
],
},
},
})
} else {
// Display N/A for previous ten blocks average speed
document.getElementById("previous-thousand").innerText =
"N/A (less than 100 blocks)"
// Hide chart
document.getElementById("myChart2").style.display = "none"
}
}
})
}
Save the file as charts.js in your ministats folder.
Step 5 - Zip up and install your MiniDapp
The next step is to zip up your wallet and install it onto your node.
You should now have a folder structure that looks like this:
- Open your ministats folder and select all the contents and send them to a zip file.(DO NOT ZIP THE helloworld FOLDER ITSELF)
- Name the zip folder ministats.mds.zip
- Log into your MiniDapp Hub at https://127.0.0.1:9003 and click on the +
button to install your MiniDapp.
- Select your ministats.mds.zip file and click install
Alternatively, from the Minima Terminal, run the command:
mds action:install file:C:Usersyournameministats.mds.zip
Example: mds action:install file:C:\Users\yourname\mywallet.mds.zip
Now you can open your MiniDapp Hub and see your MiniDapp installed!
Debug settings
When developing, it is useful to run your code locally to avoid having to zip up and install your miniDapp onto a node every time you make a change.
- To see live changes in your MiniDapp we recommend installing the Live Server VS Code extension (or any other live server feature that your code editor has)
- Install the MiniDapp onto your MiniHub, open it and copy the long UID string located in the URL in the browser after index.html?uid=
- Open up your ministats folder and open the mds.js file
- Find the DEBUG_MINIDAPPID: variable and replace 0x00 with your minidapp UID string
- Navigate back to your MiniHub and copy the host number in the URL e.g. 127.0.0.1 and the port number e.g. 9003
- Find the DEBUG_HOST: variable and paste your host number surrounded by double quotes “
- Find the DEBUG_PORT: variable and paste your port number
- Save mds.js and start your live server. If using Live Server in VS code, click on Go Live in the bottom right to launch it.
That's it you are all set!