Marketplace Contract Part 3 - Asset for Asset Quantity

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 Asset for Asset Quantity 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 tradeListingValueWithAssetQuantity(Struct):
    asset_listed: 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 postTradeRequestWithQuantity(
        self,
        asset_to_list: gtxn.AssetTransferTransaction,
        asset_request: Asset,
        asset_request_quantity: arc4UInt64,
        box_fee_mbr_payment: gtxn.PaymentTransaction
    ) -> String:

        assert box_fee_mbr_payment.amount == 28_500
        assert asset_to_list.asset_receiver == Global.current_application_address
        assert asset_to_list.asset_amount == 1
        
        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=25)
        
        listing_box_value_info = tradeListingValueWithAssetQuantity(arc4UInt64(asset_to_list.xfer_asset.id), arc4UInt64(asset_request.id), arc4UInt64(asset_request_quantity.native), Bool(False))    
        
        listing_box.put(listing_box_value_info.bytes)
        
        return String("Trade Request Posted")
        
    @abimethod
    def fulfillTradeRequestWithQuantity(
        self,
        asset_trade_fulfillment: gtxn.AssetTransferTransaction,
        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 = tradeListingValueWithAssetQuantity.from_bytes(value)
        
        assert listing_value.asset_listed == asset_listed.id
        assert listing_value.asset_requested == asset_trade_fulfillment.xfer_asset.id
        assert asset_trade_fulfillment.asset_amount == listing_value.asset_amount_requested
                
        itxn.AssetTransfer(
            xfer_asset=asset_listed,
            asset_receiver=Txn.sender,
            asset_amount=1,
            fee=Global.min_txn_fee
        ).submit()
        
        listing_value.fulfilled = Bool(True)
        listing_box.put(listing_value.bytes)
        
        return String("Trade Request Fulfilled!")        
                
    @abimethod
    def claimFulfilledTradeRequestWithQuantity(
        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 = tradeListingValueWithAssetQuantity.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 cancelTradeRequestWithQuantity(
        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)
        
        alue, exists = listing_box.maybe()
        
        assert exists
        
        listing_value = tradeListingValueWithAssetQuantity.from_bytes(value)
        
        assert listing_value.asset_listed == listed_asset.id
        
        itxn.AssetTransfer(
            xfer_asset=listed_asset.id,
            asset_amount=1,
            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
from base64 import b64decode
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)

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=28_500)
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='postTradeRequestWithQuantity', 
    asset_to_list=wrapped_asset_list_transaction, 
    asset_request=asset_2, 
    asset_request_quantity=5,
    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 dotenv import load_dotenv

load_dotenv()
from algosdk.v2client.algod import AlgodClient
from algosdk.abi import ABIType
from base64 import b64decode

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.

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-in to the asset purchased 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)

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=5, index=asset_2)
wrapped_asset_payment = TransactionWithSigner(asset_payment_tx, signer)

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='fulfillTradeRequestWithQuantity', 
    asset_trade_fulfillment=wrapped_asset_payment,
    asset_transfer_fee=wrapped_fee_payment,
    asset_listed=asset_1, 
    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 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
from pathlib import Path
from algosdk.abi import ABIType
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'))
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='claimFulfilledTradeRequestWithQuantity', 
    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='cancelTradeRequestWithQuantity', 
    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