Marketplace Contract Part 5 - Universal Listings

Video Walkthrough

Watch the tutorial video for this chapter:

Step 1: Generate Two Accounts

  • One for Lister, one for Buyer
from algosdk.account import generate_account

private_key, address = generate_account()
print(private_key, address)

Step 2: Load each Account with Funds

Use the following link to load funds into each account:

https://bank.testnet.algorand.network/

Step 3: Create .env

algod_token = 
algod_server = https://testnet-api.4160.nodely.dev
private_key = lqPuVOtF4p6QpsaL7iTA6mbHoy7a5+nuAPjzw7HvckiXQWUlnZQVYSCZSvUK8w+MGjh09sNsx7WybOeb0MLD/Q==
buyer_private_key = jHx+aE6G/5RKAMJK7J7R+j3SyjKhzLsrbYIrzmmhWphJsT7CpTxqdhsjWQx4wDpx2I5B+1FiBSp16O5fXd/Sqg==
asset_1 = 724395141
asset_2 = 724449087
app_id = 724455779

Step 4: Compile Universal Listing Contract

Create and compile the asset listing contract:

algokit compile py fileName.py

Use the following implementation to compile your contract:

from algopy import ARC4Contract, String, gtxn, Asset, Txn, GlobalState, itxn, Global, BoxRef, UInt64, TransactionType
from algopy.arc4 import abimethod, Address, Struct, Bool
from algopy.arc4 import UInt64 as arc4UInt64

class listingName(Struct):
    lister: Address
    counter: arc4UInt64
    
class universalTradeListing(Struct):
    asset_listed: arc4UInt64
    asset_listed_quantity: arc4UInt64
    asset_requested: arc4UInt64
    asset_amount_requested: arc4UInt64
    fulfilled: Bool
    
