Как создавать децентрализованные смарт-контракты на лету на компьютере в Интернете.
В одном из своих последних сообщений в блоге я поделился решением для запроса смарт-контрактов на компьютере в Интернете в контексте NodeJS.
Эта статья была первой из серии, в которой будут показаны различные сценарии, которые я разработал для нового проекта под названием Papyrs — децентрализованной платформы для ведения блогов с открытым исходным кодом, ориентированной на конфиденциальность, в Интернет-компьютере, которая живет на 100% в сети.
Я планирую поделиться тем, как я запрашиваю оставшиеся циклы и обновляю код контейнеров моих пользователей.Но сначала я собираюсь поделиться основой архитектуры, т. е. продемонстрировать код, который позволяет программе динамически создавать контейнеры. .
Архитектура
В отличие от проектов web2, которые централизуют пользовательские данные в одной или распределенной базе данных, я применил более футуристический подход к сохранению данных в Papyrs.
Главный участник действует как менеджер, который — на лету, при создании объекта — создает децентрализованную, безопасную простую базу данных типа «ключ-значение» для каждого отдельного сохранения данных каждого пользователя.
В следующих главах я представлю такое решение на примере проекта, разработанного в Motoko.
Для получения более подробной информации об этой архитектуре вы можете прочитать статью, которую я написал об этом: Интернет-компьютер: архитектура децентрализованной базы данных веб-приложения.
Ведро
Каждый пользователь получает выделенный смарт-контракт контейнера, который я называю bucket
, чтобы следовать соглашению о примере классов акторов, предоставленном DFINITY на GitHub. На форуме его иногда также называют детской канистрой.
В демонстрационных целях я подумал, что такое ведро — actor
— может предоставить функцию, которая say
привет. Кроме того, чтобы предвосхитить мои будущие записи, он также получает параметр user
в качестве параметра и содержит нестабильный номер version
, который может увеличиваться каждый раз при установке нового кода контейнера.
Обратите внимание, что я использую тип Text
для пользователя только для упрощения примера. В реальном случае я бы использовал Principal
.
import Nat "mo:base/Nat"; actor class Bucket(user: Text) = this { var version: Nat = 1; public query func say() : async Text { return "Hello World - " # user # " - v" # Nat.toText(version); }; }
Менеджер
Менеджер — или Main
актер — содержит больше логики, поэтому я разобью его на этапы, прежде чем представить его целиком.
Как упоминалось ранее, он создает динамические канистры. Поэтому он может или должен, в зависимости от варианта использования, отслеживать те, которые были созданы, например, путем объединения идентификатора контейнера, который был создан в файле Hashmap
.
Однако для простоты в этой статье я отслеживаю только последний смарт-контракт, который был инициализирован с помощью стабильной переменной canisterId
.
actor Main { private stable var canisterId: ?Principal = null; public query func getCanisterId() : async ?Principal { canisterId }; };
Для создания новой канистры нам в основном нужны две вещи:
- ведро — т. е. действующее лицо из предыдущей главы
- циклы библиотека
Поскольку мы реализуем код корзины в том же проекте, мы можем включить его с относительным путем импорта. Каждый вызов Bucket.Bucket(param)
создает экземпляр новой корзины, т. е. динамически создает новый смарт-контракт канистр.
Библиотека используется для совместного использования циклов менеджера с созданным им сегментом. Соответствующие вычислительные затраты составляют 100 000 000 000 циклов, то есть около 0,142 доллара США, согласно документации.
import Cycles "mo:base/ExperimentalCycles"; import Principal "mo:base/Principal"; import Error "mo:base/Error"; import Bucket "./bucket"; actor Main { private stable var canisterId: ?Principal = null; public shared({ caller }) func init(): async (Principal) { Cycles.add(1_000_000_000_000); let b = await Bucket.Bucket("User1"); canisterId := ?(Principal.fromActor(b)); switch (canisterId) { case null { throw Error.reject("Bucket init error"); }; case (?canisterId) { return canisterId; }; }; }; public query func getCanisterId() : async ?Principal { canisterId }; };
Хотя это может быть и конец истории, я хотел бы добавить еще один кусочек головоломки.
Действительно, может быть интересно установить контроллеры, которые могут изменять ведро — например, может быть интересно разрешить вашему директору и/или одному из менеджеров обновлять код канистр.
Для этой цели нам сначала нужно добавить в проект спецификацию Интернет-компьютера в виде нового Motoko module
. Вы можете либо конвертировать скрытый файл, либо взять тот, который я использовал в Papyrs (исходник).
Наконец, мы можем объявить переменную, которая будет использоваться для вызова адреса накопителя управления ИС (aaaaa-aa
) и использовать ее для эффективного обновления настроек вновь созданного накопителя.
import Cycles "mo:base/ExperimentalCycles"; import Principal "mo:base/Principal"; import Error "mo:base/Error"; import IC "./ic.types"; import Bucket "./bucket"; actor Main { private stable var canisterId: ?Principal = null; private let ic : IC.Self = actor "aaaaa-aa"; public shared({ caller }) func init(): async (Principal) { Cycles.add(1_000_000_000_000); let b = await Bucket.Bucket("User1"); canisterId := ?(Principal.fromActor(b)); switch (canisterId) { case null { throw Error.reject("Bucket init error"); }; case (?canisterId) { let self: Principal = Principal.fromActor(Main); let controllers: ?[Principal] = ?[canisterId, caller, self]; await ic.update_settings(({canister_id = canisterId; settings = { controllers = controllers; freezing_threshold = null; memory_allocation = null; compute_allocation = null; }})); return canisterId; }; }; }; public query func getCanisterId() : async ?Principal { canisterId }; };
Веб приложение
После реализации двух смарт-контрактов канистр мы можем разработать фиктивный интерфейс для проверки их функциональности. Он может содержать два действия: одно для создания ведра, а другое для его вызова, т. е. для вызова его функции say
.
<html lang="en"> <body> <main> <button id="init">Init</button> <button id="say">Say</button> </main> </body> </html>
Если вы когда-либо создавали пример проекта с помощью командной строки dfx, то нижеследующее покажется вам знакомым.
Я создал проект под названием buckets_sample
. Dfx автоматически устанавливает зависимости и функцию, которая предоставляет актера main
. Поэтому функция JavaScript, которая вызывает менеджера для создания нового контейнера, использует эти готовые методы. Я также сохраняю ведро — основной идентификатор последней созданной емкости — в глобальной переменной для повторного использования.
import { buckets_sample } from '../../declarations/buckets_sample'; let bucket; const initCanister = async () => { try { bucket = await buckets_sample.init(); console.log('New bucket:', bucket.toText()); } catch (err) { console.error(err); } }; const init = () => { const btnInit = document.querySelector('button#init'); btnInit.addEventListener('click', initCanister); }; document.addEventListener('DOMContentLoaded', init);
Напротив, процесс, создающий новый образец проекта, не знает, что мы хотим динамически создавать контейнеры. Вот почему мы должны сгенерировать понятные интерфейсы и соответствующий код JavaScript для корзины, которую мы написали ранее.
В настоящее время нет другого способа сгенерировать эти файлы, кроме следующего обходного пути:
1. Отредактируйте конфигурацию dfx.json
, чтобы отобразить действующее лицо корзины.
"canisters": { "buckets_sample": { "main": "src/buckets_sample/main.mo", "type": "motoko" }, "bucket": { <----- add an entry for the bucket "main": "src/buckets_sample/bucket.mo", "type": "motoko" },
2. Запустите команду dfx deploy
для создания файлов. Команда завершится ошибкой («Ошибка: недопустимые данные: ожидаемые аргументы, но ничего не найдено»), которую можно смело игнорировать 😉.
3. Отменить изменение в dfx.json
.
4. Скопируйте сгенерированные файлы в исходную папку, чтобы мы могли использовать их в веб-приложении.
rsync -av .dfx/local/canisters/bucket ./src/declarations --exclude=bucket.wasm
Немного мубо-джамбо, но это помогает 😁.
Благодаря недавно сгенерированным файлам объявлений мы можем создать пользовательскую функцию, которая создает экземпляр субъекта для корзины — ID контейнера — который мы генерируем на лету.
import { Actor, HttpAgent } from '@dfinity/agent'; import { idlFactory } from '../../declarations/bucket'; export const createBucketActor = async ({ canisterId }) => { const agent = new HttpAgent(); if (process.env.NODE_ENV !== 'production') { await agent.fetchRootKey(); } return Actor.createActor(idlFactory, { agent, canisterId }); };
Обратите внимание, что в приведенном выше фрагменте я явно import
другой idlFactory
, который соответствует определению ведра.
В конечном итоге мы можем реализовать код, вызывающий функцию say
, что также завершает разработку демонстрационного приложения.
import { Actor, HttpAgent } from '@dfinity/agent'; import { buckets_sample } from '../../declarations/buckets_sample'; import { idlFactory } from '../../declarations/bucket'; export const createBucketActor = async ({ canisterId }) => { const agent = new HttpAgent(); if (process.env.NODE_ENV !== 'production') { await agent.fetchRootKey(); } return Actor.createActor(idlFactory, { agent, canisterId }); }; let bucket; const initCanister = async () => { try { bucket = await buckets_sample.init(); console.log('New bucket:', bucket.toText()); } catch (err) { console.error(err); } }; const sayHello = async () => { try { const actor = await createBucketActor({ canisterId: bucket }); console.log(await actor.say()); } catch (err) { console.error(err); } }; const init = () => { const btnInit = document.querySelector('button#init'); btnInit.addEventListener('click', initCanister); const btnSay = document.querySelector('button#say'); btnSay.addEventListener('click', sayHello); }; document.addEventListener('DOMContentLoaded', init);
Демо
Всему приходит конец, пример проекта наконец-то можно протестировать 😉.
Кнопка init динамически создает новую корзину, анализирует ее ID в консоли, а кнопка «говорит» вызывает функцию новой корзины.
Заключение
На написание этой статьи у меня ушло гораздо больше времени, чем я ожидал 😅. Я надеюсь, что это будет полезно, и я с нетерпением жду возможности поделиться другими приемами, которые я изучил, разрабатывая Papyrs.
Кстати говоря, если у вас есть какие-либо связанные вопросы или предложения, которые могли бы стать интересными сообщениями в блоге, свяжитесь со мной и дайте мне знать!?!
В бесконечность и дальше,
Дэвид
Чтобы узнать больше о приключениях, подпишитесь на меня в Twitter.
____