Digital Marketplace

Creator Algorand Foundation

Source
python
smart-contract
Install with AlgoKit CLI
    algokit init example digital-marketplace-smart-contract
  

A smart contract for a digital marketplace application for creating and trading digital assets on Algorand.

Features

Algorand Python
ABI Method Integration
Digital Marketplace

Digital Marketplace Smart Contract

This example demonstrates a Digital Marketplace smart contract on Algorand. It allows users to list assets for sale, either for a fixed price or through an auction-style bidding system.

Features

  • Deposits: Users can deposit and withdraw ALGOs into the contract to be used for purchases and bids.
  • Asset Sales: Users can list their assets (ASAs) for sale at a fixed price.
  • Bidding: For assets on sale, users can place bids. A new bid must be higher than the current highest bid.
  • Purchases: A buyer can purchase a listed asset directly if a fixed price is set.
  • Auctions: Sellers can accept the highest bid on their asset, transferring the asset to the bidder and the bid amount to the seller.
  • Bid Management: Bidders can withdraw their bids if they are not the leading bid on any auction.

How it works

The contract manages user balances in a BoxMap and keeps track of sales and bids.

Key Functions

  • deposit(payment: gtxn.PaymentTransaction): Deposits ALGOs into the user’s account within the contract.
  • withdraw(amount: arc4.UInt64): Withdraws a specified amount of ALGOs from the user’s account.
  • open_sale(asset_deposit: gtxn.AssetTransferTransaction, cost: arc4.UInt64): Creates a new sale for an asset at a given cost.
  • close_sale(asset: Asset): Allows the seller to close their sale and reclaim their asset.
  • buy(sale_key: SaleKey): Allows a user to buy an asset for the specified cost.
  • bid(sale_key: SaleKey, new_bid_amount: arc4.UInt64): Places a bid on an asset. The bid must be higher than the current one.
  • accept_bid(asset: arc4.UInt64): The seller accepts the highest bid. The asset is transferred to the bidder, and the funds are transferred to the seller.
  • claim_unencumbered_bids(): Allows a user to recover funds from bids that are no longer active or winning.
import typing

from algopy import (
    Account,
    ARC4Contract,
    Asset,
    BoxMap,
    Global,
    ImmutableArray,
    Txn,
    UInt64,
    arc4,
    gtxn,
    itxn,
    subroutine,
)
from algopy.arc4 import abimethod

import smart_contracts.digital_marketplace.errors as err
from smart_contracts.digital_marketplace.subroutines import (
    find_bid_receipt,
)


class SaleKey(arc4.Struct, frozen=True):
    owner: arc4.Address
    asset: arc4.UInt64


class Bid(arc4.Struct, frozen=True):
    bidder: arc4.Address
    amount: arc4.UInt64


class Sale(arc4.Struct, frozen=True):
    amount: arc4.UInt64
    cost: arc4.UInt64
    # Ideally we'd like to write:
    #  bid: Optional[Bid]
    # Since there's no Optional in Algorand Python, we use the truthiness of bid.bidder
    # to know if a bid is present
    bid: Bid


class BidReceipt(arc4.Struct, frozen=True):
    sale_key: SaleKey
    amount: arc4.UInt64


class UnencumberedBidsReceipt(typing.NamedTuple):
    total_bids: UInt64
    unencumbered_bids: UInt64


