diff --git a/modules/ROOT/nav.adoc b/modules/ROOT/nav.adoc index 995a56c74..58be93747 100644 --- a/modules/ROOT/nav.adoc +++ b/modules/ROOT/nav.adoc @@ -14,6 +14,7 @@ *** link:https://github.com/dfinity/examples[Dfinity Examples^] *** link:https://github.com/dfinity/awesome-dfinity[Open-source Community Projects^] *** xref:examples:hackathon-projects.adoc[Hackathon Projects] +*** xref:examples:codelabs.adoc[CodeLabs] * Developer Docs ** Getting Started diff --git a/modules/examples/images/3bd3a2a8bbbc3902.png b/modules/examples/images/3bd3a2a8bbbc3902.png new file mode 100644 index 000000000..205feef51 Binary files /dev/null and b/modules/examples/images/3bd3a2a8bbbc3902.png differ diff --git a/modules/examples/images/82c8493b03d8157d.png b/modules/examples/images/82c8493b03d8157d.png new file mode 100644 index 000000000..32ee1a8f2 Binary files /dev/null and b/modules/examples/images/82c8493b03d8157d.png differ diff --git a/modules/examples/images/af3e45eb47eb3f14.png b/modules/examples/images/af3e45eb47eb3f14.png new file mode 100644 index 000000000..4e429b25c Binary files /dev/null and b/modules/examples/images/af3e45eb47eb3f14.png differ diff --git a/modules/examples/images/d71d39c63ca9f522.png b/modules/examples/images/d71d39c63ca9f522.png new file mode 100644 index 000000000..1a79b530f Binary files /dev/null and b/modules/examples/images/d71d39c63ca9f522.png differ diff --git a/modules/examples/images/efd35606ec992f9.png b/modules/examples/images/efd35606ec992f9.png new file mode 100644 index 000000000..349b331c5 Binary files /dev/null and b/modules/examples/images/efd35606ec992f9.png differ diff --git a/modules/examples/images/f824214c6a3e694a.png b/modules/examples/images/f824214c6a3e694a.png new file mode 100644 index 000000000..0b87ffb75 Binary files /dev/null and b/modules/examples/images/f824214c6a3e694a.png differ diff --git a/modules/examples/pages/codelabs.adoc b/modules/examples/pages/codelabs.adoc new file mode 100644 index 000000000..422a7174c --- /dev/null +++ b/modules/examples/pages/codelabs.adoc @@ -0,0 +1,290 @@ += CodeLabs +:description: CodeLab tutorials +:keywords: Internet Computer,blockchain,cryptocurrency,ICP tokens,smart contracts,cycles,wallet,software canister,developer onboarding,dapp,example,code,rust,Motoko +:proglang: Motoko +:IC: Internet Computer +:company-id: DFINITY +ifdef::env-github,env-browser[:outfilesuffix:.adoc] + +The CodeLab tutorials provide step-by-step guides to how to build dapps, how to use specific features, use Motoko and much more. + +++++ + + +
The Internet Computer features orthogonal persistence, which means the state of the canisters are automatically stored, so data persist when canister code is updated. Data can be stored in stable variables instead of traditional databases.
+For variables to be stable, the type has to be stable. Number is a stable type, but types like objects are generally not stable types, but there is a way to work with making objects stable.
+This CodeLab shows how to make both stable types and types like objects stable, so data is persistence and survive canister code upgrades.
+ + +The CodeLab "Minimalistic Motoko Dapp" is a good example of a simple Dapp, where the stable variable can be implemented. As the Dapp is, the counter variable will be reset when the canister code is upgraded.
+This behaviour may be acceptable in a Dapp like this, but if the data is more sensitive and must be retained, let's say the Dapp is logging the value and wants it to persist, we need to make the variable stable.
+The "Minimalistic Motoko Dapp" Motoko code looks like this:
+actor {
+
+ var counter : Nat = 0;
+
+ public func increment() : async Nat {
+ counter += 1;
+ return counter;
+ };
+
+ public query func get() : async Nat {
+ return counter;
+ };
+
+ public func reset() : async Nat {
+ counter := 0;
+ return counter;
+ };
+};
+
+In this example the variable is a stable type, which means it's predictable what the type will be - in this case the variable is number. Therefore making the variable is easy, stable
is just added to the variable declaration:
stable var counter : Nat = 0;
+
+Now the counter value will persist, and not be reset if the canister code is upgraded.
+ + +As mentioned in the introduction, persistence of the data requires stable data types. Types like objects are generally not stable, but there is a need to have stable objects. Think about user accounts, historical data with timestamps etc.
+The way these types are made stable is, to copy non-stable data to a stable variable right before the canister code is upgraded, and once the upgrade has been completed, the non-stable data is loaded back into the non-stable variable.
+Copying the non-stable data to and from the stable variable is made easy by using system hooks, which will be triggered pre and post upgrades.
+system func preupgrade() { };
+
+system func postupgrade() { };
+
+The preupgrade
method can make final updated before the runtime commits values to the Internet Computer's stable memory before the canister upgrade. The postupgrade
method runs after the upgrade has been completed, and after the stable variables have been populated.
To demonstrate how this works, let's look at a code example:
+actor Registry {
+
+ stable var entries : [(Text, Nat)] = [];
+
+ let map = Map.fromIter<Text,Nat>(
+ entries.vals(), 10, Text.equal, Text.hash);
+
+ public func register(name : Text) : async () {
+ switch (map.get(name)) {
+ case null {
+ map.put(name, map.size());
+ };
+ case (?id) { };
+ }
+ };
+
+ public func lookup(name : Text) : async ?Nat {
+ map.get(name);
+ };
+
+ system func preupgrade() {
+ entries := Iter.toArray(map.entries());
+ };
+
+ system func postupgrade() {
+ entries := [];
+ };
+}
+
+The code has two public functions, which can register a name and an ID, and lookup stored registered names. Think of this as a simple user registry.
+First the stable variable, in this case an array, is defined. This is the variable we are using to hold the non-stable data during the upgrade.
+stable var entries : [(Text, Nat)] = [];
+
+The registered names and IDs are stored in the map
variable, and the default values are loaded from the entries
when it's being declared. If entries
is empty, map
will also be empty.
let map = Map.fromIter<Text,Nat>(
+ entries.vals(), 10, Text.equal, Text.hash);
+
+The entries
stable variable is populated with the map
data right before the upgrade using the preupgrade
method, to ensure the data is current.
system func preupgrade() {
+ entries := Iter.toArray(map.entries());
+};
+
+The postupgrade
method can be used to clear the entries
array so it's not holding stale data next time the canister code is upgraded
system func postupgrade() {
+ entries := [];
+};
+
+
+
+ The stable variable storage makes it safe to store Dapp data in the canister, and preupgrade
and postupgrade
in Motoko provides system methods to retain data in the canister, but it's up to you as a developer how you wish to implement data storage and data persistence.
This CodeLab shows how to build a minimalistic dapp based on the default dapp template installed by DFX when creating a new project. The dapp is a simple website with a counter. Every time a button is pressed, a counter is incremented.
+This CodeLab covers:
+The frontend will look like this:
+Run this command to create project:
+$ dfx new minimal_dapp
+
+DFX will create a new directory called minimal_dapp, and in this directory you will find all the files, both frontend, backend, configurations etc. for the default project. The default project can be deployed without any changes as it is.
+The src
directory will contain the default frontend and backend code.
The dfx.json
file contains the canister configuration. It defines the canister(s), where the source code for the canister(s) is located, the type of canister(s) and which version of DFX the project was created with.
As the first step, add a few backend functions. The backend functions are located in the src/minimal_dapp/main.mo
Motoko file.
The existing code from the default project is not needed, so the greet() function is deleted.
+actor {
+
+}
+
+Three functions are created to make the counter work: count(), getCount() and reset(). The current counter value is stored as a number in the actor.
+actor {
+ var counter : Nat = 0;
+}
+
+The count()
function increments the counter variable. This function is envoked when the user is clicking the button on the frontend, or when the function is called through the Candid interface.
public func count() : async Nat {
+ counter += 1;
+ return counter;
+};
+
+The function is returning the incremented counter variable.
+The getCount()
function returns the current counter value.
public query func getCount() : async Nat {
+ return counter;
+};
+
+The reset()
function resets the counter value to 0 and returns the value.
public func reset() : async Nat {
+ counter := 0;
+ return counter;
+};
+
+The main.mo
file looks like this when it's all put together:
actor {
+
+ var counter : Nat = 0;
+
+ public func count() : async Nat {
+ counter += 1;
+ return counter;
+ };
+
+ public query func getCount() : async Nat {
+ return counter;
+ };
+
+ public func reset() : async Nat {
+ counter := 0;
+ return counter;
+ };
+};
+
+
+
+ At this point the backend can be deployed and its functionality can be tested. The backend can be tested in different ways, and in this step the backend is tested by making requests through DFX calls and by using the web interface created by Candid.
+First the dapp has to be deployed, which is done locally for this CodeLab. The local network is started by running this command:
+$ dfx start --background
+
+When the local network is up and running, run this command to deploy the canisters:
+$ dfx deploy
+
+DFX has a subset of commands for canister operations, and one of them enables calling the public functions added to the main.mo
file in the previous step. In the following examples the initial value is 0. count
will increment value and return 1, getCount
will return the current value and reset
will set the value to 0.
Command usage: dfx canister call <project> <function>
$ dfx canister call minimal_dapp count
+(1 : Nat)
+
+$ dfx canister call minimal_dapp getCount
+(1 : Nat)
+
+$ dfx canister call minimal_dapp reset
+(0 : Nat)
+
+The Candid UI provides an easy, user friendly interface for testing the backend. The UI is automatically generated, and the canister ID can be found in the canister_ids.json
file.
The localhost version of the canister_ids.json
file can be found in .dfx/local/canister_ids.json
and the URL is:
http://<candid_canister_id>.localhost:8000/?id=<backend_canister_id>
+The default project has an index.html
file with page HTML and an index.js
file with an implementation of the backend functions.
For this CodeLab the changes to the index.html
file is minor. The button is kept and so is the section showing the result, just simplified.
<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width">
+ <title>hack</title>
+ <base href="/">
+
+ <link type="text/css" rel="stylesheet" href="main.css" />
+ </head>
+ <body>
+ <img src="logo.png" alt="DFINITY logo" />
+ <section>
+ <button id="clickMeBtn">Click Me!</button>
+ </section>
+ <section id="counter"></section>
+ </body>
+</html>
+
+The existing event listener for button click is modified to call the count()
function, and an event listener for page load is added to get the initial value of the counter with getCount()
. The backend functions are still imported through the Candid interface.
import { minimaldapp } from "../../declarations/minimal_dapp";
+
+document.addEventListener('DOMContentLoaded', async function () {
+ const counter = await minimaldapp.getCount();
+ document.getElementById("counter").innerText = "Counter: " + counter;
+})
+
+document.getElementById("clickMeBtn").addEventListener("click", async () => {
+ const counter = await minimaldapp.count();
+ document.getElementById("counter").innerText = "Counter: " + counter;
+});
+
+
+
+ The canisters must be re-deployed since the frontend has changed since the deployment of the backend changes in step . Assuming the local network is still running, re-deploy with this command:
+$ dfx deploy
+
+The URL for the frontend is depending on the canister ID. As described step 4, get the canister ID, the UI canister in this case, from the canister_IDs.json file. The URL will look like this:
+https://<ui_canister_id>.localhost:8000
+This CodeLab walks through the very basic steps of creating and deploying a dapp locally, using Motoko and HTML/Javascript.
+For information about deploying the dapp to the Internet Computer, see the documentation here.
+ + +This CodeLab shows how to build a minimalistic dapp based on the default dapp template installed by DFX when creating a new Rust project. The dapp is a simple website with a counter. Every time a button is pressed, a counter is incremented.
+This CodeLab covers:
+The frontend will look like this:
+Run this command to create project:
+$ dfx new --type=rust minimal_rust_dapp
+
+DFX will create a new directory called minimal_rust_dapp, and in this directory you will find all the files, both frontend, backend, configurations etc. for the default project. The default project can be deployed without any changes as it is.
+The src
directory will contain the default frontend and backend code.
The dfx.json
file contains the canister configuration. It defines the canister(s), where the source code for the canister(s) is located, the type of canister(s) and which version of DFX the project was created with.
As the first step, add a few backend functions. The backend functions are located in the src/minimal_rust_dapp/lib.rs
Rust file.
The existing code from the default project is not needed, so the greet() function is deleted.
+Two functions are created to make the counter work: get() and increment(). The current counter value is stored as a number.
+static mut COUNTER: u64 = 0;
+
+The get()
function returns the current counter value.
#[ic_cdk_macros::query]
+fn get() -> u64 {
+ unsafe { COUNTER }
+}
+
+The increment()
function increments the counter variable. This function is envoked when the user is clicking the button on the frontend, or when the function is called through the Candid interface.
#[ic_cdk_macros::update]
+fn increment() -> u64 {
+ unsafe {
+ COUNTER += 1;
+ COUNTER
+ }
+}
+
+The function is returning the incremented counter variable.
+The main.mo
file looks like this when it's all put together:
static mut COUNTER: u64 = 0;
+
+#[ic_cdk_macros::query]
+fn get() -> u64 {
+ unsafe { COUNTER }
+}
+
+#[ic_cdk_macros::update]
+fn increment() -> u64 {
+ unsafe {
+ COUNTER += 1;
+ COUNTER
+ }
+}
+
+
+
+ After modifying the backend, the Candid interface must be modified to match the backend. The Candid interface is located in the src/minimal_rust_dapp/minimal_rust_dapp.did
Candid file.
Remove the existing code in the Candid interface file, and replace it with the following:
+service : {
+ "get": () -> (nat64) query;
+ "increment": () -> (nat64);
+}
+
+
+
+ At this point the backend can be deployed and its functionality can be tested. The backend can be tested in different ways, and in this step the backend is tested by making requests through DFX calls and by using the web interface created by Candid.
+First the dapp has to be deployed, which is done locally for this CodeLab. The local network is started by running this command:
+$ dfx start --background
+
+When the local network is up and running, run this command to deploy the canisters:
+$ dfx deploy
+
+DFX has a subset of commands for canister operations, and one of them enables calling the functions added to the lib.rs
file in the previous steps. In the following examples the initial value is 0. increment
will increment value and return 1 and get
will return the current value.
Command usage: dfx canister call <project> <function>
$ dfx canister call minimal_rust_dapp increment
+(1 : Nat)
+
+$ dfx canister call minimal_rust_dapp get
+(1 : Nat)
+
+The Candid UI provides an easy, user friendly interface for testing the backend. The UI is automatically generated, and the canister ID can be found in the canister_ids.json
file.
The localhost version of the canister_ids.json
file can be found in .dfx/local/canister_ids.json
and the URL is:
http://<candid_canister_id>.localhost:8000/?id=<backend_canister_id>
+The default project has an index.html
file with page HTML and an index.js
file with an implementation of the backend functions.
For this CodeLab the changes to the index.html
file is minor. The button is kept and so is the section showing the result, just simplified.
<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width">
+ <title>hack</title>
+ <base href="/">
+
+ <link type="text/css" rel="stylesheet" href="main.css" />
+ </head>
+ <body>
+ <img src="logo.png" alt="DFINITY logo" />
+ <section>
+ <button id="clickMeBtn">Click Me!</button>
+ </section>
+ <section id="counter"></section>
+ </body>
+</html>
+
+The existing event listener for button click is modified to call the increment()
function, and an event listener for page load is added to get the initial value of the counter with get()
. The backend functions are still imported through the Candid interface.
import { minimaldapp } from "../../declarations/minimal_rust_dapp";
+
+document.addEventListener('DOMContentLoaded', async function () {
+ const counter = await minimaldapp.get();
+ document.getElementById("counter").innerText = "Counter: " + counter;
+})
+
+document.getElementById("clickMeBtn").addEventListener("click", async () => {
+ const counter = await minimaldapp.increment();
+ document.getElementById("counter").innerText = "Counter: " + counter;
+});
+
+
+
+ The canisters must be re-deployed since the frontend has changed since the deployment of the backend changes in step . Assuming the local network is still running, re-deploy with this command:
+$ dfx deploy
+
+The URL for the frontend is depending on the canister ID. As described step 4, get the canister ID, the UI canister in this case, from the canister_IDs.json file. The URL will look like this:
+https://<ui_canister_id>.localhost:8000
+This CodeLab walks through the very basic steps of creating and deploying a dapp locally, using Motoko and HTML/Javascript.
+For information about deploying the dapp to the Internet Computer, see the documentation here.
+ + +This CodeLab shows a very simple implementation of NFT minting. The project does not include functionality for payment transactions or an UI, but the functionality can be tested through the Candid interface.
The project has the following functionality:
Other features like payment, approvals and file uploads will be covered in future CodeLabs, as an extension of this project.
+ + +Before you start this tutorial, verify the following:
+This project is inspired by the ERC721 token standard, and the standard's metadata format is being used. The functions are not strictly following the ERC721 standard.
+{
+ "title": "Asset Metadata",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Identifies the asset to which this NFT represents"
+ },
+ "description": {
+ "type": "string",
+ "description": "Describes the asset to which this NFT represents"
+ },
+ "image": {
+ "type": "string",
+ "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
+ }
+ }
+}
+
+For more information about the ERC721 standard see here
+ + +In this simplified NFT dapp uploading an image, and filling out name and description in a form in an UI, is not covered. Instead it's assumed that the URL for the image is already known.
+Minting the NFT is done in two steps. First the metadata is stored with a consecutive token ID. The next step is to register the token ID with the owner ID.
+Two HashMaps are used to store the metadata and the registry with token and owner IDs. The concurrent token ID is stored as a Nat32.
+ private stable var _registryState : [(TokenIndex, Owner)] = [];
+ private var _registry : HashMap.HashMap<TokenIndex, Owner> = HashMap.fromIter(_registryState.vals(), 0, Core.TokenIndex.equal, Core.TokenIndex.hash);
+
+ private stable var _tokenState : [(TokenIndex, Metadata)] = [];
+ private var _token : HashMap.HashMap<TokenIndex, Metadata> = HashMap.fromIter(_tokenState.vals(), 0, Core.TokenIndex.equal, Core.TokenIndex.hash);
+
+ private stable var _nextTokenId : TokenIndex = 0;
+
+The metadata is defined as described in the previous step, following the ERC721 token standard, and it's implemented like this:
+public type Properties = {
+ kind : Text;
+ description : Text;
+};
+
+public type Property = {
+ name : Properties;
+ description : Properties;
+ image : Properties;
+};
+
+public type Metadata = {
+ title : Text;
+ kind : Text;
+ properties : Property;
+};
+
+The function will take the parameter to
, which is the owner of the NFT, and the metadata parameters name
, description
and tokenURI
.
First the metadata variable is populated with the parameter values, and then the metadata is stored in the token HashMap with the next token ID as the key and the metadata as the value.
+After storing the token metadata, the relationship between the owner and the token is stored in the registry HashMap, with the token ID as the key and the owner ID as the value. Finally the concurrent token ID is incremented.
+public func mintNFT(to: Owner, name: Text, description: Text, tokenURI: Text) : async TokenIndex {
+
+ let tokenId = _nextTokenId;
+
+ let _name : Properties = {
+ kind = "string";
+ description = name;
+ };
+
+ let _description : Properties = {
+ kind = "string";
+ description = description;
+ };
+
+ let _image : Properties = {
+ kind = "string";
+ description = tokenURI;
+ };
+
+ let _properties : Property = {
+ name = _name;
+ description = _description;
+ image = _image;
+ };
+
+ let metadata : Metadata = {
+ title = "Asset Metadata";
+ kind = "object";
+ properties = _properties;
+ };
+
+ _token.put(tokenId, metadata);
+ _registry.put(tokenId, to);
+ _nextTokenId := _nextTokenId + 1;
+
+ return tokenId;
+};
+
+The token ID will be returned on success.
+Deploy the project with dfx deploy
(locally) and use dfx canister call to test the function:
dfx canister call ic_simple_nft mintNFT '(principal "xxxxx-...-xxx", "My NFT", "My first NFT", "http://link-to-nft.com/img.gif")'
+
+
+
+ Once created, the NFT's ownership can be transferred to a new owner. The transfer()
function takes the three parameters from
, to
and tokenId
and will register the to
owner as the new owner of the NFT.
public func transfer(from: Owner, to: Owner, tokenId: TokenIndex) : async TransferResponse {
+
+ switch (_registry.get(tokenId)) {
+ case (?token_owner) {
+ if(Principal.equal(from, to)) {
+ return #err(#InvalidToken(to));
+ };
+
+ let prev = _registry.replace(tokenId, to);
+ return #ok(?to);
+ };
+ case (_) {
+ return #err(#InvalidToken(to));
+ };
+ };
+};
+
+First the function checks if the NFT's token ID exists in the registry. If it does, and if from
and to
are not the same, the registry is updated, and the NFT has transferred the NFT to a different owner.
Deploy the project with dfx deploy
(locally) and use dfx canister call to test the function:
dfx canister call ic_simple_nft transfer '(principal "xxxxx-...-xxx", principal "yyyyy-...-yyy", 20)'
+
+
+
+ The function ownerOf
simply looks up the owner of a given NFT. This can be useful in many ways and in this small sample project the function can be used to verify a transfer was successfully.
public func ownerOf(tokenId : TokenIndex) : async ?Owner {
+
+ let owner = _registry.get(tokenId);
+
+ return owner;
+};
+
+Deploy the project with dfx deploy
(locally) and use dfx canister call to test the function:
dfx canister call ic_simple_nft ownerOf 20
+
+
+
+ The full code for the project in this CodeLab can be found here
+ + +This CodeLab shows how to build a minimalistic, static website and deploy it on the Internet Computer. Since this is a very simple project, without any backend, most of the files the default project comes with when running dfx new ...
is not needed, and therefore this project is created manually with only the needed files.
The website is really simple, it consists of a HTML file, CSS file and a PNG file. All it does is displaying a logo on the website, but the idea would be the same for more advanced static websites.
+The content of the HTML and CSS files:
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Static Website</title>
+ <base href="/">
+ <link type="text/css" rel="stylesheet" href="styles.css" />
+ </head>
+ <body>
+ <img src="logo.png" alt="DFINITY logo" />
+ </body>
+</html>
+
+img {
+ max-width: 50vw;
+ max-height: 25vw;
+ display: block;
+ margin: auto;
+}
+
+The file structure can look like this, where assets and source code is separated:
+└── assets
+ ├── assets
+ │ └── styles.css
+ │ └── logo.png
+ └── src
+ └── index.html
+
+
+
+ The dfx.json file is a configuration file which specifies the canister(s) used for the dapp. In this case only one canister is needed, and besides the canister configuration, dfx.json
also includes information about DFX version, build settings and network settings.
{
+ "canisters": {
+ "www": {
+ "frontend": {
+ "entrypoint": "assets/src/index.html"
+ },
+ "source": [
+ "assets/assets",
+ "assets/src"
+ ],
+ "type": "assets"
+ }
+ },
+ "defaults": {
+ "build": {
+ "args": "",
+ "packtool": ""
+ }
+ },
+ "dfx": "0.8.3",
+ "networks": {
+ "local": {
+ "bind": "127.0.0.1:8000",
+ "type": "ephemeral"
+ }
+ },
+ "version": 1
+}
+
+
+
+ First the local network has to be started, and it is started by running this command:
+$ dfx start --background
+
+When the local network is up and running, run this command to deploy the canisters:
+$ dfx deploy
+
+To go to the deployed website, the canister ID is needed. The canister ID is shown in the output from the deployment, but it can alse be found in the canister_ids.json
file.
The localhost version of the canister_ids.json
file can be found in .dfx/local/canister_ids.json
and the URL for the website is:
http://<canister_id>.localhost:8000
+ + +Deploying the website to the IC is not very different from deploying locally. The command dfx deploy
is also used for deployment on the IC, but with the added network parameter:
$ dfx deploy --network ic
+
+The canister must have cycles assigned in order to deploy, see how to add cycles to the canister in the documentation here.
+ + +