class listings(ARC4Contract):
    def __init__(self) -> None:
        self.listingCounter = GlobalState(arc4UInt64(0))
        
    @abimethod
    def triggerOptIn(
        self,
        asset_to_opt_into: Asset,
        fee_payment: gtxn.PaymentTransaction
    ) -> tuple[String, UInt64]:

        assert fee_payment.amount >= 101_000
        assert fee_payment.receiver == Global.current_application_address
        
        itxn.AssetTransfer(
            xfer_asset=asset_to_opt_into,
            asset_receiver=Global.current_application_address,
            fee=Global.min_txn_fee
        ).submit()
        
        return String("Successfully opted in to asset: "), asset_to_opt_into.id
        
    @abimethod
    def triggerOptIn(
        self,
        asset_to_opt_into: Asset,
        fee_payment: gtxn.PaymentTransaction
    ) -> tuple[String, UInt64]:

        assert fee_payment.amount >= 101_000
        assert fee_payment.receiver == Global.current_application_address
        
        itxn.AssetTransfer(
            xfer_asset=asset_to_opt_into,
            asset_receiver=Global.current_application_address,
            fee=Global.min_txn_fee
        ).submit()
        
        return String("Successfully opted in to asset: "), asset_to_opt_into.id
            
    @abimethod
    def postUniversalListing(
        self,
        asset_to_list: gtxn.AssetTransferTransaction,
        asset_request: arc4UInt64,
        asset_request_quantity: arc4UInt64,
        box_fee_mbr_payment: gtxn.PaymentTransaction
    ) -> String:
        
        assert box_fee_mbr_payment.amount == 31_700
        assert asset_to_list.asset_receiver == Global.current_application_address
        
        self.listingCounter.value = arc4UInt64(self.listingCounter.value.native + 1)
        
        listing_box_name_info = listingName(Address(Txn.sender), self.listingCounter.value)
        
        listing_box = BoxRef(key=listing_box_name_info.bytes)
        
        value, exists = listing_box.maybe()
        assert not exists
        
        listing_box.create(size=33)
        
        listing_box_value_info = universalTradeListing(arc4UInt64(asset_to_list.xfer_asset.id), arc4UInt64(asset_to_list.asset_amount), asset_request, arc4UInt64(asset_request_quantity.native), Bool(False))
            
        listing_box.put(listing_box_value_info.bytes)
        
        return String("Trade Request Posted")
        
    @abimethod
    def fulfillUniversalListing(
        self,
        asset_transfer_fee: gtxn.PaymentTransaction,
        asset_listed: Asset,
        listing_name: listingName,
        
    ) -> String:
        
        assert asset_transfer_fee.amount == 1000

        listing_box = BoxRef(key=listing_name.bytes)
        
        value, exists = listing_box.maybe()
        assert exists
        
        listing_value = universalTradeListing.from_bytes(value)

        if Global.group_size == 3:
            assert gtxn.Transaction(0).type == TransactionType.Payment
            payment_fulfillment_tx = gtxn.PaymentTransaction(0)
            assert payment_fulfillment_tx.receiver == Global.current_application_address
            assert listing_value.asset_listed == asset_listed.id
            assert listing_value.asset_requested == 0
            assert payment_fulfillment_tx.amount == listing_value.asset_amount_requested.native

            itxn.AssetTransfer(
                xfer_asset=asset_listed,
                asset_receiver=Txn.sender,
                asset_amount=listing_value.asset_listed_quantity.native,
                fee=Global.min_txn_fee
            ).submit()            
            
            itxn.Payment(
                receiver=listing_name.lister.native,
                amount=listing_value.asset_amount_requested.native,
                fee=Global.min_txn_fee
            ).submit()
            
            listing_box.delete()
            
            return String("Buyer paid with Algo, Listing Fulfilled!")
            
        elif Global.group_size == 5:
            assert gtxn.Transaction(2).type == TransactionType.AssetTransfer
            asset_trade_fulfillment_tx = gtxn.AssetTransferTransaction(2)
            assert asset_trade_fulfillment_tx.asset_receiver == Global.current_application_address
            assert listing_value.asset_listed == asset_listed.id
            assert listing_value.asset_requested == asset_trade_fulfillment_tx.xfer_asset.id
            assert asset_trade_fulfillment_tx.asset_amount == listing_value.asset_amount_requested
                        
            itxn.AssetTransfer(
                xfer_asset=asset_listed,
                asset_receiver=Txn.sender,
                asset_amount=listing_value.asset_listed_quantity.native,
                fee=Global.min_txn_fee
            ).submit()
                    
        listing_value.fulfilled = Bool(True)
        listing_box.put(listing_value.bytes)
        
        return String("Trade Request Fulfilled! Lister can now claim")        
                
    @abimethod
    def claimUniversalListing(
        self,
        asset_transfer_fee: gtxn.PaymentTransaction,
        asset_requested: Asset,
        listing_name: listingName,
    ) -> String:
                
        assert listing_name.lister == Address(Txn.sender)
        assert asset_transfer_fee.amount == 1000
        
        listing_box = BoxRef(key=listing_name.bytes)
        
        value, exists = listing_box.maybe()
        assert exists
        
        listing_value = universalTradeListing.from_bytes(value)
        
        assert asset_requested.id == listing_value.asset_requested
        assert listing_value.fulfilled == Bool(True)
        
        itxn.AssetTransfer(
            xfer_asset=asset_requested.id,
            asset_receiver=Txn.sender,
            asset_amount=listing_value.asset_amount_requested.native,
            fee=Global.min_txn_fee
        ).submit()
        
        listing_box.delete()
        
        return String("Trade Request Completed!")
        
    @abimethod
    def cancelUniversalListing(
        self,
        listed_asset: Asset,
        listing_name: listingName,
        transfer_fee: gtxn.PaymentTransaction
    ) -> String:
        
        assert transfer_fee.amount == 1000
        assert listing_name.lister == Address(Txn.sender)
        
        listing_box = BoxRef(key=listing_name.bytes)
        
        value, exists = listing_box.maybe()
        
        assert exists
        
        listing_value = universalTradeListing.from_bytes(value)
        
        assert listing_value.asset_listed == listed_asset.id
        
        itxn.AssetTransfer(
            xfer_asset=listed_asset.id,
            asset_amount=listing_value.asset_listed_quantity.native,
            asset_receiver=Txn.sender,
            fee=Global.min_txn_fee
        ).submit()
        
        listing_box.delete()    
        
        return String("Trade Request Cancelled!")

