Turning NFTs into Coffee

A demonstration at the Taipei Smart City Summit and Expo showed a system for using an NFT to purchase a cup of coffee.

Turning NFTs into Coffee

At the Taipei Smart City Summit and Expo, which ran from March 28 to 31, 2023, attendees exchanged NFTs for fresh cups of coffee. The system allowing this exchange, created by the Sui Foundation in conjunction with MomentX and Suia, demonstrates how NFTs on Sui can be redeemed for goods or services.

In practice, expo attendees were prompted to chat with a virtual AI assistant, who would ask if the attendee wanted a cup of coffee. If they replied yes, the assistant presented a QR code. When scanned, the system generated a new wallet and NFT for the attendee. That NFT could be taken to a nearby coffee stand and redeemed for a cup of coffee.

A photo of the Smart City Coffee shop at the expo
This coffee shop at the Taipei Smart City Summit and Expo accepted redeemable NFTs as payment.

Most importantly, when the attendee received their coffee, the system sent another transaction to the Sui network, changing the NFT’s status from unspent to spent.

Turning Objects into Coffee

On Sui, all NFTs are really just objects, but objects are powerful. In this example, a URL in the object code points to an image. That URL will change depending on whether the NFT has been redeemed. The code below shows the CoffeeNFT object.

struct CoffeeNFT has key, store {
		id: UID,
		name: String,
		description: String,
		url: String,
		redeemed: bool,
	}

This code represents a pretty standard NFT implementation on Sui. The most notable difference compared to a typical object is the redeemed field. We use this field to set whether an NFT has been redeemed. The URL pointing to the NFT image changes depending on the state of this field.

An NFT showing a cup of coffee
Attendees at the Taipei Smart City Summit and Expo could redeem this NFT for a cup of coffee, after which the NFT would change to a spent status.

Note that instead of including a URL pointing to an image file, as is typical for NFTs on other blockchains, this object could have used an on-chain image file, a capability supported by Sui that enhances permanence. Further, this object code relies on an older version of Move, prior to the .28 release and the introduction of the Object Display standard. We provide an updated example of how this code could be written at the end of this article.

Gifting an NFT

The system uses the gift_nft function to give the original NFT to the attendee. This function is called when the user scans the QR code presented by the virtual assistant.

public entry fun gift_nft(
		global: &mut Global,
		to: address,
		name: vector<u8>,
		description: vector<u8>,
		ctx: &mut TxContext,
	) {
		assert!(tx_context::sender(ctx) == global.admin, ENOT_AUTHORIZED);
		let nft = CoffeeNFT {
			id: object::new(ctx),
			name: utf8(name),
			description: utf8(description),
			url: global.url_init,
			redeemed: false,
		};
		let coffee_nft_config = CoffeeNFTConfig {
			merchant_white_list: vec_set::empty(),
			merchant_redeemed: none(),
		};
		table::add(&mut global.nfts, object::id(&nft), coffee_nft_config);
		transfer(nft, to)
	}

When the system initializes the NFT using the code let nft = CoffeeNFT { … }, it sets the url to global.url_init and sets redeemed to false. The module also includes checks to ensure that the user cannot redeem a coffee that may not exist anymore, as shown in the redeem_request function below.

public entry fun redeem_request(
		global: &mut Global,
		nft_id: ID,
		ctx: &mut TxContext,
	) {
		let merchant = tx_context::sender(ctx);
		assert!(vec_set::contains(&global.merchants, &merchant), EMERCHANT_NOT_AUTHORIZED);
		let nft_config = table::borrow_mut(&mut global.nfts, nft_id);
		assert!(option::is_none(&nft_config.merchant_redeemed), ENFT_ALREADY_REDEEMED);
		assert!(!vec_set::contains(&nft_config.merchant_allow_list, &merchant), EMERCHANT_ALREADY_AUTHORIZED);
		vec_set::insert(&mut nft_config.merchant_allow_list, merchant);
	}

