Exploring Create-Aptos-DApp

Osikhena OshomahOsikhena Oshomah
19 min read

This article will explore create-aptos-dapp, a powerful tool designed to bootstrap Aptos Move projects. It will guide you through the initial setup and demonstrate how to create a simple auction project where buyers can place bids using Aptos' native token, APT.

Additionally, the article covers how to send transactions to the Aptos blockchain and retrieve data using the Aptos TypeScript SDK. Finally, it will analyze the project structure generated by create-aptos-dapp.

Prerequisites

Install the following software before using the create-aptos-dapp CLI tool:

Bootstrapping an auction project

Create a folder for the project, open it in the terminal, and install the create-aptos-dapp CLI by running this code:

npx create-aptos-dapp@latest

The code opens a dialog on the command line where you can set up your project.

Listed below is the dialogue the tool displays with the suggested inputs:

  1. Enter project name

    Enter "auction-contract".

  2. Choose how to start

    Select the Boilerplate template, which sets up a complete minimalistic smart contract example. You will modify this template to create your DApp.

  3. Choose your network

    Select testnet.

  4. Help us improve the Aptos tool by collecting anonymous information.

    Optional.

  5. Do you want to make any changes to your selection?

    No.

After setting up the project, the tool installs all dependencies, and you are ready to start exploring.

Understanding the folder structure

The create-aptos-dapp tool creates a TypeScript project integrated with a smart contract backend. Look at the project folder and note the frontend, move, and scripts folders. You can see the structure in Figure 1.

Figure 1: Folder structure

The frontend folder contains the application's frontend code. It has two subfolders: entry-functions and view-functions.

The entry-functions folder contains functions for sending transactions to the smart contract. In this boilerplate template, it includes two files: transferAPT.ts and writeMessage.ts.

You place any function interacting with and sending transactions to the Aptos blockchain in the entry-functions folder and functions that pull data from the blockchain in the view-functions folder.

The move folder consists of a sources folder where the move smart contract backend is located. You place any smart contract here: move/sources/my_smart_contract.move.

There is also a move.toml configuration file with entries for addresses and project dependencies.

The scripts folder contains NPM scripts. It includes an init.js file and a move folder, which contains the scripts compile.js, publish.js, test.js, and upgrade.js.

Running the project

To start the project's dev server, run npm run dev in the project directory's command line.

Next, you need to deploy the smart contract to the Aptos blockchain. But before deploying, you must initialize the project.

Run the following command in the command line:

npm run move:init

This command does the following:

When run for the first time, it downloads the latest version of the Aptos CLI to the project as can be seen in Figure 2.

Figure 2: Downloading the CLI to the project.

Next, it creates a private key for the account you will use to deploy the project. If you don't provide a private key, it creates an account for you and funds it with 1 APT.

Figure 3: Creating a private key for default account profile

Figure 4: Success message after running the init script

After successfully running the init script, you have an .aptos folder in the project that contains a config.yaml file with your deployment profile.

To deploy the boilerplate template project, run the following command:

npm run move:publish

This will compile the project and ask if you want to publish the package at an object address and if you want to send a transaction, which will cost you a gas fee paid in Octas (the smallest unit of APT: 1 APT = 100,000,000 Octas).

Figure 5: Smart contract deployed success message
At this point, you successfully deployed the template and can interact with the application from the frontend.

Creating the auction smart contract

So far, you have shown how to create a complete Aptos Move project using the boilerplate template. Now, you will customize the boilerplate template to create the auction smart contract.

First, delete the message board smart contract file at move/sources/message_board.move and create a new file in the same location named move/sources/auction_contract.move.

Copy and paste the following code into the auction_contract.move file. Then, open the Move.toml file and change message_board_addr to auction. This gives the deployer address the alias auction.