Ensure you have the necessary imports and class definitions for your contract.

Step 5: Create Two Arbitrary Assets for Experimenting

Follow the steps to create and configure two assets for testing purposes.

from algosdk.v2client.algod import AlgodClient
from algosdk.transaction import AssetConfigTxn, wait_for_confirmation
from algosdk.account import address_from_private_key
import os
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

private_key = os.getenv('buyer_private_key')
address = address_from_private_key(private_key)

params = algod_client.suggested_params()

asset_creation_txn = AssetConfigTxn(
    sender=address,
    sp=params,
    total=100,
    default_frozen=False,
    asset_name='Test Asset Two',
    unit_name='TA2',
    manager=address,
    reserve=address,
    strict_empty_address_check=False,
)

signed_tx = asset_creation_txn.sign(private_key)
tx_id = algod_client.send_transaction(signed_tx)
print(tx_id)

wait_for_confirmation(algod_client, tx_id)

asset_id = algod_client.pending_transaction_info(tx_id)['asset-index']
asset_info = algod_client.asset_info(asset_id)
print(asset_info)

Step 6: Store Generated Asset IDs in .env

asset_1 = 123456
asset_2 = 654321

Step 7: Experiment with Opting the Contract into an Asset

If you're creating multiple listings, you need to opt the contract into each asset the first time.

from algokit_utils import ApplicationClient
from algosdk.v2client.algod import AlgodClient
from algosdk.account import address_from_private_key
from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionWithSigner, AtomicTransactionComposer
from algosdk.transaction import PaymentTxn
from pathlib import Path
import os
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

private_key = os.getenv('private_key')
address = address_from_private_key(private_key)

app_spec = Path(__file__).parent / './listings.arc32.json'
app_id = int(os.getenv('app_id'))
signer = AccountTransactionSigner(private_key)
params = algod_client.suggested_params()

app_client = ApplicationClient(
    algod_client=algod_client,
    app_spec=app_spec,
    app_id=app_id,
    signer=signer,
    sender=address,
    suggested_params=params,
)

atc = AtomicTransactionComposer()

asset_1 = int(os.getenv('asset_1'))

fee_payment_tx = PaymentTxn(sender=address, sp=params, receiver=app_client.app_address, amt=101_000)
wrapped_payment = TransactionWithSigner(fee_payment_tx, signer)

app_client.compose_call(atc, call_abi_method='triggerOptIn', asset_to_opt_into=asset_1, fee_payment=wrapped_payment)

results = atc.execute(algod_client, 2)

tx_ids = [results.tx_ids[i] for i in range(len(results.tx_ids))]
abi_results = [results.abi_results[i].return_value for i in range(len(results.abi_results))]

print(tx_ids)
print(abi_results)

Step 8: Make an Asset for Algo Listing Box (All listing types are supported from parts 1-4)

Create an asset listing box to manage your asset listings.

from algokit_utils import ApplicationClient
from algosdk.v2client.algod import AlgodClient
from algosdk.account import address_from_private_key
from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionWithSigner, AtomicTransactionComposer
from algosdk.transaction import PaymentTxn, AssetTransferTxn
from pathlib import Path
from algosdk.abi import ABIType
import os
from base64 import b64decode
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

private_key = os.getenv('private_key')
address = address_from_private_key(private_key)

