Dive into Ethereum

    Сегодня платформа Ethereum стала одним из самых узнаваемых брендов блокчейн сферы, вплотную приблизившись по популярности (и капитализации) к Bitcoin. Но из-за отсутствия "полноценного" рускоязычного гайда, отечественные разработчики все еще не очень понимают, что это за зверь и как с ним работать. Поэтому в данной статье я попытался максимально подробно охватить все аспекты разработки умных контрактов под Ethereum.


    Я расскажу про инструменты разработки, сам ЯП, процесс добавления UI и еще много интересного. В конечном итоге мы получим обычный сайт-визитку, но "под капотом" он будет работать на умных контрактах Ethereum. Кого заинтересовало — прошу под кат.


    preview



    Содержание



    Введение в Ethereum


    Эта статья не расчитана на тех, кто совсем не знаком с Ethereum (или технологией блокчейн вообще), поэтому объяснений базовых вещей вроде блоков, транзакций или контрактов здесь не будет. Я подразумеваю, что вы хотя бы чуть-чуть в курсе происходящего. В противном случае полистайте статьи из списка ниже, а потом возвращайтесь :)



    Больше ссылок на интересные статьи вы найдете в конце.


    P.S. Я работаю под Ubuntu 16.04, так что весь процесс установки, разработки и деплоя будет описан под эту ОС. Тем не менее все используемые инструменты кроссплатформенны (скорее всего, не проверял), так что при желании можете поэкспериментировать на других ОС.


    Инструменты


    Geth


    Работа с Ethereum возможна через огромное число клиентов, часть из которых terminal-based, часть GUI и есть несколько гибридных решений. Своего рода стандартом является [Geth](), который разрабатывается командой Ethereum. Про него я уже писал в предыдущих статьях, но на всякий случай повторюсь.


    Клиент написан на Go, устанавливается стандартным способом:


    sudo apt-get install software-properties-common
    sudo add-apt-repository -y ppa:ethereum/ethereum
    sudo apt-get update
    sudo apt-get install ethereum

    Сам Geth не имеет GUI, но работать с ним из терминала довольно приятно. Здесь описан весь набор аргументов командной строки, я же опишу несколько самых популярных.


    Вот команда, которую я чаще всего использую в работе: $ geth --dev --rpc --rpcaddr "0.0.0.0" --rpcapi "admin,debug,miner,shh,txpool,personal,eth,net,web3" console


    • --dev запускает geth в режиме приватного блокчейна, то есть не синхронизирет основную / тестовую ветку. Вместо этого вы получаете стерильную цепочку без единого блока. Это самый удобный вариант в плане разработки, так как, например, майнинг блока занимает несколько секунд и нет никакой нагрузки на сеть или диск.


    • --rpc включает RPC-HTTP сервер. По сути это API к вашей ноде — через него сторонние приложения, вроде кошельков или IDE, смогут работать с блокчейном: загружать контракты, отправлять транзакции и так далее. По дефолту запускается на localhost:8545, можете изменить эти параметры с помощью --rpcaddr и --rpcport соответственно.
    • --rpcapi устанавливает что-то вроде прав доступа для приложений, подключенных к RPC серверу. Например, если вы не укажете "miner", то, подключив к ноде кошелек и запустив майнер, вы получите ошибку. В примере я указал все возможные права, подробнее можете почитать здесь.
    • console — как можно догадаться, эта опция запускает консоль разработчика. Она поддерживает самый обычный JS и ряд встроенных функций для работы с Ethereum, вот простой пример (пункт — Поднимаем ноду).

    Parity


    Geth довольно хорош, но в последнее время все чаще можно встретить другой клиент — Parity, написанный на Rust. Главным его отличием от Geth является встроенный web интерфейс, на мой взгляд, самый удобный среди всех ныне существующих. Установка:


    sudo <(curl https://get.parity.io -Lk)

    По окончании загрузки запустите в консоли parity и по адресу localhost:8180 можете найти сам кошелек.


    parity


    Еще один плюс: Parity быстрее своих конкурентов. По крайней мере так утверждают авторы, но по моим ощущениям это действительно так, особенно в плане синхронизации блокчейна.


    Единственный нюанс — своей консоли в parity нет. Но можно без проблем использовать для этих целей Geth:


    $ parity --geth # Run parity in Geth mode
    $ geth attach console # Attach Geth to the PArity node (Do it in another window)

    TestRPC


    Этот инструмент, в отличие от предыдущих, будет полезен только разработчикам. Он позволяет одной командой testrpc поднять приватный блокчейн с включенным RPC протоколом, десятком заранее созданных аккаунтов с этерами на счету, работающим майнером и так далее. Весь список здесь. По сути, testrpc — это тот же geth --dev --rpc ..., только на этот раз не надо тратить время на создание аккаунтов, включение / выключение майнера и прочие рутинные действия.


    Установка — npm install -g ethereumjs-testrpc.


    testrpc


    Mist


    Самый популярный кошелек для Ethereum, хотя на самом деле он умеет намного больше. Вот отличная статья, где step-by-step объясняется весь процесс работы с Mist. Скачать самую свежую версию можно со страницы релизов. Помимо работы с кошельком, есть возможность работы с контрактами.


    mist


    Remix


    Самая популярная IDE для разработки контрактов. Работает в браузере по адресу ethereum.github.io/browser-solidity/, поддерживает огромное число функций:


    • Подключение к указанному RPC провайдеру
    • Компиляция кода в байткод / опкоды
    • Публикация в Github gist
    • Пошаговый дебагер
    • Подсчет стоимости исполнения функций в газе
    • Сохранение вашего кода в localstorage
    • И многое другое

    При этом нет автокомплита, что очень печально.


    remix


    Cosmo


    Еще одна IDE для разработки умных контрактов, написана на Meteor, работает из коробки. Для начала откройте новый терминал и поднимите ноду с включенным RPC интерфесом geth --rpc --rpcapi="db,eth,net,web3,personal" --rpcport "8545" --rpcaddr "127.0.0.1" --rpccorsdomain "localhost" console. После этого можете запускать саму IDE:


    $ git clone http://github.com/SilentCicero/meteor-dapp-cosmo.git
    $ cd meteor-dapp-cosmo/app
    $ meteor

    Далее открываете localhost:3000 и можете начинать работать:


    cosmo_screenshot


    Etheratom


    Последний на сегодня инструмент для ускорения разработки умных контрактов. Это плагин для редактора Atom, устанавливается с помощью apm install atom-ethereum-interface. Штука удобная, сам пользуюсь. Позволяет работать c JS EVM или подключиться к ноде через RPC. Компилирует контракт на CTRL + ALT + C, деплоит в сеть на CTRL + ALT + S. Ну и предоставляет неплохой интерфейс для работы с самим контрактом.


    atom_ethereum


    Если вам не нужен такой навороченный функционал внутри редактора, то для Atom есть отдельный плагин с подсветкой синтаксиса Solidity — language-ethereum. Последний по сути является плагином под Sublime text, только конвертированный для работы в Atom.


    Solidity


    Возможно, вы слышали про то, что можно писать контракты не только на Solidity, но и на других языках, например Serpent (внешне напоминает Python). Но последний комит в develop ветке ethereum/serpent был примерно полгода назад, так что, по-видимому, язык, увы, deprecated.


    Поэтому писать будем только на Solidity. Пока что язык находится на относительно раннем этапе развития, так что никаких сложных конструкций или уникальных абстракций в нем нет. Поэтому отдельно рассказывать про него я не вижу смысла — любой человек с опытом в программировании сможет свободно писать на нем после 20 минут чтения документации. На случай, если у вас такого опыта нет, — ниже я довольно подробно прокомментировал весь код контракта.


    Для самостоятельного обучения есть несколько очень хороших примеров с максимально подробными описаниями:



    Еще раз отмечу (отличную!) документацию языка, местами даже переведена на русский язык.


    Создаем контракт-визитку


    Самое время создать наш контракт. В конечном итоге это будет приложение-визитка, на которую мы поместим само "резюме":


    • Имя, почта, контакты и так далее
    • Список проектов
    • Образование: вузы, курсы и тд
    • Навыки
    • Публикации

    Первый шаг


    Первым делом создадим шаблон контракта и функцию-конструктор. Она должна называться также как и сам контракт и вызывается лишь однажды — при загрузке контракта в блокчейн. Мы будем использовать ее для инициализации одной единственной переменной — address owner. Как вы уже наверное догадались, в нее будет записан адрес того, кто залил контракт в сеть. А использоваться она будет для реализации функций администратора контракта, но об этом позже.


    pragma solidity ^0.4.0;
    
    contract EthereumCV is Structures {
        address owner;
    
        // =====================
        // ==== CONSTRUCTOR ====
        // =====================
        function EthereumCV() {
            owner = msg.sender;
        }
    }

    Базовая информация


    Следующим шагом добавим возможность указывать базовую информацию об авторе — имя, почту, адрес и так далее. Для этого будем использовать самый обычный mapping, который нужно объявить в начало контракта:


    address owner;
    mapping (string => string) basic_data;

    Для того, чтобы иметь возможность "получать" от контракта эти данные, создадим следующую функцию:


    function getBasicData (string arg) constant returns (string) {
        return basic_data[arg];
    }

    Здесь все просто, стоит только отметить модификатор constant — его можно (и нужно) использовать для тех функций, которые не изменяют state приложения. Главный плюс таких функций (sic!), в том что их можно использовать как обычные функции.


    Администрирование


    Теперь стоит задуматься о наполнении своего резюме контентом. В самом простом случае мы могли бы обойтись функцией вроде


    function setBasicData (string key, string value) {
        basic_data[key] = value;
    }

    Но в этом случае любой при желании смог бы изменить, например, наше имя, вызвав setBasicData("name", "New Name"). К счастью, есть способ всего в одну строку пресечь любые такие попытки:


    function setBasicData (string key, string value) {
        if (msg.sender != owner) { throw; }
        basic_data[key] = value;
    }

    Так как нам еще не раз придется использовать подобную конструкцию (при добавлении нового проекта, например), то стоит создать специальный модификатор:


    modifier onlyOwner() {
        if (msg.sender != owner) { throw; }
        _; // Will be replaced with function body
    }
    
    // Now you can use it with any function
    function setBasicData (string key, string value) onlyOwner() {
        basic_data[key] = value;
    }

    При желании, можно использовать другие способы авторизации, например по паролю. Хэш будет храниться в контракте и сравниваться с введенным при каждом вызове функции. Но понятно, что этот способ не такой безопасный, благо радужные таблицы и атаки по словарю никто не отменял. С другой стороны, наш способ тоже не идеален, так как если вы потеряете доступ к адресу owner, то ничего редактировать вы уже не сможете.


    Модульность


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


    Для этого в той же директории создадим новый файл structures.sol и библиотеку Structures. А уже внутри нее опишем каждую из структур:


    pragma solidity ^0.4.0;
    
    library Structures {
        struct Project {
            string name;
            string link;
            string description;
        }
    
        struct Education {
            string name;
            string speciality;
            int32 year_start;
            int32 year_finish;
        }
    
        struct Publication {
            string name;
            string link;
            string language;
        }
    
        struct Skill {
            string name;
            int32 level;
        }
    }

    Теперь осталось только импортировать полученный файл


    pragma solidity ^0.4.0;
    
    import "./structures.sol";
    
    contract EthereumCV {
        mapping (string => string) basic_data;
        address owner;
    
        Structures.Project[] public projects;
        Structures.Education[] public educations;
        Structures.Skill[] public skills;
        Structures.Publication[] public publications;
    
        // ...
    }

    Самые сообразительные уже догадались, что нотация Structures.Project[] projects означает создание динамического массива с элеметнами типа Project. А вот с модификатором public уже сложнее. По сути, он заменяет нам написание функции вроде get_project(int position) { return projects[position]; } — компилятор сам создаст такую функцию. Называться она будет так же как и переменная, в нашем случае — projects.


    Вы можете спросить — почему мы в самом начале не написали mapping (string => string) public basic_data, а вместо этого сами создавали такую функцию? Причина банальна — public пока что не умеет работать c переменными, для которых ключом является динамический тип данных (string именно такой тип).


    Unimplemented feature (/src/libsolidity/codegen/ExpressionCompiler.cpp:105): Accessors for mapping with dynamically-sized keys not yet implemented.

    Для этого нужно объявлять basic_data как например mapping (bytes32 => string).


    BTW На всякий случай отмечу, что кроме локального файла, Remix умеет импортировать .sol файлы по ссылке на Github и даже с помощью протокола Swarm (это что-то вроде распределенного хранилища для Ethereum, подробнее здесь)


    Загружаем и удаляем данные


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


    function editPublication (bool operation, string name, string link, string language) onlyOwner() {
        if (operation) {
            publications.push(Structures.Publication(name, link, language));
        } else {
            delete publications[publications.length - 1];
        }
    }

    С помощью параметра operation мы избавились от написания отдельной функции для удаления последней публикации (костыльно, но мы ведь только учимся). Хотя нужно отметить, что такой способ избавления от элемента в массиве на самом деле не совсем корректный. Сам элемент конечно будет удален, но на месте индекса останется пустое место. В нашем случае это не смертельно (мы будем проверять пустоту отдельных элементов на стороне клиента), но, вообще говоря, про это не стоит забывать. Тем более что сдвинуть весь массив и уменьшить счетчик длины не так уж сложно.


    Отдаем данные


    Как я уже сказал, модификатор public в строке Project[] public projects обеспечил нас функцией которая по индексу i вернет проект projects[i]. Но мы не знаем, сколько у нас всего проектов, и здесь есть два пути. Первый — итерироваться по i до того момента, пока мы не получим ошибку о несуществующем элементе. Второй — написать отдельную функцию, которая вернет нам размер projects. Я пойду вторым путем, чуть позже скажу почему:


    function getSize(string arg) constant returns (uint) {
        if (sha3(arg) == sha3("projects")) { return projects.length; }
        if (sha3(arg) == sha3("educations")) { return educations.length; }
        if (sha3(arg) == sha3("publications")) { return quotes.length; }
        if (sha3(arg) == sha3("skills")) { return skills.length; }
        throw;
    }

    Заметьте, что мы не можем сравнить две строки привычным способом 'aaa' == 'bbb'. Причина все та же, string — это динамический тип данных, работа с ними довольно болезненна. Так что остается либо сравнивать хэши, либо использовать функцию для посимвольного сравнения. В этом случае можете использовать популярную библиотеку stringUtils.sol, в ней есть такая функция.


    Деплой


    В разных средах разработки процесс компиляции и деплоя разумеется отличается, поэтому я ограничусь Remix, как самым популярным.


    Сначала, само собой, заливаем весь код (финальную версию можете найти в репозитории проекта). Далее в выпадающем списке Select execution environment выберите Javascript VM — пока что протестируем контракт на JS эмуляторе блокчейна, чуть позже научимся работать и с настоящим. Если с контрактом все в порядке, то вам будет доступна кнопка Create — нажимаем и видим:


    remix_create


    Теперь, когда контракт залит в блокчейн (его эмуляцию, но не суть), можем попробовать вызвать какую-нибудь функцию и посмотреть, что из этого выйдет. Например можно сохранить в контракте email — для этого найдите функцию setBasicData, заполните поле и нажмите кнопку с именем функции:


    remix_set_basic_data


    Функция ничего не возвращает, поэтому result: 0x. Теперь можно запросить у контракта email: ищем функцию getBasicData и пробуем:


    remix_get_basic_data


    С остальными функциями предлагаю вам поэксперементировать самим.


    Добавляем UI


    Ниже я расскажу про самый распостраненный способ добавить UI к вашему контракту. Он позволяет с помощью JS и HTML создавать интерфейсы любой сложности, достаточно иметь доступ к рабочей ноде Ethereum (или ее аналогам).


    Web3.js


    This is the Ethereum compatible JavaScript API which implements the Generic JSON RPC spec. It's available on npm as a node module, for bower and component as an embeddable js and as a meteor.js package.

    Это JS библиотека, позовляющая использовать API Ethereum с помощью обычного JS. По сути с ее помощью вы просто подключаетесь ноде и у вас появляется что-то вроде консоли geth в браузере. Устанавливается через npm или bower:


    $ sudo npm install web3
    $ bower install web3

    Вот пример работы с web3 через node.js (предварительно запустите testrpcили любую другую ноду с RPC интерфейсом):


    $ node
    > var Web3 = require('web3');
    > var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
    > web3.eth.accounts
    [ '0x5f7aaf2199f95e1b991cb7961c49be5df1050d86',
      '0x1c0131b72fa0f67ac9c46c5f4bd8fa483d7553c3',
      '0x10de59faaea051b7ea889011a2d8a560a75805a7',
      '0x56e71613ff0fb6a9486555325dc6bec8e6a88c78',
      '0x40155a39d232a0bdb98ee9f721340197af3170c5',
      '0x4b9f184b2527a3605ec8d62dca22edb4b240bbda',
      '0x117a6be09f6e5fbbd373f7f460c8a74a0800c92c',
      '0x111f9a2920cbf81e4236225fcbe17c8b329bacd7',
      '0x01b4bfbca90cbfad6d6d2a80ee9540645c7bd55a',
      '0x71be5d7d2a53597ef73d90fd558df23c37f3aac1' ]
    >

    Тоже самое, только из JS консоли браузера (не забудьте про <script src="path_to/web3.js"></script>)


    browser_js_web3


    То есть мы уже на этом моменте можем запустить ноду, синхронизировать ее с текущей цепочкой и останется только сверстать наше приложение. Но тут есть два тонких момента: во-первых, вам нужно синхронизировать блокчейн Ethereum, а вы этого скорее всего до сих пор не сделали.


    Второй нюанс — RPC не имеет никакого встроенного механизма авторизации, поэтому любой желающий может узнать адрес вашей ноды из исходников JS и пользоваться ей в свое удовольствие. Тут конечно можно писать какую-нибудь обертку на Nginx с простейшей HTTP basic auth, но это как-нибудь в другой раз.


    Metamask


    Поэтому сейчас мы воспользуемся плагином Metamask (увы, только для Chrome). По сути это и есть та прослойка между нодой и браузером, которая позволит вам использовать web3 в браузере, но без своей ноды. Metamask работает очень просто — в каждую страницу он встраивает web3.js, который автоматически подключается к RPC серверам Metamask. После этого вы можете использовать Ethereum на полную катушку.


    После установки плагина, в левом верхнем углу выберите Testnet и получите несколько эфиров на кране Metamask. На этом моменте вы должны получить что-то вроде такого (с чистой историей разумеется):


    metamask_ready


    Deploy with Metamask


    С Metamask задеплоить контракт в сеть так же просто, как и в случаем с JS EVM. Для этого снова открываем Remix и в списке Select execution environment выбираем пункт Injected Web3 (скорее всего он выбран автоматически). После этого нажимаем Create и видим всплывающее окно:


    metamask_popup


    Чуть позже надпись Waiting for transaction to be mined... сменится на информацию об опубликованном контракте — это значит что он попал в блокчейн. Адрес контракта можете узнать, открыв Metamask и нажав на запись вида:


    metamask_info


    Однако теперь, если вы захотите, например, вызвать функцию editProject(...), то вам так же придется подтвержать транзакцию и ждать, пока она будет замайнена в блок.


    Пример


    Теперь дело за малым — надо научиться получать данные от контракта через Web3. Для этого, во-первых, надо научиться определять наличие web3 на странице:


    window.addEventListener('load', function() {
      // Checking if Web3 has been injected by the browser (Mist/MetaMask)
      if (typeof web3 !== 'undefined') {
        // Use Mist/MetaMask's provider
        console.log("Web3 detected!");
        window.web3 = new Web3(web3.currentProvider);
        // Now you can start your app & access web3 freely:
        startApp()
      } else {
        alert('Please use Chrome, install Metamask and then try again!')
      }
    })

    Внутри startApp() я определелил всю логику работы с контрактом, тем самым избегая ложных срабатываний и ошибок.


    function startApp() {
      var address = {
        "3" : "0xf11398265f766b8941549c865d948ae0ac734561" // Ropsten
      }
    
      var current_network = web3.version.network;
      // abi initialized ealier, in abi.js
      var contract = web3.eth.contract(abi).at(address[current_network]);
    
      console.log("Contract initialized successfully")
    
      contract.getBasicData("name", function(error, data) {
        console.log(data);
      });
    
      contract.getBasicData("email", function(error, data) {
        console.log(data);
      });
    
      contract.getSize("skills", function(error, data) {
        var skills_size = data["c"][0];
        for (var i = 0; i < skills_size; ++i) {
          contract.skills(i, function(error, data) {
            // Don't forget to check blank elements!
            if (data[0]) { console.log(data[0], data[1]["c"][0]); }
          })
        }
      })
    }

    js_logs


    Итог


    Теперь, когда вы со всем разобрались, можно браться за верстку и JS. Я использовал Vue.js и Spectre.css, для визуализации навыков добавил Google Charts. Результат можете увидеть на pavlovdog.github.io:


    cv


    Вместо заключения


    Только что вы увидели, как можно довольно быстро создать приложение, которое самым непосредственным образом использует технологию blockchain. Хотя в погоне за простотой (все таки это обучающая статья) я допустили некоторые упрощения, которые по-хорошему допускать нельзя.


    Например, мы используем чей-то шлюз (я про Metamask), вместо того, чтобы работать со своей нодой. Это удобно, но технология блокчейн в первую очередь — децентрализация и отсутствие посредников. У нас же всего этого нет — мы доверяем парням из Metamask.


    Другая, не такая критичная проблема, — мы забыли про стоимость деплоя контрактов и транзакций к ним. На практике, стоит десять раз подумать, прежде чем использовать string вместо bytes, потому как такие вещи прежде всего влияют на затраты при работе с контрактом. Опять же, в примере я использовал Testnet, так что никаких денег мы не потратили, но при работе с Main net не стоит быть такими расточительными.


    В любом случае, я надеюсь что статья оказалась полезной, если есть вопросы — задавайте в комментариях или пишите мне на почту.


    Ссылки


    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 12

      0
      Интересная статья, спасибо.
      Существуют ли инструменты, позволяющие создавать умный контракт без знания программирования?
      И возможен ли умный контракт в биткоине или только с помощью Etherium?
        0
        Да, на блокчейне биткоина возможна запись смарт контрактов, но его скриптовый язык очень ограничен и не является тьюринг-полным. Из-за таких недостатков и был разработан Ethereum. Кстати, Multisig-транзакции — это и есть тип смарт контрактов на блокчейне биткоина
        0
        Советую добавить в статью ссылку на monax.io. Довольно полезный сайт для тех, кто занимается/хочет заниматься разработкой на Ethereum, описывает довольно много паттернов проектирования контрактов.
          0

          Автор, спасибо.

            0
            Немного странно, что нет ни слова про фреймворк Truffle, который покрывает автоматический деплой контрактов и их тестирование в связке с testrpc.
              0
              Получается, что, чтобы не иметь вот этой фигни с метамаском и хромом, есть такие пути:
              1) Делать бэкенд, который реализует ограниченное апи и сам общается с нодой эфира. Централизует на нем.
              2) Упомянутый nginx. Но там, получается, надо фильтровать запросы с внешнего интернета — разрешать нелокалам только запрос на чтение данных из контракта, ведь читателю ни к чему доступ к редактирующим функциям контракта. Ну и в этом случае, получается, клиент может, теоретически использовать свою локальную ноду, а не обращаться к ноде автора? Ведь чтение данных доступно всем? Или нет?
                0
                Есть ещё отличный линтер Solium и соответствующий плагин к sublime
                  0
                  Спасибо за прекрасный мануал. Особенно за объяснение создания UI. Попробую.
                    0
                    А где хранится информация, переданная в контракт? В блокчейне в виде транзакции?
                    Тогда как происходит её обновление?
                    Создаётся новая транзакция? В этом случае получается, что удалённые и изменённые данные всегда можно прочитать?
                      0
                      Вызовы функций, вместе с параметрами, хранятся в блокчейне в виде транзакций. И да, оно там остается навсегда, и его может прочитать «кто угодно», если сеть настоящая, а не dev.
                      0
                      Код из «Первого шага» contract EthereumCV is Structures {
                      в remix выдает ошибку:
                      DeclarationError: Identifier not found or not unique
                      похоже что он считает Structures идентефикатором, а не специальным словом (которое обозначает что?)

                      Если «is Structures» удалить то ошибка компилятора пропадает
                        0
                        Немного поздновато, но всё же: упоминается вариант аутентификации в методах контракта через хранение хеша пароля и передачу пароля в методы контракта. Но тогда, если я правильно понимаю, этот пароль будет светиться во всех транзакциях в качестве параметров к вызову метода?

                        Only users with full accounts can post comments. Log in, please.