Розробка маркетплейсу віртуальної нерухомості

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка маркетплейсу віртуальної нерухомості
Складний
від 1 тижня до 3 місяців
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1285
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1198
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    902
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1120
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    588
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    855

Розробка маркетплейсу віртуальної нерухомості

Віртуальна нерухомість — це NFT, що представляє координати або ділянку в цифровому просторі. Decentraland LAND, Sandbox LAND, Otherside Otherdeed — найвідоміші. Кожен з них має маркетплейс для вторинної торгівлі, оренди та забудови. Розробка власного маркетплейсу віртуальної нерухомості — це перетин інфраструктури NFT маркетплейсу, on-chain механіки оренди та управління просторовими даними.

Технічні особливості в порівнянні з універсальним NFT маркетплейсом: ділянки мають координати (x, y), можлива сусідність та премії за суміжність, оренда є тимчасовою з поверненням прав, забудова створює зв'язок метаданих між LAND NFT і content NFT.

LAND NFT: особливості даних

Система координат on-chain

Кожна ділянка — це NFT з координатами на сітці. Стандартний підхід: tokenId кодує координати.

contract VirtualLand is ERC721 {
    struct Parcel {
        int256 x;
        int256 y;
        address tenant;         // поточний орендатор (якщо здано)
        uint256 leaseExpiry;    // дата закінчення оренди
        string contentURI;      // що побудовано на ділянці
        uint8 zoneType;         // 0=residential, 1=commercial, 2=plaza
    }

    mapping(uint256 => Parcel) public parcels;
    mapping(int256 => mapping(int256 => uint256)) public coordToTokenId;
    // coordToTokenId[x][y] = tokenId

    int256 public constant GRID_MIN = -150;
    int256 public constant GRID_MAX = 150;

    // tokenId = унікальний індекс з координат
    function coordsToTokenId(int256 x, int256 y) public pure returns (uint256) {
        // Зміщуємо в невід'ємні значення
        uint256 ux = uint256(x - GRID_MIN);
        uint256 uy = uint256(y - GRID_MIN);
        uint256 size = uint256(GRID_MAX - GRID_MIN + 1);
        return ux * size + uy;
    }

    function tokenIdToCoords(uint256 tokenId) public pure returns (int256 x, int256 y) {
        uint256 size = uint256(GRID_MAX - GRID_MIN + 1);
        x = int256(tokenId / size) + GRID_MIN;
        y = int256(tokenId % size) + GRID_MIN;
    }
}

Estate: об'єднані ділянки

Estate = кілька суміжних ділянок, об'єднаних в один актив. Це значуще: велика забудована ділянка дорожча за суму частин. Механіка:

contract EstateRegistry is ERC721 {
    struct Estate {
        uint256[] parcels;      // масив tokenId включених ділянок
        address landContract;
    }

    mapping(uint256 => Estate) public estates;
    // ділянка → estate (якщо входить в estate)
    mapping(uint256 => uint256) public parcelToEstate;

    function createEstate(uint256[] calldata parcelIds) external returns (uint256 estateId) {
        // Перевіряємо суміжність
        require(_areAdjacent(parcelIds), "Parcels must be adjacent");
        // Перевіряємо право власності всіх ділянок
        for (uint i = 0; i < parcelIds.length; i++) {
            require(landNft.ownerOf(parcelIds[i]) == msg.sender, "Not owner");
        }

        estateId = ++_estateIdCounter;
        // Передаємо ділянки в escrow цього контракту
        for (uint i = 0; i < parcelIds.length; i++) {
            landNft.transferFrom(msg.sender, address(this), parcelIds[i]);
            parcelToEstate[parcelIds[i]] = estateId;
        }

        estates[estateId] = Estate({ parcels: parcelIds, landContract: address(landNft) });
        _mint(msg.sender, estateId);
    }
}

On-chain оренда (Rental Protocol)

Оренда віртуальної нерухомості — значущий case: власник зберігає LAND як інвестицію, орендатор використовує для забудови/подій. Ключове питання: як розділити право власності (NFT у власника) та права користування (у орендатора)?

ERC-4907: стандарт Rentable NFT

ERC-4907 додає роль user до ERC-721 — тимчасовий користувач з дата закінчення. Контракти можуть перевіряти userOf(tokenId) замість ownerOf для доступу.