module auction::auction_contract {
    use std::signer;
    use std::string::{Self, String};
    use std::option::{Self, Option};
    use aptos_framework::timestamp;
    use aptos_framework::aptos_account;
    use aptos_framework::account::SignerCapability;
    use aptos_framework::account;
    use aptos_framework::coin;
    use aptos_framework::aptos_coin::AptosCoin;
    #[test_only]
    use aptos_framework::stake;
 //error constant
    const ERR_OBJECT_DONT_EXIST: u64 = 700;
    const ERR_BID_SMALLER_THAN_HIGHEST_BID: u64 = 705;
    const ERR_AUCTION_TIME_LAPSED: u64 = 706;
    const ERR_AUCTION_ENDED: u64 = 707;
    const ERR_AUCTION_TIME_NOT_LAPSED: u64 = 708;
    const ERR_AUCTION_STILL_RUNNING: u64 = 709;
    const ERR_NOT_THE_OWNER: u64 = 710;
    const ERR_BID_TOO_SMALL: u64 = 711;
    const ERR_AUCTION_NOT_ENDED: u64 = 712;
    const WALLET_SEED: vector<u8> = b"Wallet seed for the object";
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
    struct AuctionData has key {
       seller: address,
       start_price: u64,
       highest_bidder: Option<address>,
       highest_bid: Option<u64>,
       auction_end_time: u64,
       auction_ended: bool,
       auction_url: String
    }

   struct AuctionDataDetails has copy, drop {
         seller: address,
         start_price: u64,
         highest_bidder: Option<address>,
         highest_bid: Option<u64>,
         auction_end_time: u64,
         auction_ended: bool,
         auction_url: String
}
    #[resource_group_member(group = aptos_framework::object::ObjectGroup)]
       struct SignerCapabilityStore has key {
            signer_capability: SignerCapability,
    }

    fun init_module(creator: &signer) {
        create_contract_resource(creator);
        create_new_auction(creator, 86400);
    }

    fun create_contract_resource(creator: &signer){
      //create a resource account to hold the contract funds
    let (_, signer_capability) = account::create_resource_account(creator,WALLET_SEED);
     move_to(creator, SignerCapabilityStore { signer_capability });
    }

    fun get_auction_signer(): signer acquires SignerCapabilityStore {
    let signer_capability = &borrow_global<SignerCapabilityStore>(@auction).signer_capability;
    account::create_signer_with_capability(signer_capability)
    }

    fun create_new_auction(creator: &signer, bid_time: u64) {
    //create a resource account to hold the contract funds
         move_to(creator, AuctionData {seller: @auction ,
                 start_price: 2000000,
                 highest_bidder: option::none(),
                 highest_bid: option::none(),
                 auction_end_time:
                 timestamp::now_seconds() + bid_time,
                 auction_ended: false,
                 auction_url: string::utf8(b"https://img.freepik.com/premium-photo/stunning-highresolution-depiction-krom-god-war-image-is-seamless-blend-photo_1164885-2776.jpg?w=1060")}
        )
    }

    public entry fun place_auction_bid(
            bidder: &signer,
            bid_amount: u64
        ) acquires AuctionData, SignerCapabilityStore {
        let auction_data = borrow_global_mut<AuctionData>(@auction);
        let auction_signer = get_auction_signer();
        let resource_account_address = signer::address_of(&auction_signer);        
        let bidder_address = signer::address_of(bidder);
        check_auction_ended(auction_data.auction_end_time);
        if (auction_data.auction_ended) {
            abort (ERR_AUCTION_ENDED)
        };
        if ( auction_data.start_price > bid_amount ){
                abort (ERR_BID_TOO_SMALL)
        };
    //check if the bid is greater than the existing highest bid
       let former_highest_bid: u64 = 0;
       if (option::is_some(&auction_data.highest_bid) &&   option::is_some(&auction_data.highest_bidder)) {
       former_highest_bid = *option::borrow(&auction_data.highest_bid);
    };

    //check if the user bid is greater than the former bid

    if (former_highest_bid > bid_amount) {
         abort (ERR_BID_SMALLER_THAN_HIGHEST_BID)
    };

    if ( former_highest_bid > 0 ){
         //make refunds here
    let former_highest_bidder = *option::borrow(&auction_data.highest_bidder);
    aptos_account::transfer(&auction_signer, former_highest_bidder, former_highest_bid);
    };
    auction_data.highest_bid = option::some(bid_amount);
    auction_data.highest_bidder = option::some(bidder_address);
    //pay the auction bid amount here
    aptos_account::transfer(bidder, resource_account_address, bid_amount);
    }


