Подробный обзор — Berry Club
Это руководство предназначено для того, чтобы помочь вам понять основные моменты, которые позволят вам создать игру, чтобы заработать c NEAR.
Возможно игра не сделает вас или ваших игроков богатыми, но вы поймете как работает механика Near, как небольшие транзакции очень полезны в простом пользовательском интерфейсе HTML5. В итоге вы сами сможете создать простую и увлекательную игру для фарминга.
Мы проанализируем смарт-контракт и файлы js/rust существующей фармерской игры berry club. Если вы примените те же методы к своим идеям игры, вы сможете получить еще лучшие результаты!
Чтобы следовать технической стороне этого руководства, мы настоятельно рекомендуем проверить Near Pathway, чтобы создать транзакцию и создать свой первый смарт-контракт на Near. Концепции, представленные в этих материалах, не включены в это руководство. Вам будет намного проще изучать это руководство, если вы будете держать открытой вкладку связанного исходного файла во время чтения , потому что большая часть упомянутого кода имеет номера строк, но не воспроизводится здесь. Исходный репозиторий Berry Club был разветвлен для этого руководства, чтобы сохранить актуальность, поскольку код работает на блокчейне. Как вы увидите в следующем разделе, сама игра развивалась с течением времени и будет развиваться в дальнейшем, мы рассмотрим ее состояние на текущий момент .
Интерфейс Berry Club
Berry Сlub создан на React, поэтому самое первое, что мы рассмотрим файл app.js, он расположен в папке src репозитория github, это сэкономит нам время при анализе контракта и позволит нам сосредоточиться на том, что нам нужно изучить:
import React from “react”;
import BN from “bn.js”;
import * as nearAPI from “near-api-js”;
import { AlphaPicker, HuePicker, GithubPicker } from “react-color”;
import Switch from “react-switch”;
import { Weapons } from “./Weapons”;
import Timer from “react-compound-timer”;
После импорта react первой необходимой библиотекой будет bn.js, простая утилита для управления целыми или десятичными числами, многие функции появятся в вашем интерфейсе с этой библиотекой, здесь она используется для управления транзакциями:
const PixelPrice = new BN(“10000000000000000000000”);
Игра berry club основана на экономике недвижимости, есть доска, управляемая частью контракта, называемой board.rs, она разделена на пиксели, и у каждого пикселя есть цена, которую нужно заплатить, чтобы рисовать на нем. Механизм действия «вытягивания» является ядром фармерских способностей игры и будет подробно проанализирован, когда мы доберемся до смарт-контракта.
Как вы можете видеть здесь, цена одного пикселя объявлена как константа в самом начале нашего приложения и может быть изменена с помощью инструментов внешнего интерфейса и библиотеки bn.js. Второй инструмент – это Near sdk, который позволяет нам взаимодействовать с Near blockchain, как описано в Figment Near. Первое использование API — это объявление используемых переменных контракта и обеспечение использования основной сети при запуске кода с URL-адреса berry club:
const IsMainnet = window.location.hostname === "berryclub.io";
const TestNearConfig = {
networkId: "testnet",
nodeUrl: "https://rpc.testnet.near.org",
contractName: "berryclub.testnet",
walletUrl: "https://wallet.testnet.near.org",
};
const MainNearConfig = {
networkId: "mainnet",
nodeUrl: "https://rpc.mainnet.near.org",
contractName: "berryclub.ek.near",
walletUrl: "https://wallet.near.org",
};
const NearConfig = IsMainnet ? MainNearConfig : TestNearConfig;
Затем мы импортируем утилиты пользовательского интерфейса, чтобы построить наш интерфейс и позволить людям рисовать, реагировать на цвет, реагировать на переключение и реагировать на таймер. Первой из этих утилит, которая используется, является таймер, он используется для установки тайм-аута для обновления доски в строке 62.
«Обновление» доски выполняется внешним интерфейсом для отображения обновленного состояния доски с помощью RPC-вызова смарт-контракта.
const BatchTimeout = 500;
const RefreshBoardTimeout = 1000;
const MaxWorkTime = 10 * 60 * 1000;
const OneDayMs = 24 * 60 * 60 * 1000;
Что мы видим здесь, на две константы больше, чем те, которые необходимы для обновления, последние две фактически используются для обработки пикселей после их рисования, и устанавливают временной интервал в один день для расчета вознаграждения. Другие константы служат для управления доской в соответствии со смарт-контрактом, и здесь мы впервые пересекаем концепцию линий, которая будет очень важна для понимания управления доской и является наиболее используемым элементом всего интерфейса:
const BoardHeight = 50;
const BoardWidth = 50;
const NumLinesPerFetch = 50;
const ExpectedLineLength = 4 + 8 * BoardWidth;
После разделения доски 50×50 мы сообщаем нашему интерфейсу, чтобы он извлекал только те строки, которые следуют за RefreshBoardTimeout, и рассматривал длину каждой строки как ширину доски, умноженную на 12, размер одной ячейки.
const CellWidth = 12;
const CellHeight = 12;
const MaxNumColors = 31;
const BatchOfPixels = 100;
Пиксели учитываются пакетно, а не независимо, как при вызове действия рисования, так и при обновлении интерфейса.
И последнее, но не менее важное: наш импорт включает настраиваемый компонент пользовательского интерфейса под названием Weapons,js: этот компонент был разработан позже в berry club, чтобы каждый пользователь мог загружать и рисовать на доске целое изображение, а также отчеканьте его на торговой площадке berrycards NFT.
Механика DeFi
Строки между 27 и 51 показывают, как это децентрализованное приложение строит свои возможности ведения фарминга с помощью некоторых базовых механизмов DeFi, которые будут проанализированы в последних частях этого руководства. Сейчас мы лишь вкратце упомянем, что для того, чтобы нарисовать/купить пиксель, berry club проводит вас через пару операций DeFi на Ref.finance, используя свои собственные токены, авокадо для покупки пикселей и бананы, заработанные на купленных вами пикселях.
Существует простой клон uniswap, созданный для обмена бананов/авокадо, который работает на том же смарт-контракте, созданном для других токенов этой игры. Для игры также создан фармерский токен, который называется огурец. Он позволяет людям зарабатывать часть токенов, которые все пользователи, играющие в игру, платят за газ, чтобы рисовать на доске.
Как пользователи зарабатывают деньги
Это самый первый шаг, который мы делаем в коде смарт-контракта, но хотелось бы напомнить вам, что механика DeFi — не единственный способ, которым Berry club позволяет вам зарабатывать токены. У учетной записи есть определенный файл в смарт-контракте berry club, изучим его. Нам нужно знать, что в учетной записи собирается некоторая информация, которая имеет решающее значение для механики фарма и заработка:
- идентификатор учетной записи
- accountIndex для списка учетных записей, которые рисовали на пиксельной доске последний раз
- баланс
- количество пикселей
- запрошенная временная метка (наносекунды, когда данная учетная запись запрашивала вознаграждение в последний раз)
- фармерские предпочтения (бананы или огурцы)
Последние два пункта предназначены для расчета вознаграждения , например, если у вас есть 5 пикселей в течение одного дня, вы получаете 5 бананов. Если вы покупаете у кого-то еще, их заработок уменьшается, потому что количество пикселей, которыми они владеют, уменьшается, сумма заработка рассчитывается, а временная метка обновляется с учетом нового количества принадлежащих пикселей. Как мы видим, вознаграждение рассчитывается на основе этих двух переменных. Операция, применяемая к учетной записи предыдущего владельца при отрисовке пикселя, называется «touch», и вы можете найти ее в файле account.rs. Владение единством одного пикселя является основой для заработка на berry club, таким образом, эта механика почти такая же, как интерфейс стейкинга NFT, вознаграждающий владельцев NFT.
pub fn touch(&mut self) -> (Berry, Balance) {
let block_timestamp = env::block_timestamp();
let time_diff = block_timestamp - self.claim_timestamp;
let farm_bonus = if self.farming_preference == Berry::Avocado {
1
} else {
0
};
let farmed = Balance::from(self.num_pixels + farm_bonus)
* Balance::from(time_diff)
* REWARD_PER_PIXEL_PER_NANOSEC;
self.claim_timestamp = block_timestamp;
self.balances[self.farming_preference as usize] += farmed;
(self.farming_preference, farmed)
}
Первоначальный владелец доски = 0, это сам контракт, и если найти предыдущего владельца невозможно, то используется контракт. Чтобы запустить игру, некоторые токены хранятся на счете контракта, и они всегда увеличиваются с использованием цены на газ, установленной для людей, чтобы покупать авокадо и бананы. Так что «хранилище» игры всегда заполнено некоторыми токенами для пользователей. Теперь вернемся к нашему интерфейсу.
Цифры в цвета и обратно
Строки между 67 и 82 в app.js используются для декодирования чисел в цвета и обратно, чтобы элементы пользовательского интерфейса могли взаимодействовать с доской, определены две постоянные переменные – intToColor и rgbaToInt. Здесь мы видим, что для преобразования целого числа в цвет используются строковые методы, чтобы разделить 3 числа на красный, зеленый и синий:
const intToColor = (c) => `#${c.toString(16).padStart(6, "0")}`;
const intToColorWithAlpha = (c, a) =>
`#${c.toString(16).padStart(6, "0")}${Math.round(255 * a)
.toString(16)
.padStart(2, "0")}`;
Чтобы преобразовать строку цвета в целое число, мы просто применяем функцию math.round() и используем полученное целое число.
const rgbaToInt = (cr, cg, cb, ca, bgColor) => { const bb = bgColor & 255; const bg = (bgColor >> 8) & 255; const br = (bgColor >> 16) & 255; const r = Math.round(cr * ca + br * (1 - ca)); const g = Math.round(cg * ca + bg * (1 - ca)); const b = Math.round(cb * ca + bb * (1 - ca)); return (r << 16) + (g << 8) + b; };
Строки ниже относятся к загрузке и выводу изображений на доске с помощью компонент, мы не будем подробно рассматривать их. imgColorToInt и int2hsv преобразуют числа в два разных вида цветовых шкал, затем определяется TransparentColor и гамма изображение для печати с помощью generateGamma. В decodeLine мы преобразуем буфер в массив пикселей для печати на доске с использованием вышеуказанных цветов, перебирая их с помощью for.
Первый конструктор React
В следующих строках app.js мы определяем конструктор, который будет определять состояния, его мы будем использовать позже в нашем пользовательском интерфейсе для взаимодействия с блокчейном.
class App extends React.Component {
constructor(props) {
super(props);
Определенные здесь состояния являются выбранным по умолчанию цветом и цветовой палитрой:
const colors = [
"#000000",
"#666666",
"#aaaaaa",
"#FFFFFF",
"#F44E3B",
"#D33115",
"#9F0500",
"#FE9200",
"#E27300",
"#C45100",
"#FCDC00",
"#FCC400",
"#FB9E00",
"#DBDF00",
"#B0BC00",
"#808900",
"#A4DD00",
"#68BC00",
"#194D33",
"#68CCCA",
"#16A5A5",
"#0C797D",
"#73D8FF",
"#009CE0",
"#0062B1",
"#AEA1FF",
"#7B64FF",
"#653294",
"#FDA1FF",
"#FA28FF",
"#AB149E",
].map((c) => c.toLowerCase());
// const currentColor = parseInt(colors[Math.floor(Math.random() * colors.length)].substring(1), 16);
const currentColor = parseInt(colors[0].substring(1), 16);
const defaultAlpha = 0.25;
И для таймера, который обновляет доску:
const timeMs = new Date().getTime();
const freeDrawingStartMsEstimated =
timeMs -
((timeMs - new Date("2021-05-09")) % (7 * OneDayMs)) +
OneDayMs * 6;
Затем определяются состояния, используемой учетной записи пользователя. Важно знать, вошел ли пользователь в систему, есть какие-либо ожидающие транзакции (определенные как pendingPixels). boardLoaded загрузит доску холста для рисования альфа-канала selectedCell, а состояния pickerColor определяют состояния интерактивных компонентов для добавления цветов на доску. pickingColor служит для выбора цвета с доски, а gammaColors полезно для печати изображения на доске вместе с состояниями WeaponOn и WeaponCodePosition.
Эти и другие состояния, нужны для учетной записи, они дают возможность зарабатывать на игре при помощи пикселей и DeFi:
owners: [],
accounts: {},
highlightedAccountIndex: -1,
selectedOwnerIndex: false,
farmingBanana: false,
Последние три состояния настраивают таймер для последующего использования:
freeDrawingStart: new Date(freeDrawingStartMsEstimated),
freeDrawingEnd: new Date(freeDrawingStartMsEstimated + OneDayMs),
watchMode: false,
Следующий список (строки 203-215) определяет объекты и действия, которые будут взаимодействовать с состояниями, впервые ссылаясь на элемент DOM, доску холста.
this._buttonDown = false;
this._oldCounts = {};
this._numFailedTxs = 0;
this._balanceRefreshTimer = null;
this.canvasRef = React.createRef();
this._context = false;
this._lines = false;
this._queue = [];
this._pendingPixels = [];
this._refreshBoardTimer = null;
this._sendQueueTimer = null;
this._stopRefreshTime = new Date().getTime() + MaxWorkTime;
this._accounts = {};
Наконец, мы определяем некоторые состояния после входа в систему:
this._initNear().then(() => {
this.setState(
{
connected: true,
signedIn: !!this._accountId,
accountId: this._accountId,
ircAccountId: this._accountId.replace(".", "_"),
freeDrawingStart: this._freeDrawingStart,
freeDrawingEnd: this._freeDrawingEnd,
},
() => {
if (window.location.hash.indexOf("watch") >= 0) {
setTimeout(() => this.enableWatchMode(), 500);
}
}
);
});
Базовые взаимодействия
Теперь приступим к описанию взаимодействий на доске/холсте, связывая их с ранее определенными состояниями. Для этих взаимодействий мы используем функции. Первая будет использовать нашу предыдущую ссылку на элемент доски, чтобы создать его и сообщить информацию о типе движения мыши. При первом нажатии мы включаем режим наблюдения для запуска нашего таймера:
const click = async () => {
if (this.state.watchMode) {
return;
}
И режим рендеринга изображения, если пользователь хочет разместить изображение на доске:
if (this.state.rendering) {
await this.drawImg(this.state.selectedCell);
} else if (this.state.pickingColor) {
this.pickColor(this.state.selectedCell);
} else {
this.saveColor();
await this.drawPixel(this.state.selectedCell);
}
Следующая важная часть, мы определяем, как интерфейс считывает движения мыши и касания доски:
if ("touches" in e) {
if (e.touches.length > 1) {
return true;
} else {
const rect = e.target.getBoundingClientRect();
x = e.targetTouches[0].clientX - rect.left;
y = e.targetTouches[0].clientY - rect.top;
}
} else {
x = e.offsetX;
y = e.offsetY;
}
Используемый код учитывает мобильных пользователей, создавая специальную функцию для расчета положения и добавляя прослушиватель на холст/доску для событий касания: canvas.addEventListener(“touchmove”, mouseMove). Затем эти взаимодействия используются для установки состояния selectedCell и отслеживания как начала, так и конца действия мыши/касания на холсте, а также его перемещения по каждой ячейке:
const mouseDown = async (e) => { this._buttonDown = true; if (this.state.selectedCell !== null) { await click(); } }; canvas.addEventListener("mousedown", mouseDown); canvas.addEventListener("touchstart", mouseDown); const unselectCell = () => { this.setState( { selectedCell: null, }, () => this.renderCanvas() ); }; const mouseUp = async (e) => { this._buttonDown = false; if ("touches" in e) { unselectCell(); } }; canvas.addEventListener("mouseup", mouseUp); canvas.addEventListener("touchend", mouseUp); canvas.addEventListener("mouseleave", unselectCell); canvas.addEventListener("mouseenter", (e) => { if (this._buttonDown) { if (!("touches" in e) && !(e.buttons & 1)) { this._buttonDown = false; } } });
Взаимодействие здесь работает с ранее определенными состояниями, например, палитра цветов позволяет нам выбирать цвета с доски и использовать их для рисования. Клавиша, используемая палитрой цветов — это клавиша alt, и мы можем загружать и печатать изображения на доске, только если палитра цветов отключена, потому что тогда мы активируем функцию generategamma. Таким образом, функцию pickcolor(), ссылающуюся на ячейку, можно будет использовать для установки одного пикселя или всей доски для рендеринга изображения:
pickColor(cell) { if (!this.state.signedIn || !this._lines || !this._lines[cell.y]) { return; } const color = this._lines[cell.y][cell.x].color; this.setState( { currentColor: color, alpha: 1, pickerColor: intToColorWithAlpha(color, 1), gammaColors: generateGamma(int2hsv(color)[0]), pickingColor: false, }, () => { this.renderCanvas(); } ); }
Теперь мы добрались до сути. Давайте рассмотрим смарт-контракт. Мы знаем, как рисовать пиксель в интерфейсе, но нам нужно привязать к ним транзакции, чтобы наш интерфейс был реальной игрой для заработка. Поэтому, пожалуйста, обратите внимание на то, что мы собираемся сделать, потому что даже если ваша игра выглядит совершенно иначе с точки зрения пользовательского интерфейса, механика заработка вполне может подойти для любой другой игры и будет объяснена здесь в самом простом виде.
Смарт-контракт Berry Club
Линии
Впервые мы познакомились с линиями в начале этой статьи, когда рассматривали определения состояний пользовательского интерфейса. Линии — важная концепция интерфейса berry club, это строки, на которые делится доска, и каждый пиксель в них — это часть метаданных. Они являются частью пользовательского интерфейса, который взаимодействует со смарт-контрактом, и они являются наиболее используемыми объектами игры. Мы потратим немного времени на анализ того, как они работают и используются для хранения данных с доски.
Прежде всего, в файле board.rs мы находим определение PixelLine сразу после определения Pixel:
pub struct PixelLine(pub Vec<Pixel>);impl Default for PixelLine { fn default() -> Self { Self(vec![Pixel::default(); BOARD_WIDTH as usize]) } }
Вектор (массив) строковых данных, разделенных по ширине доски.
Затем мы определяем в PixelBoard как вектор PixelLines:
pub struct PixelBoard {
pub lines: Vector<PixelLine>,
pub line_versions: Vec<u32>,
}
Таким образом, каждая строка хранится на доске как отдельная запись с полем метаданных, называемым line_versions, которая увеличивается каждый раз, когда вы изменяете строку. Каждый раз, когда наш интерфейс извлекает доску, вы получаете 50 строк, а также метаданные для каждой строки, которые показывают, сколько раз строка была обновлена. Получая эти метаданные, интерфейс знает, сколько раз строка была изменена. Если строка была изменена по сравнению с предыдущей выборкой, вы извлекаете данные для каждого пикселя, если нет, вы просто этого не делаете.
impl Place { pub fn get_lines(&self, lines: Vec<u32>) -> Vec<Base64VecU8> { lines .into_iter() .map(|i| { let line = self.board.get_line(i); line.try_to_vec().unwrap().into() }) .collect() } pub fn get_line_versions(&self) -> Vec<u32> { self.board.line_versions.clone() } }
Это хороший способ хранения и извлечения данных из интерфейса, которые могут быть полезны для использования в вашей игре.
Транзакции
Давайте вернемся к нашему пользовательскому интерфейсу в app.js, чтобы убедиться, что мы понимаем, как транзакциями управляют из внешнего интерфейса. Во-первых, нам нужна функция для проверки учетной записи, если что-то пойдет не так, и вот она:
async refreshAllowance() {
alert(
"You're out of access key allowance. Need sign in again to refresh it"
);
await this.logOut();
await this.requestSignIn();
}
Вы помните массивы _queue и _pendingPixels, которые мы определили в нашем конструкторе? Пора их использовать, так как транзакции управляются в зависимости от того, какие пиксели вы нарисовали на доске:
async _sendQueue() { const pixels = this._queue.slice(0, BatchOfPixels); this._queue = this._queue.slice(BatchOfPixels); this._pendingPixels = pixels; try { await this._contract.draw( { pixels, }, new BN("75000000000000") ); this._numFailedTxs = 0; } catch (error) { const msg = error.toString(); if (msg.indexOf("does not have enough balance") !== -1) { await this.refreshAllowance(); return; } console.log("Failed to send a transaction", error); this._numFailedTxs += 1; if (this._numFailedTxs < 3) { this._queue = this._queue.concat(this._pendingPixels); this._pendingPixels = []; } else { this._pendingPixels = []; this._queue = []; } } try { await Promise.all([this.refreshBoard(true), this.refreshAccountStats()]); } catch (e) { // ignore } this._pendingPixels.forEach((p) => { if (this._pending[p.y][p.x] === p.color) { this._pending[p.y][p.x] = -1; } }); this._pendingPixels = []; }
Давайте изучим это более детально. Мы создаем объект пикселей (вектор), мы модифицируем наш объект _queue, чтобы он соответствовал пикселям, и мы присваиваем его значение объекту _pendingPixel в асинхронной функции.
Потом мы просто рисуем объект контракта, который вызывается из sdk, а действие для рисования (часть из действий, которые мы определили для пользователя) определяется в файле lib.rs .
pub fn draw(&mut self, pixels: Vec<SetPixelRequest>) { if pixels.is_empty() { return; } let mut account = self.get_mut_account(env::predecessor_account_id()); let new_pixels = pixels.len() as u32; if ms_time() < self.get_free_drawing_timestamp() { let cost = account.charge(Berry::Avocado, new_pixels); self.burned_balances[Berry::Avocado as usize] += cost; } let mut old_owners = self.board.set_pixels(account.account_index, &pixels); let replaced_pixels = old_owners.remove(&account.account_index).unwrap_or(0); account.num_pixels += new_pixels - replaced_pixels; self.save_account(account); for (account_index, num_pixels) in old_owners { let mut account = self.get_internal_account_by_index(account_index).unwrap(); self.touch(&mut account); account.num_pixels -= num_pixels; self.save_account(account); } self.maybe_send_reward(); }
Пиксели смарт-контракта — это цвет и идентификатор учетной записи. У нас есть старый владелец, который рисовал пиксель раньше, и новый владелец, который хочет нарисовать его сейчас. С помощью действия рисования мы получаем old_owner и заменяем его учетной записью нового владельца, изменяя значение цвета всех пикселей внутри вектора PixelRequest. Затем мы отправляем вознаграждение старому владельцу, взимая плату с нового. Временные метки для наград сбрасываются, и отсчет начинается снова с нуля, на один пиксель меньше для старого владельца и на один больше для нового. Действие setPixelRequest определено в файле board.rs нашего контракта, но давайте вернемся к нашему libs.rs.
Вот так выглядит функция may_send_rewards:
impl Place {
fn maybe_send_reward(&mut self) {
let current_time = env::block_timestamp();
let next_reward_timestamp: u64 = self.get_next_reward_timestamp().into();
if next_reward_timestamp > current_time {
return;
}
self.last_reward_timestamp = current_time;
let reward: Balance = self.get_expected_reward().into();
env::log(format!("Distributed reward of {}", reward).as_bytes());
Promise::new(format!(
"{}.{}",
FARM_CONTRACT_ID_PREFIX,
env::current_account_id()
))
.function_call(
b"take_my_near".to_vec(),
b"{}".to_vec(),
reward,
GAS_BASE_COMPUTE,
);
}
}
Не ленитесь и разберитесь в этом. Вот видео от автора проекта
Функция проверяет время в блокчейне (здесь мы не используем таймер в интерфейсе, потому что мы хотим быть уверенными!) и использует возможности фарма контракта по глобальной метке времени с помощью функций get_next_reward_timestamp() и last_reward_timestamp(). затем вызывает get_expected_reward() для расчета вознаграждения, положенных учетной записи.
pub fn get_expected_reward(&self) -> U128 {
let account_balance = env::account_balance();
let storage_usage = env::storage_usage();
let locked_for_storage = Balance::from(storage_usage) * STORAGE_PRICE_PER_BYTE + SAFETY_BAR;
if account_balance <= locked_for_storage {
return 0.into();
}
let liquid_balance = account_balance - locked_for_storage;
let reward = liquid_balance / PORTION_OF_REWARDS;
reward.into()
}
Итак, мы берем текущий баланс из учетной записи berry club (помните, у нас есть поле баланса в учетной записи?), текущее использование хранилища и затраты, а также порог безопасности 50 авокадо. Если баланс есть, мы делим его на 24 (часа) * 60 (минут) часть вознаграждения, что означает, что вы в основном получаете точно такой же баланс, который у вас есть один раз, если вы вызываете его каждую минуту. Вы можете найти его в начале файла lib.rs
const PORTION_OF_REWARDS: Balance = 24 * 60;
const SAFETY_BAR: Balance = 50_000000_000000_000000_000000;
Вы думаете процесс вознаграждения окончен ? Нет!
Нам нужно вернуться к нашей функции may_send_reward(), чтобы увидеть, что она вызывает новый контракт фарма berry club для распределения вознаграждений за стейкинг, которые представляют собой… огурцы, токен стейкинга на berryclub.
const FARM_CONTRACT_ID_PREFIX: &str = "farm";
На самом деле это не единственный источник распределения доходов с помощью этой функции, он также использует затраты на газ, которые люди платят за покупку авокадо и обмен бананами.
Как это возможно? Прежде всего, GAS_BASE_COMPUTE определяется в файле token.rs, где задается количество газа для смарт-контракта. Цена на газ очень низкая, и ее можно использовать для вознаграждения пользователей, взаимодействующих с вашей игрой!!!
Чтобы лучше понять, как работают сборы GAS на Near, обратитесь к этой подробной документации!