contract RentalMarketplace {
    struct RentalOffer {
        uint256 tokenId;
        address landContract;
        uint256 pricePerDay;
        uint256 minDays;
        uint256 maxDays;
        address paymentToken;    // ERC-20 або address(0) для native
        bool active;
    }

    mapping(bytes32 => RentalOffer) public rentalOffers;

    function createRentalOffer(
        uint256 tokenId,
        address landContract,
        uint256 pricePerDay,
        uint256 minDays,
        uint256 maxDays,
        address paymentToken
    ) external {
        require(IERC721(landContract).ownerOf(tokenId) == msg.sender, "Not owner");

        bytes32 offerId = keccak256(abi.encode(tokenId, landContract, msg.sender, block.timestamp));
        rentalOffers[offerId] = RentalOffer({
            tokenId: tokenId,
            landContract: landContract,
            pricePerDay: pricePerDay,
            minDays: minDays,
            maxDays: maxDays,
            paymentToken: paymentToken,
            active: true
        });
    }

    function rent(bytes32 offerId, uint256 days) external payable {
        RentalOffer storage offer = rentalOffers[offerId];
        require(offer.active, "Offer not active");
        require(days >= offer.minDays && days <= offer.maxDays, "Invalid duration");

        uint256 totalCost = offer.pricePerDay * days;
        uint256 expiry = block.timestamp + days * 1 days;

        // Оплата
        if (offer.paymentToken == address(0)) {
            require(msg.value >= totalCost, "Insufficient payment");
        } else {
            IERC20(offer.paymentToken).safeTransferFrom(msg.sender, address(this), totalCost);
        }

        // Встановлюємо user через ERC-4907
        IERC4907(offer.landContract).setUser(offer.tokenId, msg.sender, uint64(expiry));

        // Виплачуємо власнику (мінус комісія протоколу)
        uint256 fee = totalCost * PROTOCOL_FEE_BPS / 10000;
        _transferPayment(offer.paymentToken, IERC721(offer.landContract).ownerOf(offer.tokenId), totalCost - fee);

        emit Rented(offerId, msg.sender, days, expiry);
    }
}

Оренда з застави (без ERC-4907)

Якщо контракт LAND не підтримує ERC-4907: тимчасова передача NFT з застава. Орендатор вносить залог (рівний або більший вартості LAND), NFT передається, при закінченні — автоматичне повернення через keeper або ручне отримання.

Проблема: орендодавець втрачає фізичне володіння NFT протягом оренди (хоча має право його повернути). Ризик: орендатор продає NFT незважаючи на залог. Рішення: NFT передається в escrow контракт, а не орендатору.

Механіка маркетплейсу

Листинг і аукціони

enum SaleType { FIXED_PRICE, ENGLISH_AUCTION, DUTCH_AUCTION }

struct Listing {
    uint256 tokenId;
    address seller;
    SaleType saleType;
    address paymentToken;
    uint256 startPrice;
    uint256 endPrice;          // для Dutch auction: кінцева ціна
    uint256 startTime;
    uint256 endTime;
    uint256 highestBid;        // для English auction
    address highestBidder;
}

Dutch Auction особливо релевантний для первинної продажі LAND: ціна починається високо, автоматично знижується до резервної. Усуває газові війни при mint.

English Auction для вторинного ринку рідких Estate: бідинг з захистом від перебивання (мінімальне підвищення ставки на X%).

Роялті та структура комісій

ERC-2981 для on-chain роялті. Стандартна структура маркетплейсу віртуальної нерухомості:

Комісія Отримувач Розмір
Комісія маркетплейсу Казна протоколу 2-2.5%
Роялті творця Оригінальний творець метавселенної 2.5-5%
Реферальна Якщо є програма реферання 0.5-1%
Продавець Власник LAND Залишок

Роялті для віртуальної нерухомості — контроверсійна тема. Платформи типу Blur підірвали enforcement. Рішення: роялті enforcement через контракт (незалежно від маркетплейсу), або безроялтійна модель з іншим типом ділення доходу.

Премія за суміжність і bundle pricing

Унікальна ознака земельних маркетплейсів: суміжні ділянки коштують разом більше, ніж окремо. Алгоритм пошуку суміжності:

function findAdjacentParcels(parcels: Parcel[], targetParcel: Parcel): Parcel[] {
    const adjacent: Parcel[] = []
    const directions = [[-1,0],[1,0],[0,-1],[0,1],[-1,-1],[1,1],[-1,1],[1,-1]]

    for (const parcel of parcels) {
        for (const [dx, dy] of directions) {
            if (parcel.x === targetParcel.x + dx && parcel.y === targetParcel.y + dy) {
                adjacent.push(parcel)
                break
            }
        }
    }
    return adjacent
}

Фронтенд відображає на карті виділені суміжні ділянки при наведенні на одну — користувачі бачать потенційні bundle покупки.

Просторові дані та інтерфейс карти

Інтерактивна карта — основний UI маркетплейсу. Вимоги: відображення тисяч ділянок з кольоровою кодировкою (продається, здається в оренду, займає), плавний zoom/pan, клік на ділянку → детальна інформація.

Mapbox GL JS / deck.gl — найпродуктивніші варіанти для просторового рендерингу тисяч об'єктів. deck.gl (Uber) оптимізований для геоданих і працює з WebGL.