    fun check_auction_ended(auction_end_time: u64){
           if (timestamp::now_seconds() > auction_end_time) {
                   abort (ERR_AUCTION_TIME_LAPSED)
           };
    }
//close_auction
    public entry fun close_auction() acquires AuctionData
       {
         //get the auction
         let auction = borrow_global_mut<AuctionData>(@auction);
         if (timestamp::now_seconds() < auction.auction_end_time) {
                abort (ERR_AUCTION_TIME_NOT_LAPSED)
         };
         if (auction.auction_ended) {
             abort (ERR_AUCTION_ENDED)
         };
         //end the auction
         auction.auction_ended = true;
    }
    public entry fun collect_auction_money() acquires AuctionData, SignerCapabilityStore
        {
           //get the auction
           let auction = borrow_global_mut<AuctionData>(@auction);
           let resource_account_signer = &get_auction_signer();
           if (!auction.auction_ended) {
                abort (ERR_AUCTION_NOT_ENDED)
           };
          //end the auction
          auction.auction_ended = true;
          let bid_amount = option::borrow(&auction.highest_bid);
          coin::transfer<AptosCoin>(resource_account_signer, @auction,    *bid_amount);
    }
 }

At the top of the file, you define the module and import different packages.

Figure 6: Module naming
The auction_contract tracks two pieces of state: AuctionData and SignerCapabilityStore. AuctionData keeps information about the auction, while SignerCapabilityStore holds a signer reference for a resource account you will create.

Remember, a signer is needed to perform actions that require authentication.

#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct AuctionData has key {
        seller: address,
        start_price: u64,
        highest_bidder: Option<address>,
        highest_bid: Option<u64>,
        auction_end_time: u64,
        auction_ended: bool,
        auction_url: String
 }
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
 struct SignerCapabilityStore has key {
 signer_capability: SignerCapability,
 }

A resource account does not have a private key; signing with a resource account is done through code. The auction contract will create a resource account to hold the auction funds. This setup prevents the premature withdrawal of funds by the deployer or contract owner.

fun create_contract_resource(creator: &signer){
// create a resource account to hold the contract funds
let (_, signer_capability) = account::create_resource_account(creator, WALLET_SEED);
move_to(creator, SignerCapabilityStore { signer_capability });
}

create_contract_resource is a private function that creates a resource account using the deployer's account and a seed phrase represented by the constant WALLET_SEED. With the move_to function, you save the created signer_capability to the creator's address (the named address, auction).

Next, create the AuctionData state and move it into the auction address. You do this with the create_new_auction function. The seller's address is the contract address, and auctions start with a start_price of 0.02 APT. The fields highest_bidder and highest_bid are of option type, meaning they may or may not have a value. For this tutorial, the auction_url is an image URL that bidders will bid for.

fun create_new_auction(creator: &signer, bid_time: u64) {
// create a resource account to hold the contract funds
 move_to(creator,
          AuctionData {
            seller: @auction ,
            start_price: 2000000,
            highest_bidder: option::none(),
            highest_bid: option::none(),
            auction_end_time: timestamp::now_seconds() + bid_time           
            auction_ended: false,
            auction_url: string::utf8(b"https://img.freepik.com/premium-photo/stunning-highresolution-depiction-krom-god-war-image-is-seamless-blend-photo_1164885-2776.jpg?w=1060"),
}
)
}

When the contract is deployed for the first time, it calls init_module. Inside the module, the auction data and resource account are created.

fun init_module(creator: &signer) {
    create_contract_resource(creator);
    create_new_auction(creator, 86400);
}

Entry function of the auction contract

The entry function place_auction_bid is called from the front end via a transaction. It takes a signer (connected wallet) and a bid amount as parameters.

Create a mutable reference to the AuctionData state owned by the contract address alias auction. You skip checking for the existence of AuctionData because you created it in the init_module, which is called during the contract deployment.

let auction_data = borrow_global_mut<AuctionData>(@auction);

Then, you call the private function get_auction_signer, which returns the signer of the resource account created during contract deployment. The contract owner creator owns the SignerCapabilityStore, which is retrieved and used to create a signer. The address of the resource account is retrieved and saved in a variable, resource_account_address. The bid amount is transferred to the resource_account_address and can only be transferred out using the signer returned by the get_auction_signer private function.

fun get_auction_signer(): signer acquires SignerCapabilityStore {
let signer_capability = &borrow_global<SignerCapabilityStore>(@auction).signer_capability;
account::create_signer_with_capability(signer_capability)
}

The check_auction_ended function ensures the auction is still running before proceeding. Two if statements check if the auction has ended or if the start_price is greater than the bid_amount; an error is thrown if either condition is met.

From the AuctionData state, check the highest_bid field and compare it with the bid_amount. The highest_bid field is optional, so you must ensure it has a value before attempting to read it.

