A Giftcard HashMap based
We implemented a gift card contract that allows users to create a transaction to lock assets into the smart contract, which any user can redeem.
Redeeming a gift card means burning it and transferring the assets to the redeemer.
A proper way to implement a gift card is via NFT, but here we opted for using a HashMap. The NFT implementation will be in the next chapter.
Why Use HashMaps Instead of NFTs?
- Simplicity: Avoids NFT standard complexity for educational purposes
- Direct Ownership Tracking: Uses simple owner-value mappings
- Minimal Dependencies: No external libraries required
- Focus on Core Concepts: Demonstrates basic value locking/transfers
Implementation
#![cfg_attr(not(feature = "std"), no_std, no_main)]
#![allow(unexpected_cfgs)] // Allows for contract-specific configurations
/// A simple gift card contract using HashMap-like storage
#[ink::contract]
mod giftcard {
use ink::prelude::string::String;
use ink::storage::Mapping;
//----------------------------------
// Contract Storage Definition
//----------------------------------
/// The contract's storage structure
#[ink(storage)]
pub struct Giftcard {
/// Maps gift card IDs to their owners (AccountId)
owners: Mapping<u64, AccountId>,
/// Maps gift card IDs to their stored value (Balance)
values: Mapping<u64, Balance>,
/// Maps gift card IDs to their human-readable names
names: Mapping<u64, String>,
/// Auto-incrementing ID counter for new gift cards
next_id: u64,
}
//----------------------------------
// Initialization Implementation
//----------------------------------
/// Default implementation for contract initialization
impl Default for Giftcard {
fn default() -> Self {
Self {
owners: Mapping::new(),
values: Mapping::new(),
names: Mapping::new(),
next_id: 0,
}
}
}
//----------------------------------
// Error Handling
//----------------------------------
/// Custom error types for the gift card contract
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
#[repr(u8)]
pub enum Error {
NotOwner = 0, // Caller doesn't own the gift card
InvalidValue = 1, // Transferred value is invalid
TransferFailed = 2, // Value transfer failed
}
//----------------------------------
// Core Contract Logic
//----------------------------------
impl Giftcard {
/// Constructor initializes a new gift card contract
#[ink(constructor)]
pub fn new() -> Self {
Self::default()
}
/// Create a new gift card by locking funds
#[ink(message, payable)]
pub fn create_giftcard(&mut self, name: String) -> Result<u64, Error> {
// Verify transferred value is greater than 0
let transferred = self.env().transferred_value();
if transferred == 0 {
return Err(Error::InvalidValue);
}
// Generate new ID and get caller
let id = self.next_id;
let caller = self.env().caller();
// Store gift card details
self.owners.insert(id, &caller);
self.values.insert(id, &transferred);
self.names.insert(id, &name);
// Safely increment ID (prevents overflow)
self.next_id = self.next_id.saturating_add(1);
Ok(id) // Return new gift card ID
}
/// Redeem a gift card and claim its value
#[ink(message)]
pub fn redeem_giftcard(&mut self, id: u64) -> Result<(), Error> {
let caller = self.env().caller();
// Verify ownership and existence
let owner = self.owners.get(id).ok_or(Error::InvalidValue)?;
if owner != caller {
return Err(Error::NotOwner);
}
// Retrieve stored value
let value = self.values.get(id).ok_or(Error::InvalidValue)?;
// Clean up storage
self.owners.remove(id);
self.values.remove(id);
self.names.remove(id);
// Transfer value to caller
self.env()
.transfer(caller, value)
.map_err(|_| Error::TransferFailed)
}
/// Retrieve gift card details
#[ink(message)]
pub fn get_giftcard(&self, id: u64) -> Option<(AccountId, Balance, String)> {
Some((
self.owners.get(id)?,
self.values.get(id)?,
self.names.get(id)?,
))
}
}
// ... let's skip the tests for now (it's implemented below) ...
}
Key components explained
1. Storage Structure
#[ink(storage)]
pub struct Giftcard {
owners: Mapping<u64, AccountId>,
values: Mapping<u64, Balance>,
names: Mapping<u64, String>,
next_id: u64,
}
- owners: Tracks ownership using simple ID → Account mapping
- values: Stores locked balance for each gift card
- names: Human-readable identifier for each card
- next_id: Simple counter for unique IDs
2. Gift Card Creation
#[ink(message, payable)]
pub fn create_giftcard(&mut self, name: String) -> Result<u64, Error> {
// Value validation
let transferred = self.env().transferred_value();
if transferred == 0 {
return Err(Error::InvalidValue);
}
// Store details
let id = self.next_id;
self.owners.insert(id, &caller);
self.values.insert(id, &transferred);
self.names.insert(id, &name);
// Increment ID safely
self.next_id = self.next_id.saturating_add(1);
Ok(id)
}
- Requires attached value (> 0)
- Uses auto-incrementing IDs
- Stores all components separately
3. Gift Card Redemption
#[ink(message)]
pub fn redeem_giftcard(&mut self, id: u64) -> Result<(), Error> {
// Ownership check
let owner = self.owners.get(id).ok_or(Error::InvalidValue)?;
if owner != caller {
return Err(Error::NotOwner);
}
// Cleanup storage
self.owners.remove(id);
self.values.remove(id);
self.names.remove(id);
// Value transfer
self.env().transfer(caller, value)
}
- Ensures caller owns the gift card
- Removes all stored data
- Transfers locked value
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
use ink::env::test;
/// Helper function to set up testing environment
fn setup_contract() -> Giftcard {
Giftcard::new()
}
#[ink::test]
fn create_giftcard_works() {
// Arrange
let mut contract = setup_contract();
let accounts = test::default_accounts::<ink::env::DefaultEnvironment>();
let value = 1000;
// Act
test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
test::set_value_transferred::<ink::env::DefaultEnvironment>(value);
let result = contract.create_giftcard("Birthday".to_string());
// Assert
assert_eq!(result, Ok(0));
assert_eq!(contract.next_id, 1);
let (owner, stored_value, name) = contract.get_giftcard(0).unwrap();
assert_eq!(owner, accounts.alice);
assert_eq!(stored_value, value);
assert_eq!(name, "Birthday");
}
#[ink::test]
fn redeem_giftcard_works() {
// Arrange
let mut contract = setup_contract();
let accounts = test::default_accounts::<ink::env::DefaultEnvironment>();
let value = 1000;
// Create giftcard first
test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
test::set_value_transferred::<ink::env::DefaultEnvironment>(value);
let id = contract.create_giftcard("Christmas".to_string()).unwrap();
// Get initial balance
let initial_balance =
test::get_account_balance::<ink::env::DefaultEnvironment>(accounts.alice).unwrap();
// Act
let redeem_result = contract.redeem_giftcard(id);
// Assert
assert_eq!(redeem_result, Ok(()));
// Verify balance increased
let final_balance =
test::get_account_balance::<ink::env::DefaultEnvironment>(accounts.alice).unwrap();
assert_eq!(final_balance, initial_balance + value);
// Verify storage cleanup
assert_eq!(contract.get_giftcard(id), None);
}
#[ink::test]
fn non_owner_cannot_redeem() {
// Arrange
let mut contract = setup_contract();
let accounts = test::default_accounts::<ink::env::DefaultEnvironment>();
let value = 500;
// Alice creates giftcard
test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
test::set_value_transferred::<ink::env::DefaultEnvironment>(value);
let id = contract.create_giftcard("Anniversary".to_string()).unwrap();
// Bob tries to redeem
test::set_caller::<ink::env::DefaultEnvironment>(accounts.bob);
let result = contract.redeem_giftcard(id);
// Assert
assert_eq!(result, Err(Error::NotOwner));
}
#[ink::test]
fn redeem_invalid_id_fails() {
// Arrange
let mut contract = setup_contract();
let accounts = test::default_accounts::<ink::env::DefaultEnvironment>();
// Act (try to redeem non-existent ID)
test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
let result = contract.redeem_giftcard(999);
// Assert
assert_eq!(result, Err(Error::InvalidValue));
}
#[ink::test]
fn create_with_zero_value_fails() {
// Arrange
let mut contract = setup_contract();
let accounts = test::default_accounts::<ink::env::DefaultEnvironment>();
// Act
test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
test::set_value_transferred::<ink::env::DefaultEnvironment>(0);
let result = contract.create_giftcard("Empty".to_string());
// Assert
assert_eq!(result, Err(Error::InvalidValue));
}
#[ink::test]
fn get_giftcard_returns_correct_data() {
// Arrange
let mut contract = setup_contract();
let accounts = test::default_accounts::<ink::env::DefaultEnvironment>();
let value = 750;
test::set_caller::<ink::env::DefaultEnvironment>(accounts.bob);
test::set_value_transferred::<ink::env::DefaultEnvironment>(value);
let id = contract.create_giftcard("Graduation".to_string()).unwrap();
// Act
let details = contract.get_giftcard(id).unwrap();
// Assert
assert_eq!(details.0, accounts.bob);
assert_eq!(details.1, value);
assert_eq!(details.2, "Graduation");
}
#[ink::test]
fn get_giftcard_returns_none_for_invalid_id() {
// Arrange
let contract = setup_contract();
// Act
let result = contract.get_giftcard(42);
// Assert
assert_eq!(result, None);
}
}
Config file
Let’s not forget to update the package manager and build system Cargo.toml
[package]
name = "giftcard"
version = "0.1.0"
authors = ["Your Name <your.email@example.com>"]
edition = "2021"
[dependencies]
ink = { version = "5.1.1", default-features = false }
scale = { package = "parity-scale-codec", version = "3.7.4", default-features = false, features = ["derive"] }
scale-info = { version = "2.11.6", default-features = false, features = ["derive"], optional = true }
[lib]
name = "giftcard"
path = "lib.rs"
[features]
default = ["std"]
std = [
"ink/std",
"scale/std",
"scale-info/std",
]
ink-as-dependency = []
Trade-offs of HashMap Approach
No Standardization:
- No metadata standardization
Manual ID Management:
- Requires custom ID tracking
- No built-in uniqueness guarantees
No Transferability:
- Simple ownership model
- No built-in transfer function
Storage Efficiency:
- Separate mappings for each property
Preview of the NFT-Based implementation
Why adopt NFTs?
Standardization:
- PSP34 compliance for interoperability
- Compatible with NFT marketplaces
Built-in functionality:
- Automatic ownership tracking
- Metadata support
- Transfer functions
Enhanced features:
- Royalty support
- Provenance tracking
- Collection management
Key differences to expect
Token-centric design:
#[ink(storage)]
pub struct Giftcard {
psp34: psp34::Data,
values: Mapping<Id, Balance>,
}
Standardized interface:
impl PSP34 for Giftcard {
// Inherit standard functions
}
Metadata support:
#[derive(Default, Debug)]
pub struct GiftcardMetadata {
value: Balance,
created_at: Timestamp,
}