|
| 1 | +module @minimarket::minimarket { |
| 2 | + use std::signer; |
| 3 | + use std::vector; |
| 4 | + |
| 5 | + /// Ошибки |
| 6 | + const E_ALREADY_INITIALIZED: u64 = 1; |
| 7 | + const E_NOT_INITIALIZED: u64 = 2; |
| 8 | + const E_BAD_FEE_BPS: u64 = 3; |
| 9 | + const E_ONLY_ADMIN: u64 = 4; |
| 10 | + const E_BAD_ITEM_ID: u64 = 5; |
| 11 | + const E_ALREADY_LISTED: u64 = 6; |
| 12 | + const E_NOT_LISTED: u64 = 7; |
| 13 | + const E_PRICE_MISMATCH: u64 = 8; |
| 14 | + |
| 15 | + /// Простая запись о лоте |
| 16 | + struct Item has drop, store { |
| 17 | + seller: address, |
| 18 | + price: u64, |
| 19 | + listed: bool, |
| 20 | + } |
| 21 | + |
| 22 | + /// Баланс внутри модуля (псевдо-коины для демо) |
| 23 | + struct Balance has drop, store { |
| 24 | + owner: address, |
| 25 | + amount: u64, |
| 26 | + } |
| 27 | + |
| 28 | + /// Синглтон маркетплейса, хранится под адресом администратора (инициатора init). |
| 29 | + struct Market has key { |
| 30 | + admin: address, |
| 31 | + fee_bps: u64, // 0..=10_000 |
| 32 | + fees_accumulated: u64, // Накопленные комиссии |
| 33 | + items: vector<Item>, // По индексу = item_id |
| 34 | + balances: vector<Balance>, // Простая "карта": поиск по адресу линейный |
| 35 | + initialized: bool, |
| 36 | + } |
| 37 | + |
| 38 | + /// === helpers === |
| 39 | + |
| 40 | + fun assert_initialized(market_addr: address) { |
| 41 | + assert!(exists<Market>(market_addr), E_NOT_INITIALIZED); |
| 42 | + } |
| 43 | + |
| 44 | + fun only_admin(market: &Market, caller: address) { |
| 45 | + assert!(market.admin == caller, E_ONLY_ADMIN); |
| 46 | + } |
| 47 | + |
| 48 | + fun get_mut(market_addr: address): &mut Market acquires Market { |
| 49 | + let m = borrow_global_mut<Market>(market_addr); |
| 50 | + m |
| 51 | + } |
| 52 | + |
| 53 | + fun get(market_addr: address): &Market acquires Market { |
| 54 | + let m = borrow_global<Market>(market_addr); |
| 55 | + m |
| 56 | + } |
| 57 | + |
| 58 | + /// Найти индекс баланса по адресу, либо вернуть опционально None. |
| 59 | + fun find_balance_index(balances: &vector<Balance>, owner: address): (bool, u64) { |
| 60 | + let i = 0u64; |
| 61 | + let len = vector::length(balances); |
| 62 | + while (i < len) { |
| 63 | + let b = vector::borrow(balances, i); |
| 64 | + if (b.owner == owner) { |
| 65 | + return (true, i) |
| 66 | + }; |
| 67 | + i = i + 1; |
| 68 | + }; |
| 69 | + (false, 0) |
| 70 | + } |
| 71 | + |
| 72 | + /// Кредитовать внутренний баланс. |
| 73 | + fun credit_balance(balances: &mut vector<Balance>, owner: address, amount: u64) { |
| 74 | + let (found, idx) = find_balance_index(balances, owner); |
| 75 | + if (found) { |
| 76 | + let b = vector::borrow_mut(balances, idx); |
| 77 | + b.amount = b.amount + amount; |
| 78 | + } else { |
| 79 | + vector::push_back(balances, Balance { owner, amount }); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + /// === entry === |
| 84 | + |
| 85 | + /// Инициализация маркетплейса под адресом администратора (signer). |
| 86 | + public entry fun init(admin: &signer, fee_bps: u64) acquires Market { |
| 87 | + let admin_addr = signer::address_of(admin); |
| 88 | + assert!(!exists<Market>(admin_addr), E_ALREADY_INITIALIZED); |
| 89 | + assert!(fee_bps <= 10000, E_BAD_FEE_BPS); |
| 90 | + |
| 91 | + move_to(admin, Market { |
| 92 | + admin: admin_addr, |
| 93 | + fee_bps, |
| 94 | + fees_accumulated: 0, |
| 95 | + items: vector::empty<Item>(), |
| 96 | + balances: vector::empty<Balance>(), |
| 97 | + initialized: true |
| 98 | + }); |
| 99 | + } |
| 100 | + |
| 101 | + /// Размещение лота. Если item_id == len, пушим новый; если меньше len — ячейка должна быть свободна. |
| 102 | + public entry fun list(seller: &signer, market_addr: address, item_id: u64, price: u64) acquires Market { |
| 103 | + assert_initialized(market_addr); |
| 104 | + let seller_addr = signer::address_of(seller); |
| 105 | + let m = get_mut(market_addr); |
| 106 | + |
| 107 | + let len = vector::length(&m.items); |
| 108 | + if (item_id == len) { |
| 109 | + vector::push_back(&mut m.items, Item { seller: seller_addr, price, listed: true }); |
| 110 | + } else { |
| 111 | + assert!(item_id < len, E_BAD_ITEM_ID); |
| 112 | + let it = vector::borrow_mut(&mut m.items, item_id); |
| 113 | + // Разрешаем перезалистинг только если ячейка свободна |
| 114 | + assert!(!it.listed, E_ALREADY_LISTED); |
| 115 | + it.seller = seller_addr; |
| 116 | + it.price = price; |
| 117 | + it.listed = true; |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + /// Покупка лота. Внутренняя «оплата»: увеличиваем баланс продавца, удерживаем комиссию в пуле. |
| 122 | + public entry fun buy(buyer: &signer, market_addr: address, item_id: u64, pay_amount: u64) acquires Market { |
| 123 | + assert_initialized(market_addr); |
| 124 | + let m = get_mut(market_addr); |
| 125 | + let len = vector::length(&m.items); |
| 126 | + assert!(item_id < len, E_BAD_ITEM_ID); |
| 127 | + |
| 128 | + let it = vector::borrow_mut(&mut m.items, item_id); |
| 129 | + assert!(it.listed, E_NOT_LISTED); |
| 130 | + assert!(pay_amount == it.price, E_PRICE_MISMATCH); |
| 131 | + |
| 132 | + // комиссия: (price * fee_bps) / 10000 |
| 133 | + let fee = (it.price * m.fee_bps) / 10000; |
| 134 | + let seller_amount = it.price - fee; |
| 135 | + |
| 136 | + // начисляем продавцу, накапливаем комиссию |
| 137 | + credit_balance(&mut m.balances, it.seller, seller_amount); |
| 138 | + m.fees_accumulated = m.fees_accumulated + fee; |
| 139 | + |
| 140 | + // снимаем лот |
| 141 | + it.listed = false; |
| 142 | + } |
| 143 | + |
| 144 | + /// Вывод комиссий админом во внутренний баланс админа |
| 145 | + public entry fun withdraw_fees(admin: &signer, market_addr: address) acquires Market { |
| 146 | + assert_initialized(market_addr); |
| 147 | + let caller = signer::address_of(admin); |
| 148 | + let m = get_mut(market_addr); |
| 149 | + only_admin(m, caller); |
| 150 | + |
| 151 | + let amount = m.fees_accumulated; |
| 152 | + if (amount > 0) { |
| 153 | + m.fees_accumulated = 0; |
| 154 | + credit_balance(&mut m.balances, caller, amount); |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + /// Просмотр баланса (для тестов/демо) |
| 159 | + public fun view_balance(market_addr: address, owner: address): u64 acquires Market { |
| 160 | + let m = get(market_addr); |
| 161 | + let (found, idx) = find_balance_index(&m.balances, owner); |
| 162 | + if (!found) { 0 } else { vector::borrow(&m.balances, idx).amount } |
| 163 | + } |
| 164 | + |
| 165 | + /// Демонстрационная процедура (сценарий happy-path) |
| 166 | + public entry fun entry_demo(admin: &signer) acquires Market { |
| 167 | + // 1) init с 250 bps = 2.5% |
| 168 | + init(admin, 250); |
| 169 | + let admin_addr = signer::address_of(admin); |
| 170 | + |
| 171 | + // 2) Продавец = админ для простоты. Листинг item_id=0 по цене 10_000 |
| 172 | + list(admin, admin_addr, 0, 10_000); |
| 173 | + |
| 174 | + // 3) Покупка тем же аккаунтом для демо (в реале будет другой signer) |
| 175 | + buy(admin, admin_addr, 0, 10_000); |
| 176 | + |
| 177 | + // 4) Вывод комиссии админом |
| 178 | + withdraw_fees(admin, admin_addr); |
| 179 | + // Теперь у админа во внутреннем балансе 10_000 (от продажи) + 250 (комиссия) не будет, |
| 180 | + // комиссия не прибавляется к продавцу; продавцу ушло 9_750, комиссия 250 ушла в пул и затем на баланс админа. |
| 181 | + // Проверки оставляем в юнит-тестах. |
| 182 | + } |
| 183 | +} |
0 commit comments