let former_highest_bid: u64 = 0;
if (option::is_some(&auction_data.highest_bid) && option::is_some(&auction_data.highest_bidder)) {
former_highest_bid = *option::borrow(&auction_data.highest_bid);
};

If highest_bidder has a value, save it in a variable, former_highest_bid, and compare it with the bid_amount. If the bid_amount is greater than the former_highest_bid, the contract refunds the previous bid to the former highest bidder and updates the highest_bid and highest_bidder fields of the AuctionData state with the new bid and the address of the new bidder.

aptos_account::transfer(bidder, resource_account_address, bid_amount);

The value for the bid_amount is then transferred from the bidder's account to the auction contract.

public entry fun place_auction_bid(
bidder: &signer,
    bid_amount: u64
) acquires AuctionData, SignerCapabilityStore {
    // code removed for brevity
    if (auction_data.auction_ended) {
        abort (ERR_AUCTION_ENDED)
    };
    if ( auction_data.start_price > bid_amount ){
        abort (ERR_BID_TOO_SMALL)
    };
    // check if the bid is greater than the existing highest bid
    let former_highest_bid: u64 = 0;
    if (option::is_some(&auction_data.highest_bid) && option::is_some(&auction_data.highest_bidder)) {
        former_highest_bid = *option::borrow(&auction_data.highest_bid);
    };
    //check if the user bid is greater than the former bid
    if (former_highest_bid > bid_amount) {
        abort (ERR_BID_SMALLER_THAN_HIGHEST_BID)
    };
    if ( former_highest_bid > 0 ){
        // make refunds here
        let former_highest_bidder = *option::borrow(&auction_data.highest_bidder);
        aptos_account::transfer(&auction_signer, former_highest_bidder, former_highest_bid);
    };
    auction_data.highest_bid = option::some(bid_amount);
    auction_data.highest_bidder = option::some(bidder_address);
    // pay the coin here
    aptos_account::transfer(bidder, resource_account_address, bid_amount);
}

The contract also has two entry functions, close_auction and collect_auction_money. The close_auction function is called when the auction period ends. The collect_auction_money function transfers the contract's APT from the resource account to the contract creator's address. To get the signer to transfer the APT, you need the state of the SignerCapabilityStore.

public entry fun collect_auction_money() acquires AuctionData, SignerCapabilityStore
  {
 //get the auction from the contract address alias
  let auction = borrow_global_mut<AuctionData>(@auction);
//retrieve the signer using the private fun &get_auction_signer()
 let resource_account_signer = &get_auction_signer();
       if (!auction.auction_ended) {
        abort (ERR_AUCTION_NOT_ENDED)
  };
    //end the auction
  auction.auction_ended = true;
  let bid_amount = option::borrow(&auction.highest_bid)
  //transfer the APT to the contract owner address
 coin::transfer<AptosCoin>(resource_account_signer, @auction, *bid_amount);
}

Auction view function

The view function get_auction returns the fields of AuctionData as AuctionDataDetails struct, which isn’t stored at an address but is returned directly.

struct AuctionDataDetails has copy, drop {
    seller: address,
    start_price: u64,
    highest_bidder: Option<address>,
    highest_bid: Option<u64>,
    auction_end_time: u64,
    auction_ended: bool,
    auction_url: String
}
#[view]
public fun get_auction(): AuctionDataDetails acquires AuctionData {
    let auction = borrow_global<AuctionData>(@auction);
    let auction_details = AuctionDataDetails {
        seller: auction.seller,
        start_price: auction.start_price,
        highest_bidder: auction.highest_bidder,
        highest_bid: auction.highest_bid,
        auction_end_time: auction.auction_end_time,
        auction_ended: auction.auction_ended,
        auction_url: auction.auction_url
    };
    auction_details
}

Testing the auction contract

Add the following test after the get_auction view function.

#[test_only]
fun setup_test(
    creator: &signer,
    owner_1: &signer,
    owner_2: &signer,
    aptos_framework: &signer,
)   {
timestamp::set_time_has_started_for_testing(aptos_framework);
stake::initialize_for_test(&account::create_signer_for_test(@0x1));
account::create_account_for_test(signer::address_of(aptos_framework));
account::create_account_for_test(signer::address_of(creator));
account::create_account_for_test(signer::address_of(owner_1));
account::create_account_for_test(signer::address_of(owner_2));
create_contract_resource(creator);
create_new_auction(creator, 86400);
test_mint_aptos(creator, owner_1, owner_2)
}
#[test_only]
fun test_mint_aptos(creator: &signer,
                    owner_1: &signer,
                   owner_2: &signer) {
    stake::mint(creator, 10000000000);
    stake::mint(owner_1, 10000000000);
    stake::mint(owner_2, 10000000000);
}
#[test(creator = @auction, owner_1 = @0x124,
       owner_2 = @0x125,
       aptos_framework = @0x1, )]
