Как создавать децентрализованные смарт-контракты на лету на компьютере в Интернете.

В одном из своих последних сообщений в блоге я поделился решением для запроса смарт-контрактов на компьютере в Интернете в контексте 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.

____

Начните строить на smartcontracts.org и присоединяйтесь к сообществу разработчиков на forum.dfinity.org.