Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)

Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)

Всем привет!

В первой части мы подробно рассмотрели как создавать и работать с dApp (децентрализованным приложением) в Waves RIDE IDE.

Давайте сейчас немного потестируем разобраный пример.

Этап 3. Тестирование dApp аккаунта

Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)

Какие проблемы сразу бросаются на гласа с Alice dApp Account?
Во-первых:
Boob и Cooper могут случайно отправить на адрес dApp средства с помощью обычной transfer транзакции и, таким образом, не смогут получить к ним доступ обратно.

Во-вторых:
Мы никак не ограничиваем Alice в выводе средств без согласования с Boob или(и) Cooper. Так как, обратите внимание на verify, все транзакции от Alice будут исполняться.

Давайте исправим 2-е, запретив Alice transfer транзакции. Деплоим исправленный скрипт:
Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)

Пробуем вывести монеты с dApp Alice и ее подписью. Получаем ошибку:
Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)

Пробуем вывести через withdraw:

broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"withdraw",args:[{type:"integer", value: 1000000}]}, payment: []}))

Скрипт работает и со 2-м пунктом мы разобрались!

Этап 4. Создаем DAO с голосованием

К сожалению, в языке RIDE пока не предусмотрены возможности работы с коллекциями (словари-словарей, итераторы, редьюсеры и проч). Однако, для любых операций с плоскими коллекциями key-value мы можем спроектировать систему работы со строками, соотвественно с ключами и их расшифровкой.

Строки очень просто конкатенировать, строки можно разделять по индексам.
Давайте в качестве тестового примера соберем и разберем строку и проверим как это повлияет на исход транзакции.
Мы остановились на том, что Alice не могла подписать Transfer транзакцию, так как эта возможность была заблокирована в @verifier для такого типа транзакций.

Давайте поупражняемся со строками и потом разрешим это.

RIDE Strings

Транзакция снова возможна, мы умеем работать со строками.
Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)


Итого, мы имеем все необходимое для написания сложной логики DAO dApp.

Data Transactions

Data Transactions:
“The maximum size for a key is 100 characters, and a key can contain arbitrary Unicode code points including spaces and other non-printable symbols. String values have a limit of 32,768 bytes and the maximum number of possible entries in data transaction is 100. Overall, the maximum size of a data transaction is around 140kb — for reference, almost exactly the length of Shakespeare’s play ‘Romeo and Juliet’.”

Создаем DAO со следующими условиями:
Для того, чтобы стартапу получить финансирование, вызвав getFunds() необходима поддержка минимум 2-х участников — инвесторов DAO. Вывести можно будет ровно столько, сколько в сумме указали на голосовании владельцы DAO.

Давайте сделаем 3 типа ключей и добавим логику по работе с балансами в 2-х новых функциях vote и getFunds:
xx…xx_ia = инвесторы, доступный баланс (vote, deposit, withdrawal)
xx…xx_sv = стартапы, количество голосов (vote, getFunds)
xx…xx_sf = стартапы, количество голосов (vote, getFunds)
xx…xx = публичный адрес (35 символов)

Заметьте в Vote нам понадобилось обновлять сразу несколько полей:

WriteSet([DataEntry(key1, value1), DataEntry(key2, value2)]),

WriteSet позволяет нам делать сразу несколько записей в рамках одной invokeScript транзакции.

Вот так это выглядит в key-value хранилище DAO dApp, после того как Bob и Cooper пополнили ia-депозиты:
Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)

Функция депозита у нас слегка изменилась:
Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)

Сейчас наступает самый важный момент в деятельности DAO — голосование за проекты для финансирования.

Bob голосует за проект Neli на 500000 wavelets:

broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"vote",args:[{type:"integer", value: 500000}, {type:"string", value: "3MrXEKJr9nDLNyVZ1d12Mq4jjeUYwxNjMsH"}]}, payment: []}))

Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)

В хранилище данных мы видим все необходимые записи для адреса Neli:
Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)
Купер также проголосовал за проект Neli.
Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)

Давайте взглянем на код функции getFunds. Neli должна собрать минимум 2 голоса, чтобы иметь возможность вывести средства из DAO.
Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)

Neli собирается вывести половину доверенной ей суммы:

broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"getFunds",args:[{type:"integer", value: 500000}]}, payment: []}))

Учимся писать Waves смарт-контракты на RIDE и RIDE4DAPPS. Часть 2 (DAO — Decentralized Autonomous Organization)

Ей это удается, то есть DAO работает!

Мы рассмотрели процесс создания DAO на языке RIDE4DAPPS.
В следующих частях мы подробнее займемся рефакторингом кода и тестированием кейсов.

Полная версия кода в Waves RIDE IDE:

# In this example multiple accounts can deposit their funds to DAO and safely take them back, no one can interfere with this.
# DAO participants can also vote for particular addresses and let them withdraw invested funds then quorum has reached.
# An inner state is maintained as mapping `address=>waves`.
# https://medium.com/waves-lab/waves-announces-funding-for-ride-for-dapps-developers-f724095fdbe1

