Перейти к основному содержимому

Земли и вышки

Страница описывает модель: что такое земля и вышка, как земли выстраиваются в дерево, как вышки занимают места и что из этого хранится в блокчейне, а что — только в Neo4j. Распределение денег — на странице Партнёрка, кешбэк и компрессия, пошаговые покупки — в Флоу покупки.

Земля (Ground, Land NFT)​

Земля — токен ERC721 контракта MagnetLiquidityGround («Magnet Liquidity Ground», MLG) в BSC (apps/contracts-v2/contracts/MagnetLiquidity/MagnetLiquidityGround.sol). Контракт UUPS-апгрейдируемый, с AccessControl (только DEFAULT_ADMIN_ROLE).

У земли четыре характеристики:

ПолеГде хранитсяСмысл
groundIdконтракт, графID токена, сквозной счётчик _groundIdCounter
levelконтракт, графУровень земли, 1–15
groundIndexконтракт, графПорядковый номер земли пользователя на этом уровне: первая — 0, вторая — 1 и т. д.
uplineGroundId / uplineGroundChainIdконтракт, граф (связь)Земля-родитель в дереве земель и её сеть
placesтолько графСколько свободных мест под вышки осталось
groundCashback, towerCashbackтолько графКешбэк владельца по линиям, см. партнёрку

Структура Ground в контракте — MagnetLiquidityGround.sol:42-53. Кроме неё контракт ведёт groundMatrix[uid][level][groundIndex] → groundId (MagnetLiquidityGround.sol:88-90): по нему нельзя купить вторую землю с теми же uid, level и groundIndex (GroundAlreadyExists).

Земля привязана к uid из Diamond, а не к адресу: контракт получает uid покупателя из PartnerChainBridge.userIds(msg.sender) (MagnetLiquidityGround.sol:302-307), бэкенд и граф тоже работают с uid. Поэтому купить землю может только пользователь, которого оракул уже записал в PartnerChainBridge в BSC, — см. мост.

Уровни​

Уровней 15, как и в программах Diamond. Купить землю уровня L можно, только если у пользователя уровень L и в Program 1, и в Program 2: user.lp1 >= L && user.lp2 >= L (apps/backend/src/liquidity/liquidity.service.ts:992). Проверка есть только в бэкенде при выдаче подписи — контракт уровни Diamond не видит. Наличие земли уровня L − 1 бэкенд и контракт не требуют.

Уровни не связаны друг с другом: у каждого уровня своё дерево земель.

Индекс земли​

На одном уровне пользователь может купить несколько земель. Следующий индекс бэкенд берёт как max(groundIndex) + 1 по землям пользователя на этом уровне (apps/p2-backend/src/tree.service.ts:2057-2075). Выбрать индекс вручную нельзя.

Индекс — это не просто номер: земли с разными индексами живут в разных деревьях. Землю с индексом k можно поставить только под землю аплайна с тем же индексом k (tree.service.ts:2079-2086, liquidityGroundEvents.job.ts:53). Так у каждой пары «уровень + индекс» получается своё независимое дерево.

Вторая земля на уровнях 2–15

Если ни у одного аплайна нет земли с нужным индексом, бэкенд отдаёт фолбэк groundId = 1 (tree.service.ts:2164-2171). Корневая земля с groundId = 1 — это земля первого уровня, и контракт на уровнях 2–15 отклоняет такую покупку с InvalidUpline (MagnetLiquidityGround.sol:158-161). Первую землю с индексом 1 и выше на уровнях 2–15 купить нельзя, поэтому и последующие тоже. Подробнее — в известных проблемах.

Корневые земли​

initialize(rootGroundOwner) минтит 15 корневых земель, по одной на уровень: groundId = level, groundIndex = 0, владелец — rootGroundOwner, в groundMatrix[1][level][0] (MagnetLiquidityGround.sol:343-364). Деплой (apps/contracts-v2/deploy/magnetLiquidityGround.deploy.ts) передаёт второй подписант или деплойера.

В графе корни создаются при старте LiquidityGroundEventsJob: 15 узлов Ground с ownerUid = 1, places = 4294967295 (по сути без ограничения), chainId = 56 и нулевым кешбэком (liquidityGroundEvents.job.ts:17-29).

Для индексов ≥ 1 отдельных корней в контракте нет. Если покупка пришла с uplineGroundId = 1, индексатор сначала создаёт «виртуальный» корень Ground {groundId: 1, groundIndex: k} с уровнем покупки (liquidityGroundEvents.job.ts:69-79, 147-157). Ограничение уникальности (groundId, groundIndex) допускает только один такой узел на индекс — на практике это работает только для первого уровня.

Дерево земель и размещение​

У каждой земли ровно один родитель и сколько угодно детей: земли под землёй места не занимают, places расходуют только вышки. Родителя для новой земли выбирает p2-backend в getFreeGroundId (tree.service.ts:2032-2175):

  1. Берёт цепочку аплайнов пользователя из program1 (см. цепочку партнёров) — все uid выше пользователя, отсортированные по убыванию uid, то есть от ближайшего к дальнему.
  2. Считает индекс новой земли: max(groundIndex) + 1.
  3. Ищет среди земель аплайнов землю того же уровня и того же индекса, берёт землю ближайшего аплайна (ORDER BY apoc.coll.indexOf(...)).
  4. Не нашлось — ищет корневую землю uid 1 c groundId = 1 и этим индексом.
  5. Не нашлось и её — возвращает фолбэк groundId = 1.

