Skip to content

Thiết kế: NFT Management Feature

1. Tổng quan kiến trúc

flowchart TB
    subgraph Client
        App["Mobile App"]
    end

    subgraph Web["web module (port 8080)"]
        HC["NftHunterController\n/api/nft-hunter/*"]
        GC["NftGauntletController\n/api/nft-gauntlet/*"]
        BBC["NftBountyBallController\n/api/nft-bounty-ball/*"]
    end

    subgraph Marketplace["webmarketplace module (port 8084)"]
        RC["NftRentalController\n/api/nft-rental/*"]
    end

    subgraph Core["application-core"]
        ROS["NftRentalOrderService"]
        NHS["NftHunterService"]
        NGS["NftGauntletService"]
        NBS["NftBountyBallService"]
        Coin["UserSystemCoinService"]
        Node["NodeServerService"]
        Notify["NotificationHandleService"]
        Product["ProductService"]
    end

    subgraph Batch
        EXPIRY["NftRentalExpirationCheckBatch\n(scheduled)"]
    end

    App --> HC & GC & BBC
    App --> RC
    HC --> NHS
    GC --> NGS
    BBC --> NBS
    RC --> ROS
    ROS --> NBS & Coin & Node & Notify & Product
    EXPIRY --> ROS

2. Rental order sequence - confirm rent

sequenceDiagram
    participant C as Renter App
    participant RC as NftRentalController
    participant ROS as NftRentalOrderService
    participant NBS as NftBountyBallService
    participant Coin as UserSystemCoinService
    participant Node as NodeServerService
    participant Notify as NotificationHandleService
    participant DB as MySQL

    C->>RC: PUT /api/nft-rental/confirm-rent/{rentalOrderId}
    RC->>ROS: confirmNftRental(renterId, orderId)

    ROS->>DB: findRentalOrder(orderId)
    ROS->>ROS: validate status == NEW
    ROS->>ROS: validate renter eligibility (max active rentals, etc.)

    ROS->>Coin: charge renter B-Coin (rental_fee)
    ROS->>Coin: deposit owner B-Coin (fee - admin_fee)
    ROS->>Coin: admin fee deduction

    ROS->>DB: update order: status=RENTING, renterId, startTime, endTime
    ROS->>DB: create NftRentalOrderHistory record

    alt NFT is minted (on-chain)
        ROS->>Node: confirmSolanaNftRental(orderId, ...)
    end

    ROS->>Notify: notify owner "NFT rented out"
    ROS-->>RC: void
    RC-->>C: Result.OK()

3. Rental order state machine

stateDiagram-v2
    [*] --> NEW: POST /api/nft-rental\n(owner lists NFT)

    NEW --> NEW_VERIFIED: verify-order-creation success
    NEW --> CANCELLED: verify-order-creation fail\n(on-chain reject)
    NEW --> CANCELLED: cancel-rental-order

    NEW_VERIFIED --> RENTING: confirm-rent\n(renter confirms)

    RENTING --> FINISHED: rental_end_time reached\n(batch processExpiredRental)
    RENTING --> RETURN_EARLY: return-rental-early\n(renter returns early)
    RENTING --> EXPIRED_HANDLED: NftRentalExpirationCheckBatch

    FINISHED --> [*]
    RETURN_EARLY --> [*]
    EXPIRED_HANDLED --> [*]
    CANCELLED --> [*]

    note right of NEW: product.status = RENTING
    note right of CANCELLED: product.status = AVAILABLE
    note right of FINISHED: product.status = AVAILABLE\nowner notified

4. Product status sync

flowchart LR
    Available["AVAILABLE"] -->|"Create rental order"| Renting["RENTING"]
    Renting -->|"Rental finishes/returns/expires"| Available
    Renting -->|"verify fail"| Available
    Available -->|"In gameplay"| InGame["IN_GAME"]
    InGame -->|"Game ends"| Available

5. Minted vs unminted NFT branches

// In NftRentalOrderService.confirmNftRental():
public void confirmNftRental(String renterId, String orderId) {
    // ... coin transactions, DB updates ...

    NftBountyBallModel ball = nftBountyBallService.findById(order.getBountyBallId());

    if (ball.isMinted()) {
        // On-chain: notify node server to update blockchain state
        nodeServerService.confirmSolanaNftRental(
            order.getId(),
            ball.getTokenId(),
            renter.getPublicAddress()
        );
    }
    // If not minted: DB-only, no blockchain call needed
}

6. Rental fee calculation

// NftRentalController.getListRentalDuration()
BigDecimal coefficientA = getCoefficientA(rarity);  // per rarity
BigDecimal rentalFeeSuggest = coefficientA.multiply(BigDecimal.valueOf(duration));

// getCoefficientA():
COMMON     50
UNCOMMON   100
RARE       300
EPIC       500
LEGENDARY  1000

7. Market rental filter logic

// NftRentalController.getMarketRentalPageRentalOrder()
FilterNftRentalOrder filter = new FilterNftRentalOrder();
filter.setRentalStatus(NftEnum.RentalOrderStatus.NEW);  // only available listings
filter.setOwnerId("!" + loginUserId);  // exclude own listings (! prefix = NOT equal)
// Sort options: NEWEST (default) or other RentalOrderSortType values
// Lang key is required for ball localization

8. Error handling

Situation Exception Message
Rental order không tìm thấy BadRequestException RENTAL_ORDER_NOT_FOUND
NFT information không tìm thấy BadRequestException NFT_INFORMATION_NOT_FOUND
Rental order history không tìm thấy BadRequestException RENTAL_ORDER_HISTORY_NOT_FOUND
Thiếu lang_key param BadRequestException LANG_KEY_IS_INVALID
Invalid sort value BadRequestException INVALID_SORT_VALUE
Owner không tồn tại NotfoundException USER_NOT_FOUND

9. Điểm cần chú ý

# Vấn đề Chi tiết
1 testChangeOwner endpoint Test endpoint không nên tồn tại trên production
2 finishRental endpoint Cũng là test endpoint (PUT /finish-rental-order/{id})
3 Admin fee logic Xác nhận admin fee percentage/flat được config ở đâu
4 Concurrent confirm Race condition khi 2 renters confirm cùng lúc → cần transaction lock