Introducing Olive Oil Trust: front end
This post is part of a series of posts regarding Olive Oil Trust:
- Introducing Olive Oil Trust
- Introducing Olive Oil Trust: smart contracts
- Introducing Olive Oil Trust: subgraph
- Introducing Olive Oil Trust: front end
Developing a user interface that simplifies our interactions with the Olive Oil Trust smart contracts is key to increasing the activity in the supply chain and to improving the visibility of its members.
This app must be able to request relevant data to the subgraph in a per use case scenario and provide the user with them.
In this post, I will be introducing a Next.js application that gives support to members and customers in Olive Oil Trust, and reduces the complexity of all the tasks at hand.
In this article
Approach
The user interface that I introduce in this post is essentially a tool to interact with the Olive Oil Trust contracts in the blockchain, and to collect and represent data from the subgraph.
This interface has two main sections: management
and explore
.
On one hand, the management
section gathers all the pages whose purpose is to interact with the contracts through the use cases that the contracts implement, for instance, create a token type, mint a batch of tokens, etc.
Given that an account is necessary to sign transactions in the blockchain, all the paths in /management
are restricted to logged in users.
Furthermore, most of these routes are restricted to members in Olive Oil Trust who fill a specific role.
For instance, it makes no sense that a bottling plant is given the option to certify tokens, or a certifier to mint tokens.
On the other hand, the explore
section gathers all the pages whose purpose is to display information about the main entities in Olive Oil Trust (certificates, escrows, tokens and token types) of all users.
This section does not require a login process; however, if you are logged in you can also interact with escrows in /explore/escrows
(make a payment, revert or close an escrow) to ease the discovery and acquisition of new tokens.
There are also dynamic paths in the explore
section to show unique pages of all the tokens, token types and certificates.
For example, /explore/tokens
will show a list of token cards but /explore/tokens/0x84641...
will show the main data about the batch of tokens with the id "0x84641...".
A page URL of a token could be encoded into a QR code and printed in the final product label so that an end customer can navigate to it and check the product data, including the trace of that product to its origin.
I have chosen Next.js to develop this app because it allows different data fetching strategies to be used per page in a simple manner, plus many other things: nested routing, built-in optimizations, etc.
My implementation is based on a hexagonal architecture where the global state is outside the core and persists globally using Redux toolkit.
The bulk of the implementation is separated into two independent features, management
and explore
, which represent the two sections of the webpage.
Therefore, each of them have their own core, UI components and data sources.
Finally, the core is where the business logic is defined, i.e. entities, repositories, use cases and adapters.
Data Fetching
Using Next.js along with TheGraph Client allows us to choose between different alternatives when it comes to fetch data from the subgraph.
TheGraph Client creates a runtime artifact to execute queries to the subgraph that we can use in a data source on client side, however it also exports an SDK that we can use to execute our queries on server side.
Therefore, we can combine both to fetch data with hooks on client side, or pre-render in server side, or update or create content at runtime with Incremental Static Regeneration.
Although one could go for just using one of these methods to fetch data in all cases, CSR, SSR and ISR are used to deal with three different scenarios that require data to be fetched differently in order to optimise the user experience.
All the pages in
/explore
(with the exception of/explore/escrows
) contain three versions of the components to choose from, one for every data fetching method.Therefore, uncomment the component that fits your data fetching strategy best.
Client side rendering
Pages in management
, as well as the page /explore/escrows
, require client side rendering with a polling interval in order to keep fetching data periodically so that new transactions are reflected on the page without making further requests.
Therefore, when a user navigates to one of these pages, the server will yield the cached page from when the app was built.
Once the client receives the page and executes React.js, the hook to get the data will be called.
For instance, if an end customer goes to /management/my-tokens
and logs in, MyTokensPage.tsx
will render the component MyTokensByAccount
which will call the hook useTokensByAccount
to get tokens by account address.
We can see the implementation of that hook below:
The callback function to fetch data in useTokensByAccount
only calls one interactor, which is the use case of getting tokens by account.
We can see the flow chart of the hook useTokensByAccount
below:
As we can see, the interactors call a method of a repository to get an entity, which is adapted to a generic interface thereafter.
The repository is injected to the interactor as an abstraction of a data source class, where that repository is implemented.
We will see in interactors how this dependency injection is done.
Server side rendering
I use server side rendering on pages in explore
, excluding dynamic paths and /explore/escrows
.
These pages are expected to list all the available data of a particular entity (tokens, certificates, etc.), so it is reasonable to only show data that is existent at the moment of the request.
This is, there is no need to keep fetching data periodically but data should be fetched on demand.
Since this project uses Next.js 12, this data fetch is done using the getServerSideProps
function.
Next.js 13 introduces an easy way of fetching data (either dynamically, statically, and in revalidation) that works in a specific directory called
app
.
We can see below the use of getServerSideProps
for the /explore/tokens
route:
In getServerSideProps
we can use the GraphClient function getBuiltGraphSDK
which returns an SDK object that gathers methods to execute the queries defined in next-app/.graphclientrc.yaml
.
Data are then adapted, a state object is formed and returned so that when a user requests this page the server will call this function, the page will be generated on the fly and served to the browser.
We can see below the flow chart of getServerSideProps
:
Incremental static regeneration
Dynamic pages display data that are immutable, and some other that may be updated periodically (such as metadata).
Therefore, it is reasonable to use ISR to fetch data at build time and re-generate the page per request at most once every revalidate interval on server side.
Moreover, if there was much activity within the supply chain, incremental static regeneration would allow us to efficiently handle a number of dynamic pages that would escalate significantly.
getStaticProps
and getStaticPaths
have to be exported in order to enable ISR in a dynamic page in Next.js 12.
We can see below the implementation of these two functions in the token page component:
As we can see, the SDK method associated with the query we want to perform is called in getStaticProps
.
Once data are returned, they are adapted and a token state is formed and returned alongside a revalidate
number.
This is the number in seconds that will tell Next.js the time interval in which a page can be re-generated at most once if there is a request.
getStaticPaths
, in turn, will return all the ids that are expected and it will fallback to blocking
if an id is not included in this list, which would render the page on server side .
We can see below the flow chart of these two functions:
File structure
The file structure schema of the workspace next-app
can be seen below:
Next.js expects a folder named pages
, at the root level or at src/
, which alongside app
and features
conform the main folders in our application.
App
The folder next-app/src/app
contains all the logic that neither is about any feature nor any page, but about the application in general.
This folder contains the Redux store as well as the global states, the global styles and the layout components.
One of these components is Header.tsx
which contains the connect and disconnect button. This button calls the methods connectWallet
and disconnectWallet
on click.
connectWallet
creates a request to connect to the user's wallet and, ultimately, set data about the account, chain id and contract (if the user is a member), and it also stores in a global state variables about the state of the connection.
The app will use these data to implement restriction access and state messages in the pages
components that we will see in the next section.
Pages
In Next.js the folder pages
contains React.js components that will be rendered in a route based on their file name.
For instance the following component will be rendered when a user navigates to the route /management/my-tokens
:
As we can see, this component will render different components depending on whether the user is logged in, and if logged in whether the user is a member, and if the user is a member if it is a certifier or not.
This is because there is a restricted access to this page, but also because not every logged in user will make the app to execute the same query to the subgraph or to show the same use cases to the user (to interact with the contracts).
Features
As I mentioned previously, the logic of this app is mainly contained in two independent features: management
and explore
. There is also a shared folder that includes code shared among the two features.
We can see below the file structure of the management
feature:
In the following sections I will go over the main folders in next-app/src/features/management
to explain their purpose.
Core
core
gathers the business logic of the feature. It consists of entities, repositories, interactors and adapters.
When a hook is called in a UI component, it will invoke at least one use case, or interactor, which will interact with a repository to get an entity.
That entity will be adapted to another interface through an adapter function.
Entities
Entities are interfaces that represent the objects that must be returned from the adapters.
Given that this app uses TheGraph Client to request data to the subgraph, the TypeScript types that it offers for the entities that it returns are used.
Therefore, the adapters will adapt a TheGraph Client query type to an entity defined in the folder
entities
.
We can see below the interface that the tokens adapter should return:
As we can see, there are entities that are imported from the folder shared
. This is due to the usage of these entities in both features in the app.
Repositories
A repository is an interface that defines all the methods that are necessary to handle all the use cases in a particular feature.
These methods are implemented in a data source class outside the core.
For example, we can see the repository MyTokensRepository
in the feature management
with its two methods highlighted below:
Interactors
Interactors are the result of the implementation of the use cases in the corresponding feature.
For instance, the interactor queryTokensByAccountWithDep
is used for getting tokens by account (through the hook useTokensByAccount
).
Hooks use interactors that already have the dependencies injected in order to keep the business logic independent from the infrastructure.
This injection is done in next-app/src/features/management/core/interactors/index.ts
.
We can see highlighted below the injection of a MyTokensDataSource
instance to queryTokensByAccount
:
The logic of the interactors can be found in the same folder. Below is the code for the interactor queryTokensByAccount
:
As we can see, this interactor expects a MyTokensRepository
entity that is used to call its method getTokensByAccount
, but the interactor does not depend on its implementation.
Using this clean architecture, any change that could be done to the infrastructure would not affect the business logic, i.e. the code in
core
.
Adapters
Adapters are functions whose sole purpose is to adapt an object from one interface to another. This is very useful as a way to prevent changes in the object that is requested from affecting the rest of the code.
For example, if the interface of the token entity in the subgraph was to change, we might just have to edit the adapter (assuming the returned object by the adapter remained with the same interface).
We can see in the previous code block that the interactor queryTokensByAccount
passes the data from the request to tokensByAccountAdapter
in line 9.
Below it is shown how this function gets an object of type TokensByAccountQuery
and returns another one with the interface Tokens
(or null if the account in question has no tokens in Olive Oil Trust).
Data sources
Data sources in this app are fundamentally the implementation of the methods of the repositories.
I use TypeScript classes to leverage its implements
clause and verify that the class conforms to the interface of the repository.
For instance, we can see below the data source that implements the methods to query tokens in management
:
The function execute
gets a document (which is the template string with the query that generates TheGraph Client), and the variables that the query expects (which are passed to the data source method).
Utils
The folder utils
in every feature contains constants, interfaces and helper functions that are used in the code of that feature, but also a folder with all the queries that are used in the data sources of that feature.
User interface
All the React.js components, as well as hooks for a particular folder, are included in a folder named ui
at the feature level.
These components get state objects (with data and an error field) from the hooks, and render components according to the actions of the user and the state of the request.
For instance, as we can see below MyTokensByAccount.tsx
calls the hook useTokensByAccount
to get an TokensState
object:
That hook calls the interactor and returns the state of the request, which makes the component display one of the following options:
-
an error message if there has been an error while executing the query.
-
the list of token cards, and buttons for the use cases if the request is executed without errors.
-
a loading message while
error
isnull
.
Interacting with the UI
As we said earlier in this series of posts, Olive Oil Trust leverages the ERC-1155 multi token standard to manage one or multiple batch transfers at once.
It also allows for the creation and certification of one or multiple token types.
That means a lot of data may need to be passed to the appropriate contract function to perform the transaction.
For instance, we can see below the arguments expected by the mintBatch
method of a TypeScript abstraction of a bottling plant contract:
So, in order for that member to mint 4 batches of olive oil bottle tokens in Olive Oil Trust, a transaction like the one below should be made:
As we can see, it is very convenient, from a UX perspective, to reduce the amount of complexity left to the user to perform a task like this.
In the next sections, I will show how I have tried to achieve this while introducing the main use cases in Olive Oil Trust.
I will adopt the roles of a certifier, a bottling plant, two bottle manufacturers, an olive oil mill and a distributor to set these stages.
Certifying token types
Once a member creates a token type, it will be extracted, processed and stored by the subgraph, so that it can be queried by the user interface.
When a certifier then logs in and goes to /management/my-certificates
, it will see an enabled button to certify token types.
In the image below, the certifier is creating two certificates, one for the Picual olive oil and another one for 750 ml bottles:
As we can see the certificate 5544876543211C is certifying two different types of 750 ml glass bottles (those types are from different manufacturers).
We will see later in minting tokens how the usage of this certificate will let the minter to select tokens of different types when minting a new token.
The dropdown list of the token types select element contains all the types created right until that moment in time in Olive Oil Trust, and grouped by roles:
Once both certificates are added and "Certify" is clicked, the injected wallet will ask the user to confirm or revert the transaction, i.e. the certifications:
If the transaction is confirmed, both certificates will appear listed after the following fetch of data:
These certificates will be available (alongside other certificates and token types) in the dropdown list options when creating a new token type.
Adding token types
The creation of a token type is the way for a member to set an irreversible manner of minting a dependent token of that type; therefore, it being a way of setting instructions for its creation.
If a bottling plant was to mint olive oil bottle tokens of a new type of olive oil, it would have to create a token type for that type of olive oil bottles.
Let's say it wants to create a type of extra virgin olive oil in high quality bottles of 750 ml.
We could either tell the contract that the instructions for that type will have to compel the minter to use some type of olive oil and bottle in particular, or specify a certificate instead.
Designating a certificate would allow the minter to use any type of token (that has already been certified with that same certificate) in order to mint the token.
That would allow the minter to purchase tokens of one or multiple types, from one or multiple sources for the creation of a token, as long as the certificate for that token type allows it.
When a bottling plant logs in, goes to /management/my-token-types
and clicks on "Add Token Types", a form will appear for the creation of one or multiple token types.
In the image below, two types are being created, one for an extra virgin smooth olive oil 5l plastic bottle, and another one for an extra virgin intense olive oil 750 ml glass bottle:
The dropdown list of the instructions id select element contains all the token types and certificates created right until that moment in time in Olive Oil Trust, and grouped by type:
Once the types are added and the transaction is confirmed and complete, the bottling plant will be able to mint tokens of that type.
However, it will first need to buy the tokens that it needs, following the instructions of the corresponding token type.
Buying tokens
The role of a bottling plant depends on the acquisition of other tokens in order to mint olive oil bottle tokens, namely olive oil and bottle tokens.
The process of buying tokens is through escrows, which get different states depending on the interactions of seller and buyer with them.
The user has to go to /explore/escrows
to buy tokens, where all the escrows that have been created in the supply chain are listed.
The escrows that are neither closed nor reverted will show interactable buttons if the user is logged in and is expected to consider an escrow from that seller.
For instance, in this app bottling plants will only be able to buy tokens from bottle manufacturers and olive oil mills because its role is next in line in the olive oil long value chain.
Let's say a bottling plant is interested in buying bottle tokens from two different suppliers, and olive oil tokens from just one olive oil mill.
It would have to wait until the corresponding escrows are active to interact with them.
These escrows would be similar to the ones below:
Once it clicks on "Make payment" in one of these escrows, the following form will appear:
The buyer wallet address that is requested will be the address where the funds that are deposited in the escrow contract will be sent if the payment is cancelled, or if the escrow is reverted.
If it clicks on "Make Payment" in the form, the address is valid and the transaction (for an amount equal to the price of the escrow) is confirmed, ether will be deposited in the escrow contract and the escrow card will update:
Once ether has been deposited to an escrow, a logged in user will be able to see and interact with that escrow in
/management/my-participations
.
We can see that the state of the escrow changed to "EtherDeposited" and that there is a "Buyer candidate" field with the name (or address) of the user that made the payment.
That payment is represented in an "Ether balance" field.
At this stage, the escrow will only be interactable for the seller and buyer candidate, i.e. no other bottling plant will be able to deposit ether.
The seller will see, either in /management/my-escrows
or in /explore/escrows
, that its escrow shows the options to revert and close:
Either way a transaction will be made once it is confirmed through the injected wallet.
In the case of "Revert", the funds will be transferred back to the buyer candidate and the tokens back to the seller.
While in the case of "Close", the funds will be transferred to the seller and the tokens to the buyer.
The escrow card will reflect these changes thereafter:
As we can see, the card shows that the ether balance is now zero as well as the token balance, and that the state of the escrow is "Closed".
The buyer will now see those tokens in /management/my-tokens
when logged in.
Minting tokens
In /management/my-tokens
, right above the token cards, there is one button for every action regarding tokens.
The buttons will be enabled if the minter has the appropriate tokens for that action in particular.
When clicking on "Mint" a form will appear.
Depending on the type that is chosen in the dropdown list at the top, the instructions list will differ as well as the options that are given to the select fields.
Choosing a different token type will most likely compel the minter to choose different tokens from which the new token is minted.
This is the reason why every time a different type is chosen in the dropdown list, the token options given to the mintage might vary.
Furthermore, the total amount of units to mint will be used to automatically calculate the amount of tokens needed for every instruction.
That amount is reflected but also available to be set to the input element on click, in order to ease the process.
The following image shows an example of a form where 3 batches of 100 tokens each are already added to the list, and a fourth is being completed:
As we can see above, two batches from two different manufacturers have been chosen from which to obtain the glass bottles.
The dropdown list of the batch select element for the bottle instruction contains all the batches owned by the minter that have been certified (by the certificate in the selected token type instructions) right until that moment in time in Olive Oil Trust, and grouped by type:
As we can see there are four options which are four batches minted by the bottle manufacturers (two each).
Once the tokens are added, the user clicks on "Mint" and the transaction is completed, the new batches of tokens will appear.
Packing tokens
Members in the industrial phase of the olive oil long value chain (bottling plants and distributors) sell industrial units (pallets) instead of commercial units.
Let's say a bottling plant wants to create two pallets containing four different batches of 30 tokens each.
We can see below a recreation of that operation:
The dropdown list of the instructions id select element contains all packable user owned batches created up to that point in Olive Oil Trust and grouped by type:
The list above will not contain user owned batches that have not been minted by the user.
For example, this app will not allow a bottling plant to pack olive oil or glass bottles tokens even if they are their property, but only bottles of olive oil tokens.
If the user continues with the operation and completes the transaction, the amount of units that have been packed will be transferred to the industrial unit contract, i.e. they will no longer be shown in the token list.
Furthermore, the list will include new units, which will be the two industrial unit tokens that have just been minted:
Depositing tokens
Once a member owns the appropriate tokens to be deposited to the escrow, it will be able to click on "Deposit".
We can see below how to deposit two of the newly created industrial units and set a price of 0.1 ETH:
Once the tokens have been deposited, i.e. once the user has clicked on "Deposit" and completed the transaction, the user has to wait for a buyer to deposit ether to be able to close the escrow.
It can also revert the operation at any time.
The process of interacting with an escrow is recreated in buying tokens.
Unpacking tokens
If a distributor acquires industrial unit tokens from a bottling plant, it has to unpack them in order to get the ownership of the tokens in the pallet.
Let's say a distributor has bought the two pallets deposited previously.
In order to unpack them the distributor would have to click on "Unpack" in /management/my-tokens
.
A form like the one below will then appear, where the tokens to unpack can be selected in a dropdown list.
To complete the action just click on "Unpack" and accept the transaction.
The distributor will see (once data have been fetched again) the four batches contained in both pallets:
All the batches contain 60 units which are the sum of all the batches in the industrial unit tokens packed by the bottling plant.
Tracing tokens
As we saw earlier, every batch of tokens in this app will have a page that will contain all the available information about it.
The React.js component of this page is shown in the incremental static regeneration
At the bottom of the rendered page there is information about the ancestry of the token.
For example, we can see below the ancestry of the batch 1072583 minted by Bottling Company:
This image is useful to see what batches of tokens were used to mint every token at every level of the tree.
Given that we used a 750 ml glass bottle certificate to mint tokens of the type Extra Virgin Intense Olive Oil 750 ml Glass Bottle, we were able to use 750 ml glass bottles of two different types, both certified by the same certificate.
Conclusion
In this series of posts I have attempted to explain how to create a complete set of contracts that implements all the use cases present in the olive oil long value chain.
Also, a subgraph has been introduced which extracts data from these contracts, processes them through mapping functions and stores them.
Finally, a front-end application is introduced where these data are easily queried, and the user can interact with the contracts efficiently.