fun test_auction_creation(
    creator: &signer,
    owner_1: &signer,
    owner_2: &signer,
    aptos_framework: &signer
) acquires AuctionData {
    setup_test(creator, owner_1, owner_2, aptos_framework);
    let auction = get_auction();
    assert!(auction.seller == @auction, 400);
}

#[test(creator = @auction, owner_1 = @0x124,
       owner_2 = @0x125,
       aptos_framework = @0x1, )]
fun test_first_depositor(creator: &signer, owner_1: &signer, owner_2: &signer, aptos_framework: &signer ) acquires AuctionData, SignerCapabilityStore {
    setup_test(creator, owner_1, owner_2, aptos_framework);
    let bid_amount:u64 = 3000000;
    place_auction_bid(owner_1, bid_amount);
    let auction_data = get_auction();
    let highest_bid = *option::borrow(&auction_data.highest_bid);
    assert!( highest_bid == bid_amount, 401);
}

#[test(creator = @auction, owner_1 = @0x124, owner_2 = @0x125,
aptos_framework = @0x1, )]
fun test_highest_bidder_deposit(creator: &signer, owner_1: &signer,
owner_2: &signer, aptos_framework: &signer) acquires AuctionData, SignerCapabilityStore {
 setup_test(creator, owner_1, owner_2, aptos_framework);
      let bid_amount1:u64 = 3000000;
      let bid_amount2:u64 = 4000000;
      let owner_1_address = signer::address_of(owner_1);
      let owner_2_address = signer::address_of(owner_2);
      let bal_before_owner_1 =  coin::balance<AptosCoin>(owner_1_address);
      let bal_before_owner_2 = coin::balance<AptosCoin>(owner_2_address);
      place_auction_bid(owner_1, bid_amount1);
      place_auction_bid(owner_2 , bid_amount2);
      let contract_balance  = coin::balance<AptosCoin>(signer::address_of(&get_auction_signer()));
      let bal_after_owner_1 = coin::balance<AptosCoin>(owner_1_address);
      let bal_after_owner_2 = coin::balance<AptosCoin>(owner_2_address);
      assert!(bal_before_owner_1 == bal_after_owner_1, 500);
      assert!(bal_before_owner_2 > bal_after_owner_2, 501);
      assert!(contract_balance == bid_amount2, 503);
}

#[test(creator = @auction, owner_1 = @0x124,
owner_2 = @0x125, aptos_framework = @0x1, )]
fun test_withdraw_auction_money(
    creator: &signer,
    owner_1: &signer,
    owner_2: &signer,
    aptos_framework: &signer) acquires AuctionData, SignerCapabilityStore {
    setup_test(creator, owner_1, owner_2, aptos_framework);
    let bid_amount1:u64 = 3000000;
    let bid_amount2:u64 = 4000000;
    place_auction_bid(owner_1, bid_amount1);
    place_auction_bid(owner_2 , bid_amount2);
    timestamp::fast_forward_seconds(1727040012);
    let owner_balance_before  = coin::balance<AptosCoin>(@auction);
     close_auction();
     collect_auction_money();
let owner_balance_after  = coin::balance<AptosCoin>(@auction);
assert!(owner_balance_after > owner_balance_before, 600);
}

To run the test, execute the command below:

npm run move:test

Review the test to understand the contract.

Figure 7: Test passing success message

You completed the smart contract part. Next, you will create the frontend to learn how to connect to smart contracts using the Aptos TypeScript SDK provided by the create-aptos-dapp tool.

Smart contract interaction

The auction contract has three entry functions: place_auction_bid, close_auction, and collect_auction_money.

Open frontend/entry-functions/ and create three files: closeAuction.ts, collectAuctionMoney.ts, and placeAuctionBid.ts.

Two files from the boilerplate template are already in that location; leave them as they are.

Figure 8: View of entry-functions folder

Open the placeAuctionBid.ts file and add this code inside the file.

