As a senior front-end engineer who has long been pessimistic about the traditional Internet industry's Web front-end development, I have been searching for a way to maximize the use of my existing knowledge and skills. I chose to transition to full-stack development in the Web3 field.
When I made this decision, I knew little about Web3, whether from the perspective of a practitioner or an ordinary user. I urgently needed a way to get started quickly!
By chance, I learned about OpenBuild's Web3 Front-end Bootcamp. Judging from the content introduction, it should meet my needs, so I signed up without hesitation—it's free, and there are even "Manis" to earn. Why hesitate?
The content of this article is the practical notes of the boot camp course, focusing on "How a pure novice in smart contracts with front-end development foundation can develop their first NFT market dApp". That is to say, it will cover task 3, task 4, and task 5.
Since I am a beginner transitioning to Web3, I don't understand many things. The following content represents my personal understanding. If there are any errors, omissions, or mistakes, please point them out.
I think the name "smart contract" comes from its business role, and for developers, it is just a software program that needs to be written in a certain programming language.
Therefore, to write Ethereum smart contracts, you need to learn and understand Solidity syntax, ERC, and on-chain interaction processes. Once you understand these, you can write the code correctly. The rest is deployment.
Learning Solidity
Experienced programmers can tell at a glance that Solidity is an object-oriented static type language. Although there are some unfamiliar keywords, it does not prevent me from seeing it as a "class" wrapped in a "contract".
Therefore, if you are familiar with typed, class-based programming languages such as TS and Java, you can quickly get a preliminary understanding of Solidity by establishing a mapping relationship.
The contract
keyword can be considered a domain-specific deformation of the class
keyword, more semantically expressing the concept of "contract". Thus, writing a contract is equivalent to writing a class.
State variables are used to store data within the contract, equivalent to class member variables, that is, class properties.
Functions can be defined both inside and outside the contract—the former is equivalent to class member functions, that is, class methods; the latter is ordinary functions, usually some utility functions.
Unlike TS and Java, in Solidity, the visibility identifiers for variables and functions are not at the front, and their positions are inconsistent, which is somewhat counterintuitive.
The semantics of private
and public
are the same as in other languages, but there is no protected
. Instead, there is internal
, and there is also an additional external
for functions that are only callable externally.
Function modifiers are equivalent to TS decorators or Java annotations and can be used for aspect-oriented programming (AOP). Both functions and function modifiers can be overridden by derived contracts.
The following types can all be regarded as objects in ES, but they are used in different scenarios:
- Structures (
struct
) are used to define entities; - Enumerations (
enum
) are collections of limited options; - Mappings (
mapping
) are infinite options.
Solidity supports multiple inheritance and function polymorphism, which can better combine and reuse; since contract development tends to be driven by ERC, the side effects of multiple inheritance should not be as severe as in other languages.
Given that Solidity is born for blockchain and the characteristics of blockchain itself and application scenarios, communicating with the outside world through events and rolling back previous operations when encountering errors are "must-haves", so the syntax supports event and error-related processing.
The usage of the require()
function is also a bit special to me. require(initialValue > 999, "Initial supply must be greater than 999.");
is equivalent to the following ES code in a concise and semantic way:
if (initialValue <= 999) {
throw new Error('Initial supply must be greater than 999.');
}
Understanding ERC
In Ethereum, "ERC" stands for "Ethereum Request for Comments", which is a type of EIP (Ethereum Improvement Proposal) that defines standards and conventions for smart contract applications.
Since Web3 promotes decentralization and openness, ensuring the interoperability of smart contract applications is a basic requirement, so ERC, as the standard in this regard, is very important.
The most basic ERCs in Ethereum smart contract application development are as follows:
- ERC-20—Fungible tokens, as the infrastructure of a financial system, such as virtual currencies, contribution points;
- ERC-721—Non-fungible tokens (NFTs), as the infrastructure of an identity system, such as medals, certificates, tickets.
In fact, ERC can be regarded as authoritative API documentation.
Writing Smart Contracts
When developing smart contract applications, you need to choose a framework to assist. It seems that Hardhat and Foundry are used more often—I chose the former because it is friendly to the JS technology stack, that is, it is friendly to people transitioning from front-end development.
In terms of IDE selection, many people will use Remix provided by the Ethereum official, while I continue to use VS Code, mainly to reduce learning costs as much as possible when just getting started.
If you are not familiar with Hardhat, you can follow the official tutorial step by step to selectively set up the running environment. In the generated directory structure, in addition to the hardhat.config.ts
configuration file, you basically only need to pay attention to 4 folders and their files:
-
contracts
—Smart contract source code; -
artifacts
—Compiled files generated byhardhat compile
; -
ignition
—For deploying smart contracts based on Hardhat Ignition; -
test
—Smart contract function test code.
In ignition
, compiled files will also be generated, but they are different from artifacts
and are bound to the target chain to be deployed, that is, they are generated in the folder of the chain ID to be deployed.
As the 3 tasks of the boot camp assignment involve ERC-20 tokens, ERC-721 tokens, and NFT markets, the first two token contracts can be based on the verified OpenZeppelin Contracts for extension.
My implementation code for the ERC-20 token RaiCoin is as follows:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract RaiCoin is ERC20("RaiCoin", "RAIC") {
constructor(uint256 initialValue) {
require(initialValue > 999, "Initial supply must be greater than 999.");
_mint(msg.sender, initialValue * 10 ** 2);
}
function decimals() public view virtual override returns (uint8) {
return 2;
}
}
It is best to mint a certain amount of tokens (usually a large number) at initialization and set the owner to your own account address. Otherwise, you will be prompted that there is no balance when conducting transactions later, which is more troublesome to handle.
The msg.sender
in the constructor()
is actually the account address that deploys the contract. If you deploy it with your own account address, the initial tokens will all go into your account.
Since my own ERC-20 token is just for fun and will not appreciate, you can consider overriding the decimals()
in OpenZeppelin to set the value smaller.
Below is the implementation code for the ERC-721 token RaiE:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract RaiE is ERC721 {
uint256 private _totalCount;
constructor() ERC721("RaiE", "RAIE") {
_totalCount++;
}
function mint() external returns (uint256) {
uint256 tokenId = _totalCount;
_safeMint(msg.sender, tokenId);
_totalCount++;
return tokenId;
}
}
Why did I only additionally implement a mint()
without any parameters, just simply issuing coins? Aren't NFTs supposed to have corresponding pictures? The specific reasons will be explained later.
These two token contracts are a piece of cake. You don't need to write much code yourself. The real thinking is mainly focused on the NFT market contract, such as—
Should the NFT list in the market be paginated?
If paginated, the delay when flipping pages will be more noticeable, and the user experience on the front end will not be good. But if not paginated, there will also be this problem when there are many NFTs.
Where should the NFT picture URL be stored? In the NFT contract or the market contract?
In theory, it should be stored in the NFT contract, but if so, when obtaining the NFT list, it will frequently access the NFT contract through external calls, affecting performance and user experience.
Should a list of "who owns which tokens" that can be externally obtained be maintained in the NFT contract?
If there is, the data is redundant compared to the market contract, making the NFT contract very bloated. If not, it is not possible to explicitly know which tokens there are and who they belong to.
It can be seen that relying solely on blockchain-related technologies to make a product-level application currently has great limitations, and the user experience will be very poor!
That is to say, the performance and experience of the product still need to be supported by the traditional application architecture, and the blockchain is only used as identity verification and a "backup" for some data.
Therefore, I temporarily gave up the way of thinking oriented towards making products and stopped worrying about whether it is reasonable or not, and turned to first meeting the requirements of the assignment—just having the relevant functions is fine.
In this way, the decision is very easy to make—just do it in a way that can complete the assignment more quickly! As a result, the above three doubts were quickly eliminated:
- The NFT list in the market is not paginated—there will only be a few NFTs;
- The NFT picture URL is stored in the market contract—the NFT contract is only used by its own market contract;
- The NFT contract does not maintain a list of token ownership—temporary operations can remember which account minted which token.
When implementing the NFT market RaiGallery, I found that only arrays can be traversed, and mapping
cannot, and arrays with a specified length when initialized cannot use .push()
to add elements, only indexes can be used:
contract RaiGallery {
struct NftItem { address seller; address nftContract; uint tokenId; string tokenUrl; uint256 price; uint256 listedAt; bool listing; }
struct NftIndex { address nftContract; uint tokenId; }
NftIndex[] private _allNfts;
function getAll() external view returns (NftItem[] memory) {
// The length of the array is specified when initialized
NftItem[] memory allItem = new NftItem[](_allNfts.length);
NftIndex memory nftIdx;
for (uint256 i = 0; i < _allNfts.length; i++) {
nftIdx = _allNfts[i];
// Here, using `allItem.push(_listedNfts[nftIdx.nftContract][nftIdx.tokenId])` will report an error
allItem[i] = _listedNfts[nftIdx.nftContract][nftIdx.tokenId];
}
return allItem;
}
}
Debugging Smart Contracts
After writing the smart contract source code, you need to write test code to go through it first, expose some basic problems, and solve them.
As mentioned above, in the Hardhat project, the test code is placed in the test
folder, and it is basically one file for each contract, of course, the reusable logic between different files can also be extracted into additional files, such as helper.ts
.
The test code is written based on the API of Mocha and Chai. Before really starting to test the contract functions, you need to deploy the contract to the local environment first, which can be the built-in hardhat
, or you can start a local node localhost
, I temporarily choose the former.
At this time, the deployment method can reuse the Hardhat Ignition module, but I haven't figured out how to use it yet, so I use the easier-to-understand loadFixture()
.
Testing is quite troublesome, and it feels like almost a day's time is spent on it, but in this process, I have a deeper understanding of how ERC-20 tokens, ERC-721 tokens, NFT markets, and users should interact, such as:
- If you directly use the contract instance to call the method, the caller is the contract itself. You need to use
contract instance.connect(some account)
and then call it to simulate the operation with the user; - The owner of the NFT needs to authorize all their NFTs to the NFT market through
.setApprovalForAll(market contract address, true)
before they can be listed for sale in the market.
I think the single-party testing of the smart contract is almost done, and it is time to deploy it to the local node for joint debugging with the front end, which will use the Hardhat Ignition module this time.
When I went to read the documentation to learn, I felt a bit abstruse and difficult to understand, and I wanted to sleep when I read it; but now when I look back, each module is actually describing how to initialize when deploying the corresponding contract of the module.
Hardhat Ignition supports sub-modules, which can be used through .useModule()
, and can be processed together with sub-modules when compiling and deploying modules, that is to say—
Assuming I have RaiCoin.ts
, RaiE.ts
, and RaiGallery.ts
three modules, among which RaiGallery.ts
needs the address returned by the deployment of RaiCoin.ts
when deploying, then RaiCoin.ts
can be used as a sub-module of RaiGallery.txt
:
import { buildModule } from '@nomicfoundation/hardhat-ignition/modules';
import RaiCoin from './RaiCoin';
export default buildModule('RaiGallery', m => {
const { coin } = m.useModule(RaiCoin);
const gallery = m.contract('RaiGallery', [coin]);
return { gallery };
});
In this way, RaiE.ts
is deployed separately, and when deploying RaiGallery.txt
, RaiCoin.txt
will be cascaded deployed, so only two deployment commands need to be executed.
Then, change the defaultNetwork
configuration item in hardhat.config.txt
to 'localhost'
, execute npx hardhat node
in the root directory of the Hardhat project to start the local node, and then open another terminal window to deploy the smart contract:
- Execute
npx hardhat ignition deploy ./ignition/modules/RaiE.txt
to deploy the ERC-721 token contract; - Execute
npx hardhat ignition deploy ./ignition/modules/RaiGallery.txt
to deploy the ERC-20 token contract and NFT market contract.
After all deployments are successful, the compiled contract-related files will be generated in the ignition/deployments/chain-31337
folder (31337
is the chain ID of the local node):
-
deployed_addresses.json
lists the contract addresses; - The JSON files in the
artifacts
folder contain the contract's ABI.
The above two key pieces of information need to be copied and pasted into the global shared variables of the front-end project for use in joint debugging.
Before starting joint debugging, you need to do two things in the MetaMask wallet:
- Add the Hardhat local node to the network, you can refer to the YouTube video How to Add a Local Test Network to Metamask;
- As shown on the official website, add the ERC-20 token contract address you are testing to facilitate viewing of account balances.
The third-party libraries and frameworks I depend on in the front-end part mainly include Vite, React, Ant Design Web3, and Wagmi. Since the front end is what I am familiar with, there is no experience to share, so I won't elaborate.
However, when developing the front-end part, there was one point that puzzled me for a while—
Although programmatically, a new NFT needs to be minted before it can be listed on the market for trading, the interface should be one-step, that is, after filling in the NFT-related information and clicking "OK", it is directly listed.
The assignment requires a two-step operation of minting first and then listing, which I think is a bit unreasonable, or the user experience is not good.
In the end, due to my unfamiliarity with Wagmi and really not thinking of an implementation plan, and eager to submit the assignment, I didn't continue to worry about it...😂😂😂
When debugging, if you encounter problems that block you, you can troubleshoot in the following steps:
- When listing NFTs for sale, you need to first call the
setApprovalForAll
of the NFT token contract to authorize the market contract and use the escrow market to transfer NFTs on your behalf; - Before sending the listing request, use viem or ethers'
parseUnits
to convert it into a number that conforms to thedecimals()
defined in your ERC-20 token contract (the default is18
); - Before purchasing NFTs, check the balance of your custom ERC-20 token in your wallet to avoid mistaking Ether (ETH) for the balance of your ERC-20 token;
- When purchasing NFTs, you need to first call the
approve
of your ERC-20 token contract to authorize the market contract and use the escrow market to transfer funds on your behalf.
The joint debugging is also over, finally, it is time for the last link—deploying to the Sepolia test network!
This requires Sepolia Ether, and the general way to obtain it is to drip it from those "faucets", and you can only get a little bit every day. Fortunately, @Mika-Lahtinen provided a PoW method, see @zer0fire's note 🚀 Ultra-simple faucet tutorial - No transaction records or account balance required.
At this time, turn your attention back to the Hardhat project, open the hardhat.config.txt
file, temporarily change the defaultNetwork
to 'sepolia'
, and add a sepolia
in networks
:
const config: HardhatUserConfig = {
defaultNetwork: 'sepolia', // Temporarily change the default network to this
networks: {
sepolia: {
url: 'Your Sepolia endpoint URL',
accounts: ['Your wallet account private key'],
},
},
};
The Sepolia endpoint can be obtained by registering an account on Infura or Alchemy.
Then, follow the process of deploying to the local node mentioned above again, and after verifying the functions of the test network environment in the front end, you can submit the assignment!
Conclusion
I have open-sourced all the code related to the NFT market dApp in ourai/my-first-nft-market
, and plan to solve the points discussed above in the future, and make it a benchmark for this kind of demo.
Since the Sepolia contract address has been configured inside, you can directly run it locally and operate it. Welcome to refer to, discuss, and point out.
Top comments (0)