Building a Telegram Bot for the Rootstock Blockchain: A Comprehensive Guide


Blockchain technology is changing the way we use decentralized systems, making them more accessible and efficient. One useful application of this technology is blockchain bots, which are automated assistants that help users get blockchain information easily. Instead of manually searching through complex block explorers or using special wallets, these bots allow users to access data directly through messaging apps like Telegram. In this guide, we’ll show you how to create a Telegram bot that interacts with the Rootstock blockchain, making it simple to check balances, track transactions, and get real-time blockchain updates.
What is Rootstock?
Rootstock (RSK) is a smart contract platform that is built on top of the Bitcoin blockchain. It aims to extend Bitcoin's functionality by enabling the execution of smart contracts, similar to what Ethereum offers. Rootstock achieves this by utilizing a two-way peg mechanism that allows Bitcoin to be converted into RSK tokens (RBTC) and vice versa. This integration allows developers to create decentralized applications (dApps) that benefit from Bitcoin's security while leveraging the flexibility of smart contracts.
Key Features of Rootstock:
EVM Compatibility: Rootstock is compatible with the Ethereum Virtual Machine (EVM), allowing developers to easily port their existing Ethereum dApps to the Rootstock platform without significant modifications. This compatibility fosters a broader ecosystem of decentralized applications and services.
Smart Contracts: Rootstock supports smart contracts, enabling developers to create complex applications that can automate processes and facilitate transactions without intermediaries.
Decentralized Finance (DeFi): Rootstock incorporates DeFi functionalities, making it a versatile platform for developers and users alike.
Scalability: By utilizing sidechains, Rootstock addresses some of the limitations of the Ethereum network, such as high gas fees and slow transaction times, providing a robust environment for building and deploying decentralized applications.
What are Bots?
Bots, short for "robots," are software applications designed to automate tasks typically performed by humans, operating over the internet or within specific software environments. They come in various forms, including:
Web Crawlers: Index content for search engines.
Chatbots: Simulate conversation for customer service.
Social Media Bots: Automate posting and engagement.
Gaming Bots: Assist players or simulate opponents.
Scraping Bots: Extract data from websites.
Transactional Bots: Facilitate online purchases.
While many bots serve beneficial purposes, such as enhancing user experience and streamlining processes, some are malicious, designed for harmful activities like launching DDoS attacks or spreading spam. With advancements in technology, particularly in artificial intelligence and machine learning, the use of bots has grown significantly, making them an integral part of the digital landscape.
Understanding Blockchain Bots
Blockchain bots serve as intermediaries between users and blockchain networks, providing several key benefits:
Real-time Blockchain Data Access: Users can access up-to-date information about transactions, balances, and more.
User-friendly Interaction: Bots provide a familiar messaging interface, making it easier for casual users to interact with blockchain networks.
Automated Responses: Bots can handle common queries automatically, reducing the need for human intervention.
Reduced Complexity: Casual users can interact with blockchain technology without needing to understand the underlying complexities.
Consistent Availability: Bots are available 24/7, providing reliable access to information.
Technical Architecture Overview
Let's examine how our Telegram bot will interact with the Rootstock network:
Let's clarify some key elements from the diagram:
User: The user initiates the process of checking the wallet balance by sending a command to the Telegram bot.
Telegram Bot: The Telegram bot receives the command and passes it to the Web3 provider.
Web3 Provider: The Web3 provider is a library or service that allows the Telegram bot to interact with the Rootstock blockchain. The Web3 provider initializes itself and connects to the Rootstock Testnet to send the requested information.
Rootstock Network: The Rootstock Network is a blockchain platform that is compatible with Ethereum. The network receives the information from the Web3 provider and processes the request.
Web3 Instance Ready: When the connection to the Rootstock Testnet is established, the Web3 provider signals that it is ready to process the request to the bot.
get balance(address): The bot requests the balance of a specific address.
Return Balance in Wei: The Rootstock Network returns the balance of the specified address in Wei.
Convert Wei to ETH: The bot converts the balance from Wei to ETH to display to the user.
Display Balance in ETH: The bot sends the balance to the user.
Alternative Path (Invalid Address)
If the address provided by the user is invalid, the bot will receive an error message and display it to the user.
Let's Start with Making a Telegram Bot
Step 1: Create a New Bot with BotFather
Open Telegram and search for the @BotFather.
Or Simply Scan to the below QR which will redirect you to the @BotFather id.
Click on the Start button at the bottom and then enter the /newbot command to initialize a new bot.
Give a name for your bot (e.g., RootstockDapp).
Give a username to your bot, ending with the keyword bot (e.g., RootstockDappBot).
Congratulations! You have created a new bot named RootstockDappBot. Copy the API key provided by BotFather.
Step 2: Set Up Your Project
Create a new folder for this project:
mkdir RootstockDappBot
cd RootstockDappBot
Initialize npm in the root directory:
npm init -y
Install the required dependencies:
npm install node-telegram-bot-api web3 dotenv body-parser express nodemon
Brief Explanation of Dependencies:
node-telegram-bot-api: A library that interacts with the Telegram Bot API, allowing you to create and manage Telegram bots.
web3: A JavaScript library for interacting with the Ethereum virtual machine (EVM), enabling interaction with smart contracts and blockchain data.
dotenv: Loads environment variables from a
.env
file intoprocess.env
, helping manage configuration settings and secrets securely.body-parser: Middleware that parses incoming request bodies, making it easier to handle JSON data.
express: A minimalist web framework for Node.js, providing a robust set of features for web applications.
nodemon: A utility that monitors for changes in your source code and automatically restarts your server, enhancing the development experience.
Create a src
folder:
mkdir src
Create a .env
file and paste your API keys:
Visit the Registration Page: Navigate to the RSK dashboard at https://dashboard.rpc.rootstock.io/register.
Complete the Registration Form: Fill in the required details in the sign-up form. Ensure that all information is accurate and up-to-date to facilitate a smooth registration process.
Access the API Key Section: Once registered, log in to your account and locate the "New API Key" section.
Fill in the Necessary Details: Provide the required information for the new API key. This may include naming your key and specifying any relevant permissions or settings.
Generate Your API Key: After submitting the details, you will receive your API Key.
Now Paste Both the keys in the .env
RSK_API_KEY=
BOT_TOKEN=
Create a .gitignore
file to exclude unnecessary files:
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
*.env
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
Update your package.json
file to add scripts:
{
"name": "rootstockdappbot",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon src/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"body-parser": "^1.20.3",
"dotenv": "^16.4.5",
"express": "^4.21.0",
"node-telegram-bot-api": "^0.66.0",
"nodemon": "^3.1.7",
"web3": "^4.13.0"
}
}
Create an index.js
file in the src
folder. Your project directory should look like this:
Let's set up the index.js file
import dotenv from "dotenv";
import express from "express";
import bodyParser from "body-parser";
import TelegramBot from "node-telegram-bot-api";
import Web3 from "web3";
dotenv: Loads environment variables from a
.env
file.express: A web framework for Node.js to create the server.
body-parser: Middleware to parse incoming request bodies.
node-telegram-bot-api: Library to interact with the Telegram Bot API.
web3: Library to interact with Ethereum-compatible blockchains (in this case, RSK).
Main Function
async function main() {
const app = express();
const port = 3000;
app.use(bodyParser.json());
Initializes an Express application and sets it to listen on port 3000.
Uses
body-parser
middleware to parse JSON request bodies.
Telegram Bot and Web3 Initialization
const botToken = process.env.BOT_TOKEN;
const bot = new TelegramBot(botToken, { polling: true });
const web3 = new Web3(`https://rpc.testnet.rootstock.io/${process.env.RSK_API_KEY}`);
Retrieves the bot token from environment variables and initializes the Telegram bot with polling enabled.
Creates a Web3 instance connected to the RSK testnet using the API key from environment variables.
Command Definitions
const commands = {
start: "/start - Start the bot",
balance: "/balance <address> - Check wallet balance",
transactions: "/transactions <address> - Get transaction count",
latestblock: "/latestblock - Get latest block number",
pegIn: "/pegIn <amount> - Convert BTC to RBTC",
pegOut: "/pegOut <amount> - Convert RBTC to BTC",
rbtcPrice: "/rbtcPrice - Get current RBTC price",
gasprice: "/gasprice - Get current gas price",
convert: "/convert <weiAmount> - Convert WEI to ETH",
validate: "/validate <address> - Check if an address is valid",
help: "/help - Show this help message",
};
- Defines a set of commands that the bot can respond to, providing a description for each.
Help Message Function
const sendHelpMessage = (chatId) => {
const helpMessage = "You can use the following commands:\n" + Object.values(commands).join("\n");
bot.sendMessage(chatId, helpMessage);
};
- A helper function to send a formatted help message containing all available commands to the user.
Command Handlers
/help Command
bot.onText(/\/help/, (msg) => {
sendHelpMessage(msg.chat.id);
});
- Listens for the
/help
command and sends the help message.
/start Command
bot.onText(/\/start/, (msg) => {
bot.sendMessage(msg.chat.id, `Hello! Welcome to the RootstockBot.`);
});
- Responds to the
/start
command with a welcome message.
/balance Command
bot.onText(/\/balance (.+)/, async (msg, match) => {
const chatId = msg.chat.id;
const walletAddress = match[1];
try {
const balance = await web3.eth.getBalance(walletAddress);
bot.sendMessage(
chatId,
`Balance of ${walletAddress} is ${web3.utils.fromWei(balance, "ether")} RBTC.`
);
} catch (error) {
bot.sendMessage(chatId, "Error fetching balance. Try again.");
console.error(error);
}
});
- Listens for the
/balance <address>
command, retrieves the balance of the specified wallet address usingweb3.eth.getBalance
, and sends the result back to the user.
/gasprice Command
bot.onText(/\/gasprice/, async (msg) => {
const chatId = msg.chat.id;
try {
const gasPrice = await web3.eth.getGasPrice();
bot.sendMessage(
chatId,
`Current gas price: ${web3.utils.fromWei(gasPrice, "ether")} RBTC.`
);
} catch (error) {
bot.sendMessage(chatId, "Error fetching gas price.");
console.error(error);
}
});
- Listens for the
/gasprice
command, retrieves the current gas price usingweb3.eth.getGasPrice
, and sends the result.
/transactions Command
bot.onText(/\/transactions (.+)/, async (msg, match) => {
const chatId = msg.chat.id;
const walletAddress = match[1];
try {
const transactionCount = await web3.eth.getTransactionCount(walletAddress);
bot.sendMessage(chatId, `${walletAddress} has ${transactionCount} transactions.`);
} catch (error) {
bot.sendMessage(chatId, "Error fetching transaction count.");
console.error(error);
}
});
- Listens for the
/transactions <address>
command, retrieves the transaction count for the specified address, and sends the result.
/latestblock Command
bot.onText(/\/latestblock/, async (msg) => {
try {
const latestBlock = await web3.eth.getBlockNumber();
bot.sendMessage(msg.chat.id, `Latest block number: ${latestBlock}.`);
} catch (error) {
bot.sendMessage(msg.chat.id, "Error fetching latest block.");
console.error(error);
}
});
- Listens for the
/latestblock
command, retrieves the latest block number, and sends the result.
/rbtcPrice Command
bot.onText(/\/rbtcPrice/, async (msg) => {
const chatId = msg.chat.id;
try {
const response = await axios.get('https://api.coingecko.com/api/v3/simple/price?ids=rsk-smart-bitcoin&vs_currencies=usd', {
headers: {
'x-cg-demo-api-key': 'YOUR_API_KEY'
}
});
const rbtcPrice = response.data['rsk-smart-bitcoin'].usd;
bot.sendMessage(chatId, `Current RBTC price: $${rbtcPrice} USD.`);
} catch (error) {
bot.sendMessage(chatId, "Error fetching RBTC price.");
console.error(error);
}
});
- Listens for the
/rbtcPrice
command, retrieves the current price of RBTC in USD from the CoinGecko API, and sends the result.
/pegIn Command
bot.onText(/\/pegIn (\d+)/, async (msg, match) => {
const chatId = msg.chat.id;
const amount = match[1];
// Provide instructions for peg-in
bot.sendMessage(chatId, `To peg in ${amount} BTC, send it to the designated Rootstock address. Once confirmed, the equivalent RBTC will be credited to your wallet.`);
// Implement peg-in logic if needed (this usually involves interacting with a specific contract)
});
- Listens for the
/pegIn <amount>
command converts the specified amount of BTC to RBTC by sending BTC to the designated Rootstock peg-in address.
/pegOut Command
bot.onText(/\/pegOut (\d+)/, async (msg, match) => {
const chatId = msg.chat.id;
const amount = match[1];
// Provide instructions for peg-out
bot.sendMessage(chatId, `To peg out ${amount} RBTC, send it to the designated address. Once confirmed, the equivalent BTC will be credited to your wallet`);
// Implement peg-out logic if needed (this usually involves interacting with a specific contract)
});
- Listens for the
/pegOut <amount>
command converts the specified amount of RBTC back to BTC by following the peg-out process.
/convert Command
bot.onText(/\/convert (\d+)/, async (msg, match) => {
const chatId = msg.chat.id;
const weiAmount = match[1];
try {
const ethAmount = web3.utils.fromWei(weiAmount, "ether");
bot.sendMessage(chatId, `${weiAmount} WEI = ${ethAmount} ETH.`);
} catch (error) {
bot.sendMessage(chatId, "Error converting WEI to ETH.");
console.error(error);
}
});
- Listens for the
/convert <weiAmount>
command, converts the specified WEI amount to ETH usingweb3.utils.fromWei
, and sends the result.
/validate Command
bot.onText(/\/validate (.+)/, async (msg, match) => {
const chatId = msg.chat.id;
const address = match[1];
if (web3.utils.isAddress(address)) {
bot.sendMessage(chatId, `✅ ${address} is a valid Ethereum/RSK address.`);
} else {
bot.sendMessage(chatId, `❌ ${address} is NOT a valid address.`);
}
});
- Listens for the
/validate <address>
command, checks if the address
The final code will look alike:
import dotenv from "dotenv";
import express from "express";
import bodyParser from "body-parser";
import TelegramBot from "node-telegram-bot-api";
import Web3 from "web3";
// Load environment variables
dotenv.config();
async function main() {
const app = express();
const port = 3000;
app.use(bodyParser.json());
const botToken = process.env.BOT_TOKEN;
const bot = new TelegramBot(botToken, { polling: true });
const web3 = new Web3(
`https://rpc.testnet.rootstock.io/${process.env.RSK_API_KEY}`
);
const commands = {
start: "/start - Start the bot",
balance: "/balance <address> - Check wallet balance",
transactions: "/transactions <address> - Get transaction count",
latestblock: "/latestblock - Get latest block number",
pegIn: "/pegIn <amount> - Convert BTC to RBTC",
pegOut: "/pegOut <amount> - Convert RBTC to BTC",
rbtcPrice: "/rbtcPrice - Get current RBTC price",
gasprice: "/gasprice - Get current gas price",
convert: "/convert <weiAmount> - Convert WEI to ETH",
validate: "/validate <address> - Check if an address is valid",
help: "/help - Show this help message",
};
const sendHelpMessage = (chatId) => {
const helpMessage =
"You can use the following commands:\n" + Object.values(commands).join("\n");
bot.sendMessage(chatId, helpMessage);
};
bot.onText(/\/help/, (msg) => {
sendHelpMessage(msg.chat.id);
});
bot.onText(/\/start/, (msg) => {
bot.sendMessage(msg.chat.id, `Hello! Welcome to the RootstockBot.`);
});
// Handle /balance command
bot.onText(/\/balance (.+)/, async (msg, match) => {
const chatId = msg.chat.id;
const walletAddress = match[1];
try {
const balance = await web3.eth.getBalance(walletAddress);
bot.sendMessage(
chatId,
`Balance of ${walletAddress} is ${web3.utils.fromWei(balance, "ether")} RBTC.`
);
} catch (error) {
bot.sendMessage(chatId, "Error fetching balance. Try again.");
console.error(error);
}
});
// Handle /gasprice command
bot.onText(/\/gasprice/, async (msg) => {
const chatId = msg.chat.id;
try {
const gasPrice = await web3.eth.getGasPrice();
bot.sendMessage(
chatId,
`Current gas price: ${web3.utils.fromWei(gasPrice, "ether")} RBTC.`
);
} catch (error) {
bot.sendMessage(chatId, "Error fetching gas price.");
console.error(error);
}
});
// Handle /transactions command
bot.onText(/\/transactions (.+)/, async (msg, match) => {
const chatId = msg.chat.id;
const walletAddress = match[1];
try {
const transactionCount = await web3.eth.getTransactionCount(walletAddress);
bot.sendMessage(chatId, `${walletAddress} has ${transactionCount} transactions.`);
} catch (error) {
bot.sendMessage(chatId, "Error fetching transaction count.");
console.error(error);
}
});
// Handle /latestblock command
bot.onText(/\/latestblock/, async (msg) => {
try {
const latestBlock = await web3.eth.getBlockNumber();
bot.sendMessage(msg.chat.id, `Latest block number: ${latestBlock}.`);
} catch (error) {
bot.sendMessage(msg.chat.id, "Error fetching latest block.");
console.error(error);
}
});
// Handle /pegIn command
bot.onText(/\/pegIn (\d+)/, async (msg, match) => {
const chatId = msg.chat.id;
const amount = match[1];
// Provide instructions for peg-in
bot.sendMessage(chatId, `To peg in ${amount} BTC, send it to the designated Rootstock address. Once confirmed, the equivalent RBTC will be credited to your wallet.`);
// Implement peg-in logic if needed (this usually involves interacting with a specific contract)
});
// Handle /pegOut command
bot.onText(/\/pegOut (\d+)/, async (msg, match) => {
const chatId = msg.chat.id;
const amount = match[1];
// Provide instructions for peg-out
bot.sendMessage(chatId, `To peg out ${amount} RBTC, send it to the designated address. Once confirmed, the equivalent BTC will be credited to your wallet`);
// Implement peg-out logic if needed (this usually involves interacting with a specific contract)
});
// Handle /rbtcPrice command
bot.onText(/\/rbtcPrice/, async (msg) => {
const chatId = msg.chat.id;
try {
const response = await axios.get('https://api.coingecko.com/api/v3/simple/price?ids=rsk-smart-bitcoin&vs_currencies=usd', {
headers: {
'x-cg-demo-api-key': 'YOUR_API_KEY'
}
});
const rbtcPrice = response.data['rsk-smart-bitcoin'].usd;
bot.sendMessage(chatId, `Current RBTC price: $${rbtcPrice} USD.`);
} catch (error) {
bot.sendMessage(chatId, "Error fetching RBTC price.");
console.error(error);
}
});
// Handle /convert command
bot.onText(/\/convert (\d+)/, async (msg, match) => {
const chatId = msg.chat.id;
const weiAmount = match[1];
try {
const ethAmount = web3.utils.fromWei(weiAmount, "ether");
bot.sendMessage(chatId, `${weiAmount} WEI = ${ethAmount} ETH.`);
} catch (error) {
bot.sendMessage(chatId, "Error converting WEI to ETH.");
console.error(error);
}
});
// Handle /validate command
bot.onText(/\/validate (.+)/, async (msg, match) => {
const chatId = msg.chat.id;
const address = match[1];
if (web3.utils.isAddress(address)) {
bot.sendMessage(chatId, `✅ ${address} is a valid Ethereum/RSK address.`);
} else {
bot.sendMessage(chatId, `❌ ${address} is NOT a valid address.`);
}
});
// Handle unknown commands
bot.on("message", (msg) => {
const chatId = msg.chat.id;
if (!msg.text.startsWith("/")) return;
if (!Object.keys(commands).some((cmd) => msg.text.startsWith(`/${cmd}`))) {
bot.sendMessage(chatId, `❌ Unrecognized command.\n${Object.values(commands).join("\n")}`);
}
});
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
}
main().catch(console.error);
To run your bot, execute the following command in your terminal:
npm start
Step 3: Interacting with the RootstockDapp Bot
/help
: Displays all available commands for the bot.
/balance <address>
: Checks the available amount of RBTC at the specified address.
/transactions <address>
: Retrieves the total number of transactions for the specified address.
/latestblock
: Retrieves the latest block number on the Rootstock blockchain.
/pegIn <amount>
and /pegOut <amount>
: The /pegIn <amount>
command provides converts the specified amount of BTC to RBTC by sending BTC to the designated Rootstock peg-in address, while the /pegOut <amount>
command offers converting the specified amount of RBTC back to BTC by following the peg-out process.
/gasprice
: Retrieves the current gas price on the Rootstock network.
/convert
: Converts the specified 2 WEI amount to X ETH.
/validate <address>
: Checks whether the specified address is a valid Ethereum/RSK address.
If an unknown command is given, the bot will respond with an "Unrecognized command" message.
Why this bot is unique to Rootstock and its benefits over other EVM chains?
The Telegram bot designed for the Rootstock blockchain exemplifies the intersection of user-friendly technology and advanced blockchain capabilities. By providing real-time access to blockchain data, facilitating interactions with Bitcoin, and supporting DeFi applications, the bot enhances the overall user experience. Additionally, the general benefits of blockchain bots such as accessibility, automation, and scalability make them valuable tools in the broader blockchain ecosystem. As blockchain technology continues to evolve, the role of bots will likely expand, further bridging the gap between users and decentralized systems.
1. Key Features of our Rootstock Bot
While the bot is designed specifically for the Rootstock blockchain, it can also be adapted to work with other EVM-compatible chains. Here are some features that make the Rootstock bot unique:
Real-Time Data Access: The bot allows users to query real-time data from the Rootstock blockchain, such as wallet balances, transaction counts, and gas prices. This feature is crucial for users who want immediate insights into their blockchain activities.
User-Friendly Interface: By leveraging Telegram, the bot provides a familiar and accessible interface for users. This is particularly beneficial for those who may not be tech-savvy or familiar with blockchain technology.
Automated Responses: The bot can handle common queries automatically, reducing the need for human intervention. This feature enhances user experience by providing quick answers to frequently asked questions.
Command-Based Interaction: Users can interact with the bot using simple commands (e.g.,
/balance <address>
), making it easy to retrieve information without needing to understand the underlying blockchain mechanics.
2. Benefits of Using a Bot with Rootstock
The integration of a bot with the Rootstock blockchain offers several advantages:
Enhanced Accessibility: Users can access blockchain information directly from their Telegram app, eliminating the need to use complex block explorers or specialized wallets. This accessibility encourages broader adoption of blockchain technology.
Seamless Interaction with Bitcoin: Since Rootstock is built on the Bitcoin blockchain, the bot can facilitate interactions that leverage Bitcoin's security and liquidity. Users can check balances and perform transactions using their Bitcoin holdings in a straightforward manner.
Support for DeFi Applications: The bot can provide information related to decentralized finance (DeFi) applications built on Rootstock, allowing users to engage with various financial services directly from Telegram.
Community Engagement: The bot can serve as a bridge between users and the Rootstock community, providing updates, announcements, and support channels. This fosters a sense of community and encourages user participation.
3. General Benefits of Blockchain Bots
Beyond the specific features of the Rootstock bot, there are general benefits to using blockchain bots across various platforms:
24/7 Availability: Bots operate continuously, providing users with access to information and services at any time, which is particularly valuable in the global and decentralized nature of blockchain.
Reduced Complexity: Bots simplify interactions with blockchain networks, allowing users to perform tasks without needing to understand the technical details of blockchain operations.
Scalability: Bots can handle multiple user requests simultaneously, making them scalable solutions for providing information and services to a large number of users.
Integration with Other Services: Bots can be integrated with other APIs and services, allowing for enhanced functionality, such as price alerts, transaction notifications, and more.
Conclusion
This comprehensive guide provides everything needed to create a functional Rootstock blockchain bot. Remember to regularly update dependencies and monitor your bot's performance in production environments. The modular design allows for easy extension with additional features as needed. With this bot, users can easily access important blockchain information directly from Telegram, making it a valuable tool for anyone interacting with the Rootstock network.
If facing any errors, join Rootstock discord and ask under the respective channel.
Until then, dive deeper into Rootstock by exploring its official documentation. Keep experimenting, and happy coding!
Subscribe to my newsletter
Read articles from Pranav Konde directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
