25 февраля 2020

How to: бесплатные (для пользователя) децентрализованные приложения

При обсуждении нескольких тем лица разработчиков dApp искажает гримаса боли. Это:
  1. Работа с ключами. Просить у пользователя ключи нельзя, но он должен как-то подписывать транзакции.
  2. Необходимость платить комиссию в токенах за каждую транзакцию. Как объяснить пользователям, что каждая транзакция требует комиссии в токенах платформы, и, главное, где взять токены для оплаты комиссии за первые транзакции?

Из-за этих проблем стоимость привлечения одного пользователя становится заоблачной. Например, у одного популярного dApp в экосистеме Waves она была около 80$ (!) при LTV ниже $10. Конверсию портили именно барьеры с расширением и комиссиями.

Первая проблема часто решается с помощью браузерных расширений вроде Metamask и Waves Keeper. Но, во-первых, здесь нужно потратить слишком много усилий, а, во-вторых, результат получится не дружественным для пользователей.

Поэтому в экосистеме Waves появился Signer — он не требует предоставлять ключи dApp, но и не заставляет устанавливать браузерные расширения. В своей статье @Vladimir Zhuravlev рассказал о Waves Signer и его интеграции в приложение.

А что же насчет второй проблемы? Одних создателей dApp она просто не волнует: они считают, что пользователи просто должны где-то взять токены для оплаты комиссий. Другие требуют от пользователей привязывать банковские карты во время регистрации, а это сильно снижает мотивацию.

Сделать так, чтобы dApp не требовал от пользователя нативных токенов для оплаты комиссии, можно с помощью тестовых периодов. В Waves для этого есть два способа.

Спонсирование транзакций

Если пользователю нужно заплатить комиссию в нативных токенах вашего dApp, можно использовать механизм спонсирования транзакций. Тогда пользователи будут платить комиссию в ваших токенах, но, так как майнеры получают комиссию только в WAVES, фактически WAVES будут списываться с аккаунта, выпустившего токен.

Распишем это по шагам:

  • Пользователь платит комиссию за транзакцию в ваших токенах
  • Вы получаете эти токены
  • С вашего аккаунта списывается соответствующее количество WAVES и уходит майнерам.

Возникает вопрос: сколько токенов заплатит пользователь, и сколько их спишется с аккаунта спонсора?

Ответ: создатель dApp может сам установить это соотношение. В момент начала спонсирования он определяет, какая сумма в его токенах соответствует минимальной комиссии (0,001 WAVES или 100 000 в минимальной фракции).

Перейдем к примерам и коду.

Для включения спонсирования нужно отправить транзакцию типа Sponsorship. Это можно сделать либо в пользовательском интерфейсе в Waves.Exchange, либо с помощью waves-transactions и такого кода:

const { sponsorship } = require('@waves/waves-transactions')

const seed = 'example seed phrase'

const params = {
  assetId: '4uK8i4ThRGbehENwa6MxyLtxAjAo1Rj9fduborGExarC',
  minSponsoredAssetFee: 100
}

const signedSponsorshipTx = sponsorship(params, seed)

Этот код сформирует (но не отправит в блокчейн) транзакцию:

{
  "id": "A",
  "type": 14,
  "version": 1,
  "senderPublicKey": "3SU7zKraQF8tQAF8Ho75MSVCBfirgaQviFXnseEw4PYg",
  "minSponsoredAssetFee": 100,
  "assetId": "4uK8i4ThRGbehENwa6MxyLtxAjAo1Rj9fduborGExarC",
  "fee": 100000000,
  "timestamp": 1575034734209,
  "proofs": [
    "42vz3SxqxzSzNC7AdVY34fM7QvQLyJfYFv8EJmCgooAZ9Y69YDNDptMZcupYFdN7h3C1dz2z6keKT9znbVBrikyG"
  ]
}

Главный параметр транзакции — minSponsoredAssetFee, задающий соответствие: 100 токенов A равны 0,001 WAVES. Значит, чтобы отправить Transfer, пользователь должен заплатить комиссию в 100 токенов A.

Но есть и ограничения на спонсирование. Использовать спонсированные токены в качестве комиссии можно только для транзакций типов Transfer и Invoke. Спонсировать токен может только аккаунт, выпустивший этот токен. То есть, вы не сможете спонсировать токены, выпущенные не вами.

