Skip to main content

Implementing the Hot Potato Pattern

In this tutorial, we will demonstrate how to implement the Hot Potato pattern in the Sui ecosystem. The Hot Potato pattern involves a struct with no abilities, meaning it can only be packed and unpacked in its module. This pattern ensures that certain functions must be called in a specific order.

For this example, we will implement a simple phone trade-in module, which allows users to buy a phone and pay later or trade-in their old phone for a discount on a new phone.

Follow these steps to implement the Hot Potato pattern:

  1. Create the trade_in module: Define a new module examples::trade_in that will implement the Hot Potato pattern.
module examples::trade_in {
use sui::transfer;
use sui::sui::SUI;
use sui::coin::{Self, Coin};
use sui::object::{Self, UID};
use sui::tx_context::{TxContext};
}
  1. Define constants and errors: Define constants for the phone model prices and error codes for wrong model selection and incorrect payment amounts.
const MODEL_ONE_PRICE: u64 = 10000;
const MODEL_TWO_PRICE: u64 = 20000;
const EWrongModel: u64 = 1;
const EIncorrectAmount: u64 = 2;
  1. Define the Phone and Receipt structs: Create a Phone struct that can be purchased or traded in for a newer model, and a Receipt struct that represents a payable receipt.
struct Phone has key, store { id: UID, model: u8 }
struct Receipt { price: u64 }
  1. Create the buy_phone function: Define a public function buy_phone that takes a phone model and a mutable transaction context as arguments. It returns a tuple containing a Phone and a Receipt.
public fun buy_phone(model: u8, ctx: &mut TxContext): (Phone, Receipt) {
assert!(model == 1 || model == 2, EWrongModel);

let price = if (model == 1) MODEL_ONE_PRICE else MODEL_TWO_PRICE;

(
Phone { id: object::new(ctx), model },
Receipt { price }
)
}
  1. Create the pay_full function: Define a public function pay_full that takes a Receipt and a payment Coin<SUI> as arguments. The function consumes the Receipt and transfers the payment to the @examples account.
public fun pay_full(receipt: Receipt, payment: Coin<SUI>) {
let Receipt { price } = receipt;
assert!(coin::value(&payment) == price, EIncorrectAmount);

// for simplicity's sake transfer directly to @examples account
transfer::public_transfer(payment, @examples);
}
  1. Create the trade_in function: Define a public function trade_in that takes a Receipt, an old Phone, and a payment Coin<SUI> as arguments. The function consumes the Receipt, transfers the old phone and the payment to the @examples account, and applies a discount based on the old phone's model.
public fun trade_in(receipt: Receipt, old_phone: Phone, payment: Coin<SUI>) {
let Receipt { price } = receipt;
let tradein_price = if (old_phone.model == 1) MODEL_ONE_PRICE else MODEL_TWO_PRICE;
let to_pay = price - (tradein_price / 2);

assert!(coin::value(&payment) == to_pay, EIncorrectAmount);

transfer::public