Tutorials
Technical Guides
Featured Guides
Beginner Sample App
The following documentation aims to educate you on building a web-based application on Flow. It walks through the key concepts required to create a dApp (distributed application) for Flow with code snippets.
Monster Maker is a web-based dApp (distributed application) that allows users to connect a wallet, sign a transaction to mint an NFT (a monster) and display their collection of NFTs (their monsters) within the app. It’s meant to be a lightweight sample project to exemplify how to build a web-based Flow project. If you are looking to build a web-based application for Flow, exploring the Monster Maker code base first or even building off of it is a great way to get started.
You can see a fully working demo of MonsterMaker at:
Github Repository:
The project is divided into front-end and back-end. The front-end (web-client) provides the user interface, and the back-end (server) provides the minting service. Both the front-end and back-end are built using next-js.
Run the Project
To start the front-end, open another terminal and from the command prompt run:
> cd web-client
> cp .env.example .env.local
> yarn install
> yarn dev
.env.example
.
In order for the server to support minting transactions, a private key is required. This key is generated in the process of creating a FLOW account where your contracts are deployed. Instructions can be found in Adding Contracts to a Flow Account. The private key generated there can be copied into the server side .env.local
file. You will then be able to run your own monster maker server locally.Go to http://localhost:3001
.
Connecting to a Wallet
Click Connect and select a wallet to use with MonsterMaker (note: MonsterMaker is configured by default to work with testnet
).
testnet
is a flow network that can be used to test flow code before deploying it into the production environment mainnet
. It works exactly like mainnet, but you can get flow token for free to execute transactions through a “faucet” that transfers flow tokens to your wallet. You can also run a flow network locally using an emulator.In order for a dApp to interact with the FLOW blockchain, a connection to a user’s wallet is required. All wallet and blockchain interaction is managed through the FCL (Flow Client Library).
All Web3 related connection and execution calls will be provided through a React context provider, and accessed through a React hook. This allows all FCL related code to be located in one spot that components can use conveniently.
Wallet Connection is configured using the following code:
const {
flowNetwork,
accessApi,
walletDiscovery,
walletDiscoveryApi,
walletDiscoveryInclude,
addresses,
} = NETWORK;
const iconUrl = window.location.origin + '/images/wallet-icon.png';
const appTitle = process.env.NEXT_PUBLIC_APP_NAME || 'MonsterMaker';
fcl.config({
'app.detail.title': appTitle,
'app.detail.icon': iconUrl,
'accessNode.api': accessApi, // connect to Flow
'flow.network': flowNetwork,
'discovery.wallet': walletDiscovery, // use wallets on public discovery
'discovery.authn.endpoint': walletDiscoveryApi, // public discovery api endpoint
'discovery.authn.include': walletDiscoveryInclude, // opt-in wallets
'0xFungibleToken': addresses.FungibleToken, // fcl replaces alias with network address
'0xFlowToken': addresses.FlowToken,
'0xNonFungibleToken': addresses.NonFungibleToken,
'0xMetadataViews': addresses.MetadataViews,
'0xMonsterMaker': addresses.MonsterMaker,
});
This code is part of the Web3ContextProvider
(See the full code). This component manages the Web3 connection via FCL and provides global access to all web3 information to the application, such as the user, transaction and wrapper functions to execute transactions/scripts on the blockchain. When the application is loaded, the Web3ContextProvider
runs fcl.config
to ensure that the connection to the appropriate flow network is ready. Components can then access all flow related functionality through the useWeb3Context
hook.
The network
object in the code above is derived from a constant that defines configuration parameters for each network:
const FLOW_ENV = process.env.NEXT_PUBLIC_FLOW_ENV || 'testnet';
const NETWORKS = {
emulator: {
flowNetwork: 'local',
accessApi: process.env.NEXT_PUBLIC_EMULATOR_API || 'http://localhost:8888',
walletDiscovery: 'https://fcl-discovery.onflow.org/local/authn',
walletDiscoveryApi: 'https://fcl-discovery.onflow.org/api/local/authn',
walletDiscoveryInclude: [],
addresses: {
FlowToken: '0x0ae53cb6e3f42a79',
NonFungibleToken: '0x0ae53cb6e3f42a79',
MetadataViews: '0x0ae53cb6e3f42a79',
MonsterMaker: '0x0ae53cb6e3f42a79',
FungibleToken: '0xee82856bf20e2aa6',
},
},
testnet: {
flowNetwork: 'testnet',
accessApi: 'https://rest-testnet.onflow.org',
walletDiscovery: 'https://fcl-discovery.onflow.org/testnet/authn',
walletDiscoveryApi: 'https://fcl-discovery.onflow.org/api/testnet/authn',
walletDiscoveryInclude: [
'0x82ec283f88a62e65', // Dapper Wallet
],
addresses: {
FlowToken: '0x7e60df042a9c0868',
NonFungibleToken: '0x631e88ae7f1d7c20',
MetadataViews: '0x631e88ae7f1d7c20',
MonsterMaker: '0xfd3d8fe2c8056370',
FungibleToken: '0x9a0766d93b6608b7',
},
},
mainnet: {
flowNetwork: 'mainnet',
accessApi: 'https://rest-mainnet.onflow.org',
walletDiscovery: 'https://fcl-discovery.onflow.org/authn',
walletDiscoveryApi: 'https://fcl-discovery.onflow.org/api/authn',
walletDiscoveryInclude: [
'0xead892083b3e2c6c', // Dapper Wallet
],
addresses: {
FlowToken: '0x1654653399040a61',
NonFungibleToken: '0x1d7e57aa55817448',
MetadataViews: '0x1d7e57aa55817448',
MonsterMaker: '',
FungibleToken: '0xf233dcee88fe0abe',
},
},
} as const;
type NetworksKey = keyof typeof NETWORKS;
export const NETWORK = NETWORKS[FLOW_ENV as NetworksKey];
https://fcl-discovery.onflow.org/testnet/authn
).
Note: There is also another way of connecting to wallets via the WalletConnect protocol. This is a chain agnostic protocol that allows a user to select a wallet from a list of wallets or scan a QR code to launch their wallet from their smartphone. WalletConnect can be added the list of wallets presented as part of wallet discovery. Code required for adding WalletConnect can be seen here.walletDiscoveryInclude
with the appropriate address for the flow environment as shown above.
Testnet: 0x82ec283f88a62e65
Mainnet: 0xead892083b3e2c6c
For more information see the FLOW documentation.The following function is provided for connecting the wallet through fcl.authenticate
const connect = useCallback(() => {
fcl.authenticate();
}, []);
Which can be called as follows:
const { connect } = useWeb3Context();
// ...
<Button
src="/images/ui/connect_button.png"
width={576}
height={208}
onClick={connect}
alt="Connect wallet"
/>
// ...
Executing a Script
After connecting your chosen wallet, you are presented with the Initialize screen (if you haven’t initialized previously). Initializing will ensure that your wallet has a MonsterMaker Collection that can be used to transfer a MonsterMaker NFT into your wallet. All NFTs have to exist within a collection - if your wallet does not have the required collection, then any transfer of NFTs to your wallet will fail.
To check whether or not a wallet has been initialized with a MonsterMaker Collection, a Cadence script is used to check if the user has a MonsterMaker Collection in their wallet.
A script on the FLOW blockchain is code that performs a read only transaction and does not modify the blockchain. In our web applications, scripts are Cadence code that are represented as strings that will be passed to the fcl.query
function:
const isInitialized = `
import NonFungibleToken from 0xNonFungibleToken
import MonsterMaker from 0xMonsterMaker
pub fun main(address: Address) : Bool {
let account = getAccount(address)
let vaultRef = account
.getCapability<&{NonFungibleToken.CollectionPublic}>(MonsterMaker.CollectionPublicPath)
.check()
return vaultRef
}
`;
export default isInitalized;
true
is returned otherwise false
.In MonsterMaker, calling a script is done using the executeScript
wrapper function. Here we check if the user has a MonsterMaker Collection initialized, and we redirect accordingly:
import isInitializedScript from 'cadence/scripts/isInitialized';
// ...
const [isInitialized, setIsInitialized] = useState<boolean | null>(null);
const { connect, user, executeScript } = useWeb3Context();
const checkIsInitialized = async () => {
try {
const res: boolean = await executeScript(
isInitalizedScript,
(arg: any, t: any) => [arg(user.addr, t.Address)],
);
setIsInitialized(res);
} catch (error) {
console.error(error);
}
};
useEffect(() => {
if (user.loggedIn) {
checkIsInitialized();
}
}, [user]);
useEffect(() => {
if (user.loggedIn && isInitialized === false) {
router.push(ROUTES.INITIALIZE);
} else if (user.loggedIn && isInitialized === true) {
router.push(ROUTES.CREATE);
}
}, [user, router, isInitialized]);
// ...
Behind the scenes, executeScript
is just a wrapper defined for convenience in the Web3ContextProvider
that is calling fcl.query
. If you wanted, you could call fcl.query
from right within your components.
const executeScript = useCallback(async (cadence: string, args: any = () => []) => {
try {
const res: any = await fcl.query({
cadence: cadence,
args,
});
return res;
} catch (error) {
console.error(error);
}
}, []);
If the wallet does not have a MonsterMaker Collection, then we will have to initialize the account through a Cadence transaction, shown in the next section.
Executing a Transaction
MonsterMaker has two transactions - the initialize transaction and minting transaction. Transactions modify the Flow Blockchain and require approval from the user before they are executed, as well as some gas in order to execute (gas is a small amount of Flow Token required to pay for the cost of executing the transaction). Executing transactions is very similar to scripts, but a transaction will go through different statuses and finally be SEALED if the transaction was successful.
Like scripts, transactions are Cadence code that are stored as strings that will be passed to the fcl.mutate
function:
const initAccount = `
import NonFungibleToken from 0xNonFungibleToken
import MonsterMaker from 0xMonsterMaker
import MetadataViews from 0xMetadataViews
transaction {
prepare(signer: AuthAccount) {
// if the account doesn't already have a collection
if signer.borrow<&MonsterMaker.Collection>(from: MonsterMaker.CollectionStoragePath) == nil {
// create a new empty collection
let collection <- MonsterMaker.createEmptyCollection()
// save it to the account
signer.save(<-collection, to: MonsterMaker.CollectionStoragePath)
// create a public capability for the collection
signer.link<&MonsterMaker.Collection{NonFungibleToken.CollectionPublic, MonsterMaker.MonsterMakerCollectionPublic, MetadataViews.ResolverCollection}>(MonsterMaker.CollectionPublicPath, target: MonsterMaker.CollectionStoragePath)
}
}
}
`;
export default initAccount;
Calling a transaction is simple:
const { executeTransaction, transaction } = useWeb3Context();
const handleInit = async () => {
await executeTransaction(initAccountTxn, () => [], {
limit: 9999,
});
};
useEffect(() => {
if (transaction.id !== null) {
router.push(ROUTES.CREATE);
}
}, [router, transaction]);
// ...
<Button
src="/images/ui/initialize_button.png"
width={640}
height={208}
onClick={handleInit}
alt="Initialize wallet"
/>
// ...
Note that here, no arguments are required for the transaction which is why we pass a function that returns an empty array (() ⇒ []
), and in order to ensure that the transaction executes, we pass in an option with a gas limit
set to 9999
. If the limit is a lower value, for example 10
, then if the transaction costs 50
, it will fail. Limits allow us to set a ceiling on how much gas we are willing to pay for a transaction.
Behind the scenes, executeTransaction
is a wrapper function for fcl.mutate
created for convenience in Web3ContextProvider
:
const executeTransaction = useCallback(
async (cadence: string, args: any = () => [], options: any = {}) => {
setTransactionInProgress(true);
setTransactionStatus(-1);
const transactionId = await fcl
.mutate({
cadence,
args,
payer: fcl.authz,
proposer: fcl.authz,
authorizations: [fcl.authz],
limit: options.limit || 50,
})
.catch((e: Error) => {
setTransactionInProgress(false);
setTransactionStatus(500);
setTransactionError(String(e));
});
if (transactionId) {
setTxId(transactionId);
fcl.tx(transactionId).subscribe((res: any) => {
setTransactionStatus(res.status);
setTransactionInProgress(false);
});
}
},
[],
);
fcl.query
is used for calling a script (ie cadence code that “queries” the blockchain), and fcl.mutate
is used for calling a transaction (ie cadence code that “mutates” the blockchain). Note that while the initAccountTxn
does not require any arguments, arguments can be passed into a transaction. In order to pass arguments to a cadence transaction, they are passed using a function that returns an array of arguments as follows:
executeTransaction(
cadenceScript,
(arg: any, t: any) => [
arg('0xf3792e919674928c', t.Address),
arg(1234, t.Int),
arg('string arg', t.String),
// etc...
],
});
After your wallet is initialized, you will be presented with the Minting screen:
Clicking Mint will call the minting transaction which will execute a Flow Transaction that will mint the NFT and transfer to your wallet address. The minter address is required, so there will be a call to the backend api /api/signAsMinter
. This will return the signature of the app created from the private key.
Minting the MonsterMaker NFT
The following Cadence Script will mint a MonsterMaker NFT:
const mintMonster = `
import NonFungibleToken from 0xNonFungibleToken
import MonsterMaker from 0xMonsterMaker
import MetadataViews from 0xMetadataViews
import FungibleToken from 0xFungibleToken
// This transction uses the NFTMinter resource to mint a new NFT.
//
// It must be run with the account that has the minter resource
// stored at path /storage/NFTMinter.
transaction(
recipient: Address,
background: Int,
head: Int,
torso: Int,
leg: Int
) {
// local variable for storing the minter reference
let minter: &MonsterMaker.NFTMinter
/// Reference to the receiver's collection
let recipientCollectionRef: &{NonFungibleToken.CollectionPublic}
/// Previous NFT ID before the transaction executes
let mintingIDBefore: UInt64
prepare(signer: AuthAccount) {
self.mintingIDBefore = MonsterMaker.totalSupply
// Borrow a reference to the NFTMinter resource in storage
self.minter = signer.borrow<&MonsterMaker.NFTMinter>(from: MonsterMaker.MinterStoragePath)
?? panic("Could not borrow a reference to the NFT minter")
// Borrow the recipient's public NFT collection reference
self.recipientCollectionRef = getAccount(recipient)
.getCapability(MonsterMaker.CollectionPublicPath)
.borrow<&{NonFungibleToken.CollectionPublic}>()
?? panic("Could not get receiver reference to the NFT Collection")
}
execute {
let componentValue = MonsterMaker.MonsterComponent(background: background, head: head, torso: torso, leg: leg)
// TODO: Add royalty feature to KI using beneficiaries, cuts, and descriptions. At the moment, we don't provide royalties with KI, so this will be an empty list.
let royalties: [MetadataViews.Royalty] = []
// mint the NFT and deposit it to the recipient's collection
self.minter.mintNFT(
recipient: self.recipientCollectionRef,
component: componentValue,
royalties: royalties
)
}
post {
self.recipientCollectionRef.getIDs().contains(self.mintingIDBefore): "The next NFT ID should have been minted and delivered"
MonsterMaker.totalSupply == self.mintingIDBefore + 1: "The total supply should have been increased by 1"
}
}
`;
export default mintMonster;
prepare
, execute
and post
.
prepare: The transaction values are set up and a reference to the wallet’s MonsterMaker collection is retrieved.
execute: The MonsterMaker component is created describing the background, head, etc the user selected, and the NFT is minted which will transfer the newly minted NFT to the user’s wallet.
post: A check is done to ensure that the supply has incremented correctly. Transactions go through several statuses, from Pending
to Sealed
. Once a transaction is submitted to the FLOW blockchain, is it not officially done until it is in the Sealed status.
This code snippet will call the minting api, check for when the status is sealed, and redirect the user once it is all complete:
// ...
const handleClickMint = async () => {
setIsMintInProgress(true);
const txId = await fcl.mutate({
cadence: mintMonster,
args: (arg: any, t: any) => [
arg(backgroundSelector.index, t.Int),
arg(headSelector.index, t.Int),
arg(torsoSelector.index, t.Int),
arg(legsSelector.index, t.Int),
arg(monsterPrice, t.UFix64),
],
authorizations: [fcl.currentUser, minterAuthz],
});
setTxId(txId);
};
useEffect(() => {
if (txId) {
fcl.tx(txId).subscribe(setTxStatus);
}
}, [txId]);
useEffect(() => {
if (txStatus?.statusString === 'SEALED') {
router.push(ROUTES.VIEW);
}
}, [txStatus, router]);
// ...
Once the transaction is Sealed, you will be redirected to the View page which will show the newly minted NFT, and any other NFTs you have previously minted:
The script for viewing the NFTs is as follows:
const getMonstersScript = `
import NonFungibleToken from 0xNonFungibleToken
import MetadataViews from 0xMetadataViews
import MonsterMaker from 0xMonsterMaker
pub struct Monster {
pub let name: String
pub let description: String
pub let thumbnail: String
pub let itemID: UInt64
pub let resourceID: UInt64
pub let owner: Address
pub let component: MonsterMaker.MonsterComponent
init(
name: String,
description: String,
thumbnail: String,
itemID: UInt64,
resourceID: UInt64,
owner: Address,
component: MonsterMaker.MonsterComponent
) {
self.name = name
self.description = description
self.thumbnail = thumbnail
self.itemID = itemID
self.resourceID = resourceID
self.owner = owner
self.component = component
}
}
pub fun getMonsterById(address: Address, itemID: UInt64): Monster? {
if let collection = getAccount(address).getCapability<&MonsterMaker.Collection{NonFungibleToken.CollectionPublic, MonsterMaker.MonsterMakerCollectionPublic}>(MonsterMaker.CollectionPublicPath).borrow() {
if let item = collection.borrowMonsterMaker(id: itemID) {
if let view = item.resolveView(Type<MetadataViews.Display>()) {
let display = view as! MetadataViews.Display
let owner: Address = item.owner!.address!
let thumbnail = display.thumbnail as! MetadataViews.HTTPFile
return Monster(
name: display.name,
description: display.description,
thumbnail: thumbnail.url,
itemID: itemID,
resourceID: item.uuid,
owner: address,
component: item.component
)
}
}
}
return nil
}
pub fun main(address: Address): [Monster] {
let account = getAccount(address)
let collectionRef = account.getCapability(MonsterMaker.CollectionPublicPath)!.borrow<&{NonFungibleToken.CollectionPublic}>()
?? panic("Could not borrow capability from public collection")
let ids = collectionRef.getIDs()
let monsters : [Monster] = []
for id in ids {
if let monster = getMonsterById(address: address, itemID: id) {
monsters.append(monster)
}
}
return monsters
}
`;
export default getMonstersScript;
Calling the getMonsters
script looks like this:
useEffect(() => {
const getMonsters = async () => {
const res: GetMonstersResponse = await executeScript(
getMonstersScript,
(arg: any, t: any) => [arg(user.addr, t.Address)],
);
setMonsters(res || []);
};
getMonsters();
}, [executeScript, user.addr]);
If you used Dapper Wallet, you can see the transactions executed, and the NFTs in your Inventory:
Adding App to Dapper Platform
In order to use a dApp with the Dapper Wallet and execute transactions in the Dapper ecosystem, there are additional steps.
Dapper Wallet requires transactions to be whitelisted to ensure that they are safe for users to execute - this allows Dapper to maintain quality control for the dApps that want to be part of the Dapper eco system. The basic steps to enable a dApp to interact with Dapper Wallet is to register an organization, create an app as part of that organization, and add all the required transactions for that app.
The following steps provide an overview of how to add an app to Dapper Platform. For an in depth guide see Setup with Dapper on Testnet.
- Gain access to the Developer Dashboard (staging / production) through your Customer Representative.
- Login to your organization on the Dapper Platform Developer Dashboard
- Add the app, in the case of this example, our app is MonsterMaker
- Add Information about the app
- Add any required contracts. In this case, we will include the MonsterMaker contract:
- Add any required transactions. Note there are two types of transactions: Custom and Purchase. A Custom transaction is any transaction that does not involve going through Dapper Wallet’s Purchase flow. A Purchase transaction is one that will go through Dapper Wallet’s purchase flow potentially involving a purchase via a user’s credit card.
Note that for the Code section, you will need to ensure that the Cadence code matches the code you are calling from the client character for character, otherwise the transaction will not be supported.
Also note that the contract addresses must match for the flow environment you are setting up (testnet/mainnet). If you are using aliases such as 0xMonsterMaker
, fcl
will replace with the appropriate address before executing the transaction.
Resources
FCL JS
FCL JS is the Javacript SDK for FCL. This SDK is integrated into the Monster Maker sample.
- FCL API Reference
- Flowscan on Testnet/Mainnet - evaluate transaction statuses and account transactions on chain.
- NFT Catalog - a catalog of registered NFTs on testnet/mainnet.
Useful Network Addresses
Contract | Emulator | Testnet | Mainnet |
FungibleToken | 0xee82856bf20e2aa6 | 0x9a0766d93b6608b7 | 0xf233dcee88fe0abe |
FlowToken | 0x0ae53cb6e3f42a79 | 0x7e60df042a9c0868 | 0x1654653399040a61 |
NonFungibleToken | 0x631e88ae7f1d7c20 | 0x1d7e57aa55817448 | |
MetadataViews | 0x631e88ae7f1d7c20 | 0x1d7e57aa55817448 |
See Flow Core Contracts for more information