Децентрализованная аффилейт-программа на блокчейне Waves, реализованная в рамках гранта Waves Labs командой Bettex.
Пост не является рекламным! Программа имеет открытый исходный код, ее использование и распространение бесплатно. Применение программы стимулирует развитие dApp приложений и в целом способствует децентрализации, что идет на благо каждому пользователю Сети.
Представленный dApp для affiliate-программ является шаблоном для проектов, включающих affiliate как часть своего функционала. Код может быть использован как шаблон для копирования, как библиотека или как набор идей для технической реализации.
По функционалу это обычная affiliate-система, реализующая регистрацию с указанием реферера, многоуровневое начисление вознаграждений за рефералов и мотивацию для регистрации в системе (кешбэк). Система является “чистым” dApp, то есть веб-приложение взаимодействует напрямую с блокчейном, не имея своего бэкэнда, базы данных и т.п.
Используются техники, которые также могут быть полезны во многих других проектах:
- Вызов смарт-аккаунта в долг с немедленным погашением (в момент вызова на аккаунте нет токенов для оплаты вызова, но они появляются там в результате вызова).
- PoW-captcha — защита от высокочастотного автоматизированного вызова функций смарт-аккаунта — аналог капчи, но через доказательство использования вычислительных ресурсов.
- Запрос к ключам data по шаблону.
Приложение состоит из:
- кода смарт-аккаунта на языке ride4dapps (который по задумке мержится в основной смарт-аккаунт, для которого надо реализовать affiliate-функционал);
- js-обертки, реализующей уровень абстракции над WAVES NODE REST API;
- кода на vuejs framework, являющимся примером использования библиотеки и RIDE-кода.
Опишем все перечисленные особенности.
Вызов смарт-аккаунта в долг с немедленным погашением
Вызов InvokeScript требует оплаты комиссии с инициирующего транзакцию аккаунта. Это не проблема, если вы делаете проект для блокчейн-гиков, у которых на учетной записи есть какое-то количество токенов WAVES, но если продукт ориентирован на использование в широких массах — это становится серьезной проблемой. Ведь пользователь должен озаботиться покупкой токенов WAVES (или иного подходящего ассета, которым можно оплачивать транзакции), что повышает и без того немалый порог входа в проект. Мы можем раздать пользователям ассет, которым позволит оплатить транзакции и столкнемся с риском их нецелевого использования, когда создаются автоматизированные системы для выкачивания ликвидного ассета из нашей системы.
Было бы очень удобно, если была бы возможность производить вызов InvokeScript “за счет получателя” (того смарт-аккаунта, на котором установлен скрипт), и такая возможность, хотя и не очевидным образом, существует.
Если внутри InvokeScript произвести ScriptTransfer на адрес вызывающего, который компенсирует затраченные на fee токены, то такой вызов пройдет успешно, даже если в момент вызова на вызывающем аккаунте не было никаких ассетов. Это возможно благодаря тому, что проверка наличия достаточного количества токенов производится после вызова транзакции, а не перед ней, так что можно производить транзакции в кредит при условии их немедленного погашения.
ScriptTransfer(i.caller, i.fee, unit)
Приведенный ниже код возмещает потраченное fee за счет средств смарт-аккаунта. Для защиты от нецелевого использования этой возможности необходимо использовать проверку, что вызывающий тратит fee в нужном ассете и в разумных пределах:
func checkFee(i:Invocation) = {
if i.fee > maxFee then throw(“unreasonable large fee”) else
if i.feeAssetId != unit then throw(“fee must be in WAVES”) else true
}
Также для защиты от злонамеренной и бессмысленной растраты средств необходима защита от автоматического вызова (PoW-captcha)
PoW-captcha
Сама идея proof-of-work captcha не нова и уже реализована в различных проектах, в том числе реализованных на базе WAVES. Смысл идеи в том, чтобы для совершения действия, тратящего ресурсы нашего проекта, вызывающий должен потратить и свои собственные ресурсы, что делает атаку на истощение ресурсов довольно затратной. Для очень легкой и малозатратной валидации того, что отправитель транзакции решил PoW-задачу, существует проверка id транзакции:
if take(toBase58String(i.transactionId), 3) != “123” then throw(“proof of work failed”) else
Для того, чтобы провести транзакцию, вызывающий должен подобрать такие параметры, чтобы ее base58 код (id) начинался на цифры 123, что соответствует в среднем паре десятков секунд процессорного времени и в целом разумно для нашей задачи. Если требуется более простой или более сложный PoW, то задачу легко доработать очевидным способом.
Запрос к ключам data по шаблону
Для того, чтобы использовать blockchain в качестве базы данных, жизненно необходимо иметь инструменты API для запросов к базе как key-val по шаблонам. Такой инструментарий появился в начале июля 2019 в виде параметра ?matches у запроса REST API /addresses/data?matches=regexp. Теперь, если нам нужно из веб-приложения получить не один ключ и не все ключи сразу, а только какую-то группу, то можно сделать выборку по имени ключа. Например, в данном проекте транзакции вывода средств кодируются в виде
withdraw_${userAddress}_${txid}
что позволяет получить список транзакций на вывод средств для любого заданного адреса по шаблону:
?matches=withdraw_${userAddress}_.*
Теперь разберем компоненты готового решения.
Код на vuejs
Код является рабочей демонстрацией, близкой к реальному проекту. Он реализует вход через Waves Keeper и работу с библиотекой affiliate.js, с помощью которой он регистрирует пользователя в системе, опрашивает данные о транзакциях, а также позволяет выводить заработанные средства на аккаунт пользователя.
Код на RIDE
Состоит из функций register, fund и withdraw.
Функция register регистрирует в системе пользователя. У нее два параметра: referer (адрес рефера) и не используемый в коде функции параметр salt, который нужен для подбора id транзакции (задача PoW-captcha).
Функция (как и остальные функции из данного проекта) использует технику вызова в долг, результатом функции является финансирование выплаты fee за вызов этой функции. Благодаря данному решению, только что создавший кошелек пользователь сразу может работать с системой и ему не нужно озадачиваться вопросом приобретения или получения ассета, позволяющего оплатить комиссию за транзакцию.
Результатом работы функции регистрации являются две записи:
${owner)_referer = referer
${referer}_referral_${owner} = owner
Это позволяет производить прямой и обратный поиски (реферер данного пользователя и все рефералы данного пользователя).
Функция fund является скорее шаблоном для разработки настоящего функционала. В представленном виде она берет все перечисляемые транзакцией средства и распределяет их на аккаунты рефереров 1, 2, 3 уровня, на аккаунт “кешбэка” и аккаунт “сдачи” (сюда попадает все, что осталось при распределении по предыдущим аккаунтам).
Кешбэк — это средство стимулирования конечного пользователя для участия в реферальной системе. Выплачиваемую часть комиссии системой в виде “кешбэка” пользователь может снять так же, как вознаграждения за рефералов.
При использовании реферальной системы функцию fund следует видоизменить, встроить в основную логику того смарт-аккаунта, на котором будет работать система. Например, если реферальное вознаграждение выплачивается за сделанную ставку, то функция fund должна быть встроена в логику, где делается ставка (или производится иное целевое действие, за которое и выплачивается вознаграждение). В данную функцию закодировано три уровня реферальных вознаграждений. Если требуется сделать больше или меньше уровней, то это также правится в коде. Процент вознаграждения задается константами level1-level3, в коде он считается как amount * level / 1000, то есть значение 1 соответствует 0,1% (это тоже можно менять в коде).
Вызов функции изменяет баланс учетной записи и также создает записи для целей логгирования вида:
fund_address_txid = address:owner:inc:level:timestamp
Для получения timestamp (текущего времени) используется такая вот связка
func getTimestamp() = {
let block = extract(blockInfoByHeight(height))
toString(block.timestamp)
}
То есть время транзакции — это время блока, в которой он находится. Это надежнее, чем использовать timestamp из самой транзакции, тем более, что он не доступен из callable.
Функция withdraw выводит все накопленные вознаграждения на учетную запись пользователя. Создает записи для целей логгирования:
# withdraw log: withdraw_user_txid=amount:timestamp
Приложение
Основная часть приложения — библиотека affiliate.js, являющаяся мостом между моделями данных affiliate и WAVES NODE REST API. Реализует уровень абстракции, независимый от фреймворка (может быть использован любой). Активные функции (register, withdraw) предполагают, что в системе установлен Waves Keeper, сама библиотека это не проверяет.
Реализует методы:
fetchReferralTransactions
fetchWithdrawTransactions
fetchMyBalance
fetchReferrals
fetchReferer
withdraw
register
Функционал методов очевиден из названий, параметры и возвращаемые данные описаны в коде. Дополнительных комментариев требует функция register — она запускает цикл подбора id транзакции, чтобы он начинался на 123 — это описанная выше PoW-captcha, защищающая от массовых регистраций. Функция находит транзакцию с нужным id, а потом подписывает через Waves Keeper.
DEX affiliate программа доступна на
Источник: habr.com