import { DeckGL } from '@deck.gl/react'
import { ScatterplotLayer } from '@deck.gl/layers'

const parcelLayer = new ScatterplotLayer({
    data: parcels,
    getPosition: (d) => [d.x * PARCEL_SIZE, d.y * PARCEL_SIZE, 0],
    getFillColor: (d) => {
        if (d.forSale) return [0, 200, 100]       // зелений — на продаж
        if (d.forRent) return [0, 100, 200]        // синій — в оренду
        if (d.hasContent) return [150, 100, 200]   // фіолетовий — забудований
        return [100, 100, 100]                     // сірий — порожній
    },
    getRadius: PARCEL_SIZE / 2,
    pickable: true,
    onClick: ({ object }) => setSelectedParcel(object),
})

Індексування даних. The Graph subgraph для on-chain подій (Transfer, Rented, Listed). PostgreSQL для off-chain метаданих та швидких просторових запитів. Розширення Postgis для геопросторових запитів:

-- Знайти всі ділянки в радіусі від координати
SELECT * FROM parcels
WHERE ST_DWithin(
    ST_MakePoint(x, y)::geometry,
    ST_MakePoint($1, $2)::geometry,
    $3 -- radius
)
AND for_sale = true;

Шар контенту: що будується на LAND

Забудова LAND — окремий шар даних. Стандартні формати:

Decentraland SDK scene. Babylon.js-based 3D сцена. Описується в TypeScript, розгортається на content server. Прив'язана до координат LAND.

GLTF / GLB assets. 3D об'єкти, завантажені в просторовий контент. NFT можуть представляти конкретні 3D об'єкти (wearables, будівлі).

Iframe-based content. Простий веб-контент у VR-overlay. Менш immersive, але легко створюється.

Content URI зберігається в метаданих LAND NFT. При зміні забудови власник оновлює contentURI через setContentURI(tokenId, newURI). Це on-chain транзакція, історія змін зберігається.

Аналітика та price discovery

Маркетплейс без аналітики — не конкурентоспроможний. Необхідний мінімум:

  • Floor price за зонами (residential vs commercial vs plaza adjacency)
  • Price history per ділянка (через on-chain event indexing)
  • Volume за днями/тижнями
  • Heatmap активності: які райони торгуються більше
  • Rental yield calculator: річний rental income / поточний floor price
interface PriceAnalytics {
    floorPrice: bigint
    avgPrice: bigint
    volumeLast7d: bigint
    salesCountLast7d: number
    priceChange7d: number      // %
    estateFloorPrice?: bigint  // окремий floor для Estate
}

async function getZoneAnalytics(zoneId: number): Promise<PriceAnalytics> {
    // З subgraph або PostgreSQL
    const sales = await db.query(`
        SELECT price, timestamp FROM sales
        WHERE zone_id = $1 AND timestamp > NOW() - INTERVAL '7 days'
        ORDER BY price ASC
    `, [zoneId])

    return {
        floorPrice: sales[0]?.price ?? 0n,
        avgPrice: average(sales.map(s => s.price)),
        volumeLast7d: sum(sales.map(s => s.price)),
        salesCountLast7d: sales.length,
        priceChange7d: calculateChange(sales),
    }
}

Стек розробки

Компонент Технологія
LAND NFT Solidity ERC-721 + ERC-4907
Estate контракт Solidity з валідацією суміжності
Rental контракт Solidity + ERC-4907
Marketplace контракт Solidity + ERC-2981
Indexer The Graph (subgraph)
Spatial DB PostgreSQL + PostGIS
Map frontend deck.gl / Mapbox GL JS + React
3D preview Three.js / Babylon.js
Backend API Node.js + Fastify
Storage IPFS (Pinata) + Arweave

Процес розробки

Product design (1-2 тиждня). Карта світу, зонування, модель первинної продажі (Dutch auction?), модель оренди, структура комісій.

Smart contracts (4-6 тижнів). LAND NFT з системою координат, Estate контракт з логікою суміжності, Rental marketplace з ERC-4907, Sale marketplace з аукціонами. Аудит обов'язковий.

Backend і indexer (3-4 тиждня). Subgraph для подій, REST/GraphQL API, просторові запити в PostGIS, price analytics.

Map Frontend (4-6 тижнів). Інтерактивна карта (deck.gl), сторінка деталей ділянки, UI листинга та оренди, dashboard аналітики.

3D Content preview (2-3 тиждня, опціонально). GLTF preview для забудованих ділянок, базовий 3D viewer.

Тестування та запуск. End-to-end тест повного потоку (mint → list → buy → rent → build), нагрузочний тест карти (5000+ ділянок у viewport).

MVP без Estate та 3D контенту — 3-4 місяці. Повний маркетплейс з Estate, орендою, аналітикою та 3D preview — 6-8 місяців.