Земли и вышки
Страница описывает модель: что такое земля и вышка, как земли выстраиваются в дерево, как вышки занимают места и что из этого хранится в блокчейне, а что — только в 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).
Так у каждой пары «уровень + индекс» получается своё независимое дерево.
Если ни у одного аплайна нет земли с нужным индексом, бэкенд отдаёт фолбэк
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):
- Берёт цепочку аплайнов пользователя из
program1(см. цепочку партнёров) — все uid выше пользователя, отсортированные по убыванию uid, то есть от ближайшего к дальнему. - Считает индекс новой земли:
max(groundIndex) + 1. - Ищет среди земель аплайнов землю того же уровня и того же индекса, берёт
землю ближайшего аплайна (
ORDER BY apoc.coll.indexOf(...)). - Не нашлось — ищет корневую землю uid 1 c
groundId = 1и этим индексом. - Не нашлось и её — возвращает фолбэк
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):
- У пользователя должна быть земля с выбранными
levelиgroundIndex(иначе404 User ground not found), и на ней должно быть свободное место (иначе400 No free places found for user). - Среди земель аплайнов из той же цепочки
program1с тем же уровнем, тем же индексом иplaces > 0берётся земля ближайшего аплайна. - Не нашлось —
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.
Что где хранится
| Данные | Контракт земли | MagnetLiquidityShared | Neo4j groundmatrix |
|---|---|---|---|
| Земля, уровень, индекс, родитель | ✅ | — | ✅ |
| Свободные места | — | — | ✅ |
| Кешбэк | ⚠️ configureCashback, не используется | — | ✅ |
| Вышка, её доля и доход | — | ✅ | — |
| Связь вышки с землями | — | только в событии | ✅ |
| Партнёрские балансы по линиям | ✅ (с земель) | ✅ (с вышек) | — |
Гр аф — единственный источник для мест и кешбэка, и бэкенд выдаёт подписи по нему. Если индексатор отстал, подписи выдаются по устаревшим данным, поэтому бэкенд ограничивает частоту подписей (см. флоу покупки).