Построение на Flow | Изучение FCL — 14. Как мутировать состояние цепочки, подписывая транзакции закрытым ключом


Предисловие

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

Не бойся, мой друг, ведь именно этому мы тебя и научим. После проработки материалов этой статьи вы будете знать, как:

  • выполнять различные роли для вашей транзакции
  • получить базовое представление о процессе multisig
  • подписывать транзакцию своим закрытым ключом

Шаг 1 — Установка

Добавьте пакеты "@onflow/fcl", elliptic и sha3 в качестве зависимостей проекта.

Шаг 2 — Создание подписывающего устройства

Давайте создадим новый файл и назовем его signer.js. Файл может иметь любое имя, мы просто выбираем что-то с семантическим значением. Наш подписыватель должен иметь 3 функции.

  • Первая будет называться hashMessageHex — она будет использоваться для хэширования сообщения транзакции с помощью алгоритма SHA3. Причина, по которой мы используем SHA3, заключается в том, что мы выбрали его в качестве Hash Algorithm во время создания аккаунта на Testnet Faucet, описанного в одной из предыдущих статей. Сама функция довольно проста. Мы возьмем сообщение о транзакции, специально упакованное и представленное в виде шестнадцатеричной строки, подадим его в метод update, открытый SHA3, а затем вернем результат из метода digest:
const hashMessageHex = (msgHex) => {
  const sha = new SHA3(256);
  sha.update(Buffer.from(msgHex, "hex"));
  return sha.digest();
};
Вход в полноэкранный режим Выход из полноэкранного режима
  • Следующая функция вызовет signWithKey — мы будем использовать ее для подписания нашего хэшированного сообщения транзакции закрытым ключом. Это действительно сложная тема, и, скажем так, вы сэкономите много времени, если просто скопируете эту функцию и займетесь изучением криптовалют позже, mkey? 😅tldr: для создания нашей подписи мы будем использовать алгоритм цифровой подписи с эллиптической кривой:
const signWithKey = (privateKey, msgHex) => {
  const key = curve.keyFromPrivate(Buffer.from(privateKey, "hex"));
  const sig = key.sign(hashMessageHex(msgHex));

  const n = 32;
  const r = sig.r.toArrayLike(Buffer, "be", n);
  const s = sig.s.toArrayLike(Buffer, "be", n);

return Buffer.concat([r, s]).toString("hex");
};
Войти в полноэкранный режим Выйти из полноэкранного режима
  • Последней будет функция signer, которая будет использоваться как Authorization Function — функция, которая производит информацию о пользователе, который собирается подписать, и signing function для использования этой информации для производства подписи.
export const signer = async (account) => {
  // We are hard coding these values here, but you can pass those values from outside as well.
  // For example, you can create curried function: 
  // const signer = (keyId, accountAdddress, pkey) => (account) => {...}
  // and then create multiple signers for different key indices 

  const keyId = Number(0); // always ensure that your keyId is a number not a string
  const accountAddress = "0x5593df7d286bcdb8";
  const pkey =
    "248f1ea7b4a058c39dcc97d91e6a5d0aa7afbc931428561b6efbc7bd0f5e0875";

  // authorization function need to return an account
  return {
    ...account, // bunch of defaults in here, we want to overload some of them though
    tempId: `${accountAddress}-${keyId}`, // tempIds are more of an advanced topic, for 99% of the times where you know the address and keyId you will want it to be a unique string per that address and keyId
    addr: sansPrefix(accountAddress), // the address of the signatory, currently it needs to be without a prefix right now
    keyId // this is the keyId for the accounts registered key that will be used to sign, make extra sure this is a number and not a string

    // This is where magic happens! ✨
    signingFunction: async (signable) => {
      // Singing functions are passed a signable and need to return a composite signature
      // signable.message is a hex string of what needs to be signed.
      const signature = await signWithKey(pkey, signable.message);

      return {
        addr: withPrefix(accountAddress), // needs to be the same as the account.addr but this time with a prefix, eventually they will both be with a prefix
        keyId, // needs to be the same as account.keyId, once again make sure its a number and not a string
        signature // this needs to be a hex string of the signature, where signable.message is the hex value that needs to be signed
      };
    }
  };
};
Вход в полноэкранный режим Выход из полноэкранного режима

