This feature allows users to purchase event tickets for friends or family members by specifying a recipient wallet address at the time of purchase. The buyer pays for the ticket, but the ticket ownership is transferred to the recipient's wallet immediately after the purchase is confirmed.
Added owner_address field to track the actual ticket owner (recipient):
pub struct Payment {
pub payment_id: String,
pub event_id: String,
pub buyer_address: Address, // Who paid for the ticket
pub owner_address: Address, // Who owns the ticket (recipient)
pub ticket_tier_id: String,
// ... other fields
}- Added
recipient_address: Option<Address>parameter - If
recipient_addressisNone, the buyer is also the owner - If
recipient_addressis provided, the ticket is owned by the recipient - The
owner_addressis used for inventory tracking instead ofbuyer_address
Function Signature:
pub fn process_payment(
env: Env,
payment_id: String,
event_id: String,
ticket_tier_id: String,
buyer_address: Address,
recipient_address: Option<Address>, // NEW: Optional recipient
token_address: Address,
amount: i128,
quantity: u32,
options: PurchaseOptions,
validation_hash: BytesN<32>,
) -> Result<String, TicketPaymentError>Key Logic:
// Determine the actual owner of the ticket (recipient or buyer)
let owner_address = recipient_address.unwrap_or_else(|| buyer_address.clone());
// Use owner_address for inventory tracking
registry_client.increment_inventory(&event_id, &ticket_tier_id, &owner_address, &quantity);Updated the Ticket model to track both buyer and owner:
model Ticket {
id String @id @default(uuid())
stellarId String? @unique
eventId String
event Event @relation(fields: [eventId], references: [id])
buyerWallet String // The person who paid for the ticket
ownerWallet String // The person who owns/receives the ticket
quantity Int @default(1)
createdAt DateTime @default(now())
}Migration Required:
cd apps/web
npx prisma migrate dev --name add_owner_wallet_to_ticketstype TicketRequestBody = {
eventId?: string;
quantity?: number;
buyerWallet?: string;
recipientWallet?: string; // NEW: Optional recipient wallet
};// Validate recipientWallet if provided
if (recipientWallet !== undefined && recipientWallet !== null && typeof recipientWallet !== "string") {
throwApiError("Invalid recipientWallet", 400);
}
// Determine the actual owner of the ticket
const ownerWallet = recipientWallet || buyerWallet;
// Use ownerWallet for minting
const mintResult = await mintTicket(eventId, ownerWallet, qty);
// Store both buyer and owner in database
await prisma.ticket.create({
data: {
stellarId: mintResult.ticketId,
eventId,
buyerWallet,
ownerWallet,
quantity: qty,
},
});const [recipientWallet, setRecipientWallet] = useState<string>("");
const [isGiftMode, setIsGiftMode] = useState(false);- Gift Mode Toggle - A toggle switch to enable/disable gift mode
- Recipient Wallet Input - Text input for entering the recipient's Stellar wallet address
- Enhanced Success Message - Shows different messages for gift vs. regular purchases
{/* Gift Mode Toggle */}
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<Gift size={20} className="text-black/70" />
<span className="text-lg font-bold text-black">Gift to someone?</span>
</div>
<button onClick={() => setIsGiftMode(!isGiftMode)}>
{/* Toggle switch UI */}
</button>
</div>
{/* Recipient Wallet Input (shown when gift mode is enabled) */}
{isGiftMode && (
<div className="flex flex-col gap-2">
<label>Recipient Wallet Address</label>
<input
type="text"
value={recipientWallet}
onChange={(e) => setRecipientWallet(e.target.value)}
placeholder="G... (Stellar address)"
/>
<p>The ticket will be sent to this wallet address</p>
</div>
)}const requestBody = {
eventId: event.id.toString(),
quantity: quantity,
buyerWallet: "G...MOCK_WALLET_ADDRESS",
};
// Only include recipientWallet if gift mode is enabled
if (isGiftMode && recipientWallet.trim()) {
requestBody.recipientWallet = recipientWallet.trim();
}✅ The buyer can specify a recipient address at purchase time
- Gift mode toggle in the ticket purchase modal
- Recipient wallet address input field
- Validation of recipient wallet address
✅ The buyer pays, but the ticket is owned by the recipient
buyer_addressfield tracks who paidowner_addressfield tracks who owns the ticket- Payment is deducted from buyer's wallet
- Ticket appears in recipient's wallet
✅ The friend's wallet shows the ticket immediately after purchase
increment_inventoryusesowner_address(recipient)- Database stores
ownerWalletas the recipient - Smart contract creates ticket with
owner = recipient
- User clicks "Register" on event page
- Opens ticket modal, selects quantity
- Clicks "Confirm Purchase"
- Ticket is minted to buyer's wallet
- Success message: "Ticket purchased successfully!"
- User clicks "Register" on event page
- Opens ticket modal, selects quantity
- Toggles "Gift to someone?" switch
- Enters recipient's Stellar wallet address
- Clicks "Confirm Purchase"
- Buyer pays, but ticket is minted to recipient's wallet
- Success message: "Ticket purchased as a gift! The recipient will see it in their wallet."
- Success screen shows: "Your gift ticket has been sent to G...XXXX"
- Buyer Authentication: Only the buyer needs to authenticate (
buyer_address.require_auth()) - Recipient Validation: Recipient address is validated as a proper Stellar address
- Payment Security: Payment is always deducted from the buyer's wallet
- Ownership Transfer: Ticket ownership is atomically transferred to recipient during minting
- Refund Handling: Refunds should go back to the buyer (who paid), not the recipient
- Purchase ticket without gift mode (buyer = owner)
- Purchase ticket with gift mode (buyer ≠ owner)
- Verify ticket appears in recipient's wallet
- Verify buyer's wallet is charged
- Verify database stores both buyer and owner correctly
- Test with invalid recipient wallet address
- Test with empty recipient wallet address (should default to buyer)
- Test multiple ticket purchase as gift
- Verify inventory tracking uses owner address
- Verify per-user limits apply to owner, not buyer
- Gift Message: Allow buyer to include a personal message with the gift
- Gift Notification: Send notification to recipient when they receive a gift ticket
- Gift History: Track gift purchases in buyer's transaction history
- Gift Wrapping: Special UI treatment for gifted tickets in recipient's wallet
- Gift Redemption: Allow recipient to accept or decline gift tickets
- Bulk Gifting: Purchase multiple tickets for different recipients in one transaction
POST /api/payments/ticket
{
"eventId": "event-123",
"quantity": 2,
"buyerWallet": "GABC...XYZ"
}POST /api/payments/ticket
{
"eventId": "event-123",
"quantity": 2,
"buyerWallet": "GABC...XYZ",
"recipientWallet": "GDEF...UVW"
}SELECT * FROM "Ticket" WHERE "buyerWallet" = 'GABC...XYZ';SELECT * FROM "Ticket" WHERE "ownerWallet" = 'GABC...XYZ';SELECT * FROM "Ticket" WHERE "buyerWallet" != "ownerWallet";- The feature is backward compatible: if
recipientWalletis not provided, it defaults tobuyerWallet - The smart contract uses
Option<Address>for the recipient parameter, making it optional - The UI gracefully handles both gift and non-gift purchases
- The database migration adds the
ownerWalletfield to existing tickets (will need to backfill withbuyerWalletvalues)