Initialize a new React app with TypeScript in the same parent folder as your contract using the command below.
npx create-react-app frontend --template typescript
Icon ClipboardText
Let's go into the frontend folder:
Next, install the following packages in your frontend
folder:
npm install fuels@0.79.0 @fuels/react@0.18.0 @fuels/connectors@0.1.1 @tanstack/react-query@5.28.9
Icon ClipboardText
The fuels init
command generates a fuels.config.ts
file that is used by the SDK to generate contract types.
Use the contracts
flag to define where your contract folder is located, and the output
flag to define where you want the generated files to be created.
Run the command below in your frontend folder to generate the config file:
npx fuels init --contracts ../contract/ --output ./src/contracts
Icon ClipboardText
Now that you have a fuels.config.ts
file, you can use the fuels build
command to rebuild your contract and generate types.
Running this command will interpret the output ABI JSON from your contract and generate the correct TypeScript definitions.
If you see the folder fuel-project/counter-contract/out
you will be able to see the ABI JSON there.
Inside the fuel-project/frontend
directory run:
A successful process should print and output like the following:
Building..
Building Sway programs using built-in 'forc' binary
Generating types..
š Build completed successfully!
Icon ClipboardText
Icon InfoCircle
If you're having any issues with this part, try adding useBuiltinForc: false
to your fuels.config.ts
config file to make sure it's using the same version of forc
as your default toolchain.
Now you should be able to find a new folder fuel-project/frontend/src/contracts
.
Open the src/App.tsx
file, and replace the boilerplate code with the template below:
import { useState, useMemo } from " react " ;
import { useConnectUI, useIsConnected, useWallet } from " @fuels/react " ;
import { ContractAbi__factory } from " ./contracts " ;
import AllItems from " ./components/AllItems " ;
import ListItem from " ./components/ListItem " ;
import " ./App.css " ;
const CONTRACT_ID =
" 0x3b598fc9103ce3c5c7a29663aee51099b3374958ba1016280caf802fdeb5aad8 " ;
function App () {
const [active, setActive] = useState < " all-items " | " list-item " >( " all-items " );
const { isConnected } = useIsConnected ();
const { connect, isConnecting } = useConnectUI ();
const { wallet } = useWallet ();
const contract = useMemo (() => {
if (wallet) {
const contract = ContractAbi__factory. connect (CONTRACT_ID, wallet);
return contract;
}
return null ;
}, [wallet]);
return (
< div className = " App " >
< header >
< h1 >Sway Marketplace </ h1 >
</ header >
< nav >
< ul >
< li
className = {active === " all-items " ? " active-tab " : "" }
onClick = {() => setActive ( " all-items " )}
>
See All Items
</ li >
< li
className = {active === " list-item " ? " active-tab " : "" }
onClick = {() => setActive ( " list-item " )}
>
List an Item
</ li >
</ ul >
</ nav >
< div >
{ isConnected ? (
< div >
{ active === " all - items " && < AllItems contract = {contract} /> }
{ active === " list - item " && < ListItem contract = {contract} /> }
</ div >
) : (
< div >
< button
onClick = {() => {
connect ();
}}
>
{ isConnecting ? " Connecting " : " Connect " }
</ button >
</ div >
)}
</ div >
</ div >
);
}
export default App;
Expand Icon ClipboardText
At the top of the file, change the CONTRACT_ID
to the contract ID that you deployed earlier and set as a constant.
const CONTRACT_ID =
" 0x3b598fc9103ce3c5c7a29663aee51099b3374958ba1016280caf802fdeb5aad8 " ;
Icon ClipboardText
Copy and paste the CSS code below in your App.css
file to add some simple styling.
.App {
text - align: center;
}
nav > ul {
list - style - type: none;
display: flex;
justify - content: center;
gap: 1rem;
padding - inline - start: 0 ;
}
nav > ul > li {
cursor: pointer;
}
.form - control{
text - align: left;
font - size: 18px;
display: flex;
flex - direction: column;
margin: 0 auto;
max - width: 400px;
}
.form - control > input {
margin - bottom: 1rem;
}
.form - control > button {
cursor: pointer;
background: #054a9f;
color: white;
border: none;
border - radius: 8px;
padding: 10px 0 ;
font - size: 20px;
}
.items - container{
display: flex;
flex - wrap: wrap;
justify - content: center;
gap: 2rem;
margin: 1rem 0 ;
}
.item - card{
box - shadow: 0px 0px 10px 2px rgba ( 0 , 0 , 0 , 0.2 );
border - radius: 8px;
max - width: 300px;
padding: 1rem;
display: flex;
flex - direction: column;
gap: 4px;
}
.active - tab{
border - bottom: 4px solid #77b6d8;
}
button {
cursor: pointer;
background: #054a9f;
border: none;
border - radius: 12px;
padding: 10px 20px;
margin - top: 20px;
font - size: 20px;
color: white;
}
Expand Icon ClipboardText
Wrap your components with the FuelProvider
and QueryClientProvider
components to enable Fuel's custom React hooks for wallet functionalities in index.tsx
.
This is where you can pass in custom wallet connectors to customize which wallets your users can use to connect to your app.
import React from ' react ' ;
import ReactDOM from ' react-dom/client ' ;
import ' ./index.css ' ;
import App from ' ./App ' ;
import { FuelProvider } from ' @fuels/react ' ;
import {
FuelWalletConnector,
FuelWalletDevelopmentConnector,
FueletWalletConnector,
} from ' @fuels/connectors ' ;
import { QueryClient, QueryClientProvider } from ' @tanstack/react-query ' ;
const queryClient = new QueryClient ();
const root = ReactDOM. createRoot (
document. getElementById ( ' root ' ) as HTMLElement
);
root. render (
< React.StrictMode >
< QueryClientProvider client = {queryClient} >
< FuelProvider
fuelConfig = {{
connectors: [
new FuelWalletConnector (),
new FuelWalletDevelopmentConnector (),
new FueletWalletConnector (),
],
}}
>
< App />
</ FuelProvider >
</ QueryClientProvider >
</ React.StrictMode >
);
Expand Icon ClipboardText
React hooks from the @fuels/react
package are used in order to connect our wallet to the dapp. In the App
function, we can call these hooks like this:
const { isConnected } = useIsConnected ();
const { connect, isConnecting } = useConnectUI ();
const { wallet } = useWallet ();
Icon ClipboardText
The wallet
variable from the useWallet
hook will have the type FuelWalletLocked
.
You can think of a locked wallet as a user wallet you can't sign transactions for, and an unlocked wallet as a wallet where you have the private key and are able to sign transactions.
const { wallet } = useWallet ();
Icon ClipboardText
The useMemo
hook is used to connect to our contract with the connected wallet.
const contract = useMemo (() => {
if (wallet) {
const contract = ContractAbi__factory. connect (CONTRACT_ID, wallet);
return contract;
}
return null ;
}, [wallet]);
Icon ClipboardText
< div >
{isConnected ? (
< div >
{active === " all-items " && < AllItems contract ={contract} />}
{active === " list-item " && < ListItem contract ={contract} />}
</ div >
) : (
< div >
< button
onClick = {() => {
connect ();
}}
>
{isConnecting ? " Connecting " : " Connect " }
</ button >
</ div >
)}
</ div >
Icon ClipboardText
Now we have our contract connection ready. You can console log the contract here to make sure this is working correctly.
Icon Link UI
In our app we're going to have two tabs: one to see all of the items listed for sale, and one to list a new item for sale.
We use another state variable called active
that we can use to toggle between our tabs. We can set the default tab to show all listed items.
const [active, setActive] = useState < " all-items " | " list-item " >( " all-items " );
Icon ClipboardText
Next we can create our components to show and list items.
Create a new folder in the src
folder called components
.
Then create a file inside called ListItem.tsx
.
At the top of the file, import the useState
hook from react
, the generated contract ABI from the contracts
folder, and bn
(big number) type from fuels
.
import { useState } from " react " ;
import { ContractAbi } from " ../contracts " ;
import { bn } from " fuels " ;
Icon ClipboardText
This component will take the contract we made in App.tsx
as a prop, so let's create an interface for the component.
interface ListItemsProps {
contract : ContractAbi | null ;
}
Icon ClipboardText
We can set up the template for the function like this.
export default function ListItem ({ contract } : ListItemsProps ){
Icon ClipboardText
To list an item, we'll create a form where the user can input the metadata string and price for the item they want to list.
Let's start by adding some state variables for the metadata
and price
. We can also add a status
variable to track the submit status.
const [metadata, setMetadata] = useState < string >( "" );
const [price, setPrice] = useState < string >( " 0 " );
const [status, setStatus] = useState < ' success ' | ' error ' | ' loading ' | ' none ' >( ' none ' );
Icon ClipboardText
We need to add the handleSubmit
function.
We can use the contract prop to call the list_item
function and pass in the price
and metadata
from the form.
async function handleSubmit ( e : React . FormEvent < HTMLFormElement >){
e. preventDefault ();
setStatus ( ' loading ' )
if (contract !== null ){
try {
const priceInput = bn. parseUnits (price. toString ());
await contract.functions
. list_item (priceInput, metadata)
. txParams ({
gasPrice : 1 ,
gasLimit : 300_000 ,
})
. call ();
setStatus ( ' success ' )
} catch (e) {
console. log ( " ERROR: " , e);
setStatus ( ' error ' )
}
} else {
console. log ( " ERROR: Contract is null " );
}
}
Icon ClipboardText
Under the heading, add the code below for the form:
return (
< div >
< h2 >List an Item </ h2 >
{ status === ' none ' &&
< form onSubmit = {handleSubmit} >
< div className = " form-control " >
< label htmlFor = " metadata " > Item Metadata:</ label >
< input
id = " metadata "
type = " text "
pattern = " \w {20} "
title = " The metatdata must be 20 characters "
required
onChange = {(e) => setMetadata (e.target.value)}
/>
</ div >
< div className = " form-control " >
< label htmlFor = " price " > Item Price:</ label >
< input
id = " price "
type = " number "
required
min = " 0 "
step = " any "
inputMode = " decimal "
placeholder = " 0.00 "
onChange = {(e) => {
setPrice (e.target.value);
}}
/>
</ div >
< div className = " form-control " >
< button type = " submit " > List item </ button >
</ div >
</ form >
}
{ status === ' success ' && < div > Item successfully listed !</ div >}
{ status === ' error ' && < div > Error listing item . Please try again .</ div >}
{ status === ' loading ' && < div > Listing item ...</ div >}
</ div >
)
}
Expand Icon ClipboardText
Now, try listing an item to make sure this works.
You should see the message Item successfully listed!
.
Next, let's create a new file called AllItems.tsx
in the components
folder.
Copy and paste the template code below for this component:
import { useState, useEffect } from " react " ;
import { ContractAbi } from " ../contracts " ;
import ItemCard from " ./ItemCard " ;
import { BN } from " fuels " ;
import { ItemOutput } from " ../contracts/contracts/ContractAbi " ;
interface AllItemsProps {
contract : ContractAbi | null ;
}
export default function AllItems ({ contract } : AllItemsProps ) {
Icon ClipboardText
Here we can get the item count to see how many items are listed, and then loop through each of them to get the item details.
First, let's create some state variables to store the number of items listed, an array of the item details, and the loading status.
const [items, setItems] = useState < ItemOutput []>([]);
const [itemCount, setItemCount] = useState < number >( 0 );
const [status, setStatus] = useState < " success " | " loading " | " error " >(
" loading "
);
Icon ClipboardText
Next, let's fetch the items in a useEffect
hook.
Because these are read-only functions, we can simulate a dry-run of the transaction by using the get
method instead of call
so the user doesn't have to sign anything.
useEffect (() => {
async function getAllItems () {
if (contract !== null ) {
try {
let { value } = await contract.functions
. get_count ()
. txParams ({
gasPrice : 1 ,
gasLimit : 100_000 ,
})
. get ();
let formattedValue = new BN (value). toNumber ();
setItemCount (formattedValue);
let max = formattedValue + 1 ;
let tempItems = [];
for ( let i = 1 ; i < max; i ++ ) {
let resp = await contract.functions
. get_item (i)
. txParams ({
gasPrice : 1 ,
gasLimit : 100_000 ,
})
. get ();
tempItems. push (resp.value);
}
setItems (tempItems);
setStatus ( " success " );
} catch (e) {
setStatus ( " error " );
console. log ( " ERROR: " , e);
}
}
}
getAllItems ();
}, [contract]);
Expand Icon ClipboardText
If the item count is greater than 0
and we are able to successfully load the items, we can map through them and display an item card.
The item card will show the item details and a buy button to buy that item, so we'll need to pass the contract and the item as props.
return (
< div >
< h2 >All Items </ h2 >
{ status === " success " && (
< div >
{ itemCount === 0 ? (
< div > Uh oh ! No items have been listed yet </ div >
) : (
< div >
< div > Total items: { itemCount }</ div >
< div className = " items-container " >
{ items . map (( item ) => (
< ItemCard
key = {item.id.format()}
contract = {contract}
item = {item}
/>
))}
</ div >
</ div >
)}
</ div >
)}
{ status === " error " && (
< div > Something went wrong , try reloading the page .</ div >
)}
{ status === " loading " && < div > Loading ...</ div >}
</ div >
);
}
Icon ClipboardText
Now let's create the item card component.
Create a new file called ItemCard.tsx
in the components folder.
After, copy and paste the template code below.
import { useState } from " react " ;
import { ItemOutput } from " ../contracts/contracts/ContractAbi " ;
import { ContractAbi } from " ../contracts " ;
import { BN } from ' fuels ' ;
interface ItemCardProps {
contract : ContractAbi | null ;
item : ItemOutput ;
}
const assetId = " 0x0000000000000000000000000000000000000000000000000000000000000000 "
export default function ItemCard ({ item , contract } : ItemCardProps ) {
Icon ClipboardText
Add a status
variable to track the status of the buy button.
const [status, setStatus] = useState < ' success ' | ' error ' | ' loading ' | ' none ' >( ' none ' );
Icon ClipboardText
Create a new async function called handleBuyItem
.
Because this function is payable and transfers coins to the item owner, we'll need to do a couple special things here.
Whenever we call any function that uses the transfer or mint functions in Sway, we have to append the matching number of variable outputs to the call with the txParams
method. Because the buy_item
function just transfers assets to the item owner, the number of variable outputs is 1
.
Next, because this function is payable and the user needs to transfer the price of the item, we'll use the callParams
method to forward the amount. With Fuel you can transfer any type of asset, so we need to specify both the amount and the asset ID.
async function handleBuyItem () {
if (contract !== null ) {
setStatus ( ' loading ' )
try {
await contract.functions. buy_item (item.id)
. txParams ({
variableOutputs : 1 ,
gasPrice : 1 ,
gasLimit : 300_000 ,
})
. callParams ({
forward : [item.price, assetId],
})
. call ()
setStatus ( " success " );
} catch (e) {
console. log ( " ERROR: " , e);
}
}
}
Icon ClipboardText
Then add the item details and status messages to the card.
return (
< div className = " item-card " >
< div > Id : { new BN (item.id). toNumber ()}</ div >
< div > Metadata : { item .metadata}</ div >
< div > Price : { new BN (item.price). formatUnits ()} ETH </ div >
< h3 >Total Bought : { new BN (item.total_bought). toNumber ()}</ h3 >
{ status === ' success ' && < div > Purchased ā
</ div >}
{ status === ' error ' && < div > Something went wrong ā</ div >}
{ status === ' none ' && < button data - testid = { `buy-button- ${ item.id } ` } onClick = {handleBuyItem} > Buy Item </ button > }
{ status === ' loading ' && < div > Buying item ..</ div >}
</ div >
);
}
Icon ClipboardText
Now you should be able to see and buy all of the items listed in your contract.
Ensure that all your files are correctly configured by examining the code below. If you require additional assistance, refer to the repository
here Icon Link
App.tsx
import { useState, useMemo } from " react " ;
import { useConnectUI, useIsConnected, useWallet } from " @fuels/react " ;
import { ContractAbi__factory } from " ./contracts " ;
import AllItems from " ./components/AllItems " ;
import ListItem from " ./components/ListItem " ;
import " ./App.css " ;
const CONTRACT_ID =
" 0x3b598fc9103ce3c5c7a29663aee51099b3374958ba1016280caf802fdeb5aad8 " ;
function App () {
const [active, setActive] = useState < " all-items " | " list-item " >( " all-items " );
const { isConnected } = useIsConnected ();
const { connect, isConnecting } = useConnectUI ();
const { wallet } = useWallet ();
const contract = useMemo (() => {
if (wallet) {
const contract = ContractAbi__factory. connect (CONTRACT_ID, wallet);
return contract;
}
return null ;
}, [wallet]);
return (
< div className = " App " >
< header >
< h1 >Sway Marketplace </ h1 >
</ header >
< nav >
< ul >
< li
className = {active === " all-items " ? " active-tab " : "" }
onClick = {() => setActive ( " all-items " )}
>
See All Items
</ li >
< li
className = {active === " list-item " ? " active-tab " : "" }
onClick = {() => setActive ( " list-item " )}
>
List an Item
</ li >
</ ul >
</ nav >
< div >
{ isConnected ? (
< div >
{ active === " all - items " && < AllItems contract = {contract} /> }
{ active === " list - item " && < ListItem contract = {contract} /> }
</ div >
) : (
< div >
< button
onClick = {() => {
connect ();
}}
>
{ isConnecting ? " Connecting " : " Connect " }
</ button >
</ div >
)}
</ div >
</ div >
);
}
export default App;
Expand Icon ClipboardText
AllItems.tsx
import { useState, useEffect } from " react " ;
import { ContractAbi } from " ../contracts " ;
import ItemCard from " ./ItemCard " ;
import { BN } from " fuels " ;
import { ItemOutput } from " ../contracts/contracts/ContractAbi " ;
interface AllItemsProps {
contract : ContractAbi | null ;
}
export default function AllItems ({ contract } : AllItemsProps ) {
const [items, setItems] = useState < ItemOutput []>([]);
const [itemCount, setItemCount] = useState < number >( 0 );
const [status, setStatus] = useState < " success " | " loading " | " error " >(
" loading "
);
useEffect (() => {
async function getAllItems () {
if (contract !== null ) {
try {
let { value } = await contract.functions
. get_count ()
. txParams ({
gasPrice : 1 ,
gasLimit : 100_000 ,
})
. get ();
let formattedValue = new BN (value). toNumber ();
setItemCount (formattedValue);
let max = formattedValue + 1 ;
let tempItems = [];
for ( let i = 1 ; i < max; i ++ ) {
let resp = await contract.functions
. get_item (i)
. txParams ({
gasPrice : 1 ,
gasLimit : 100_000 ,
})
. get ();
tempItems. push (resp.value);
}
setItems (tempItems);
setStatus ( " success " );
} catch (e) {
setStatus ( " error " );
console. log ( " ERROR: " , e);
}
}
}
getAllItems ();
}, [contract]);
return (
< div >
< h2 >All Items </ h2 >
{ status === " success " && (
< div >
{ itemCount === 0 ? (
< div > Uh oh ! No items have been listed yet </ div >
) : (
< div >
< div > Total items: { itemCount }</ div >
< div className = " items-container " >
{ items . map (( item ) => (
< ItemCard
key = {item.id.format()}
contract = {contract}
item = {item}
/>
))}
</ div >
</ div >
)}
</ div >
)}
{ status === " error " && (
< div > Something went wrong , try reloading the page .</ div >
)}
{ status === " loading " && < div > Loading ...</ div >}
</ div >
);
}
Expand Icon ClipboardText
ItemCard.tsx
import { useState } from " react " ;
import { ItemOutput } from " ../contracts/contracts/ContractAbi " ;
import { ContractAbi } from " ../contracts " ;
import { BN } from ' fuels ' ;
interface ItemCardProps {
contract : ContractAbi | null ;
item : ItemOutput ;
}
const assetId = " 0x0000000000000000000000000000000000000000000000000000000000000000 "
export default function ItemCard ({ item , contract } : ItemCardProps ) {
const [status, setStatus] = useState < ' success ' | ' error ' | ' loading ' | ' none ' >( ' none ' );
async function handleBuyItem () {
if (contract !== null ) {
setStatus ( ' loading ' )
try {
await contract.functions. buy_item (item.id)
. txParams ({
variableOutputs : 1 ,
gasPrice : 1 ,
gasLimit : 300_000 ,
})
. callParams ({
forward : [item.price, assetId],
})
. call ()
setStatus ( " success " );
} catch (e) {
console. log ( " ERROR: " , e);
}
}
}
return (
< div className = " item-card " >
< div > Id : { new BN (item.id). toNumber ()}</ div >
< div > Metadata : { item .metadata}</ div >
< div > Price : { new BN (item.price). formatUnits ()} ETH </ div >
< h3 >Total Bought : { new BN (item.total_bought). toNumber ()}</ h3 >
{ status === ' success ' && < div > Purchased ā
</ div >}
{ status === ' error ' && < div > Something went wrong ā</ div >}
{ status === ' none ' && < button data - testid = { `buy-button- ${ item.id } ` } onClick = {handleBuyItem} > Buy Item </ button > }
{ status === ' loading ' && < div > Buying item ..</ div >}
</ div >
);
}
Expand Icon ClipboardText
ListItem.tsx
import { useState } from " react " ;
import { ContractAbi } from " ../contracts " ;
import { bn } from " fuels " ;
interface ListItemsProps {
contract : ContractAbi | null ;
}
export default function ListItem ({ contract } : ListItemsProps ){
const [metadata, setMetadata] = useState < string >( "" );
const [price, setPrice] = useState < string >( " 0 " );
const [status, setStatus] = useState < ' success ' | ' error ' | ' loading ' | ' none ' >( ' none ' );
async function handleSubmit ( e : React . FormEvent < HTMLFormElement >){
e. preventDefault ();
setStatus ( ' loading ' )
if (contract !== null ){
try {
const priceInput = bn. parseUnits (price. toString ());
await contract.functions
. list_item (priceInput, metadata)
. txParams ({
gasPrice : 1 ,
gasLimit : 300_000 ,
})
. call ();
setStatus ( ' success ' )
} catch (e) {
console. log ( " ERROR: " , e);
setStatus ( ' error ' )
}
} else {
console. log ( " ERROR: Contract is null " );
}
}
return (
< div >
< h2 >List an Item </ h2 >
{ status === ' none ' &&
< form onSubmit = {handleSubmit} >
< div className = " form-control " >
< label htmlFor = " metadata " > Item Metadata:</ label >
< input
id = " metadata "
type = " text "
pattern = " \w {20} "
title = " The metatdata must be 20 characters "
required
onChange = {(e) => setMetadata (e.target.value)}
/>
</ div >
< div className = " form-control " >
< label htmlFor = " price " > Item Price:</ label >
< input
id = " price "
type = " number "
required
min = " 0 "
step = " any "
inputMode = " decimal "
placeholder = " 0.00 "
onChange = {(e) => {
setPrice (e.target.value);
}}
/>
</ div >
< div className = " form-control " >
< button type = " submit " > List item </ button >
</ div >
</ form >
}
{ status === ' success ' && < div > Item successfully listed !</ div >}
{ status === ' error ' && < div > Error listing item . Please try again .</ div >}
{ status === ' loading ' && < div > Listing item ...</ div >}
</ div >
)
}
Expand Icon ClipboardText
Inside the fuel-project/frontend
directory run:
Compiled successfully!
You can now view frontend in the browser.
Local: http://localhost:3000
On Your Network: http://192.168.4.48:3000
Note that the development build is not optimized.
To create a production build, use npm run build.
Icon ClipboardText
And that's it for the frontend! You just created a whole dapp on Fuel!