app_spec = Path(__file__).parent / './listings.arc32.json'
app_id = int(os.getenv('app_id'))
signer = AccountTransactionSigner(private_key)
params = algod_client.suggested_params()

app_client = ApplicationClient(
    algod_client=algod_client,
    app_spec=app_spec,
    app_id=app_id,
    signer=signer,
    sender=address,
    suggested_params=params,
)

atc = AtomicTransactionComposer()

asset_1 = int(os.getenv('asset_1'))

fee_payment_tx = PaymentTxn(sender=address, sp=params, receiver=app_client.app_address, amt=101_000)
wrapped_payment = TransactionWithSigner(fee_payment_tx, signer)

app_client.compose_call(
    atc, 
    call_abi_method='triggerOptIn', 
    asset_to_opt_into=asset_1, 
    fee_payment=wrapped_payment)

asset_to_list_transaction = AssetTransferTxn(sender=address, sp=params, receiver=app_client.app_address, amt=1, index=asset_1)
wrapped_asset_list_transaction = TransactionWithSigner(asset_to_list_transaction, signer)

box_fee_payment_tx = PaymentTxn(sender=address, sp=params, receiver=app_client.app_address, amt=31_700)
wrapped_box_fee_payment = TransactionWithSigner(box_fee_payment_tx, signer)

current_global_listing_counter = algod_client.application_info(app_id)['params']['global-state'][0]['value']['bytes']
uint64_coder = ABIType.from_string('(uint64)')
b64_decoded_global_listing_counter = b64decode(current_global_listing_counter)
encoded_global_listing_counter = uint64_coder.decode(b64_decoded_global_listing_counter)[0]

listing_box_counter = encoded_global_listing_counter + 1
listing_box_coder = ABIType.from_string('(address,uint64)')
users_listing_box_name = listing_box_coder.encode((address, listing_box_counter))

asset_2 = int(os.getenv('asset_2'))

app_client.compose_call(
    atc, 
    call_abi_method='postUniversalListing', 
    asset_to_list=wrapped_asset_list_transaction, 
    asset_request=0, 
    asset_request_quantity=2_000_000,
    box_fee_mbr_payment=wrapped_box_fee_payment,
    transaction_parameters={'boxes': [[app_id, users_listing_box_name]]}
)

results = atc.execute(algod_client, 2)

tx_ids = [results.tx_ids[i] for i in range(len(results.tx_ids))]
abi_results = [results.abi_results[i].return_value for i in range(len(results.abi_results))]

print(tx_ids)
print(abi_results)

Step 9: Check Respective Listing Information

Retrieve and verify the listing information from the contract.

import os
from algosdk.v2client.algod import AlgodClient
from algosdk.abi import ABIType
from base64 import b64decode
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

app_id = int(os.getenv('app_id'))

boxes = algod_client.application_boxes(app_id)['boxes']

for box in boxes:
    box_name_b64encoded = box['name']
    print(b64decode(box_name_b64encoded))
    box_value = b64decode(algod_client.application_box_by_name(app_id, b64decode(box_name_b64encoded))['value'])
    print(box_value)

    #Asset for Algo: '(uint64,uint64)'
    #Asset for Asset: '(uint64,uint64,bool)'
    #Asset for Asset Quantity: '(uint64,uint64,uint64,bool)
    #Asset Quantity for Asset Quantity: '(uint64,uint64,uint64,uint64,bool)'
    #Universal Listing: '(uint64,uint64,uint64,uint64,bool)'
    listing_box_value_coder = ABIType.from_string('(uint64,uint64)')
    print(listing_box_value_coder.decode(box_value))

Step 10: Purchase the Asset with Your Buyer Private Key

When generating a listing, the counter variable should be incremented.

counter = 1

Note; counter should match the number of listings generated so far.

When generating a listing, the counter variable should be incremented.

counter = 1

Note; counter should match the number of listings generated so far.