import { InputTransactionData } from "@aptos-labs/wallet-adapter-react";
export type PlaceBidArguments = {
    bidAmount: number;
};
export const placeAuctionBid = (args: PlaceBidArguments): InputTransactionData => {
const {bidAmount } = args;
  return {
    data: {
      function: ${import.meta.env.VITE_MODULE_ADDRESS}::auction_contract::place_auction_bid,
      functionArguments: [BigInt(bidAmount)],
    },
  };
};

The place_auction_bid entry function of the contract accepts two parameters. The first is the signer, which the Aptos VM will create, and the second is the bidAmount. The contract address is obtained from the VITE_MODULE_ADDRESS in the .env file. This function is then exported from the file.

Figure 9: Entry function
Let's do the same for closeAuction.ts and collectAuctionMoney.ts.

Add this code below in closeAuction.ts:

import { InputTransactionData } from "@aptos-labs/wallet-adapter-react";
export const closeAuction = (): InputTransactionData => {
  return {
    data: {
      function: ${import.meta.env.VITE_MODULE_ADDRESS}::auction_contract::close_auction,
      functionArguments: [],
    },
  };
};

Paste the code below in collectAuctionMoney.ts:

import { InputTransactionData } from "@aptos-labs/wallet-adapter-react";
export const collectAuctionMoney = (): InputTransactionData => {
  return {
    data: {
      function: ${import.meta.env.VITE_MODULE_ADDRESS}::auction_contract::collect_auction_money,
      functionArguments: [],
    },
  };
};

Data retrieval from smart contract view functions

Open the folder frontend/view-functions and create a new file named getAuction.ts.

Add the following code:

import { aptosClient } from "@/utils/aptosClient";
import { AuctionDetails } from "@/components/interface/AuctionDetails";

export const getAuction = async (): Promise<AuctionDetails> => {
  const auction = await aptosClient()
    .view<[AuctionDetails]>({
      payload: {
        function: ${import.meta.env.VITE_MODULE_ADDRESS}::auction_contract::get_auction,
      },
    })
    .catch((error) => {
      console.error(error);
      return [];
    });
  return auction[0];
};

At the top of the file, you import aptosClient from @/utils/aptosClient and the AuctionDetails interface from @/components/interface/AuctionDetails.

The AuctionDetails interface is designed to match the contract auction data.

export interface AuctionDetails {
  seller: string; 
  start_price: number;
  highest_bidder: {vec: [string | undefined ]};
  highest_bid: {vec: [number | undefined ]};
  auction_end_time: number;
  auction_ended: boolean;
  auction_url: string;
}

The fields highest_bidder and highest_bid in the auction contract are of type move option, which means they may or may not have a value. They are returned from the contract as a JavaScript object with a property called vec and a value that is an array of the option type. The option value is at the first index of the vec array.

highest_bidder: {vec: [string | undefined ]};

Frontend integration

Add the code below to create a React component called DisplayAuctionDetails.ts inside frontend/components.

import { useState, useEffect } from "react";
import { useWallet } from "@aptos-labs/wallet-adapter-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "@/components/ui/use-toast";
import { aptosClient } from "@/utils/aptosClient";
import { AuctionDetails } from "@/components/interface/AuctionDetails";
import { getAuction } from "@/view-functions/getAuction";
import { placeAuctionBid } from "@/entry-functions/placeAuctionBid";
import { closeAuction } from "@/entry-functions/closeAuction";
import { collectAuctionMoney } from "@/entry-functions/collectAuctionMoney";

