A Starter Kit for dApps where users can create and manage multiple smart contracts
- π¦ lean vanilla smart contract factory π
- πͺ use-case flexibility π
- π§ mini tutorial π§
In order to go through the tutorial it helps if you're already familiar with the amazing scaffold-eth buidl tools. If you're not, for following this tutorial it is recommended that you're at least a web developer with some basic Solidity experience, as well as familiar with Typescript. For the vanilla JS version please visit here
π€ The tutorial below presents the essential aspects quite in detail.
If you're an absolute noob to web3, check out the Ethereum Speed Run.
Solidity & React are set up to
- create contracts
- browse created contracts
- interact with created contracts
π§ͺ Quickly experiment with Solidity using a frontend that adapts to your smart contract:
π Start with a basic master-detail UI, customize it for your needs
π Debug your contracts with a simil master-detail UI
This is based on the typescript repo of scaffold.eth. The directories that you'll use are:
packages/vite-app-ts/
packages/hardhat-ts/
Prerequisites: Node plus Yarn and Git
clone/fork π scaffold-eth:
git clone https://github.com/dvinubius/contract-factory-tutorial-typescript.git
install:
yarn
in a new terminal window, start a local hardhat node:
yarn chain
deploy your contracts:
yarn contracts:build
yarn deploy
in a new terminal window, start your frontend:
yarn start
π You need an RPC key for production deployments/Apps, create an Alchemy account and replace the value of ALCHEMY_KEY = xxx
in packages/react-app/src/constants.js
π Edit your smart contract YourContract.sol
in packages/hardhat/contracts
π Edit your frontend MainPage.tsx
in packages/react-app/src
πΌ Edit your deployment scripts in packages/hardhat/deploy
π± Open http://localhost:3000 to see the app
Whether you're a web3 noob or experienced dev, the following tutorial is a good way to
- get more familiar with scaffold-eth
- learn some ideas for design patterns
If you're in for the tutorial, you're in for a treat! π π€ Here's what we'll look at
- Explore the setup - what can a user do?
- Technicalities - how is it built so far?
- UX challenges - where can you take it from here?
π Create and track contracts that each have a "purpose" variable
In the "Your Contracts" tab, create a new contract. The dialog keeps the user informed. This is a common UX pattern beyond the generic tx status notifications.
πΊ Browse all contracts in a list :
<CreatedContractsUI/>
Your new contract should have appeared in the UI. Create a second contract. Observe how the list updates automatically as soon as the transaction is mined.
List items right now only contain data that was available at the moment when the contracts were created.
πΉ Interact with any particular contract in a detail view:
<YourContract/>
Click on a contract to enter the detailed view.
Click any button to change its purpose.
π Access controls are in place
Open a new browser window in incognito mode, go to localhost:3000.
Here you won't be able to change the purpose of existing contracts. In this incognito window you are someone else (notice the address at the top of the window). The current signer is not the owner of those contracts.
π The Debug UI enables raw interaction with the factory and any created contract instance.
π§ Check out the "Debug Contracts" tab.
- See what the public functions of YourContractFactory allow you to do. Do you find them useful?
- What else might be useful to have in there?
** π©βπ» π UX π π§βπ» Frontend Side Quest - Improve UX when setting the purpose **
Return to the UI where you have 2 buttons to set the purpose of a contract.
Issue: if you click any of the buttons, both show a spinner while the TX is pending.
Your challenge: find a way to obtain this instead
-
The core functionality of your app
-
Right now it only has a purpose that can be changed by the owner.
-
Creates instances of YourContract and keeps track of them all.
-
Kept as lean as possible.
The setup allows users to create their own YourContracts and control them independent from the factory contract.
As a starting point for developing dApps with this setup, we want loose coupling:
- keep created contracts unaware of the factory
- keep the factory unaware of what created contracts actually do
All our factory needs to know is the addresses of created contracts
We emit events on contract creation, so the frontend can easily retrieve a list of all.
We've included useful data in those events.
π©βπ» π UX π π§βπ» In a dApp based on a setup like ours, user-given individual contract names are probably a good feature to have.
We've adopted a simple and cheap solution: the user-given name is put in the creation event. If the name doesn't need to change over time this approach works fine.
This retrieval happens via a single RPC call made by the useEventListener
hook in MainPage.tsx
.
The retrieval is repeated on each block but can be configured much more specifically to suit your needs. Read more about it in the docs.
It's good to keep something like this in mind in order to have your app scale well when the UI is rich and lots of users are using it at the same time.
For contract state, like "purpose", contract owner, etc. the frontend uses the address of a particular YourContract intance address to read from the contract, which under the hood makes separate RPC calls.
This is what we do in <YourContract/>
At the moment you don't get strong typing and autocomplete when attempting to (read/write) interact with
YourContract
, like you do withYourContractFactory
. At least not out of the box.Strong typing is only available out-of-the box for contracts deployed by
yarn deploy
. In our factory setup you deploy the factory via a script, but allYourContract
instances are created by the factory contract when users interact with it.You can still obtain strong typing by doing the following:
include YourContract in the deployment script in
packages/hardhat-ts/deploy
. The code for that is commented out. Uncomment it. This will deploy one instance ofYourContract
when the deploy script runs.Uncomment the related code in
packages/vite-app-ts/src/config/contractConnectorConfig.ts
. You will easily identify that code πRun
yarn deploy --reset
You'll have an instance of
YourContract
deployed somewhere, but your App won't show it in any way. However, your IDE will now know you thatYourContract
has apurpose
, asetPurpose
etc.
Observe how yourContractRaw is now typed more precisely:
Don't forget to put the comments back in when you deploy your dApp to mainnet. Or you will **totally waste gas β½οΈ π° ** on that lonely YourContract instance!
This may be possible to solve more elegantly in the future, watch out for updates!
You may skip this section and tackle Challenge 1 below if you're eager to code some more. Just make sure to return here some time later.
Understanding this is crucial if you're serious about building factory pattern dApps, so you'll need to do it anyway. But no pressure right now π π§
π Notice the difference in working with
YourContractFactory
andYourContract
:The factory contract object is obtained neatly via a hook. Here is the pattern, in a simplified version
const ethersContext = useEthersContext(); const yourContractFactory = useAppContracts( 'YourContractFactory', ethersContext.chainId );
For the
yourContract
object we make use ofinjectableAbis
, which are configured to give you the abi forYourContract
. With that equipped, we create a "raw"BaseContract
which we then connect to a signer.Here is a simplified version of the code:
const yourContractRaw = new BaseContract( contractAddress, abi, provider ); const yourContract = yourContractRaw.connect(signer);
The useAppContracts() hook cannot be used to interact with contracts that were not deployed via
yarn deploy
.If you wonder how
injectableAbis
comes to know theYourContract
abi, it's due to the deployment setup of the hardhat project.π§ Notice the file
vite-app-ts/src/generated/injectable-abis/hardhat_non_deployed_contracts.json
This one is usually not present in scaffold-eth because we usually include all our contracts when we
yarn deploy
. Each one gets a fixed deployment address there.But in our factory setup, the
YourContract
instances are created on-chain. Only then they get their addresses, which are stored both in the factory contract state and in the contract creation events.
Lets show our users when and how purpose changes happen!
Find the Solidity code related to SetPurpose events. Uncomment it.
Redeploy with yarn deploy --reset
Find the React code that displays SetPurpose events in <YourContract/>
. It is commented out, uncomment it.
Create a new contract. Change its purpose.
Now, for any particular instance of YourContract, our app
- displays contract events
- displays contract state
- enables contract interaction
Our factory ensures that the user who creates a contract also becomes the owner
Without this code, the factory would remain the owner of all YourContract instances.
Suppose we wanted to display the owner of any contract in the master view. Probably your users want to easily identify the contracts they've created.
The owner can change over time, unlike the creator. We can't build this feature by using contract creation event data.
π€ How do we get the owners of all contracts?
In each <ContractItem />
, we apply the pattern from <YourContract/>
: we take the contract abi & address, we create a BaseContract object, so that we can read from that particular contract instance.
Go to ContractItem.tsx
and find the code that fetches owner data. Uncomment it.
Find the code that displays this data. Uncomment that.
Now you should see owner information in the contracts list of the master view.
π π€ Observe that this time we didn't connect any signer, since we only had to read from the contract. Also, we didn't specify a fourth argument to useContractReader. That one is with options on how the value should be updated. But we don't expect the owner to change while this React component is displayed, so we provide no explicit update options. When no updateOptions are provided, the default behaviour of the hook is to only update once every 100 blocks, which is acceptable in a situation like ours.
Find out how to useEventListener() such that it only updates if the owner actually changes. Perfect the code. Can you test if it works?
Owner addresses are quite hard to read. In the contracts list, let's mark items which belong to the current user so they may be identified more easily.
Go to the code inside the <ContractItem/>
component. Find the commented code which marks the item when the contract owner is the current user. Uncomment it. Do the required fixes to make these changes effective (you will have to remove / comment out some of the initial code in order to make Typescript happy)
You should now see contract items like this:
βοΈ Test the functionality by creating contracts from an incognito window. Compare the views of different users.
What if there were 100 contracts?
As soon as you receive the creation event data in
MainPage.tsx
, would you make a total of 100 requests for reading the owner of each contract within its<ContractItem />
component?
It's probably better if we retrieve the owner of a particular contract item only when the item is actually in view.
Here is a simple solution for that:
- This would improve the UX a lot, whether we display contract owners or not
- If you allow n contracts per page, only n calls to read the owner will be made at once.
π©βπ» π π§βπ» Allow users to filter contracts by name in the list view
- use an input field
- how do you combine this with the pagination feature?
π©βπ» π π§βπ» Allow users to filter contracts by only listing their own ones
- use a switch or checkbox "only mine"
- how do you combine this with the pagination feature?
A factory setup can quickly get very complex, especially if you want to provide good UX.
Your real-world project will probably need code design improvements in order to be able to scale well and be easy to use.
- good routing
- efficient data retrieval (RPC nodes)
- different empty states (waiting for data, data not available, no account connected)
- clean code
Some design patterns to help you grow can be found in this repo. It's not in typescript but still worth a dig into.
- master-detail UI pattern with shareable links to detail pages (routing with react router v6)
- a pattern on how to create a contract specific react context when opening a contract in the UI
- strategies to minimize number of RPC calls while using eth-hooks v2.
With the flexibility that eth-hooks V4 gives you in the current setup, you'd be able to optimize even easier.
Our approaches in solving UX challenges depend on many factors. If your project is going to have lots of complex data to retrieve, you'll probably also use a subgraph or other blockchain indexing tools. These are more capable than the useEventListener
hook we've used here. This would impact how you approach scaling your dApp.
There are many use cases for a setup similar to ours here. Take Uniswap:
- users create liquidity pools
- each liquidity pool is a separate contract
Sometimes the created contracts may be more tightly coupled to the factory - it depends on the use case: how much control over the contracts should a user have / should the factory keep?
** π§ββοΈ π§ββοΈ π§ββοΈ Advanced Contract Design Quest: Dig into the UniswapV3 Docs. Here the factory is indeed more tightly coupled to the created pools.
- Why, do you think, is that?
- How does Uniswap handle fees?
- pool owner fees?
- uniswap fees?
Documentation, tutorials, challenges, and many more resources, visit: docs.scaffoldeth.io
π Read the docs: https://docs.soliditylang.org
π Go through each topic from solidity by example editing YourContract.sol
in π scaffold-eth
π§ Learn the Solidity globals and units
Check out all the active branches, open issues, and join/fund the π° BuidlGuidl!
-
π« Extend the NFT example to make a "buyer mints" marketplace
-
βοΈ Learn how ecrecover works
-
π©βπ©βπ§βπ§ Build a multi-sig that uses off-chain signatures
-
βοΈ Learn how a simple DEX works
-
π¦ Ape into learning!
Join the telegram support chat π¬ to ask questions and find others building with π scaffold-eth!
π Please check out our Gitcoin grant too!