Это и есть компрессия при размещении: если у пригласителя нет земли нужного уровня и индекса, земля встаёт под следующего по цепочке, у кого она есть, и так до uid 1. Выбранный родитель фиксируется навсегда: если пропущенный пригласитель потом купит землю, его даунлайн к нему не вернётся.

Контракт родителя не выбирает, а только проверяет переданного в подписи: если родитель в той же сети, его уровень должен совпадать с уровнем покупки (MagnetLiquidityGround.sol:158-161). Совпадение индекса проверяет уже индексатор: MATCH родителя идёт по groundId, level и groundIndex новой земли.

Владение и трансфер​

MagnetLiquidityGround не переопределяет трансфер — токен земли можно свободно передать. Но весь учёт ведётся по uid покупателя, записанному в момент покупки (ownerUid в графе, ключи groundMatrix и partnerRewardsBalance в контракте). После трансфера:

  • партнёрка с даунлайна по-прежнему начисляется uid первого покупателя;
  • купить вышку на эту землю сможет только первый покупатель (бэкенд ищет землю по ownerUid);
  • configureCashback на контракте проверяет текущего владельца токена, но эти данные нигде не используются (см. кешбэк).

Вышка (Tower)​

Вышка — NFT общей позиции ликвидности MagnetLiquidityShared (apps/contracts-v2/contracts/MagnetLiquidity/MagnetLiquidityShared.sol). Владелец вышки получает долю (nftShares) в общей позиции Uniswap/PancakeSwap V3 пула и клеймит с неё комиссии. Сама механика ликвидности, своп через 0x и разделение на фасад и Ops описаны в MagnetLiquidity: свопы через 0x.

С землёй вышку связывает покупка: buyNft требует подпись бэкенда, в которую зашиты земля-хозяин и индексы (BuyNftParams, storage/SharedLiquidityStorage.sol:6-25), а событие NftPurchased эти поля отдаёт индексатору (MagnetLiquidityShared.sol:59-70). В самом контракте связь вышки с землёй не хранится: уровень, земля и места есть только в графе.

Персональные позиции (MagnetLiquidityPersonal) и MagnetIndexManager с землями не связаны.

Место вышки: две земли сразу​

Вышка одновременно занимает одно место на своей земле и одно место на земле аплайна (PLACE_TOWER_ON_UPLINE_ONLY = false, apps/p2-backend/src/jobs/liquidityTowerEvents.job.ts:13, 31-32). В графе это две пары связей:

(своя земля)-[:OWNS_TOWER]->(вышка)-[:OWNED_BY]->(своя земля)
(земля-хозяин)-[:HAS_TOWER]->(вышка)-[:TOWER_OF]->(земля-хозяин)

У обычной земли 4 места (DEFAULT_GROUND_PLACES, liquidityGroundEvents.job.ts:50), у корневой — 4 294 967 295. Места общие для своих вышек и для вышек даунлайна, которые встали на эту землю.

Выбор земли-хозяина​

Землю-хозяина выбирает getFreeTowerGroundId (tree.service.ts:2177-2280):

  1. У пользователя должна быть земля с выбранными level и groundIndex (иначе 404 User ground not found), и на ней должно быть свободное место (иначе 400 No free places found for user).
  2. Среди земель аплайнов из той же цепочки program1 с тем же уровнем, тем же индексом и places > 0 берётся земля ближайшего аплайна.
  3. Не нашлось — 404 Free ground not found. На практике этого не бывает: корневая земля индекса 0 у uid 1 всегда с местами.

Это компрессия по местам: если земля ближайшего аплайна заполнена, вышка уходит на землю следующего аплайна с местами. Земля-хозяин вышки может не совпадать с родителем земли покупателя в дереве земель.

Ёмкость: пример​

Цепочка uid 1 → A → B → C, у всех есть земля уровня 1 с индексом 0.

  • C покупает вышку: место у C (4 → 3) и место у B (4 → 3).
  • B покупает 3 вышки: каждая берёт место у B и у A. У B мест не осталось.
  • C покупает ещё вышку: своё место есть (3 → 2), у B мест нет — вышка встаёт на землю A.
  • B больше не может купить вышку на эту землю: своих мест нет. Нужна новая земля (индекс 1) — с ограничением из предупреждения выше.

Индексы мест​

При индексации считаются towerUplinePlaceIndex и towerOwnerPlaceIndex — порядковый номер вышки на земле-хозяине и на своей земле (liquidityTowerEvents.job.ts:139-175). Это справочные поля для отображения; логика мест опирается только на счётчик places.

Что где хранится​

ДанныеКонтракт землиMagnetLiquiditySharedNeo4j groundmatrix
Земля, уровень, индекс, родитель✅—✅
Свободные места——✅
Кешбэк⚠️ configureCashback, не используется—✅
Вышка, её доля и доход—✅—
Связь вышки с землями—только в событии✅
Партнёрские балансы по линиям✅ (с земель)✅ (с вышек)—

Граф — единственный источник для мест и кешбэка, и бэкенд выдаёт подписи по нему. Если индексатор отстал, подписи выдаются по устаревшим данным, поэтому бэкенд ограничивает частоту подписей (см. флоу покупки).