Разработка маркетплейса виртуальной недвижимости

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка маркетплейса виртуальной недвижимости
Сложная
от 1 недели до 3 месяцев
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

Разработка маркетплейса виртуальной недвижимости

Виртуальная недвижимость — NFT, представляющий координаты или участок в цифровом пространстве. Decentraland LAND, Sandbox LAND, Otherside Otherdeed — самые известные. Каждый из них имеет marketplace для вторичной торговли, аренды и застройки. Разработка собственного маркетплейса виртуальной недвижимости — это пересечение NFT marketplace инфраструктуры, on-chain rental механик и spatial data management.

Техническая специфика по сравнению с generic NFT marketplace: парцели имеют координаты (x, y), возможно соседство и adjacency бонусы, аренда временная с возвратом прав, застройка создаёт metadata relationship между LAND NFT и content NFT.

LAND NFT: специфика данных

Coordinate system on-chain

Каждый парцель — NFT с координатами в сетке. Стандартный подход: tokenId кодирует координаты.

contract VirtualLand is ERC721 {
    struct Parcel {
        int256 x;
        int256 y;
        address tenant;         // текущий арендатор (если сдан)
        uint256 leaseExpiry;    // timestamp окончания аренды
        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) {
        // Проверяем adjacency
        require(_areAdjacent(parcelIds), "Parcels must be adjacent");
        // Проверяем ownership всех парцелей
        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)

Аренда виртуальной недвижимости — значимый use case: владелец хранит LAND как инвестицию, арендатор использует для застройки/мероприятий. Ключевой вопрос: как разделить ownership (NFT у владельца) и usage rights (у арендатора)?

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

ERC-4907 добавляет роль user к ERC-721 — временный пользователь с expiry timestamp. Контракты могут проверять 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));

        // Выплачиваем владельцу (minus protocol fee)
        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);
    }
}

Collateral rental (без ERC-4907)

Если LAND контракт не поддерживает ERC-4907: временная передача NFT с collateral. Арендатор вносит залог (равный или больше стоимости LAND), NFT передаётся, по окончании — автоматический возврат через keeper или manual claim.

Проблема: арендодатель теряет физическое владение NFT на время аренды (хотя имеет право вернуть). Риск: арендатор продаёт NFT несмотря на collateral. Решение: NFT передаётся в escrow контракт, не арендатору.

Marketplace механики

Listing и аукционы

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: бидинг с outbid защитой (минимальное повышение ставки на X%).

Royalties и fee структура

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

Fee Получатель Размер
Marketplace fee Protocol treasury 2-2.5%
Creator royalty Оригинальный создатель метавселенной 2.5-5%
Referral Если есть referral program 0.5-1%
Seller Владелец LAND Остаток

Royalties для виртуальной недвижимости — контроверсиальная тема. Платформы типа Blur подорвали enforcement. Решение: royalties enforcement через контракт (не зависит от marketplace), или royalty-free модель с revenue sharing другого типа.

Adjacency premium и bundle pricing

Уникальная черта земельных маркетплейсов: соседние парцели стоят больше вместе, чем порознь. Алгоритм поиска adjacency:

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
}

Frontend отображает на карте выделенные смежные лоты при hover на один — пользователь видит потенциальные bundle покупки.

Spatial Data и Map Interface

Интерактивная карта — основной UI маркетплейса. Требования: отображение тысяч парцелей с цветовой кодировкой (продаётся, сдаётся, занято), плавный zoom/pan, клик на парцель → детальная информация.

Mapbox GL JS / deck.gl — наиболее производительные варианты для spatial rendering тысяч объектов. 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 metadata и быстрых spatial queries. Postgis extension для геопространственных запросов:

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

Content Layer: что строят на LAND

Застройка LAND — отдельный слой данных. Стандартные форматы:

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

GLTF / GLB assets. 3D объекты, загружаемые в spatial content. NFT могут представлять конкретные 3D объекты (wearables, buildings).

iframe-based content. Простой веб-контент в VR-overlay. Менее immersive, но легко создаётся.

Content URI хранится в LAND NFT metadata. При изменении застройки владелец обновляет contentURI через setContentURI(tokenId, newURI). Это on-chain транзакция, история изменений сохраняется.

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

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

  • Floor price по зонам (residential vs commercial vs plaza adjacency)
  • Price history per parcel (через on-chain event indexing)
  • Volume по дням/неделям
  • Heatmap активности: какие районы торгуются больше
  • Rental yield calculator: годовой rental income / current 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 с adjacency validation
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?), rental модель, fee структура.

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

Backend и indexer (3-4 недели). Subgraph для событий, REST/GraphQL API, spatial queries в PostGIS, price analytics.

Map Frontend (4-6 недель). Интерактивная карта (deck.gl), parcel detail page, listing и rental UI, analytics dashboard.

3D Content preview (2-3 недели, опционально). GLTF preview для застроенных парцелей, basic 3D viewer.

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

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