export function DisplayAuctionDetail() {
  const { account, signAndSubmitTransaction } = useWallet();
  const queryClient = useQueryClient();
  const [auction, setAuction] = useState<AuctionDetails>();
  const [bidAmount, setBidAmount] = useState(0);

  const { data } = useQuery({
    queryKey: ["get-auction"],
    refetchInterval: 10_000,
    queryFn: async () => {
      try {
        const auctionDetails = await getAuction();
        return {
          auctionDetails,
        };
      } catch (error: any) {
        toast({
          variant: "destructive",
          title: "Error",
          description: error,
        });
        return {
          auctionDetails: [],
        };
      }
    },
  });


  useEffect(() => {
    if (data) {
      setAuction(data.auctionDetails as AuctionDetails);
    }
  }, [data]);


  const placeBid = async () => {
    if (!account || !bidAmount) {
      return;
    }

    try {
      const decimal = 100_000_000;
      const committedTransaction = await signAndSubmitTransaction(
        placeAuctionBid({
          bidAmount: bidAmount * decimal,
        }),
      );

    const executedTransaction = await aptosClient().waitForTransaction({
    transactionHash: committedTransaction.hash,
    });

    queryClient.invalidateQueries();
    toast({title: "Success",
           description: Transaction succeeded, hash: ${executedTransaction.hash},
    });

    setBidAmount(0);
    } catch (error) {
        console.error(error);
        setBidAmount(0);
    }
    };


    const endAuction = async () => {
    if (!account) {
          return;
    }
     try {
       const committedTransaction = await  signAndSubmitTransaction(closeAuction());
       const executedTransaction = await aptosClient().waitForTransaction({transactionHash:committedTransaction.hash,
    });
    queryClient.invalidateQueries();
    toast({ title: "Success",
            description: Transaction succeeded, hash:  ${executedTransaction.hash},
    });
    } catch (error) {
         console.error(error);
    }
};




const claimAuctionMoney = async () => {
 if (!account) {
      return;
 }
  try {
    const committedTransaction = await signAndSubmitTransaction(collectAuctionMoney());
    const executedTransaction = await aptosClient().waitForTransaction({
            transactionHash: committedTransaction.hash,
    });
    queryClient.invalidateQueries();
       toast({title: "Success",
              description: Transaction succeeded, hash: ${executedTransaction.hash},
});
} catch (error) {
     console.error(error);
  }
};

return (
    <div>
      {auction && (
        <div className="p-6 bg-white rounded-lg shadow-md">
          {/* Header: Seller Information */}
          <div className="flex justify-between items-center border-b pb-4 mb-4">
            <h2 className="text-xl font-semibold">Auction by {auction.seller}</h2>
            <span className="text-sm text-gray-600">{auction.auction_ended ? "Auction Ended" : "Auction Ongoing"}</span>
          </div>
          {/* Main Content: Auction Details */}
          <div className="flex flex-col space-y-4">
            {/* Auction Image */}

            <div className="flex flex-col items-center">
              <span className="font-medium mb-2">Auction Image:</span>
                <img src={auction.auction_url} alt="Auction Item" className="max-w-full h-auto rounded-lg shadow-sm" />
                </div>
                  {/* Start Price */}
                  <div className="flex items-center">
                 <span className="font-medium w-1/3">Start Price:</span>
                 <span className="text-gray-700 w-2/3">${+auction.start_price / 1_00_000_000} APT</span>
                </div>    

                {/* Highest Bidder */}
                <div className="flex items-center">
                <span className="font-medium w-1/3">Highest Bidder:</span>
                 <span className="text-gray-700 w-2/3">{auction.highest_bidder.vec[0] || "No bids yet"}</span>
                </div>
                {/* Highest Bid */}    
    <div className="flex items-center">
      <span className="font-medium w-1/3">Highest Bid:</span>
      <span className="text-gray-700 w-2/3">
      {auction.highest_bid.vec[0] !== undefined
      ? $${auction.highest_bid.vec[0] / 1_00_000_000} APT
        : "No bids yet"}
      </span>
    </div>
    {/* Auction End Time */}
    <div className="flex items-center">
        <span className="font-medium w-1/3">Auction End Time:</span>
        <span className="text-gray-700 w-2/3">{new Date(auction.auction_end_time * 1000).toLocaleString()}</span>
    </div>
    </div>

    {/* Bid Input and Button */}
    <div className="mt-6 flex flex-col items-center space-y-4">
     <div className="flex items-center space-x-4 w-full">

     <Input type="number" className="w-2/3 p-2 border 
       rounded-lg focus:outline-none focus:ring-2   focus:ring-blue-500"
               value={bidAmount}
               placeholder="0.00"
               onChange={(e) => setBidAmount(+e.target.value)}
           />
    <Button className="w-1/3 p-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500" onClick={placeBid}>Place Bid
    </Button>

    </div>
    </div>

    <div className="mt-6 flex justify-between items-center w-full">
    {/* Left Button */}
     <Button onClick={endAuction} className="p-2 bg-red-500  text-white rounded-lg hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500">End Auction
    </Button>
    {/* Right Button */}
    <Button onClick={claimAuctionMoney} className="p-2 bg-green-500 text-white rounded-lg hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500">Collect Auction Money
    </Button>
    </div>

    </div>
    )}

</div>
);
}

