Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions soroban-contract/contracts/ticket_nft/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env};

#[derive(Clone)]
#[contracttype]
pub enum DataKey {
Minter,
NextTokenId,
Owner(u128),
Balance(Address),
}

#[contract]
pub struct TicketNFT;

#[contractimpl]
impl TicketNFT {
/// Initialize the contract with a minter address.
pub fn initialize(env: Env, minter: Address) {
if env.storage().instance().has(&DataKey::Minter) {
panic!("Already initialized");
}
//! Ticket NFT Contract (Stub for Issue #3)
//!
//! Minimal implementation for testing the Ticket Factory.
Expand Down Expand Up @@ -39,6 +61,28 @@ impl TicketNft {
env.storage().instance().set(&DataKey::NextTokenId, &1u128);
}

/// Mint a ticket NFT to the recipient.
/// Only the minter can call this.
/// Each recipient can only have one ticket.
pub fn mint_ticket_nft(env: Env, recipient: Address) -> u128 {
let minter: Address = env.storage().instance().get(&DataKey::Minter).expect("Not initialized");
minter.require_auth();

let balance = Self::balance_of(env.clone(), recipient.clone());
if balance > 0 {
panic!("User already has ticket");
}

let token_id: u128 = env.storage().instance().get(&DataKey::NextTokenId).unwrap();

// Update ownership
env.storage().persistent().set(&DataKey::Owner(token_id), &recipient);

// Update balance
env.storage().persistent().set(&DataKey::Balance(recipient), &1u128);

// Increment token ID
env.storage().instance().set(&DataKey::NextTokenId, &(token_id + 1));
/// Mint a new ticket NFT to the recipient
///
/// # Arguments
Expand Down Expand Up @@ -92,6 +136,7 @@ impl TicketNft {
token_id
}

/// Get the owner of a specific token ID.
/// Get the owner of a token
///
/// # Arguments
Expand All @@ -104,6 +149,11 @@ impl TicketNft {
env.storage()
.persistent()
.get(&DataKey::Owner(token_id))
.expect("Token ID does not exist")
}

/// Get the number of tickets owned by an address.
/// Since we enforce one ticket per user, this will be 0 or 1.
.unwrap()
}

Expand All @@ -122,6 +172,30 @@ impl TicketNft {
.unwrap_or(0)
}

/// Transfer a ticket NFT from one address to another.
/// Enforces the one-ticket-per-user rule for the recipient.
pub fn transfer_from(env: Env, from: Address, to: Address, token_id: u128) {
from.require_auth();

let owner = Self::owner_of(env.clone(), token_id);
if owner != from {
panic!("Not the owner");
}

if Self::balance_of(env.clone(), to.clone()) > 0 {
panic!("Recipient already has a ticket");
}

// Update ownership
env.storage().persistent().set(&DataKey::Owner(token_id), &to);

// Update balances
env.storage().persistent().set(&DataKey::Balance(from), &0u128);
env.storage().persistent().set(&DataKey::Balance(to), &1u128);
}
}

mod test;
/// Get the minter address
///
/// # Arguments
Expand Down
118 changes: 118 additions & 0 deletions soroban-contract/contracts/ticket_nft/src/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#![cfg(test)]

use super::*;
use soroban_sdk::testutils::Address as _;
use soroban_sdk::{Env};

#[test]
fn test_minting() {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register_contract(None, TicketNFT);
let client = TicketNFTClient::new(&env, &contract_id);

let minter = Address::generate(&env);
let user1 = Address::generate(&env);
let user2 = Address::generate(&env);

client.initialize(&minter);

// Mint first ticket
let token_id1 = client.mint_ticket_nft(&user1);
assert_eq!(token_id1, 1);
assert_eq!(client.owner_of(&token_id1), user1);
assert_eq!(client.balance_of(&user1), 1);

// Mint second ticket
let token_id2 = client.mint_ticket_nft(&user2);
assert_eq!(token_id2, 2);
assert_eq!(client.owner_of(&token_id2), user2);
assert_eq!(client.balance_of(&user2), 1);
}

#[test]
#[should_panic(expected = "User already has ticket")]
fn test_cannot_mint_twice_to_same_user() {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register_contract(None, TicketNFT);
let client = TicketNFTClient::new(&env, &contract_id);

let minter = Address::generate(&env);
let user = Address::generate(&env);

client.initialize(&minter);

client.mint_ticket_nft(&user);
client.mint_ticket_nft(&user); // Should panic
}

#[test]
fn test_transfer() {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register_contract(None, TicketNFT);
let client = TicketNFTClient::new(&env, &contract_id);

let minter = Address::generate(&env);
let user1 = Address::generate(&env);
let user2 = Address::generate(&env);

client.initialize(&minter);

let token_id = client.mint_ticket_nft(&user1);

client.transfer_from(&user1, &user2, &token_id);

assert_eq!(client.owner_of(&token_id), user2);
assert_eq!(client.balance_of(&user1), 0);
assert_eq!(client.balance_of(&user2), 1);
}

#[test]
#[should_panic(expected = "Recipient already has a ticket")]
fn test_cannot_transfer_to_user_with_ticket() {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register_contract(None, TicketNFT);
let client = TicketNFTClient::new(&env, &contract_id);

let minter = Address::generate(&env);
let user1 = Address::generate(&env);
let user2 = Address::generate(&env);

client.initialize(&minter);

let token_id1 = client.mint_ticket_nft(&user1);
let _token_id2 = client.mint_ticket_nft(&user2);

client.transfer_from(&user1, &user2, &token_id1); // Should panic
}

#[test]
#[should_panic] // Only authorized minter can mint
fn test_only_minter_can_mint() {
let env = Env::default();
// env.mock_all_auths(); // Don't mock auth to test failure

let contract_id = env.register_contract(None, TicketNFT);
let client = TicketNFTClient::new(&env, &contract_id);

let minter = Address::generate(&env);
let _user = Address::generate(&env);
let _non_minter = Address::generate(&env);

client.initialize(&minter);

// This should fail because we're not calling as minter and haven't mocked auths
// To be more precise, we can use env.set_auths or mock_all_auths and then check if require_auth was called.
// In Soroban tests, mock_all_auth() makes it so any require_auth() succeeds.
// To test failures, we can either not mock, or selectively mock.

// If we don't mock, it should fail.
client.mint_ticket_nft(&user);
}
Loading
Loading