Execute the purchase of the asset using the buyer's private key.

from algokit_utils import ApplicationClient
from algosdk.v2client.algod import AlgodClient
from algosdk.account import address_from_private_key
from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionWithSigner, AtomicTransactionComposer
from algosdk.transaction import PaymentTxn, AssetTransferTxn
from algosdk.abi import ABIType
from pathlib import Path
import os
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

private_key = os.getenv('buyer_private_key')
address = address_from_private_key(private_key)

app_spec = Path(__file__).parent / './listings.arc32.json'
app_id = int(os.getenv('app_id'))
signer = AccountTransactionSigner(private_key)
params = algod_client.suggested_params()

app_client = ApplicationClient(
    algod_client=algod_client,
    app_spec=app_spec,
    app_id=app_id,
    signer=signer,
    sender=address,
    suggested_params=params,
)

atc = AtomicTransactionComposer()

asset_1 = int(os.getenv('asset_1'))
asset_2 = int(os.getenv('asset_2'))

'''
User should opt into the asset if they are not prepared to receive it
'''
#asset_opt_in_transaction = AssetTransferTxn(sender=address, sp=params, receiver=address, amt=0, index=asset_1)
#wrapped_opt_in_transaction = TransactionWithSigner(asset_opt_in_transaction, signer)
#atc.add_transaction(wrapped_opt_in_transaction)

#5 tx for asset to asset payments

optin_fee_payment_tx = PaymentTxn(sender=address, sp=params, receiver=app_client.app_address, amt=101_000)
wrapped_optin_payment = TransactionWithSigner(optin_fee_payment_tx, signer)

app_client.compose_call(
    atc, 
    call_abi_method='triggerOptIn', 
    asset_to_opt_into=asset_2, 
    fee_payment=wrapped_optin_payment)

asset_payment_tx = AssetTransferTxn(sender=address, sp=params, receiver=app_client.app_address, amt=3, index=asset_2)
wrapped_asset_payment = TransactionWithSigner(asset_payment_tx, signer)

asset_payment_tx = PaymentTxn(sender=address, sp=params, receiver=app_client.app_address, amt=2_000_000)
wrapped_asset_payment = TransactionWithSigner(asset_payment_tx, signer)

atc.add_transaction(wrapped_asset_payment)

fee_payment_tx = PaymentTxn(sender=address, sp=params, receiver=app_client.app_address, amt=1000)
wrapped_fee_payment = TransactionWithSigner(fee_payment_tx, signer)

lister_address = address_from_private_key(os.getenv('private_key'))
counter = 1 #Remember to increment counter for additional listings
listing_box_coder = ABIType.from_string('(address,uint64)')
box_name = listing_box_coder.encode((lister_address, counter))

app_client.compose_call(
    atc, 
    call_abi_method='fulfillUniversalListing', 
    asset_transfer_fee=wrapped_fee_payment,
    asset_listed=asset_1, 
    listing_name=(lister_address,counter), 
    transaction_parameters={'boxes': [[app_id, box_name]], 'accounts': [lister_address]})

results = atc.execute(algod_client, 2)

tx_ids = [results.tx_ids[i] for i in range(len(results.tx_ids))]
abi_results = [results.abi_results[i].return_value for i in range(len(results.abi_results))]

print(tx_ids)
print(abi_results)

Step 11: Complete Listing Request and Redeem Buyers Asset

from algokit_utils import ApplicationClient
from algosdk.v2client.algod import AlgodClient
from algosdk.account import address_from_private_key
from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionWithSigner, AtomicTransactionComposer
from algosdk.transaction import PaymentTxn, AssetTransferTxn
from pathlib import Path
from algosdk.util import algos_to_microalgos
from algosdk.abi import ABIType
import os
from base64 import b64decode
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

private_key = os.getenv('private_key')
address = address_from_private_key(private_key)