This code creates a simple component to display auction details, allowing users to place bids, close the auction, and collect the auction contract APT.

The entry and view functions you created are imported into the component. useQuery from @tanstack/react-query fetches the auction data by running the view function getAuction. This happens on page load with useEffect.

The placeBid function sends transactions to the blockchain. Verify if the user is authenticated by checking the account state from the useWallet hook provided by create-aptos-dapp. The entered bid is multiplied by 100,000,000 to get the Octas value (the smallest unit of APT).

Sign and submit a transaction using the placeAuctionBid entry function you created earlier, passing in bidAmount.

const committedTransaction = await signAndSubmitTransaction(
        placeAuctionBid({
          bidAmount: bidAmount * decimal,
}),);

After signing, the transaction is then submitted to the blockchain.

const executedTransaction = await aptosClient().waitForTransaction({ transactionHash: committedTransaction.hash, });

The Petra wallet has a feature that simulates the transaction before submission. It shows how much APT will be deducted from your account and whether the transaction will succeed.

After submitting the transaction, you invalidate the previous query, fetching auction data to get the latest data from the blockchain.

queryClient.invalidateQueries();

Bids and changes are almost instantaneous, showcasing the speed of the Aptos blockchain.

Let's connect the component above to the application and display your DApp. Open the App.tsx file and replace the previous content with the code below:

import { useWallet } from "@aptos-labs/wallet-adapter-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Header } from "@/components/Header";
import { WalletDetails } from "@/components/WalletDetails";
import { AccountInfo } from "@/components/AccountInfo";
import { DisplayAuctionDetail } from "@/components/DisplayAuctionDetails";

function App() {
  const { connected } = useWallet();
  return (
    <>
      <Header />
      <div className="flex items-center justify-center flex-col">
        {connected ? (
          <Card>
            <CardContent className="flex flex-col gap-10 pt-6">
              <WalletDetails /> {/* This component is from the boilerplate template*/}
              <AccountInfo /> {/* This component is from the boilerplate template*/}
   <DisplayAuctionDetail /> {/* The component you just created*/}
            </CardContent>
          </Card>
        ) : (
          <CardHeader>
            <CardTitle>To get started Connect a wallet</CardTitle>
          </CardHeader>
        )}
      </div>
    </>
  );
}
export default App;

Deploying the auction contract

Deploying the contract is simple because create-aptos-dapp initializes everything you need for a smooth deployment.

npm run move:publish

This deploys the contract, and you are ready to test its functionalities. Start the frontend by running:

npm run dev

The application starts, allowing you to place bids on the image. You can then wait for the auction to end, close it, and finally withdraw the bid money.

Figure 9: The looks of the application

Upgrading a smart contract

You can update your contract without losing its previous state. Yet, the changes made to the smart contract have the following limitations:

  • You can't change or update any existing state, but you can add a new state to the contract.

Figure 10: Things to note when updating move smart contract

  • You can’t change the signatures of functions (e.g., arguments and returns), but you can change their internal functionality.

  • You can add new functions to the smart contract.

To finalize the upgrade, open the file scripts/move/upgrade.js and ensure the named address is correct. (i.e., the alias for the contract)

Figure 11: Updating name address in the upgrade.js file

After making the changes, run the command to upgrade the contract, and the contract will get upgraded.

npm run move:upgrade

Figure 12: Success message when upgrading via create-aptos-dapp tool

Final words

That was a long read, and I thank you for staying until the end. create-aptos-dapp makes creating DApps for the Aptos blockchain easy. It sets up the project for you, allowing you to focus on the main task: creating your dapp.

To recap, the high points of this blog post are;

  • you learned how to create and bootstrap a complete aptos move project using the create-aptos-dapp tool.

  • You explored the folder structure created by the tool.

  • You learned how to create a custom project, leveraging on the tool boilerplate template

  • You learned how to create a resource account and save the signer capability of the resource account.

  • You learned how to send a transaction to the Aptos blockchain using an entry function via Aptos Typescript SDK

  • You learned how to retrieve data from the blockchain

  • You learned how to deploy and upgrade smart contracts via the create-aptos-dapp tool

The code for this tutorial can be found on this GitHub link.

0
Subscribe to my newsletter

Read articles from Osikhena Oshomah directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Osikhena Oshomah
Osikhena Oshomah

Javascript developer working hard to master the craft.