This function includes three assertions. The first, EMERCHANT_NOT_AUTHORIZED, asserts that the user calling this function is an allowlisted merchant as determined by the administrator. The second, ENFT_ALREADY_REDEEMED, asserts that the NFT has not yet been redeemed. The third and final assertion, EMERCHANT_ALREADY_AUTHORIZED, asserts that the NFT has not yet been authorized by the merchant. Should all three of these conditions be met, then the redeem_request function will be successfully executed.

NFT showing a coffee plant
Once the NFT has been redeemed for a cup of coffee, the CoffeeNFT object displays a new image.

Account for Available Inventory

Advertisements occasionally advertise specials with the caveat, “while supplies last.” Using NFTs in this manner lets us fully enforce a limited supply concept on-chain!

Finally, if there is still stock available, and the user wants to redeem the NFT, the system calls the redeem_nft function, as shown in the code below.

public entry fun redeem_confirm(
		global: &mut Global,
		nft: &mut CoffeeNFT,
		merchant: address,
		_ctx: &mut TxContext,
	) {
		// check if the merchant authorized to redeem
		let nft_config = table::borrow_mut(&mut global.nfts, object::id(nft));
		assert!(vec_set::contains(&nft_config.merchant_white_list, &merchant), EMERCHANT_NOT_AUTHORIZED);
		// check stock
		let stock = vec_map::get_mut(&mut global.stocks, &merchant);
		assert!(*stock > 0, ENOT_ENOUGH_STOCK);
 
        // redeem
		// update nft config
		nft_config.merchant_redeemed = some(merchant);
		// update stock
		*stock = *stock - 1;
		// update nft
		nft.redeemed = true;
		nft.url = global.url_redeemed;
	}

The above function decrements the amount in stock by 1, changes the NFT’s status to redeemed, and displays the new image URL. In this manner, the NFT gives the attendee and the coffee stand visual confirmation that the NFT has been redeemed.

Updating the Object Display

As noted above, the program code used at the expo relied on an earlier version of Move, in particular as it relates to object display. Writing this program based on the current Object Display Standard could look like the code snippet below.

module momentx::coffee_nft {
	use sui::tx_context::{sender, TxContext};
	use std::string::{utf8, String};
	use sui::transfer;
	use sui::object::UID;
    
	use sui::package;
	use sui::display;
	
    struct CoffeeNFT has key, store {
		id: UID,
		name: String,
		description: String,
		img_url: String,
		redeemed: bool,
	}
    
	struct COFFEE_NFT has drop {}
    
	fun init(otw: COFFEE_NFT, ctx: &mut TxContext) {
		let keys = vector[
			utf8(b"name"),
			utf8(b"description"),
			utf8(b"image_url"),
			utf8(b"redeemed")
		];
        
		let values = vector[
			utf8(b"{name}"),
			utf8(b"{description}"),
			utf8(b"ipfs://{img_url}"),
			utf8(b"{redeemed}"),
		];
        
		let publisher = package::claim(otw, ctx);
        
		let display = display::new_with_fields<CoffeeNFT>(
			&publisher, keys, values, ctx
		);
        
		display::update_version(&mut display);
        
		transfer::public_transfer(publisher, sender(ctx));
		transfer::public_transfer(display, sender(ctx));
	}
    
}

Making NFTs Truly Useful

In the example above, we assign an NFT a real-world consumptive value, a cup of coffee, and create a mechanism to show whether the NFT has been exchanged for its assigned value. This usage goes beyond typical NFT trading on blockchains, demonstrating a real-world use case. Further, we have a concept of stock, or inventory, built into the system, recognizing the reality of real-world items being limited in number.

As a broader concept, the above example shows how NFTs on Sui can be digital tickets with interactive features that go far beyond digital tickets or passes used for things such as movie theaters, concerts, and planes today. Our NFT ticket enjoys built-in security, controlled transferability, and programmable display.

For additional Sui/Move learning resources, check out the links below!