Безопасность

Перед включением спонсирования надо обратить внимание на несколько моментов.

  1. Пользователь может использовать спонсируемые токены для операций не только с этим токеном. Например, аккаунт с токенами A на балансе может отправлять токены B, а в качестве комиссии приложить токены A.
  2. Пользователь может заплатить больше минимальной комиссии за транзакцию. Например, если у пользователя есть 100 000 ваших токенов, а вы задали в качестве параметра minSponsoredAssetFee 100, пользователь сможет все свои 100 000 токенов отправить в качестве комиссии. Вы получите 100 000 токенов A, а майнер получит 1000 WAVES с вашего аккаунта (100 000 / 100 = 1000), если они на нем есть.

Функция спонсирования уже долго и успешно работает на Waves, но, если у вас есть идеи, как ее улучшить, присоединяйтесь к обсуждению в WEP-2 Customizable Sponsorship.

Оплата комиссии за счет dApp

ДИСКЛЕЙМЕР: данная возможность НЕ документирована и может в будущем прекратить работать.

Второй вариант спонсирования подходит только для одного типа транзакций — InvokeScript — и это — достаточно простой механизм. Преимущество по сравнению со спонсированием — в том, что не нужно использовать свой токен.

Любой пользователь может вызвать dApp, имея 0 WAVES на своем аккаунте, но указав в качестве комиссии именно WAVES. Скрипт начнет выполняться и, если в результате на аккаунт пользователя поступят WAVES, он получит их, и с этого аккаунта спишется комиссия.

Например, есть dApp, который на основе адреса вызывающего или отправляет, или не отправляет ему 1 WAVES. Пример — небезопасный и приводится, только чтобы объяснить принцип работы. Никогда не используйте такую логику в своих децентрализованных приложениях!

@Callable(invoke)
func callMeBaby() = {

    let callerAddress = invoke.caller.bytes.toBase58String()

    if (takeRight(invoke.caller.bytes, 4) == base16'ABCD') then
        TransferSet([
            ScriptTransfer(invoke.caller, 100000000, unit)
        ])
    else
    throw("You didn't win")
}

Если вызывать эту функцию с аккаунта, на котором 0 WAVES и указать fee в 0,005 WAVES, скрипт все равно начнет выполняться. Если пользователь имеет право на получение 1 WAVES, транзакция будет считаться валидной и попадет в блокчейн. Пользователь СНАЧАЛА получит 1 WAVES в результате вызова, ЗАТЕМ с него будет списано 0.005 WAVES комиссии. В итоге пользователь получит на свой аккаунт 0,995 WAVES.

Внимательный читатель уже понял, что скрипт небезопасен, потому что его можно вызывать бесконечное количество раз: если аккаунт не выиграл, транзакция не попадет в блокчейн, а если выиграл, то получит 0,995 WAVES. Беспроигрышная лотерея получилась.

Правильный подход — всегда проверять права пользователя вызывать скрипт, например, иметь свой белый список.

@Callable(invoke)
func callMeBaby(uuid: String) = {

    if (isInWhiteList(invoke.caller) && invoke.fee == 500000) then {

        let id = extract(invoke.transactionId)
        let callerAddress = toBase58String(invoke.caller.bytes)

        ScriptResult(
            WriteSet([
                DataEntry(toBase58String(id), callerAddress + uuid)
            ]),
            TransferSet([
                ScriptTransfer(addressFromStringValue(callerAddress), invoke.fee, unit)
            ])
        )
    }
    else
        throw()
}

Замечу, что, если в будущем модель исполнения контрактов на Waves изменится и транзакции с ошибками (throw()) тоже будут записываться в блокчейн и снимать комиссию, использовать этот функционал станет невозможно.

Обсуждение описанного функционала и возможных путей развития — в этом issue на гитхаб.

Лучшие практики

Рекомендую использовать оба решения только для тестовых режимов продуктов и с проверкой всех граничных условий. Иначе вы рискуете потерять свои средства.

Присоединяйтесь к Waves Community

Читайте Waves News channel

Смотрите Waves Youtube

Подписывайтесь на Waves Twitter, Waves Subreddit

Обсудить в Discord!