Vuilder is a Solidity++ smart contract development kit designed for easy contract compilation, testing, and deployment in JavaScript. Refer to Vuilder Kit for details.
You will get the contract address after the step is complete. Next, we will lock 1000 VITE for the contract address to obtain Quota.
Rule of thumb: always supply your contract with quota. Unlike Ethereum, Vite's smart contract will consume quota to operate. Insufficient quota may cause the contract to hang up (i.e. stop processing transactions).
Alternatively, you can use the Vite Passport(opens new window) browser extension. Be sure to switch the network to Testnet after creating a wallet for this tutorial.
To view past coffee transactions, click "History" in the header to go to http://localhost:3000/history.
That covers all the major features of the dapp! Of course, the recipient does not receive a real cup of coffee, but this is a good starting point for cafes thinking about accepting orders through their website 😃
In the next section, we will look at the code to learn how the dapp is built.
In general, there are four steps in building a complete dapp: initializing the project, writing the smart contracts, using Vuilder(opens new window) to test/deploy the contracts, and developing a frontend to interact with the contracts.
Note: You should execute the command under /vite-express rather than /vite-express/contracts. Otherwise Vuilder will report the following message: Error: ENOENT: no such file or directory, open './contracts/Cafe.solpp'.
In test/vite.config.json, your settings should look like the following and not be modified due to the mnemonic phrase for the local network has been set up by default.
Copy
{"networks":{"local":{"http":"http://127.0.0.1:23456/","ws":"http://127.0.0.1:23457/","mnemonic":"record deliver increase organ subject whisper private tourist final athlete unit jacket arrow trick sweet chuckle direct print master post senior pluck whale taxi"}}}
Note: You can deploy the contract to the testnet or mainnet. For testnet the http endpoint is https://buidl.vite.net/gvite. For mainnet, it is https://node.vite.net/gvite.
The default frontend for Vite Express has many React components and helper files, This section will explain the files you will need to familiarize yourself with the most to start building on top of Vite Express.
The file structure of the frontend is as follows:
The frontend app is rendered with frontend/index.html and frontend/src/main.tsxfrontend/index.html:
The App component is the root of the app and wraps the entire app with the Provider component, a global context(opens new window) which acts as the global state that any child component can connect to. Before rendering anything, it first uses your browser's localStorage to determine the initial app state; this includes:
vcInstance - An instance of the VC class defined in frontend/src/utils/viteConnect.ts which helps you manage the Vite Wallet app connected via ViteConnect
vpAddress - The address of the Vite Passport wallet if it is connected
activeNetworkIndex - The index of the active network in networkList in frontend/src/utils/constants.ts
languageType - Initially only English, but you can add your own translations in the frontend/src/i18n folder
activeAddress - The address from the connected Vite Passport or App wallet (Both shouldn't be able to connect at the same time)
The Router component uses React Router v6(opens new window) to enable client-side routing. Every component used for a Route's element prop is put in the pages folder and all the Routes are wrapped with the PageContainer component.
The PageContainer component adds the header and footer to its children prop. It's also in the containers folder which means it's connected to the global state. To connect a component to the global state, simply wrap the component with the connecthigher-order component(opens new window).
Additional details about the Provider component include:
The initial app state can be optionally be set via the initialState prop
The onSetState prop is called every time the state changes (i.e. setState is called) with the new state and optional options arguments passed to setState.
The initial global state is set in App.tsx
Additional details about the connect higher-order component include:
All connected components are passed the global state fields and a setState function as props.
The setState method is used for mutating the global state.
The first parameter is an object and its properties will be shallow merged with the current global state.
e.g. setState({ networkType: network }) will update the all containers that use the networkType prop.
To deep merge, pass an optional second meta object argument to setState with { deepMerge: true }.
Copy
setState({ a: { b: 'c' } }, { deepMerge: true })
// { a: { d: 3 } } => { a: { d: 3, b: 'c' } }
Components that use connect are known as connected components or containers and go in the frontend/src/containers folder.
Vite Express has 3 theme settings: light, dark, and system (changes according to the theme of your computer's light/dark theme). This is achieved with Tailwind CSS' native dark mode support(opens new window). In theme.ts the initial theme is set based on localStorage.theme and in PageContainer.tsx, dark mode is toggled manually.
frontend/src/styles/theme.ts:
Copy
if (!localStorage.theme) {
localStorage.theme = 'system';
}
if (
localStorage.theme === 'dark' ||
(localStorage.theme === 'system' && prefersDarkTheme)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// does not exist on older browsers
if (window?.matchMedia('(prefers-color-scheme: dark)')?.addEventListener) {
window
?.matchMedia('(prefers-color-scheme: dark)')
?.addEventListener('change', (e) => {
if (localStorage.theme === 'system') {
if (e.matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
});
}
Throughout Vite Express, you will see i18n being passed as props to connected components. i18n is an object that contains all of the text for a particular transaction. By default, there is only English, but more languages can be added to the frontend/src/i18n folder. If you do this, make sure to add it as an option to the languages array where the first element in the nested array is the label, and the second is the filename without the .ts extension. From there, when the DropdownButton in PageContainer.tsx is used to select a new language, the global state's languageType will change, which triggers a useEffect to replace the current i18n translation with the new one.
The A component is used for both internal and external links. If you pass it a to prop, clicking the link will navigate to the route specified in Router.tsx. If you pass it an href prop, clicking it will open the URL in a new tab.
The Modal component is used for presenting components on top of the main app (e.g. when showing the QR code for ViteConnect or prompting the user to sign a transaction). To use it, you need to create a state variable (locally with useState or globally with connect, it doesn't matter) that determines if the modal is visible or not. Once you have the "switch" variable, use it to open and close the modal like in the AppHome component.
frontend/src/pages/AppHome.tsx:
Copy
const [promptTxConfirmation, promptTxConfirmationSet] = useState(false);
// ...
promptTxConfirmationSet(true); // In a button's onClick event that shows the modal
// ...
{!!promptTxConfirmation && (
<Modal onClose={() => promptTxConfirmationSet(false)}>
<p className="text-center text-lg font-semibold">
{vpAddress
? i18n.confirmTransactionOnVitePassport
: i18n.confirmTransactionOnYourViteWalletApp}
</p>
</Modal>
)}
The TextInput component is self-explanatory. It has many props for changing how it looks and behaves. However its most useful feature is validating their inputs when submitting forms. To use it, you need to create references for each TextInput and pass it to the corresponding _ref prop. To validate multiple TextInputs, pass them as an array to validateInputs which when called will return true if all the input refs are valid - else false.
The two props that alter how TextInputs are validated are optional (meaning it can be empty, else it is implicitly required) and getIssue where it is passed the current trimmed value and it can return a string if there are any errors.
The AppHome shows an example of this in action.
frontend/src/pages/AppHome.tsx:
Copy
const beneficiaryAddressRef = useRef<TextInputRefObject>();
const amountRef = useRef<TextInputRefObject>();
// ...
<TextInput
_ref={addressRef}
label={i18n.beneficiaryAddress}
initialValue={searchParams.get('address')}
getIssue={(v) => {
if (!wallet.isValidAddress(v)) {
return i18n.invalidAddress;
}
}}
/>
<TextInput
numeric
_ref={amountRef}
label={i18n.amount}
initialValue={searchParams.get('amount')}
getIssue={(v) => {
if (+v <= 0) {
return i18n.amountMustBePositive;
}
}}
/>
<button
className={`${
vcInstance ? 'bg-skin-medlight brightness-button' : 'bg-gray-400'
} h-8 px-3 rounded-md font-semibold text-white shadow`}
disabled={!vcInstance}
onClick={async () => {
if (validateInputs([beneficiaryAddressRef, amountRef])) {
// The inputs are valid according to their getIssue prop and inputs without the `optional` prop have a truthy input value.
// Do stuff with `addressRef.value` and `amountRef.value`
}
}}
>
{i18n.buyCoffee}
</button>
The Toast component renders a small ephemeral notification at the top of the screen. It should not be rendered more than once and by default it's already rendered in Router.tsx. To display a toast message, you must call setState({ toast: '<toast_message>' }) like in AppHome.tsx.
The ConnectWalletButton component will show a "Connect Wallet" button if a Vite Wallet app or Vite Passport wallet isn't connected. If either one of those wallets is connected, the connected address will be displayed instead.
If you disconnect from the frontend by logging out, the connected wallet (whether it is the app or extension) will also disconnect. If you disconnect from the frontend via the Vite Wallet app or Vite Passport, the frontend will log out for you (i.e. the address button changes back to the "Connect Wallet" button).
If a Vite Wallet app is connected and the frontend is refreshed or otherwise exited and opened within a few minutes, the ViteConnect session will persist via getValidVCSession in viteConnect.ts. Dapps connected with Vite Passport will persist indefinitely until the user or dapp programmatically disconnects.
frontend/src/utils/viteConnect.ts:
Copy
export class VC extends Connector {
// ...
// createSession and signAndSendTx are the only two methods of the VC class that are used externally
async createSession() {
await super.createSession();
return this.uri;
}
async signAndSendTx(params: object[]) {
return new Promise((resolve, reject) => {
this.sendCustomRequest({ method: 'vite_signAndSendTx', params })
.then((result: object) => {
this.saveSession();
resolve(result);
})
.catch((e: Error) => reject(e));
});
}
}
export function getValidVCSession() {
// This function returns a ViteConnect session stored in localStorage if it exists
}
export function initViteConnect(session: object) {
// The constructor of the VC class saves the newly created session in localStorage
return new VC({
session,
bridge: 'wss://biforst.vite.net',
});
}
Once a ViteConnect instance is created and contracts are configured, you can interact with a contract with the callContract method which is part of the global state and updated in Router.tsx. Calling the callContract method will return a Promise, so you may want to use it with async/await where you prompt the user to confirm the transaction before blocking the code execution with await.