Santa’s Christmas wishlist deployed on Stellar blockchain with Soroban | Part 1

Santa has got into some trouble this year, kids’ wishlists have been compromised and now he has to make sure the wishlists are not sent to him from the unknown source of truth. We are going to design a Stellar smart contract with Soroban to empower the Santa process of collecting the wishlists from kids.
If thinking about the implementation, kids usually change their minds with the gifts they want, they have a ttl by default. The data they provide becomes invalid after a while. Stellar has this feature implemented by default and we will see in a second how this can be played to take advantage of it. The concepts you will learn in this article are — storage, events, auth and ttl.
First of all make sure you have Rust installed, if not you can install it from here
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shand Stellar CLI, please follow this instruction to install it https://developers.stellar.org/docs/tools/cli/install-cli
Let’s start
- Generate stellar accounts on testnet and initiate the project
stellar keys generate santa
stellar keys generate kid1
stellar keys generate kid2
stellar keys list #to see the keys
stellar contract init ./santa --name santa_whishlistOne wishlist item would look like this
#![no_std]
use soroban_sdk::{
contract, contractimpl, contracttype, symbol_short, Address, Env, String, Vec,
};
#[derive(Clone)]
#[contracttype]
pub struct Wish {
pub id: u32,
pub text: String,
pub created_at_ledger: u32,
pub fulfilled: bool,
}In order to be able to access our storage we need a DataKey enum
#[derive(Clone)]
#[contracttype]
pub enum DataKey {
/// Persistent: Stores the vector of wishes for a given owner.
Wishes(Address),
/// Persistent: Stores the next auto-increment ID for a user.
NextId(Address),
/// Instance: Stores the Address of the Contract Admin.
Admin,
}Events we have events
#[contractevent]
pub struct WishAddedEvent {
pub user: Address,
pub add: u32,
}
#[contractevent]
pub struct WishFulfilledEvent {
pub user: Address,
pub wish_id: u32,
}And errors
#[contracterror]
pub enum ContractError
WishNotFound = 1,
TooLateToChange = 2
}Storage Strategy: Instance vs. Persistent
You might notice we are using two different storage strategies in our DataKey: Instance and Persistent.
In Soroban, Instance storage is tied to the contract itself. It is best for “Global” state — data that applies to the application as a whole. In our case, the Admin key (Santa’s address) is stored here because there is only one Santa for this contract.
Persistent storage is tied to the combination of the Contract ID and a specific key (like a user’s Address). This is perfect for the Wishes and NextId. Why? Because Kid1’s wishlist belongs to Kid1. If Kid1 stops believing in Santa (and stops interacting with the contract), we want her data to eventually expire (TTL) independently of Kid2’s data. This prevents state bloat on the blockchain.
Now, let’s look at the contract implementation.
The Contract Logic
We need to implement the SeasonaWishlist. We will use a __constructor to ensure that when the contract is deployed, we immediately set who "Santa" (the admin) is. We will also use require_auth to ensure that only Kid1 can write to Kid1's list, but only Santa can mark items as fulfilled.
Get Radu’s stories in your inbox
Join Medium for free to get updates from this writer.
Add the following logic to your lib.rs:
// ... (imports and structs from above)
#[contract]
pub struct SeasonalWishlist;
// Helpers to manage TTL (Time To Live)
// We bump the lifespan of data every time it is accessed.
fn bump_persistent_ttl(env: &Env, key: &DataKey) {
env.storage().persistent().extend_ttl(key, 2_000, 5_000); // If < 2000 ledgers, bump to 5000
}
fn bump_instance_ttl(env: &Env) {
env.storage().instance().extend_ttl(2_000, 5_000);
}
fn fail(env: &Env, e: ContractError) -> ! {
panic_with_error!(env, e);
}
fn ensure_not_christmas(env: &Env) {
// Get the current time from the blockchain ledger
let current_time = env.ledger().timestamp();
// The deadline timestamp (Unix seconds).
// Example: Dec 25, 2025 00:00:00 UTC
let christmas_deadline = env.storage().instance().get::<_, u64>(&DataKey::ChristmasDeadline).unwrap_or(1_766_620_800);
if current_time >= christmas_deadline {
// It is Christmas (or later), we cannot accept changes!
fail(&env, ContractError::TooLateToChange);
}
}
#[contractimpl]
impl SeasonalWishlist {
/// The Constructor: Runs once on deployment.
/// Sets the "Santa" (Admin) and christmas deadline of the contract.
pub fn __constructor(env: Env, admin: Address, christmas_deadline: u64) {
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::ChristmasDeadline, &christmas_deadline);
}
pub fn set_christmas_deadline(env: &Env, christmas_deadline: u64) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).expect("Santa missing");
admin.require_auth();
env.storage().instance().set(&DataKey::ChristmasDeadline, &christmas_deadline);
}
pub fn add_wish(env: Env, user: Address, text: String) -> u32 {
ensure_not_christmas(&env);
// AUTH: Ensure the transaction signer is actually the user
user.require_auth();
// 1. Generate ID
let id_key = DataKey::NextId(user.clone());
let mut next_id: u32 = env.storage().persistent().get(&id_key).unwrap_or(1);
let current_id = next_id;
next_id += 1;
env.storage().persistent().set(&id_key, &next_id);
// 2. Load existing wishes
let wish_key = DataKey::Wishes(user.clone());
let mut wishes: Vec<Wish> = env.storage().persistent().get(&wish_key).unwrap_or_else(|| Vec::new(&env));
// 3. Add new wish
wishes.push_back(Wish {
id: current_id,
text,
created_at_ledger: env.ledger().sequence(),
fulfilled: false,
});
// 4. Save and Bump TTL
env.storage().persistent().set(&wish_key, &wishes);
bump_persistent_ttl(&env, &wish_key);
bump_instance_ttl(&env);
// EVENT: Emit an event so indexers know a wish was added
WishAddedEvent {
user,
add: current_id
}.publish(&env);
current_id
}
pub fn mark_fulfilled(env: Env, user: Address, wish_id: u32) {
// AUTH: Get the admin address and require THEIR signature
let admin: Address = env.storage().instance().get(&DataKey::Admin).expect("Santa missing");
admin.require_auth();
let wish_key = DataKey::Wishes(user.clone());
let mut wishes: Vec<Wish> = env.storage().persistent().get(&wish_key).unwrap_or_else(|| Vec::new(&env));
// Iterate to find the wish and update it
let mut found = false;
for i in 0..wishes.len() {
let mut wish = wishes.get(i).unwrap();
if wish.id == wish_id {
wish.fulfilled = true;
wishes.set(i, wish);
found = true;
break;
}
}
if !found { fail(&env, ContractError::WishNotFound); }
env.storage().persistent().set(&wish_key, &wishes);
bump_persistent_ttl(&env, &wish_key);
WishFulfilledEvent {
user,
wish_id
}.publish(&env);
}
/// View function to see a user's list
pub fn get_list(env: Env, user: Address) -> Vec<Wish> {
let key = DataKey::Wishes(user.clone());
let wishes = env.storage().persistent().get(&key).unwrap_or_else(|| Vec::new(&env));
// Even reading data requires bumping TTL to keep it alive!
bump_persistent_ttl(&env, &key);
wishes
}
}Building and Deploying
Now that our code is written, we need to compile it to WASM and deploy it to the testnet.
- Build the contract:
stellar contract build2. Deploy the contract: Here is the tricky part. Because we added a __constructor, we must initialize the contract with the Admin argument (Santa's address) immediately upon deployment. This is the same as having a set_config() function that is invoked after you deploy the contract.
stellar contract deploy \
--wasm target/wasm32v1-none/release/santa_whishlist.wasm \
--source-account santa \
--network testnet \
--alias santas_wishlist \
-- \
--admin santa \ #our admin constructor arg
--christmas-deadline 1766620800 #constructor argYou will get a contract id and for the simplicity i will create an env var named WISHLIST_ID
Interaction: The Christmas Flow
Now the fun begins. Let’s roleplay the interaction.
- Kid1 makes a wish. Kid1 calls the contract. The kid must sign it (
--source kid1) souser.require_auth()passes.
stellar contract invoke \
--id $WISHLIST_ID \
--source kid1 \
--network testnet \
-- \
add_wish \
--user kid1 \
--text "A pony that runs on blockchain"2. Santa checks the list. Santa can view Kid1’s list using the public view function.
stellar contract invoke \
--id $WISHLIST_ID \
--source santa \
--network testnet \
-- \
get_list \
--user kid13. Santa fulfills the wish. Only Santa can do this. If Kid1 tries to call this function, the admin.require_auth() check will fail and the transaction will panic.
stellar contract invoke \
--id $WISHLIST_ID \
--source santa \
--network testnet \
-- \
mark_fulfilled \
--user kid1 \
--wish_id 1get_list invoked again
[{"created_at_ledger":1921551,"fulfilled":true,"id":1,"text":"A pony that runs on blockchain"}]Wrapping up with TTL
The “Time To Live” (TTL) is the unsung hero here. In our code, you saw bump_persistent_ttl.
On the Stellar network, you pay “rent” for storing data. If Kid1 sends a wishlist and then disappears for 5 years, Santa shouldn’t have to pay to store that data forever.
By default, we set the data to live for roughly 5,000 ledgers (which is a short time for this example, but configurable). Every time Kid1 interacts with its list (adds a new item) or Santa reads it, we call extend_ttl, pushing the expiration date further out. If no one touches the data, the network eventually archives it, keeping the blockchain efficient and clean.
Congratulations! You just decentralized Christmas. You’ve learned how to secure functions with require_auth, manage distinct storage types, and handle the lifecycle of data with TTL.
“But, as the song goes, ‘Santa’s got a list, he’s checking it twice, going to find out who’s naughty or nice’. Next, we will make sure Santa’s smart contract contains a naughty list that he can check twice”



