Skip to main content

5.5 Creating an auction dapp

Advanced
Tutorial

Until this point in the developer ladder series, each tutorial has focused on a specific use case or feature of ICP. To wrap up the series, this tutorial will showcase how to create a general purpose dapp that provides a real-world use case. In this tutorial, you'll create the foundation for a simple auction dapp.

In this tutorial, you'll create a simple auction dapp that provides functions such as:

  • Opening and viewing auctions.

  • Bidding on auctions within a defined deadline.

  • Logging into the dapp with Internet Identity.

How does an auction work?

To create an auction, an item needs to be put up for sale. Once that item is for sale, potential buyers can place bids on how much they'd like to pay for the item. Usually, auctions last a few minutes or hours, providing buyers the opportunity to outbid one another. The buyer with the highest bid when the auction ends receives the item.

Auctions are traditionally used for selling items such as:

  • Real estate properties.

  • Automotive sales.

  • Overstock products.

  • Storage units.

  • Estate sales.

Auctions are also popularly used for charity events to raise money for an organization.

Creating an auction dapp

By creating an auction on a decentralized platform such as ICP, there is an immutable record of the users who bid on the item and who purchased the item. This allows for verifiable records for sales of high-value items or digital assets such as NFTs.

Prerequisites

Before you start, verify that you have set up your developer environment according to the instructions in 0.3 Developer environment setup.

Cloning the auction example

To get started, open a new terminal window, navigate into your working directory (developer_ladder), then use the following commands to clone auction example repo:

git clone https://github.com/luc-blaeser/auction
cd auction

Reviewing the project's files

This project was originally developed for a Motoko workshop at the KTH summer school. To provide additional context for the students in that workshop, this project's repo includes supplemental resources for learning Motoko. This tutorial will not review those resources, but it is recommended that you review them for additional context.

In this project's directory, you will see the following files and subdirectories:

├── Installation.md
├── Motoko_Tutorial.pdf
├── README.md
├── Structure.md
├── dfx.json
├── index.html
├── package-lock.json
├── package.json
├── src
│   ├── backend
│   │   └── AuctionServer.mo
│   └── frontend
│       ├── App.scss
│       ├── App.tsx
│       ├── AuctionDetail.scss
│       ├── AuctionDetail.tsx
│       ├── AuctionForm.scss
│       ├── AuctionForm.tsx
│       ├── AuctionList.scss
│       ├── AuctionList.tsx
│       ├── Navigation.scss
│       ├── Navigation.tsx
│       ├── assets
│       │   └── motoko.png
│       ├── common.ts
│       ├── index.scss
│       ├── main.tsx
│       └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

These project files are used for the following purposes:

  • Installation.md, ICP_Programming_Tutorial.pdf, and Structure.md: Supplemental learning resources.

  • src/backend/AuctionServer.mo: The backend canister's source code.

  • src/frontend: The frontend assets for the UI of the dapp.

  • dfx.json: The project's configuration file.

This project contains three canisters, as seen in the dfx.json file:

dfx.json
{
    "canisters": {
        "backend": {
            "type": "motoko",
            "main": "src/backend/AuctionServer.mo"
        },
        "frontend": {
            "dependencies": [
                "backend"
            ],
            "type": "assets",
            "frontend": {
                "entrypoint": "dist/index.html"
            },
            "source": [
                "dist/"
            ]
        },
        "internet_identity": {
            "type": "custom",
            "candid": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity.did",
            "wasm": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity_dev.wasm.gz",
            "remote": {
                "id": {
                    "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai"
                }
            },
            "frontend": {}
        }
    }
}
  • backend: The auction's backend canister, written in Motoko.

  • frontend: The auction's frontend canister, implemented using Typescript and React.

  • internet_identity: This canister is a local instance of the Internet Identity canister, and is built from the Candid and Wasm files from the latest DFINITY Internet Identity release.

Creating the backend canister

Next, open the src/backend/AuctionServer.mo file. This file will contain a template that includes placeholder functions; to provide full functionality in your dapp, you will need to replace the template code with functioning code. To do that, start by removing the existing code in the src/backend/AuctionServer.mo file and insert the following code that has been annotated to explain the code's logic:

src/backend/AuctionServer.mo
/// Import the necessary libraries:

import Principal "mo:base/Principal";
import Timer "mo:base/Timer";
import Debug "mo:base/Debug";
import List "mo:base/List";


/// Next, define the actor for the auction platform:

