In the part 1 of the series, we had set up SDKs and tools needed for development of our DApps-like Rest API. The second part introduces a sample contract conforms with ERC-20 standard, which will be used by our web api to interact with blockchain.
Hightlights
Architecture
The overall architecture of our project would look like below.
The advantage of adding a back-end layer is that your front end Dapps is decoupled from the smart contract.
Most demonstrations of Dapps omit the API layer, the web app directly interacts with the blockchain and handles all calls to smart contract via web3js or similar frameworks. Being structured this way the Dapps is tightly coupled with the contract underneath and might end up in a spaghetti architecture where front end and back end code get messed up.
In more complex settings where there are a plethora of contracts to maintain we will need to orchestrate our services differently. If a smart contract is considered a client service, you might want to spin up or tear down new services without impacting normal function of your business. That is where microservice architecture comes in handy. Each microservice serves a smart contract and can be totally independent of other microservices. The API back-end can be hosted on different computers to Ethereum nodes (thanks to RPC comunication protocol). Therefore we achieve three separated layers for our system.
Furthermore, our Restful APIs are also independent of each other and can be hosted on separated servers. This helps you regroup services by business domains, thus achieve a modular but cohesive architecture where miroservices are bounded business context. This is also known as Domain-Driven-Design.
The architecture above somewhat resonates with Hyperledger Sawtooth design. However in Sawtooth, RestApi is tighten up to blockchain node.
With Asp.Net Core and Nethereum, the above system could be easily set up. In our demo project, we only have one smart contract corresponding to one microservice, and there is no front-end client.
1. Create .NET solution
1.1/ Create Solution
Start by create a project root for your solution. Here my solution folder is named “GamerSmartToken”.
With Visual Studio, create a new solution is trivial.
With dotnet CLI (works for Powershell or Windows Command as well as for Linux bash):
dotnet new sln
The solution file will be created and named after your folder.
1.2/ Create Asp.net Core web api project
With Visual Studio you just have to right click on to the solution and add new project.
With dotnet CLI:
dotnet new webapi --name WebApi dotnet sln add WebApi/WebApi.csproj
-
- The first command create a new Asp.Net Core web api project named “WebApi” from template.
- The second command add your WebApi project to the solution.
1.3/ Move smart contract folder into your solution
Place the smart contract folder with all of its content we have created from part 2 under the same solution for better organization.
I also move TestChain folder into the solution but this is optional.
1.4/ Create classlib projects to refactor your code
In the same solution, I have created two class libraries: ContractInterface.Common and ContractInterface.ERC20 which contains common methods used to make RPC calls to my Ethereum client. Methods in Common library, for exemple deploying or signing contract, can be reused across different web api which interface with different smart contracts. On the contrary, ERC20 library contains only RPC function specific to ERC20 compliant.
These libraries can be packaged and published to your Nuget repository in order to be easily referenced across different projects.
The rest of the tutorial will refer to this solution structure.
2. Install Nuget packages
We’ll need to add Nethereum package to our three projects: WebApi, ContractInterface.Common, ContractInterface.ERC20.
Tips: ideally I should have wrapped all my types in WebAPI project to reference only to my classlibs, so that my WebApi would never has to interface directly with Nethereum.
From Visual Studio, right click References on your project -> Manage Nuget packages.
In CLI, from solution root folder:
dotnet add WebApi/WebApi.csproj package Nethereum.Web3
dotnet add ContractInterface.Common/ContractInterface.Common.csproj package Nethereum.Web3
dotnet add ContractInterface.ERC20/ContractInterface.ERC20.csproj package Nethereum.Web3
3. Project Web Api
NOTE: before start developing, remember to start our test chain and geth node in order to test. See Test section.
3.1/ Add references:
We have to add references to our classlib in our project Web API beforehand.
From project folder:
dotnet add reference ../ContractInterface.Common/ContractInterface.Common.csproj ../ContractInterface.ERC20/ContractInterface.ERC20.csproj
3.2/ Configure dependency injections in Startup class
Dependency injection is a nice way to separate usage from creation of objects. By delegating the creation of objects to dependency controller, you no longer have to worry about properly properly instantiating and managing object’s lifetime. If you wish to know more about this design patters, please refer to links at the end of the article.
In Asp.Net Core, implementing dependency injection is trivial. All you have to do is registering your types to service container in Startup.cs.
As you can see in my Startup.cs, I have configured my ContractFacade, ContractOperation, AccountService and ContractService as services to be manage by the container. Each of these services require different life cycles and thus the service container offers three options to free you from headache:
-
- AddSingleton: created once per app instance
-
- AddScoped: created once per web request
- AddTransient: created each time requested
You also have the choice to register either your concrete classes or interfaces with their implementations. The advantage of registering interfaces is that you can change your implementation at any moment without worrying about dependencies breaking your code. Thus each time your service interface is requested by consumer classes, the service container will look for registered implementation and resolves the dependencies.
3.3/ Configure Authentication service
Our application needs an authentication/authorization scheme in order to verify user’s identity.
Asp.Net Core provides authentication service as a very neat way to implement login schema in your application. Using it is as simple as registering a dependency injection in Startup.cs.
In Startup class, I have called AddAuthentication from ConfigureServices method in order to configure a login logic using Json Web Token (JWT).
By JWT standards, when user sends an authentication request to server, if credentials match the server will response with a token to authorize user’s session. This token is included in each of subsequent requests in order to maintain login session until expires. The token must be digitally signed by server so that it cannot be tampered with. And also remember to give it an expiration to minimize consequent in case of being intercepted, because any one with the token can use it to impersonate you and send requests to the server.
Then you have to add the service into app’s middle-ware pipeline in Configure method, above UseMvc().
In AccountService class, you have to configure Authenticate method in a way that it verifies given credentials and issues new token when the verification passes correctly.
Look into Authenticate method, I have verified user’s ethereum address and password against two hard-coded accounts. The token issued expires within one hour, and contains a claim of user’s identity. The token is hashed using symmetric keyed hash algorithm HmacSha256 on the payload, header and secret key. This is tamper proof because only the server knows its secret key used against hash function, thus only the server can provide a correct hash for the token. If the token has been changed a bit when returned to server, it will be rejected because the hash does not match.
The SymmetricSecurityKey used to calculate hash for the token is created using a provided passphrase configure in appsettings.Development.json.
Tips: in production, you should not by any means expose your passphrase as above. Please refer here and here for more guidance on how to protect your cryptographic materials.
To retrieve the key from appsettings, I use GetSection method from IConfiguration service (please refer to AccountService.cs image above).
In your controllers, so as to activate authorization scheme, apply [Authorize] attribute to each controller. If you want to exclude one method from authorization scheme, use [AllowAnonymous].
So to recap:
-
- In Startup.cs: 1/ configure Authentication service, token validation scheme and the type and characteristics of token to be accepted for validation. 2/ Activate authentication middle-ware.
-
- In AccountService.cs : configure the issue of authentication token by verifying user’s credentials.
- In API controllers: apply authorization scheme if needed.
Here I used the bare minimum of Identity service provided with dotnet core to verify user’s ethereum address and password. You can very much improve it by implementing a much more complex scheme such as using Entity Framework to configure a database, or associating user’s identity with their Ethereum transaction account (similar to a wallet).
3.4/ Contract Controller
ContractController.cs contains all methods related to our smart contract such as deploying contract and retrieving information specific to contract’s characteristics.
Services:
-
- IConfiguration: configuration service of Asp.net core to retrieve information from appsetting.json
-
- IContractFacade: type of ContractInterface.Common which interfaces with Nethereum to operate upon any smart contracts.
-
- IContractOperation: type of ContractInterface.ERC20 which interfaces with Nethereum to operate upon ERC20 contract.
-
- IAccountService: service specific to our Web Api which manages authentication and ethereum accounts to make transactions.
-
- ContractService: service specific to our Web Api which manage operations on contracts.
- ILogger: logging service of Asp.Net Core
Deploying contract:
There are two deployment methods provided in the controller. Deploy() allows deploying any type of ethereum smart contracts to blockchain, and DeployDefault() helps deploy our previously created smart contract without a request body (mostly used for test).
Tips: I use wrapper types as inputs for request methods because the Asp.Net controllers implicitly derive inputs from request body (the same as using [FromBody] attribute), and it can only deserialize one input per request body.
Routing:
To retrieve information related to a particular contract, I add contract address in the route for request methods. This allows getting information of any contracts ERC20 contract given its address.
I don’t place contract address to controller route because the controller contains methods which operate on new contracts that haven’t yet existed on blockchain.
Exception handling:
Generally it is not a good practice to implement try-catch blocks in controllers because it is exhausting ! You should instead configure a middle-ware to globally handle exception.
3.5/ People Controller:
PeopleController.cs contains request methods related to operations on holders and spenders of our ERC20 contract such as adding, removing people, inquire for their balance or status, and transfer tokens.
All methods in the controller operates on an existing contract, that’s why I place contract address in the controller route.
3.6/ Account Service:
The account service contains methods for authentication and to manage user’s ethereum accounts. All transactions that change contract’s state in Ethereum need to be signed by an unlocked account. Nethereum provides ManagedAccount as a wrapper type for Ethereum account, which manages unlocking scheme.
4. Using Nethereum to interact with smart contract
The two class libraries use Nethereum to make RPC calls to our smart contract.
To make function calls, what we need:
-
- Contract: Nethereum’s wrapper type for Ethereum smart contract. Contract object is deserialized from abi by Newtonsoft, hence we need a way to retrieve the compiled contract’s abi. This is the reason why I organized our previously created smart contract into a folder (SmartContracts) under the solution root. By doing so, you can easily create another smart contract and web api for it. A cleaner way to do this is using Entity Framework core to create a database for your project.
-
- Sender’s ethereum address
-
- Function name
-
- Web3 object: I created a wrapper to retrieve transaction receipt using Web3 object and transaction hash. The built in funtion “SendTransactionAndWaitForReceiptAsync” doesn’t work as expected.
- Function inputs: in the same order of appearance as of smart contract.
The above GenericTransaction serves as base function to interact with smart contract. To call a function from our contract:
Voilà. Simple as that.
5. Test APIs
In order to test our API we need to start our Test chain and Geth node. Open TestChain folder in terminal and type:
$ ./startgeth.sh
I exported all my tests into a json file placed under Test folder. You can import it into Postman to serve as a base for your tests. The first test must be authentication using one of the two accounts provided from AccountService.cs. Next remember to change authorization token or deactivate the service before testing.
The full project is open to access on Bitbucket.
6. Hosting
Hosting option really depends on how much decentralized you want your system to be. Front end dapps and back end api can be hosted in similar ways.
Using HTTP web server
This is really the centralized way to host your services. The web server communicates with a remote blockchain node and it becomes the single entry point to serve your client apps. If you really want democracy, this is a no-go. However, it is can be a very efficient choice depending on the size and nature of your business. If your business is in legal services, you might want have a set of partners to maintain the blockchain (or a distributed ledger). Your clients on the other hand might not care about the state of your ledgers and they are not supposed to. The web server is therefore the spot to connect the outside world with your closed-circled maintained blockchain.
Using IPFS (interplanetary file system)
If you want your system to be 100% distributed, IPFS is the choice. IPFS is a peer to peer distributed file system similar to torrent. Each machine in the IPFS network is a peer (also a node), and serves as both client (leech) and host (seeder). Files are served from peers (or nodes) in the network. Your Dapps (front end and back end) can be packaged into files and distributed via IPFS. Just like blockchain, once distributed your files can no longer be changed because each is digitally signed and uniquely identified in a cryptographic file structure (Merkle DAG).
More about IPFS here.
————————————————
Other parts of the story:
————————————————
External links:
Hi to every , as I am in fact keen of reading this webpage’s post to be updated regularly.
It consists of fastidious material.
Good answers in return of this difficulty with firm arguments and telling the whole thing about that.|
Pretty nice post. I just stumbled upon your weblog and wanted to mention that I’ve really loved browsing your weblog posts. In any case I will be subscribing to your feed and I am hoping you write again soon!|
Fantastic beat ! I wish to apprentice even as you amend your web site, how could i subscribe for a weblog website? The account aided me a applicable deal. I have been tiny bit familiar of this your broadcast provided vibrant clear idea|
My brother recommended I may like this website. He was once entirely right. This publish actually made my day. You can not believe just how so much time I had spent for this info! Thanks!|
I’m really inspired along with your writing talents as smartly as with the format in your blog. Is that this a paid subject matter or did you modify it yourself? Either way keep up the nice quality writing, it is uncommon to look a great blog like this one nowadays..
thank you ! These are not sponsored content, I’m writing out of my interest for the matter 🙂
Heya excellent website! Does running a blog similar to this require a great deal of work? I’ve virtually no understanding of coding however I was hoping to start my own blog soon. Anyways, should you have any suggestions or techniques for new blog owners please share. I understand this is off topic nevertheless I just wanted to ask. Appreciate it!|
This is very interesting, You’re a very professional blogger. I’ve joined your rss feed and look ahead to in search of more of your magnificent post. Additionally, I have shared your website in my social networks|
Pretty great post. I just stumbled upon your blog and wanted to say that I’ve really loved browsing your weblog posts. In any case I’ll be subscribing to your rss feed and I hope you write again very soon!
This is really interesting, You are a very skilled blogger.
I have joined your feed and look forward to seeking more of your great post.
Also, I have shared your website in my social networks!
I am truly grateful to the owner of this site who has shared this enormous piece of writing at here.
If some one desires expert view on the topic of blogging
afterward i suggest him/her to pay a visit this website, Keep up
the good job.
You are so awesome! I don’t suppose I have read something like this before.
So wonderful to find another person with a few unique
thoughts on this topic. Seriously.. many thanks for starting this
up. This web site is something that is needed on the web,
someone with some originality!
Hiya! I know this is kinda off topic however I’d
figured I’d ask. Would you be interested in exchanging
links or maybe guest authoring a blog post or vice-versa?
My blog discusses a lot of the same topics as yours and I believe we could greatly benefit from each other.
If you happen to be interested feel free to send
me an email. I look forward to hearing from you! Wonderful blog by the way!
http://onlinecasinounion.us.com
Hi everyone, it’s my first pay a visit at this web site, and paragraph is truly
fruitful in support of me, keep up posting these types of content.
Why viewers still make use of to read news papers when in this technological globe all is presented on net?
Definitely believe that which you said. Your favorite reason appeared to be on the internet the simplest thing to be aware of.
I say to you, I definitely get annoyed while people think about worries that they just don’t know about.
You managed to hit the nail upon the top and defined out the whole thing without having side-effects ,
people could take a signal. Will probably be back to get more.
Thanks
Hello this is kinda of off topic but I was wondering if blogs use WYSIWYG editors or if you have to manually code with HTML.
I’m starting a blog soon but have no coding
experience so I wanted to get guidance from someone with experience.
Any help would be greatly appreciated!
I’ve learn a few excellent stuff here. Certainly price bookmarking for revisiting.
I surprise how much attempt you put to create this sort of excellent informative web site.
My programmer is trying to persuade me to move to .net from PHP.
I have always disliked the idea because of the expenses.
But he’s tryiong none the less. I’ve been using Movable-type on several websites for about a year and
am concerned about switching to another platform.
I have heard excellent things about blogengine.net.
Is there a way I can import all my wordpress posts into it?
Any help would be really appreciated!
Hi! Thanks for visiting ! Yes moving to .NET is certainly more costly. If your website is light any simple it doesn’t worth the hassle moving to .NET.
I love it, I read it often and you’re always coming out with some
great stuff. I shared this on my facebook and my followers love it!
Keep up the great work.
Hello, yes this piece of writing is really nice and I have learned lot of things from
it regarding blogging. thanks.
These are truly impressive ideas in regarding blogging.
You have touched some nice things here. Any way keep up wrinting.
Hello are using WordPress for your blog platform? I’m new to the
blog world but I’m trying to get started and set
up my own. Do you require any html coding knowledge to make your own blog?
Any help would be greatly appreciated!
Can I just say what a relief to uncover someone that really knows what
they’re talking about on the net. You definitely understand how to bring a problem
to light and make it important. More people really need to
read this and understand this side of the story.
It’s surprising you are not more popular since you definitely possess the gift.
Nice blog right here! Additionally your website quite a bit up fast!
What web host are you the use of? Can I get your affiliate
link in your host? I wish my site loaded up as fast as yours lol
I’m really inspired together with your writing talents and also with the structure for your
blog. Is that this a paid subject matter or did you customize it your self?
Either way keep up the excellent quality writing, it’s rare to peer a
great weblog like this one nowadays..
Hi there would you mind sharing which blog platform you’re using?
I’m looking to start my own blog in the near future but I’m having a
hard time deciding between BlogEngine/Wordpress/B2evolution and
Drupal. The reason I ask is because your design and
style seems different then most blogs and I’m looking for something unique.
P.S Apologies for getting off-topic but I had to ask!
I have learn a few just right stuff here. Certainly value bookmarking
for revisiting. I wonder how much effort you put to
make such a great informative web site.
I’m really loving the theme/design of your web site. Do you ever run into any internet browser compatibility issues?
A small number of my blog audience have complained about my site not working correctly
in Explorer but looks great in Safari. Do you have any suggestions to help fix this problem?
Informative article, totally what I needed.
I wanted to thank you for this very good read!! I definitely loved every little bit
of it. I have you saved as a favorite to look at new things you post…
Hello my loved one! I wish to say that this
article is amazing, great written and include almost all significant infos.
I’d like to see extra posts like this .
If you are going for most excellent contents like I do,
simply go to see this website all the time for the
reason that it offers quality contents, thanks
Hurrah! After all I got a weblog from where I can actually get
helpful facts concerning my study and knowledge.
Great information. Lucky me I ran across your website by accident (stumbleupon).
I’ve saved it for later!
Nice post. I used to be checking constantly this blog and I’m impressed!
Extremely helpful info specially the remaining section 🙂 I deal
with such info a lot. I used to be looking for this particular information for a very long time.
Thank you and good luck.
First off I would like to say great blog! I had a quick question that
I’d like to ask if you do not mind. I was curious to find out how you center yourself and
clear your thoughts prior to writing. I’ve had a hard time clearing my mind in getting
my ideas out. I do take pleasure in writing however it just seems
like the first 10 to 15 minutes are usually wasted just
trying to figure out how to begin. Any recommendations
or hints? Thank you!
This is very interesting, You’re a very skilled blogger. I’ve joined your rss feed and look forward to seeking more of
your wonderful post. Also, I have shared your site in my social networks!
certainly like your website but you have to test the spelling on several of your
posts. Many of them are rife with spelling problems and I to find it very bothersome to inform the reality however I’ll certainly come back again.
Hello there, You’ve done a great job. I’ll definitely digg it and personally suggest
to my friends. I am confident they will be benefited from this
site.
I’m truly enjoying the design and layout of your website.
It’s a very easy on the eyes which makes it much more enjoyable for me
to come here and visit more often. Did you hire out a designer to create your theme?
Exceptional work!
I like what you guys tend to be up too. Such clever work and
reporting! Keep up the amazing works guys I’ve included
you guys to my blogroll.
obviously like your web site however you need to take a look at the spelling on quite a few of your posts.
Several of them are rife with spelling issues and I
to find it very bothersome to tell the reality however I’ll definitely come back again.
Have you ever considered about including a little bit more
than just your articles? I mean, what you say is fundamental and all.
But think about if you added some great pictures or videos to give your posts more, “pop”!
Your content is excellent but with pics and clips, this blog could undeniably be one of the greatest in its niche.
Very good blog!
Pretty nice post. I just stumbled upon your weblog and wished to say that I have really loved
browsing your weblog posts. After all I’ll be subscribing on your feed and
I hope you write again very soon!
What’s up, I desire to subscribe for this web site to get latest updates, therefore
where can i do it please assist.
I believe this is among the so much significant information for me.
And i’m satisfied reading your article. But want to observation on few common issues, The site style is wonderful,
the articles is actually great : D. Just right process, cheers
Howdy! I understand this is kind of off-topic however I had to ask.
Does operating a well-established website like
yours require a massive amount work? I am completely new to operating
a blog but I do write in my journal every day. I’d like to start a
blog so I will be able to share my own experience and
views online. Please let me know if you have any kind of ideas or tips for new aspiring blog owners.
Thankyou!
Does your blog have a contact page? I’m having problems locating it but, I’d like
to shoot you an email. I’ve got some suggestions for your blog you
might be interested in hearing. Either way, great site and I look
forward to seeing it develop over time.
It’s the best time to make some plans for the long run and it’s time to be happy.
I have learn this post and if I could I desire to recommend you some fascinating issues
or suggestions. Maybe you can write next articles regarding this article.
I wish to read even more things about it!
Great article! This is the type of information that should be shared around the internet.
Disgrace on the seek engines for now not positioning this put up upper!
Come on over and seek advice from my site .
Thanks =)
Do you mind if I quote a couple of your articles as long
as I provide credit and sources back to your weblog?
My blog is in the exact same niche as yours and my visitors
would truly benefit from a lot of the information you present here.
Please let me know if this alright with you.
Thanks!
Thank you for any other great article. The place else may anyone get that
type of information in such a perfect method of writing?
I have a presentation subsequent week, and I’m at the search for such information.
It is appropriate time to make some plans for the future and it is time to be happy.
I’ve read this post and if I could I desire to suggest you some interesting things or suggestions.
Maybe you could write next articles referring to this article.
I desire to read even more things about it!
Simply wish to say your article is as amazing. The clarity on your publish is just great and i can think you are
an expert in this subject. Fine along with your permission let
me to grasp your feed to keep up to date with imminent post.
Thank you a million and please keep up the rewarding work.
Admiring the hard work you put into your blog and in depth
information you offer. It’s nice to come across a blog every
once in a while that isn’t the same out of date rehashed
material. Excellent read! I’ve bookmarked your site
and I’m adding your RSS feeds to my Google account.
I like the valuable information you supply in your articles.
I’ll bookmark your blog and take a look at again right here frequently.
I’m reasonably certain I’ll be told many new stuff proper here!
Best of luck for the next!
After I originally commented I seem to have clicked on the -Notify me
when new comments are added- checkbox and now each time a comment is added I get four emails with the exact same comment.
Is there a means you are able to remove me from that service?
Thanks!
Interesting blog! Is your theme custom made or did you download it from somewhere?
A theme like yours with a few simple adjustements would really make my
blog shine. Please let me know where you got your design. With
thanks
I do not know if it’s just me or if perhaps everyone else encountering
problems with your site. It appears as if some of the text on your content are running off the screen. Can someone
else please comment and let me know if this
is happening to them as well? This might be a problem with my internet browser because I’ve had
this happen before. Appreciate it
I really like what you guys are usually up too. This type
of clever work and exposure! Keep up the excellent works guys I’ve included you guys to my personal
blogroll.
I know this if off topic but I’m looking into starting my own weblog and was curious what all is needed to get
setup? I’m assuming having a blog like yours would cost
a pretty penny? I’m not very web smart so I’m not 100% positive.
Any tips or advice would be greatly appreciated. Thank you
Very nice post. I simply stumbled upon your blog and wished to
mention that I’ve really enjoyed browsing your blog
posts. In any case I will be subscribing in your rss feed and I hope you write
once more very soon!
Hello There. I discovered your blog using msn. This is a really neatly
written article. I will make sure to bookmark it and return to learn more of your useful information. Thanks
for the post. I will definitely comeback.
Thanks for your personal marvelous posting! I seriously enjoyed reading it, you are a great author.
I will remember to bookmark your blog and will often come back very soon. I want to encourage you continue your great
job, have a nice morning!
Hello, I enjoy reading all of your article. I wanted to write a little comment to support you.
Nice post. I used to be checking continuously this weblog
and I am inspired! Very helpful info specially the last
section 🙂 I maintain such information a lot. I used to be looking for this
particular info for a long time. Thank you and best of luck.
Please let me know if you’re looking for a writer for your weblog.
You have some really great articles and I feel I would be a good asset.
If you ever want to take some of the load off, I’d love
to write some articles for your blog in exchange for
a link back to mine. Please shoot me an e-mail if interested.
Many thanks!
Wonderful website you have here but I was wondering if
you knew of any community forums that cover the same topics talked
about here? I’d really love to be a part of online community where I can get advice from other experienced individuals that
share the same interest. If you have any recommendations, please let me know.
Thanks!
First off I want to say wonderful blog! I had a quick question in which I’d like to
ask if you don’t mind. I was curious to find out how you center yourself and clear your head before writing.
I’ve had a hard time clearing my mind in getting my thoughts out.
I truly do take pleasure in writing however it just seems
like the first 10 to 15 minutes are lost simply just
trying to figure out how to begin. Any ideas or hints?
Many thanks!
Hello friends, its enormous piece of writing about tutoringand completely explained, keep it up all the time.
Thank you for any other excellent post. Where else may anyone get that kind of information in such an ideal manner of writing?
I’ve a presentation next week, and I’m at the look for
such info.
Spot on with this write-up, I truly believe this amazing site
needs far more attention. I’ll probably be
returning to see more, thanks for the information!
Thank you for using your platform to spread positivity and make a difference in the world.
Your blog is a true gem! The content is both informative and engaging, providing valuable insights on various topics. Keep up the excellent work and continue to inspire your readers
Exceptional blog! Informative and engaging content. Well done.