Розробка маркетплейсу віртуальної нерухомості
Віртуальна нерухомість — це 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 місяців.