actor {
  /// Define a custom type for the auction item:
  type Item = {
    /// Define a title for the auction:
    title : Text;
    /// Define a description for the auction:
    description : Text;
    /// Define an image used as an icon for the auction:
    image : Blob;
  };

  /// Define a custom type for the auction's bid:
  type Bid = {
    /// Define the price for the bid using ICP as the currency:
    price : Nat;
    /// Define the time the bid was placed, measured as the time remaining in the auction:
    time : Nat;
    /// Define the authenticated user ID of the bid:
    originator : Principal.Principal;
  };

  /// Define a custom type for an auction ID to uniquely identify the auction:
  type AuctionId = Nat;

  /// Define an auction overview:
  type AuctionOverview = {
    id : AuctionId;
    /// Define the auction sold at the item:
    item : Item;
  };

  /// Define a custom type for the details of the auction:
  type AuctionDetails = {
    /// Item sold in the auction:
    item : Item;
    /// Bids submitted in the auction:
    bidHistory : [Bid];
    /// Time remaining in the auction:
    /// the auction winner.
    remainingTime : Nat;
  };

  /// Define a custom type for storing info about the auction:
  type Auction = {
    id : AuctionId;
    item : Item;
    var bidHistory : List.List<Bid>;
    var remainingTime : Nat;
  };

  /// Create a stable variable to store all auctions:
  stable var auctions = List.nil<Auction>();
  /// Create a stable variable for storing new auction IDs.
  stable var idCounter = 0;

  /// Define a function used to calculate the time remaining in the open auction:
  func tick() : async () {
    for (auction in List.toIter(auctions)) {
      if (auction.remainingTime > 0) {
        auction.remainingTime -= 1;
      };
    };
  };

  /// Execute a timer that calls the tick function every second:
  let timer = Timer.recurringTimer(#seconds 1, tick);

  /// Define a function to generating a new auction:
  func newAuctionId() : AuctionId {
    let id = idCounter;
    idCounter += 1;
    id;
  };

  /// Define a function to register a new auction that is open for the defined duration:
  public func newAuction(item : Item, duration : Nat) : async () {
    let id = newAuctionId();
    let bidHistory = List.nil<Bid>();
    let newAuction = { id; item; var bidHistory; var remainingTime = duration };
    auctions := List.push(newAuction, auctions);
  };

  /// Define a function to retrieve all auctions:
  /// Specific auctions can be separately retrieved by `getAuctionDetail`:
  public query func getOverviewList() : async [AuctionOverview] {
    func getOverview(auction : Auction) : AuctionOverview = {
      id = auction.id;
      item = auction.item;
    };
    let overviewList = List.map<Auction, AuctionOverview>(auctions, getOverview);
    List.toArray(List.reverse(overviewList));
  };

  /// Define an internal helper function to retrieve auctions by ID:
  func findAuction(auctionId : AuctionId) : Auction {
    let result = List.find<Auction>(auctions, func auction = auction.id == auctionId);
    switch (result) {
      case null Debug.trap("Inexistent id");
      case (?auction) auction;
    };
  };

  /// Define a function to retrieve detailed info about an auction using its ID:
  public query func getAuctionDetails(auctionId : AuctionId) : async AuctionDetails {
    let auction = findAuction(auctionId);
    let bidHistory = List.toArray(List.reverse(auction.bidHistory));
    { item = auction.item; bidHistory; remainingTime = auction.remainingTime };
  };

  /// Define an internal helper function to retrieve the minimum price for an auction's next bid; the next bid must be one unit of currency larger than the last bid:
  func minimumPrice(auction : Auction) : Nat {
    switch (auction.bidHistory) {
      case null 1;
      case (?(lastBid, _)) lastBid.price + 1;
    };
  };

  /// Make a new bid for a specific auction specified by the ID:
  /// Checks that:
  /// * The user (`message.caller`) is authenticated.
  /// * The price is valid, higher than the last bid, if existing.
  /// * The auction is still open.
  /// If valid, the bid is appended to the bid history.
  /// Otherwise, traps with an error.
  public shared (message) func makeBid(auctionId : AuctionId, price : Nat) : async () {
    let originator = message.caller;
    if (Principal.isAnonymous(originator)) {
      Debug.trap("Anonymous caller");
    };
    let auction = findAuction(auctionId);
    if (price < minimumPrice(auction)) {
      Debug.trap("Price too low");
    };
    let time = auction.remainingTime;
    if (time == 0) {
      Debug.trap("Auction closed");
    };
    let newBid = { price; time; originator };
    auction.bidHistory := List.push(newBid, auction.bidHistory);
  };
};

Starting a local replica

Before you can deploy the project locally, start the local replica:

dfx start --clean --background

Deploying the project

Deploy the project's canisters with the command:

npm run setup

In the background, this command runs the commands npm i && dfx canister create --all && dfx generate backend && dfx deploy.

Then, you can start the local development server with the command:

npm start

This command will return the local URL that the dapp is running at; by default, this will be http://localhost:3000/.

Using the dapp

It's time to use the auction dapp to create and bid on an auction! To get started, open the local URL that was returned by the npm start command, such as http://localhost:3000. You'll see the frontend of the dapp:

Auction 1

Then, select the 'Sign in' button to authenticate with a local Internet Identity. Need a reminder on how to create an Internet Identity? Review the 3.5 Identities and authentication level of the developer ladder. Once authenticated, your II principal will be shown in the dapp's UI:

Auction 2

Then, select 'Start New Auction' to create a new auction.

Auction 3

Give your auction a name, description, image, and set the amount of seconds the auction should last:

Auction 4

Then select 'Create new auction'. The auction will now be listed under 'List auctions'. You can view the auction's details by selecting 'Auction details' under the auction's image:

Auction 5

From the auction details screen, you can place a bid on the auction under 'New bid'. Enter the amount you'd like to bid, then select 'Bid ICP'.

Auction 6

Once your bid is placed, the window will show the current bid and show the II principal that placed the bid, plus the time in the auction when it was placed.

Auction 7

If you scroll down, you will see the option to place another bid, followed by the bidding history so far:

Auction 8

Need help?

Did you get stuck somewhere in this tutorial, or feel like you need additional help understanding some of the concepts? The ICP community has several resources available for developers, like working groups and bootcamps, along with our Discord community, forum, and events such as hackathons. Here are a few to check out:

Next steps