# You can try this contract by following commands in the IDE (ide.wavesplatform.com)
# Run commands as listed below
# From account #0:
#      deploy()
# From account #1: deposit funds
#      broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"deposit",args:[]}, payment: [{amount: 100000000, asset:null }]}))
# From account #2: deposit funds
#      broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"deposit",args:[]}, payment: [{amount: 100000000, asset:null }]}))
# From account #1: vote for startup
#      broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"vote",args:[{type:"integer", value: 500000}, {type:"string", value: "3MrXEKJr9nDLNyVZ1d12Mq4jjeUYwxNjMsH"}]}, payment: []}))
# From account #2: vote for startup
#      broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"vote",args:[{type:"integer", value: 500000}, {type:"string", value: "3MrXEKJr9nDLNyVZ1d12Mq4jjeUYwxNjMsH"}]}, payment: []}))
# From account #3: get invested funds
#      broadcast(invokeScript({dappAddress: address(env.accounts[1]), call:{function:"getFunds",args:[{type:"integer", value: 500000}]}, payment: []}))

{-# STDLIB_VERSION 3 #-}
{-# CONTENT_TYPE DAPP #-}
{-# SCRIPT_TYPE ACCOUNT #-}

@Callable(i)
func deposit() = {
   let pmt = extract(i.payment)
   if (isDefined(pmt.assetId)) then throw("can hodl waves only at the moment")
   else {
        let currentKey = toBase58String(i.caller.bytes)
        let xxxInvestorBalance = currentKey + "_" + "ib"
        let currentAmount = match getInteger(this, xxxInvestorBalance) {
            case a:Int => a
            case _ => 0
        }
        let newAmount = currentAmount + pmt.amount
        WriteSet([DataEntry(xxxInvestorBalance, newAmount)])
   }
}
@Callable(i)
func withdraw(amount: Int) = {
        let currentKey = toBase58String(i.caller.bytes)
        let xxxInvestorBalance = currentKey + "_" + "ib"
        let currentAmount = match getInteger(this, xxxInvestorBalance) {
            case a:Int => a
            case _ => 0
        }
        let newAmount = currentAmount - amount
     if (amount < 0)
            then throw("Can't withdraw negative amount")
    else if (newAmount < 0)
            then throw("Not enough balance")
            else ScriptResult(
                    WriteSet([DataEntry(xxxInvestorBalance, newAmount)]),
                    TransferSet([ScriptTransfer(i.caller, amount, unit)])
                )
    }
@Callable(i)
func getFunds(amount: Int) = {
        let quorum = 2
        let currentKey = toBase58String(i.caller.bytes)
        let xxxStartupFund = currentKey + "_" + "sf"
        let xxxStartupVotes = currentKey + "_" + "sv"
        let currentAmount = match getInteger(this, xxxStartupFund) {
            case a:Int => a
            case _ => 0
        }
        let totalVotes = match getInteger(this, xxxStartupVotes) {
            case a:Int => a
            case _ => 0
        }
        let newAmount = currentAmount - amount
    if (amount < 0)
            then throw("Can't withdraw negative amount")
    else if (newAmount < 0)
            then throw("Not enough balance")
    else if (totalVotes < quorum)
            then throw("Not enough votes. At least 2 votes required!")
    else ScriptResult(
                    WriteSet([
                        DataEntry(xxxStartupFund, newAmount)
                        ]),
                    TransferSet([ScriptTransfer(i.caller, amount, unit)])
                )
    }
@Callable(i)
func vote(amount: Int, address: String) = {
        let currentKey = toBase58String(i.caller.bytes)
        let xxxInvestorBalance = currentKey + "_" + "ib"
        let xxxStartupFund = address + "_" + "sf"
        let xxxStartupVotes = address + "_" + "sv"
        let currentAmount = match getInteger(this, xxxInvestorBalance) {
            case a:Int => a
            case _ => 0
        }
        let currentVotes = match getInteger(this, xxxStartupVotes) {
            case a:Int => a
            case _ => 0
        }
        let currentFund = match getInteger(this, xxxStartupFund) {
            case a:Int => a
            case _ => 0
        }
    if (amount <= 0)
            then throw("Can't withdraw negative amount")
    else if (amount > currentAmount)
            then throw("Not enough balance")
    else ScriptResult(
                    WriteSet([
                        DataEntry(xxxInvestorBalance, currentAmount - amount),
                        DataEntry(xxxStartupVotes, currentVotes + 1),
                        DataEntry(xxxStartupFund, currentFund + amount)
                        ]),
                    TransferSet([ScriptTransfer(i.caller, amount, unit)])
            )
    }
@Verifier(tx)
func verify() = {
    match tx {
        case t: TransferTransaction =>false
        case _ => true
    }
}

Первая часть
Код на гитхабе
Waves RIDE IDE
Анонс грантовой программы

Источник: habr.com

Добавить комментарий