class DigitalMarketplace(ARC4Contract):
    def __init__(self) -> None:
        self.deposited = BoxMap(Account, UInt64)

        self.sales = BoxMap(SaleKey, Sale)
        self.receipt_book = BoxMap(Account, ImmutableArray[BidReceipt])

    @abimethod
    def deposit(self, payment: gtxn.PaymentTransaction) -> None:
        assert payment.sender == Txn.sender, err.DIFFERENT_SENDER
        assert (
            payment.receiver == Global.current_application_address
        ), err.WRONG_RECEIVER

        mbr_baseline = Global.current_application_address.min_balance
        self.deposited[Txn.sender] = (
            self.deposited.get(Txn.sender, default=UInt64(0)) + payment.amount
        )
        mbr_diff = Global.current_application_address.min_balance - mbr_baseline
        self.deposited[Txn.sender] -= mbr_diff

    @abimethod
    def withdraw(self, amount: arc4.UInt64) -> None:
        self.deposited[Txn.sender] -= amount.native

        itxn.Payment(receiver=Txn.sender, amount=amount.native).submit()

    @abimethod
    def sponsor_asset(self, asset: Asset) -> None:
        assert not Global.current_application_address.is_opted_in(
            asset
        ), err.ALREADY_OPTED_IN
        assert asset.clawback == Global.zero_address, err.CLAWBACK_ASA

        self.deposited[Txn.sender] -= Global.asset_opt_in_min_balance

        itxn.AssetTransfer(
            xfer_asset=asset,
            asset_receiver=Global.current_application_address,
            asset_amount=0,
        ).submit()

    @abimethod
    def open_sale(
        self, asset_deposit: gtxn.AssetTransferTransaction, cost: arc4.UInt64
    ) -> None:
        assert asset_deposit.sender == Txn.sender, err.DIFFERENT_SENDER
        assert (
            asset_deposit.asset_receiver == Global.current_application_address
        ), err.WRONG_RECEIVER

        sale_key = SaleKey(
            arc4.Address(Txn.sender), arc4.UInt64(asset_deposit.xfer_asset.id)
        )
        assert sale_key not in self.sales, err.SALE_ALREADY_EXISTS

        mbr_baseline = Global.current_application_address.min_balance
        self.sales[sale_key] = Sale(
            arc4.UInt64(asset_deposit.asset_amount),
            cost,
            Bid(arc4.Address(), arc4.UInt64()),
        )
        mbr_diff = Global.current_application_address.min_balance - mbr_baseline

        self.deposited[Txn.sender] -= mbr_diff

    @abimethod
    def close_sale(self, asset: Asset) -> None:
        sale_key = SaleKey(arc4.Address(Txn.sender), arc4.UInt64(asset.id))

        itxn.AssetTransfer(
            xfer_asset=asset,
            asset_receiver=Txn.sender,
            asset_amount=self.sales[sale_key].amount.native,
        ).submit()

        mbr_baseline = Global.current_application_address.min_balance
        del self.sales[sale_key]
        mbr_diff = mbr_baseline - Global.current_application_address.min_balance
        self.deposited[Txn.sender] += mbr_diff

    @abimethod
    def buy(self, sale_key: SaleKey) -> None:
        assert Txn.sender != sale_key.owner.native, err.SELLER_CANT_BE_BUYER
        sale = self.sales[sale_key]

        itxn.AssetTransfer(
            xfer_asset=sale_key.asset.native,
            asset_receiver=Txn.sender,
            asset_amount=sale.amount.native,
        ).submit()

        mbr_baseline = Global.current_application_address.min_balance
        del self.sales[sale_key]
        mbr_diff = mbr_baseline - Global.current_application_address.min_balance

        self.deposited[Txn.sender] -= sale.cost.native
        self.deposited[sale_key.owner.native] += sale.cost.native + mbr_diff

    @abimethod
    def bid(self, sale_key: SaleKey, new_bid_amount: arc4.UInt64) -> None:
        new_bid = Bid(bidder=arc4.Address(Txn.sender), amount=new_bid_amount)

        assert Txn.sender != sale_key.owner, err.SELLER_CANT_BE_BIDDER

        sale = self.sales[sale_key]
        if sale.bid.bidder:
            assert sale.bid.amount.native < new_bid_amount.native, err.WORSE_BID

        self.sales[sale_key] = sale._replace(bid=new_bid)

        mbr_baseline = Global.current_application_address.min_balance
        new_bid_receipt = BidReceipt(sale_key, new_bid_amount)
        receipt_book, exists = self.receipt_book.maybe(Txn.sender)
        if exists:
            found, index = find_bid_receipt(receipt_book, sale_key)
            if found:
                self.deposited[Txn.sender] += receipt_book[index].amount.native
                self.receipt_book[Txn.sender] = receipt_book.replace(
                    index, new_bid_receipt
                )
            else:
                self.receipt_book[Txn.sender] = receipt_book.append(new_bid_receipt)
        else:
            self.receipt_book[Txn.sender] = ImmutableArray(new_bid_receipt)
        mbr_diff = Global.current_application_address.min_balance - mbr_baseline

        self.deposited[Txn.sender] -= new_bid_amount.native + mbr_diff

    @subroutine
    def is_encumbered(self, bid: BidReceipt) -> bool:
        sale, exists = self.sales.maybe(bid.sale_key)
        return exists and bool(sale.bid.bidder) and sale.bid.bidder == Txn.sender

    @abimethod
    def claim_unencumbered_bids(self) -> None:
        encumbered_receipts = ImmutableArray[BidReceipt]()

        for receipt in self.receipt_book[Txn.sender]:
            if self.is_encumbered(receipt):
                encumbered_receipts = encumbered_receipts.append(receipt)
            else:
                self.deposited[Txn.sender] += receipt.amount.native

        mbr_baseline = Global.current_application_address.min_balance
        if encumbered_receipts:
            self.receipt_book[Txn.sender] = encumbered_receipts
        else:
            del self.receipt_book[Txn.sender]
        mbr_diff = mbr_baseline - Global.current_application_address.min_balance

        self.deposited[Txn.sender] += mbr_diff

    @abimethod(readonly=True)
    def get_total_and_unencumbered_bids(self) -> UnencumberedBidsReceipt:
        total_bids = UInt64(0)
        unencumbered_bids = UInt64(0)

        receipt_book, exists = self.receipt_book.maybe(Txn.sender)
        if exists:
            for receipt in receipt_book:
                total_bids += receipt.amount.native
                if not self.is_encumbered(receipt):
                    unencumbered_bids += receipt.amount.native

        return UnencumberedBidsReceipt(total_bids, unencumbered_bids)

    @abimethod
    def accept_bid(self, asset: arc4.UInt64) -> None:
        sale_key = SaleKey(owner=arc4.Address(Txn.sender), asset=asset)
        sale = self.sales[sale_key]
        current_best_bid = sale.bid
        current_best_bidder = current_best_bid.bidder.native

        seller_mbr_baseline = Global.current_application_address.min_balance
        del self.sales[sale_key]
        seller_mbr_diff = (
            seller_mbr_baseline - Global.current_application_address.min_balance
        )

        self.deposited[Txn.sender] += current_best_bid.amount.native + seller_mbr_diff
        itxn.AssetTransfer(
            xfer_asset=asset.native,
            asset_receiver=current_best_bidder,
            asset_amount=sale.amount.native,
        ).submit()

        receipt_book = self.receipt_book[current_best_bidder]
        found, index = find_bid_receipt(receipt_book, sale_key)
        assert found

        encumbered_receipts = ImmutableArray[BidReceipt]()
        for receipt in receipt_book:
            if receipt != receipt_book[index]:
                encumbered_receipts = encumbered_receipts.append(receipt)

        bidder_mbr_baseline = Global.current_application_address.min_balance
        if encumbered_receipts:
            self.receipt_book[current_best_bidder] = encumbered_receipts
        else:
            del self.receipt_book[current_best_bidder]
        bidder_mbr_diff = (
            bidder_mbr_baseline - Global.current_application_address.min_balance
        )

        self.deposited[current_best_bidder] += bidder_mbr_diff