✨ Обратите внимание, что signingFunction является асинхронной, что означает, что она может использовать Promises внутри своего тела для получения подписи из расширения или удаленного сервера. Что очень удобно, когда вы хотите обрабатывать плату за газ для ваших пользователей 😉.

Шаг 3 — Настройка FCL

import { config, query, mutate, tx } from "@onflow/fcl";
import { signer } from "./signer"

// Contrary to our wallet signing example, we don't need most of it in our config now
// so we'll get back to simple version
config({
  "accessNode.api": "https://rest-testnet.onflow.org",
  "0xBasic": "0xafabe20e55e9ceb6"
});
Войдите в полноэкранный режим Выход из полноэкранного режима

Шаг 4 — Реализация readCounter

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

const readCounter = async () => {
  const cadence = `
    import Basic from 0xBasic

    pub fun main():UInt{
      return Basic.counter
    }
  `;
  const counter = await query({ cadence });
  console.log({ counter });
};
Вход в полноэкранный режим Выход из полноэкранного режима

Шаг 5 — Реализация shiftCounter

Функция shiftCounter также будет почти идентичной. Единственное отличие заключается в том, что на этот раз мы заполним все роли, используя нашу функцию signer. Кроме того, мы будем записывать в журнал, сколько времени ушло на завершение транзакции, используя методы console.time и console.timeEnd:

const shiftCounter = async (value) => {
  console.log("%cSigning Transaction", `color: teal`);

  // Our Cadence code. Notice the use of alias here
  const cadence = `
    import Basic from 0xBasic

    transaction(shift: UInt8){
      prepare(signer: AuthAccount){
        Basic.incrementCounterBy(shift)
      }
    }
  `;

  // List of arguments
  const args = (arg, t) => [arg(value.toString(), t.UInt8)];
  const proposer = signer;
  const payer = signer;
  const authorizations = [signer];

  // "mutate" method will return us transaction id
  const txId = await mutate({
    cadence,
    args,
    proposer,
    payer,
    authorizations,
    limit: 999
  });

  console.log(`Submitted transaction ${txId} to the network`);
  console.log("%cWaiting for transaction to be sealed...", `color: teal`);

  const label = "Transaction Sealing Time";
  console.time(label);

    // We will use transaction id in order to "subscribe" to it's state change and get the details
  // of the transaction
  const txDetails = await tx(txId).onceSealed();

  console.timeEnd(label);
  return txDetails;
};
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец

Давайте добавим IIFE в конец файла и заполним его методами, которые мы только что определили:

(async () => {
  console.clear();
  await readCounter();

  const txDetails = await shiftCounter(12);
  console.log({ txDetails });

  // we will call "readCounter" function second time to ensure 
  // that value of counter has changed
  await readCounter();
})();
Вход в полноэкранный режим Выход из полноэкранного режима

После того, как пыль осядет, ваша консоль должна иметь аналогичный вывод:

{counter: "655"}
Signing Transaction
Submitted transaction d88e98687dd98f7597aca9afaf3daaba788f644f90003c9b144bfa13440fd9ab to the network 
Waiting for transaction to be sealed...
Transaction Sealing Time: 14749.60000000149ms 
{txDetails: Object}
{counter: "667"}
Вход в полноэкранный режим Выход из полноэкранного режима

В конце концов, это было не так уж и сложно, правда? 😉

До следующего раза! 👋

Ресурсы

  • Полный исходный код — https://codesandbox.io/s/dev-to-14-mutate-with-pkey-d0jsj0
  • Пакет — SHA3 — https://www.npmjs.com/package/sha3
  • Пакет — эллиптический — https://www.npmjs.com/package/elliptic
  • FCL — Функция авторизации — https://docs.onflow.org/fcl/reference/api/#authorization-function

Другие ресурсы, которые могут быть вам полезны:

  • Flow Docs Site — https://docs.onflow.org/ — более подробная информация о блокчейне Flow и о том, как с ним взаимодействовать.
  • Flow Portal — https://flow.com/ — ваша точка входа в Flow
  • FCL JS — https://github.com/onflow/fcl-js — исходный код и возможность внести свой вклад в библиотеку FCL JS
  • Cadence — https://docs.onflow.org/cadence/ — Введение в Cadence
  • Codesandbox — https://codesandbox.io — удивительная браузерная IDE, позволяющая быстро создавать прототипы.

Оцените статью
devanswers.ru
Добавить комментарий