app_spec = Path(__file__).parent / './listings.arc32.json'
app_id = int(os.getenv('app_id'))
signer = AccountTransactionSigner(private_key)
params = algod_client.suggested_params()

app_client = ApplicationClient(
    algod_client=algod_client,
    app_spec=app_spec,
    app_id=app_id,
    signer=signer,
    sender=address,
    suggested_params=params,
)

atc = AtomicTransactionComposer()

asset_1 = int(os.getenv('asset_1'))
asset_2 = int(os.getenv('asset_2'))

'''
User should opt into the asset if they are not prepared to receive it
'''
#asset_opt_in_transaction = AssetTransferTxn(sender=address, sp=params, receiver=address, amt=0, index=asset_2)
#wrapped_opt_in_transaction = TransactionWithSigner(asset_opt_in_transaction, signer)
#atc.add_transaction(wrapped_opt_in_transaction)

fee_payment_tx = PaymentTxn(sender=address, sp=params, receiver=app_client.app_address, amt=1000)
wrapped_fee_payment = TransactionWithSigner(fee_payment_tx, signer)

lister_address = address_from_private_key(os.getenv('private_key'))
counter = 1 #Remember to increment counter for additional listings
listing_box_coder = ABIType.from_string('(address,uint64)')
box_name = listing_box_coder.encode((lister_address, counter))

app_client.compose_call(
    atc, 
    call_abi_method='claimUniversalListing', 
    asset_transfer_fee=wrapped_fee_payment,
    asset_requested=asset_2,
    listing_name=(lister_address,counter), 
    transaction_parameters={'boxes': [[app_id, box_name]]})

results = atc.execute(algod_client, 2)

tx_ids = [results.tx_ids[i] for i in range(len(results.tx_ids))]
abi_results = [results.abi_results[i].return_value for i in range(len(results.abi_results))]

print(tx_ids)
print(abi_results)

Step 12: Experiment with Cancelling a Listing Request

If you fulfilled the previous request already, you will need to generate a new listing again with Step 8, and increment the "counter" variable from 1 to 2. For example:

counter = 2
from algokit_utils import ApplicationClient
from algosdk.v2client.algod import AlgodClient
from algosdk.account import address_from_private_key
from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionWithSigner, AtomicTransactionComposer
from algosdk.transaction import PaymentTxn
from algosdk.abi import ABIType
from pathlib import Path
import os
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

private_key = os.getenv('private_key')
address = address_from_private_key(private_key)

app_spec = Path(__file__).parent / './listings.arc32.json'
app_id = int(os.getenv('app_id'))
signer = AccountTransactionSigner(private_key)
params = algod_client.suggested_params()

app_client = ApplicationClient(
    algod_client=algod_client,
    app_spec=app_spec,
    app_id=app_id,
    signer=signer,
    sender=address,
    suggested_params=params,
)

atc = AtomicTransactionComposer()

asset_1 = int(os.getenv('asset_1'))

fee_payment_tx = PaymentTxn(sender=address, sp=params, receiver=app_client.app_address, amt=1000)
wrapped_fee_payment = TransactionWithSigner(fee_payment_tx, signer)

lister_address = address_from_private_key(os.getenv('private_key'))
counter = 1 #Remember to increment counter for additional listings

listing_box_coder = ABIType.from_string('(address,uint64)')
box_name = listing_box_coder.encode((lister_address, counter))

app_client.compose_call(
    atc, 
    call_abi_method='cancelUniversalListing', 
    listed_asset=asset_1, 
    listing_name=(lister_address,counter), 
    transfer_fee=wrapped_fee_payment, 
    transaction_parameters={'boxes': [[app_id, box_name]]})

results = atc.execute(algod_client, 2)

tx_ids = [results.tx_ids[i] for i in range(len(results.tx_ids))]
abi_results = [results.abi_results[i].return_value for i in range(len(results.abi_results))]

print(tx_ids)
print(abi_results)

Code Editor