<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>ArteMm.baka `へ´</title><author><name>ArteMm.baka `へ´</name></author><id>https://teletype.in/atom/scamushka</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/scamushka?offset=0"></link><link rel="alternate" type="text/html" href="https://teletype.in/@scamushka?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=scamushka"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/scamushka?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-04-06T18:15:37.995Z</updated><entry><id>scamushka:sui1m</id><link rel="alternate" type="text/html" href="https://teletype.in/@scamushka/sui1m?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=scamushka"></link><title>как мы (не) вынесли sui quest 3 на миллион аккаунтов</title><published>2023-11-17T08:16:04.069Z</published><updated>2023-11-18T14:31:08.380Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img3.teletype.in/files/21/ca/21ca6d4d-9704-42d9-a471-17d750d84627.png"></media:thumbnail><summary type="html">&lt;img src=&quot;https://img3.teletype.in/files/6b/08/6b084a3e-e8fe-4bce-ab81-6da6bf3e59c1.png&quot;&gt;привет, я artemm.baka, а эта статья про нашу недельную историю о sui квесте, которая началась в конце октября. время прочтения 5 минут. gl hf.</summary><content type="html">
  &lt;figure id=&quot;b6CU&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/6b/08/6b084a3e-e8fe-4bce-ab81-6da6bf3e59c1.png&quot; width=&quot;1280&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;9oaa&quot;&gt;привет, я &lt;a href=&quot;https://t.me/midgura&quot; target=&quot;_blank&quot;&gt;artemm.baka&lt;/a&gt;, а эта статья про нашу недельную историю о sui квесте, которая началась в конце октября. время прочтения 5 минут. gl hf.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;h2 id=&quot;5xO4&quot;&gt;Навигация по статье:&lt;/h2&gt;
    &lt;p id=&quot;3VKx&quot;&gt;&lt;strong&gt;1. &lt;a href=&quot;https://teletype.in/@scamushka/sui1m#qwCL&quot; target=&quot;_blank&quot;&gt;начало&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;5kEA&quot;&gt;&lt;strong&gt;2. &lt;a href=&quot;https://teletype.in/@scamushka/sui1m#ix9t&quot; target=&quot;_blank&quot;&gt;билдинг и реверс-инжиниринг&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;RTnP&quot;&gt;&lt;strong&gt;3. &lt;a href=&quot;https://teletype.in/@scamushka/sui1m#I0ia&quot; target=&quot;_blank&quot;&gt;минт квест пассов и первые проблемы&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;oPsp&quot;&gt;&lt;strong&gt;4. &lt;a href=&quot;https://teletype.in/@scamushka/sui1m#NROJ&quot; target=&quot;_blank&quot;&gt;переобувка sui и следующая проблема&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;d9qz&quot;&gt;&lt;strong&gt;5. &lt;a href=&quot;https://teletype.in/@scamushka/sui1m#XbyB&quot; target=&quot;_blank&quot;&gt;итоги и ошибки&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;h2 id=&quot;qwCL&quot;&gt;1. начало&lt;/h2&gt;
  &lt;p id=&quot;Sab3&quot;&gt;изначально о sui я уже успел благополучно забыть, ведь с друзьями мы сливали пачки целестии с нод по низу рынка (о чем мы не жалеем) и в целом занимались другими вещами.&lt;/p&gt;
  &lt;p id=&quot;Yrzt&quot;&gt;однако мой коллега с ds_private &lt;a href=&quot;https://t.me/three_hundred300&quot; target=&quot;_blank&quot;&gt;яна мефе&lt;/a&gt; не забыл и предложил реверсить игрушку, а точнее мега кривой лаунчер worlds beyound.&lt;/p&gt;
  &lt;p id=&quot;9mDk&quot;&gt;для тех кто не знает что за worlds beyound и почему именно он? если кратко, во время quest 3 было много &amp;quot;даппок&amp;quot;, обязательно нужно было сделать 3 из них и набрать суммарно 2500 поинтов.&lt;/p&gt;
  &lt;figure id=&quot;2DiQ&quot; class=&quot;m_custom&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/14/63/1463c323-7c5a-4c74-86ac-721ecb53ef9e.png&quot; width=&quot;1100&quot; /&gt;
    &lt;figcaption&gt;супер дапки&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;wbZR&quot;&gt;мы взяли две гемблинг аппки (suilette и desuiflip) + сам wb. многие в комьюнити также софтили run legends. и там же кстати админы раньше всего заметили читеров. мы же решили эту игру скипнуть.&lt;/p&gt;
  &lt;h2 id=&quot;ix9t&quot;&gt;2. билдинг и реверс-инжиниринг&lt;/h2&gt;
  &lt;p id=&quot;o4pV&quot;&gt;спустя еще два дня собрались мы втроем: я, яна и &lt;a href=&quot;https://t.me/kyo_dev&quot; target=&quot;_blank&quot;&gt;кису&lt;/a&gt; с dd dao, чтобы разделить обязанности и сделать все побыстрее. я занялся играми в wb и минтом квест пасса. яна делал казино аппки. а кису регу на лаунчер wb. и естественно все втроем занимались РеВеРс-ИнЖиНиРиНгОм.&lt;/p&gt;
  &lt;p id=&quot;hDtm&quot;&gt;в лаунчере было несколько игр, как офлайн, так и онлайн. проксифаером чекнули запросы во всех, везде +- одни и те же запросы, а в хедерах &lt;code&gt;jwt&lt;/code&gt; токен и &lt;code&gt;signature&lt;/code&gt;. с первым проблем нет. кстати их &lt;code&gt;refresh&lt;/code&gt; токен после обновления не обнулялся, что забавно. гениальные кодеры на админах.&lt;/p&gt;
  &lt;p id=&quot;WUy9&quot;&gt;а вот со вторым проблемы возникли и мы полезли копаться в коде лаунчера с помощью &lt;a href=&quot;https://github.com/dnSpy/dnSpy&quot; target=&quot;_blank&quot;&gt;dnSpy&lt;/a&gt;, а потом через &lt;a href=&quot;https://www.jetbrains.com/decompiler/&quot; target=&quot;_blank&quot;&gt;dotPeek&lt;/a&gt;. хотя они и не сильно отличаются, но второй оказался лучше в декомпиляции. короче, спасибо за детство, JetBrains.&lt;/p&gt;
  &lt;p id=&quot;KCAx&quot;&gt;так выглядела функция генерации сигнатуры на C#, которую мы нашли, короче просто &lt;code&gt;HMACSHA256&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;q5Gj&quot; data-lang=&quot;clike&quot;&gt;private static string GenerateHash(string content)
{
  using (MemoryStream inputStream = new MemoryStream(Encoding.ASCII.GetBytes(content)))
  {
    byte[] hash = new HMACSHA256(Encoding.ASCII.GetBytes(&amp;quot;S0lBZsuidaidropIQ1ZCQUhEBw==&amp;quot;)).ComputeHash((Stream) inputStream);
    StringBuilder stringBuilder = new StringBuilder(hash.Length * 2);
    foreach (byte num in hash)
      stringBuilder.AppendFormat(&amp;quot;{0:x2}&amp;quot;, (object) num);
    return stringBuilder.ToString();
  }
}&lt;/pre&gt;
  &lt;p id=&quot;nuiE&quot;&gt;а так выглядит теперь на typescript.&lt;/p&gt;
  &lt;pre id=&quot;TOG2&quot; data-lang=&quot;typescript&quot;&gt;const HMAC = &amp;#x27;S0lBZsuidaidropIQ1ZCQUhEBw==&amp;#x27;;

function generateHash(data: object): string {
  return CryptoJS.HmacSHA256(JSON.stringify(data), HMAC).toString();
}&lt;/pre&gt;
  &lt;p id=&quot;18oh&quot;&gt;что за &lt;code&gt;S0lBZsuidaidropIQ1ZCQUhEBw==&lt;/code&gt; думаю понятно (ключ).&lt;/p&gt;
  &lt;p id=&quot;qfyZ&quot;&gt;но что за дата? в поисках ответа на этот вопрос я полез в ассемблер (&lt;a href=&quot;https://hex-rays.com/ida-pro/&quot; target=&quot;_blank&quot;&gt;IDA Pro&lt;/a&gt;), провел там пару часов, научился прикольным штукам и весело &amp;#x60;へ´ провел время. но ответ оказался намного проще.&lt;/p&gt;
  &lt;p id=&quot;eHc9&quot;&gt;спустя время до нас все же дошло, что контент/дата для нужных нам запросов берется из даты этих самых запросов. спасибо C# коду и unity &lt;code&gt;uploadHandler&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;AZod&quot;&gt;кстати, все онлайн игры работали через встроенный вебсокет менеджер в unity, следовательно, девы ничего не контролировали, но мы решили сделать офлайн игры для &amp;quot;уменьшения рисков&amp;quot;, хотя они и давали поменьше поинтов чем онлайн.&lt;/p&gt;
  &lt;p id=&quot;jkE1&quot;&gt;я перенес все нужные и ненужные запросы в софт, просчитал пинги, тайминги, правильный скор и другую хуйню, которую успели сделать недодевы в wb. на красивую обертку для бота (cli, db и т. д.) ушло многовато времени и это стало второй ошибкой в будущем. почему второй, а не первой узнаете soon.&lt;/p&gt;
  &lt;h2 id=&quot;I0ia&quot;&gt;3. минт квест пассов и первые проблемы&lt;/h2&gt;
  &lt;p id=&quot;G7IJ&quot;&gt;пока друзья доделывают регу и гемблинг, я делаю газлесс (спонсорский как на сайте) минт квест пассов.&lt;/p&gt;
  &lt;p id=&quot;thV0&quot;&gt;сделать газ минт - легко, как и в любом move блокчейне (просто &lt;code&gt;.moveCall&lt;/code&gt; и все). сделать спонсорский минт (без газа) - чуточку сложнее. но на самом деле нужно было просто немного посидеть подумать и почитать sui доку.&lt;/p&gt;
  &lt;p id=&quot;4L7J&quot;&gt;со спонсорским минтом 1 аккаунт выходил &lt;strong&gt;0.1$&lt;/strong&gt; (за прокси) + &lt;strong&gt;~0.2$&lt;/strong&gt; в sui на другие активности = &lt;strong&gt;~0.3$&lt;/strong&gt;. с каждого акка получаем 25 sui, итого &lt;u&gt;x50-x100&lt;/u&gt;.&lt;/p&gt;
  &lt;p id=&quot;V6If&quot;&gt;4 ноября наш софт на 2к строк готов, но нас обгоняет другой софтер и регает 140к+ акков в wb - обычные юзеры это замечают, а позже и сами админы.&lt;/p&gt;
  &lt;p id=&quot;fW3h&quot;&gt;клейм поинтов на сайте wb перестает работать, а потом и обязательный линк sui кошелька. раньше, конечно, тоже все супер ужасно работало, но сейчас же фулл упало. плюс админы пишут, что будут перераспределять поинты.&lt;/p&gt;
  &lt;figure id=&quot;M06c&quot; class=&quot;m_custom&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/65/b6/65b6939a-3289-4b9f-9d15-2e7ba6303f79.png&quot; width=&quot;320.40425531914883&quot; /&gt;
    &lt;figcaption&gt;)&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;lfVC&quot;&gt;до сих неизвестно как они будут брить, с учетом того, что админы не смогли сделать ничего нормально. ни бекенд (про RESTful вообще молчу, у них код 200 это ошибка, о чем речь), ни фронтенд.&lt;/p&gt;
  &lt;p id=&quot;XlFz&quot;&gt;кстати, вот так в wb выглядит пейлоад для регистрации.&lt;/p&gt;
  &lt;pre id=&quot;D2D8&quot; data-lang=&quot;javascript&quot;&gt;{
  username: &amp;#x27;username&amp;#x27;,
  email: &amp;#x27;email&amp;#x27;,
  name: &amp;#x27;name&amp;#x27;,
  password: &amp;#x27;password&amp;#x27;,
  confirmPassword: &amp;#x27;password&amp;#x27;,
  dateOfBirth: &amp;#x27;dateOfBirth&amp;#x27;
}&lt;/pre&gt;
  &lt;section style=&quot;background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;GkVy&quot;&gt;&lt;code&gt;confirmPassword&lt;/code&gt; - интересное и совсем не ненужное поле. и таких приколов у них очень много.&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;z5hz&quot;&gt;предполагаю, будет бритва либо по самым банальным критериям, либо фулл несправедливые баны даже не сибилов.&lt;/p&gt;
  &lt;h2 id=&quot;NROJ&quot;&gt;4. переобувка sui и следующая проблема&lt;/h2&gt;
  &lt;p id=&quot;zJoZ&quot;&gt;вскоре апи у суи загадочно &amp;quot;падает&amp;quot; и перестает работать спонсорский минт квест пасса, какое совпадение да? на самом деле, sui сами его отключили. ведь токены на газ на их кошельке не закончились, да и до этого их сервер прекрасно справлялся с подобными нагрузками.&lt;/p&gt;
  &lt;p id=&quot;9Xdd&quot;&gt;конечно, можно было минтить пассы с газом. но зачем делать большое кол-во акков неправильно, если можно правильно, верно? вот и мы так подумали.&lt;/p&gt;
  &lt;p id=&quot;2sX5&quot;&gt;но через несколько дней sui анонсят, что больше квест пассов не будет, &lt;em&gt;&amp;quot;минтите наши другие (платные) нфт&amp;quot;&lt;/em&gt;. премного благодарю, sui team.&lt;/p&gt;
  &lt;figure id=&quot;3su8&quot; class=&quot;m_custom&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/76/70/76701720-87d2-4c80-b6be-c52dc7d65feb.gif&quot; width=&quot;357&quot; /&gt;
    &lt;figcaption&gt;тут машина хотя бы едет, в отличие от sui игр, добавьте взрыв в конце для реалистичности, не знаю&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;J6tn&quot;&gt;и чуть позже суи еще убирает лимиты по рефам у &amp;quot;настоящих пользователей&amp;quot;. интересно зачем, да?&lt;/p&gt;
  &lt;figure id=&quot;h2Is&quot; class=&quot;m_custom&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/ff/d1/ffd1cb15-2f74-4d1c-a7c9-d292e351d2f4.png&quot; width=&quot;799&quot; /&gt;
    &lt;figcaption&gt;лучшие&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;h2 id=&quot;XbyB&quot;&gt;5. итоги и ошибки&lt;/h2&gt;
  &lt;p id=&quot;k0Ij&quot;&gt;что мы имеем? гениальных девов в sui играх с серверами за 5€ на contabo. wb лаунчер, который собирает бесконечность данных с твоего пк и кривым бекендом &amp;quot;работающем&amp;quot; в 1 из 100 случаев. и, конечно же, прекрасную команду sui.&lt;/p&gt;
  &lt;p id=&quot;hyRR&quot;&gt;наши ошибки:&lt;/p&gt;
  &lt;ol id=&quot;IHeS&quot;&gt;
    &lt;li id=&quot;XTnm&quot;&gt;поздно начали, но на то была уважительная причина (просто оправдание)&lt;/li&gt;
    &lt;li id=&quot;OkDm&quot;&gt;можно было ускорить разработку не делая лишние действия (то есть без перфекционизма)&lt;/li&gt;
    &lt;li id=&quot;pysD&quot;&gt;минт квест пассов, регу и линк в wb стоило сделать первыми или делегировать кому-то еще&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;XozH&quot;&gt;да мы не сделали миллион аккаунтов, но получили интересный опыт и пару синяков под глазами.&lt;/p&gt;
  &lt;figure id=&quot;ATMP&quot; class=&quot;m_custom&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/76/c3/76c33110-3832-4b21-bacc-a021d8f9b880.png&quot; width=&quot;340.0000000000001&quot; /&gt;
    &lt;figcaption&gt;да немного кликбейт уж прости&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;I7Gy&quot;&gt;спасибо, что читал, люблю! ❤️&lt;/p&gt;

</content></entry><entry><id>scamushka:three_nft_collections</id><link rel="alternate" type="text/html" href="https://teletype.in/@scamushka/three_nft_collections?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=scamushka"></link><title>Три всадника апокалипсиса: создание NFT коллекций на Aptos, Sui и Solana</title><published>2022-09-17T16:07:14.000Z</published><updated>2022-09-20T10:57:03.013Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/1a/a0/1aa0dd2f-09c9-475f-8f22-2c2d6e884722.png"></media:thumbnail><summary type="html">&lt;img src=&quot;https://img4.teletype.in/files/f7/97/f79779dc-b5f0-4951-ae95-666099fa76f9.jpeg&quot;&gt;Всем привет! С вами ArteMm aka Скамушка ツ</summary><content type="html">
  &lt;figure id=&quot;vgyy&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/f7/97/f79779dc-b5f0-4951-ae95-666099fa76f9.jpeg&quot; width=&quot;1280&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;4Xdt&quot;&gt;&lt;strong&gt;Всем привет!&lt;/strong&gt; С вами ArteMm aka &lt;a href=&quot;https://t.me/scamushka&quot; target=&quot;_blank&quot;&gt;Скамушка&lt;/a&gt; ツ&lt;/p&gt;
  &lt;p id=&quot;ADHc&quot;&gt;В этой статье мы создадим простые NFT коллекции на Aptos, Sui и Solana. Но стоит понимать, что первые два блокчейна ещё активно разрабатываются и следовательно этот туториал интересен только для ознакомления, т. к. некоторые вещи могут в любой момент поменяться.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;h2 id=&quot;eJZ5&quot;&gt;Навигация по статье:&lt;/h2&gt;
    &lt;p id=&quot;GzJh&quot;&gt;&lt;strong&gt;1. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#WbPL&quot; target=&quot;_blank&quot;&gt;Aptos&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;ul id=&quot;nAtj&quot;&gt;
      &lt;li id=&quot;1Nwe&quot;&gt;&lt;strong&gt;1.1. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#JhCI&quot; target=&quot;_blank&quot;&gt;Подготовка&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;XxAe&quot;&gt;&lt;strong&gt;1.2. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#RMhi&quot; target=&quot;_blank&quot;&gt;Инициализация клиентов&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;SgW0&quot;&gt;&lt;strong&gt;1.3. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#lhmJ&quot; target=&quot;_blank&quot;&gt;Создание локальных аккаунтов&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;YkgP&quot;&gt;&lt;strong&gt;1.4. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#whEV&quot; target=&quot;_blank&quot;&gt;Создание блокчейн аккаунтов&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;jS4T&quot;&gt;&lt;strong&gt;1.5. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#tDtc&quot; target=&quot;_blank&quot;&gt;Создание коллекции&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;LuR9&quot;&gt;&lt;strong&gt;1.6. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#pkwA&quot; target=&quot;_blank&quot;&gt;Создание токена&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;SVxG&quot;&gt;&lt;strong&gt;1.7. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#t0kd&quot; target=&quot;_blank&quot;&gt;Чтение метаданных токена и коллекции&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;8Glp&quot;&gt;&lt;strong&gt;1.8. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#Qn7z&quot; target=&quot;_blank&quot;&gt;Чтение баланса токенов&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;E8l5&quot;&gt;&lt;strong&gt;1.9. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#B4w4&quot; target=&quot;_blank&quot;&gt;Предложение и получение токена&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;a7R8&quot;&gt;&lt;strong&gt;1.10. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#atkR&quot; target=&quot;_blank&quot;&gt;Безопасная односторонняя передача токена&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p id=&quot;Ke1y&quot;&gt;&lt;strong&gt;2. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#9Noo&quot; target=&quot;_blank&quot;&gt;Sui&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;ul id=&quot;XDOl&quot;&gt;
      &lt;li id=&quot;clyR&quot;&gt;&lt;strong&gt;2.1. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#0P0U&quot; target=&quot;_blank&quot;&gt;Подготовка&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;UMGc&quot;&gt;&lt;strong&gt;2.2. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#bpGl&quot; target=&quot;_blank&quot;&gt;Реализация с помощью Sui CLI&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;gaFx&quot;&gt;&lt;strong&gt;2.3. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#IwFP&quot; target=&quot;_blank&quot;&gt;Реализация с помощью смарт-контракта&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p id=&quot;kOZG&quot;&gt;&lt;strong&gt;3. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#MkKI&quot; target=&quot;_blank&quot;&gt;Solana&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;ul id=&quot;5ZxT&quot;&gt;
      &lt;li id=&quot;nr2L&quot;&gt;&lt;strong&gt;3.1. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#wep7&quot; target=&quot;_blank&quot;&gt;Подготовка&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;EZ7U&quot;&gt;&lt;strong&gt;3.2. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#IftP&quot; target=&quot;_blank&quot;&gt;Настройка проекта&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;44Hb&quot;&gt;&lt;strong&gt;3.3. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#2SX9&quot; target=&quot;_blank&quot;&gt;Создание конфига и публикация CM2&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
      &lt;li id=&quot;lGLv&quot;&gt;&lt;strong&gt;3.4. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#tGVl&quot; target=&quot;_blank&quot;&gt;Минт NFT&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p id=&quot;eWGf&quot;&gt;&lt;strong&gt;4. &lt;a href=&quot;https://teletype.in/@scamushka/three_nft_collections#JWMa&quot; target=&quot;_blank&quot;&gt;Справочник&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;h2 id=&quot;WbPL&quot; data-align=&quot;center&quot;&gt;1. Aptos&lt;/h2&gt;
  &lt;h3 id=&quot;JhCI&quot; data-align=&quot;center&quot;&gt;1.1. Подготовка&lt;/h3&gt;
  &lt;p id=&quot;mpSS&quot;&gt;Убедитесь, что у вас установлен &lt;a href=&quot;https://nodejs.org/&quot; target=&quot;_blank&quot;&gt;Node.js&lt;/a&gt; (&amp;gt;=16.10) и включен Corepack.&lt;/p&gt;
  &lt;pre id=&quot;rmcP&quot; data-lang=&quot;bash&quot;&gt;corepack enable&lt;/pre&gt;
  &lt;p id=&quot;SoTV&quot;&gt;Установите зависимости.&lt;/p&gt;
  &lt;pre id=&quot;LhBl&quot; data-lang=&quot;bash&quot;&gt;yarn add aptos dotenv &amp;amp;&amp;amp; yarn add ts-node typescript @types/node -D&lt;/pre&gt;
  &lt;h3 id=&quot;RMhi&quot; data-align=&quot;center&quot;&gt;1.2. Инициализация клиентов&lt;/h3&gt;
  &lt;p id=&quot;iGC9&quot;&gt;На первом этапе в примере инициализируются &lt;code&gt;API&lt;/code&gt; и &lt;code&gt;faucet&lt;/code&gt; (кран) клиенты.&lt;/p&gt;
  &lt;ul id=&quot;rOBU&quot;&gt;
    &lt;li id=&quot;rVMs&quot;&gt;Клиент API взаимодействует с REST API&lt;/li&gt;
    &lt;li id=&quot;VUqD&quot;&gt;Клиент faucet взаимодействует с сервисом devnet Faucet для создания и пополнения аккаунтов.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;Gr31&quot;&gt;Создайте файл &lt;code&gt;index.ts&lt;/code&gt; и добавьте импорты:&lt;/p&gt;
  &lt;pre id=&quot;6VTr&quot; data-lang=&quot;typescript&quot;&gt;import dotenv from &amp;#x27;dotenv&amp;#x27;;

import {
  AptosClient, AptosAccount, FaucetClient, TokenClient,
} from &amp;#x27;aptos&amp;#x27;;
import { NODE_URL, FAUCET_URL } from &amp;#x27;./common&amp;#x27;;

dotenv.config();&lt;/pre&gt;
  &lt;p id=&quot;MV9W&quot;&gt;А потом:&lt;/p&gt;
  &lt;pre id=&quot;MV9W&quot; data-lang=&quot;typescript&quot;&gt;(async () =&amp;gt; {
  const client = new AptosClient(NODE_URL);
  const faucetClient = new FaucetClient(NODE_URL, FAUCET_URL);

  const tokenClient = new TokenClient(client);&lt;/pre&gt;
  &lt;p id=&quot;386D&quot;&gt;Используя клиент API, мы можем создать &lt;code&gt;TokenClient&lt;/code&gt;, который мы используем для общих операций с токенами, таких как создание коллекций и токенов, их передача, получение и т. д.&lt;/p&gt;
  &lt;pre id=&quot;tNfW&quot; data-lang=&quot;typescript&quot;&gt;  const alice = new AptosAccount();
  const bob = new AptosAccount();&lt;/pre&gt;
  &lt;p id=&quot;9E3p&quot;&gt;В &lt;code&gt;common.ts&lt;/code&gt; инициализируйте значения URL:&lt;/p&gt;
  &lt;pre id=&quot;4wxm&quot; data-lang=&quot;typescript&quot;&gt;export const NODE_URL = process.env.APTOS_NODE_URL || &amp;#x27;https://fullnode.devnet.aptoslabs.com&amp;#x27;;
export const FAUCET_URL = process.env.APTOS_FAUCET_URL || &amp;#x27;https://faucet.devnet.aptoslabs.com&amp;#x27;;&lt;/pre&gt;
  &lt;section style=&quot;background-color:hsl(hsl(199, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;7ciP&quot;&gt;По умолчанию URL-адреса обеих служб указывают на службы Aptos devnet. Однако их можно настроить с помощью следующих переменных окружения: &lt;code&gt;APTOS_NODE_URL&lt;/code&gt;, &lt;code&gt;APTOS_FAUCET_URL&lt;/code&gt;.&lt;/p&gt;
  &lt;/section&gt;
  &lt;h3 id=&quot;lhmJ&quot; data-align=&quot;center&quot;&gt;1.3. Создание локальных аккаунтов&lt;/h3&gt;
  &lt;p id=&quot;097q&quot;&gt;Следующим шагом будет создание двух аккаунтов локально. &lt;a href=&quot;https://aptos.dev/concepts/basics-accounts/&quot; target=&quot;_blank&quot;&gt;Аккаунты&lt;/a&gt; представляют собой как on-chain, так и off-chain состояние. Оффчейн состояние состоит из адреса и пары открытого и закрытого ключей, используемых для аутентификации владельца. Этот шаг демонстрирует, как создать это оффчейн состояние (продолжаем в &lt;code&gt;index.ts&lt;/code&gt;).&lt;/p&gt;
  &lt;pre id=&quot;998U&quot; data-lang=&quot;typescript&quot;&gt;  const alice = new AptosAccount();
  const bob = new AptosAccount();&lt;/pre&gt;
  &lt;h3 id=&quot;whEV&quot; data-align=&quot;center&quot;&gt;1.4. Создание блокчейн аккаунтов&lt;/h3&gt;
  &lt;p id=&quot;Tdgi&quot;&gt;В Aptos каждый аккаунт должен иметь ончейн представление, чтобы поддерживать получение токенов и монет, а также взаимодействие с другими dApps. Аккаунт представляет собой средство для хранения активов, поэтому он должен быть явно создан. В данном примере кран используется для создания аккаунтов Алисы и Боба. Финансируется только Алиса:&lt;/p&gt;
  &lt;pre id=&quot;RKMS&quot; data-lang=&quot;typescript&quot;&gt;  await faucetClient.fundAccount(alice.address(), 20_000);
  await faucetClient.fundAccount(bob.address(), 20_000);&lt;/pre&gt;
  &lt;h3 id=&quot;tDtc&quot; data-align=&quot;center&quot;&gt;1.5. Создание коллекции&lt;/h3&gt;
  &lt;p id=&quot;XXjx&quot;&gt;Теперь начинается процесс создания токенов. Во-первых, создатель должен создать коллекцию для хранения токенов. Коллекция может содержать ноль, один или много различных токенов. Коллекция не ограничивает атрибуты токенов, так как это всего лишь контейнер.&lt;/p&gt;
  &lt;p id=&quot;jl9a&quot;&gt;Ваше приложение будет вызывать &lt;code&gt;createCollection&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;lGCA&quot; data-lang=&quot;typescript&quot;&gt;  const collectionName = &amp;quot;Alice&amp;#x27;s&amp;quot;;
  const tokenName = &amp;quot;Alice&amp;#x27;s first token&amp;quot;;
  const tokenPropertyVersion = 0;

  const txnHash1 = await tokenClient.createCollection(
    alice,
    collectionName,
    &amp;quot;Alice&amp;#x27;s simple collection&amp;quot;,
    &amp;#x27;https://alice.com&amp;#x27;,
  );
  await client.waitForTransaction(txnHash1, { checkSuccess: true });&lt;/pre&gt;
  &lt;p id=&quot;l6y5&quot;&gt;Сигнатура метода &lt;code&gt;createCollection&lt;/code&gt;. Он возвращает хэш транзакции:&lt;/p&gt;
  &lt;pre id=&quot;AYd0&quot; data-lang=&quot;typescript&quot;&gt;async createCollection(
  account: AptosAccount,
  name: string,
  description: string,
  uri: string,
  maxAmount: BCS.AnyNumber = MAX_U64_BIG_INT,
): Promise&amp;lt;string&amp;gt; {&lt;/pre&gt;
  &lt;h3 id=&quot;pkwA&quot; data-align=&quot;center&quot;&gt;1.6. Создание токена&lt;/h3&gt;
  &lt;p id=&quot;hSiZ&quot;&gt;Ваше приложение будет вызывать &lt;code&gt;createToken&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;cY0B&quot; data-lang=&quot;typescript&quot;&gt;  const txnHash2 = await tokenClient.createToken(
    alice,
    collectionName,
    tokenName,
    &amp;quot;Alice&amp;#x27;s simple token&amp;quot;,
    1,
    &amp;#x27;https://aptos.dev/img/nyan.jpeg&amp;#x27;,
  );
  await client.waitForTransaction(txnHash2, { checkSuccess: true });&lt;/pre&gt;
  &lt;p id=&quot;fTL4&quot;&gt;Сигнатура метода &lt;code&gt;createToken&lt;/code&gt;. Он возвращает хэш транзакции:&lt;/p&gt;
  &lt;pre id=&quot;Uzlb&quot; data-lang=&quot;typescript&quot;&gt;async createToken(
  account: AptosAccount,
  collectionName: string,
  name: string,
  description: string,
  supply: number,
  uri: string,
  max: BCS.AnyNumber = MAX_U64_BIG_INT,
  royalty_payee_address: MaybeHexString = account.address(),
  royalty_points_denominator: number = 0,
  royalty_points_numerator: number = 0,
  property_keys: Array&amp;lt;string&amp;gt; = [],
  property_values: Array&amp;lt;string&amp;gt; = [],
  property_types: Array&amp;lt;string&amp;gt; = [],
): Promise&amp;lt;string&amp;gt; {&lt;/pre&gt;
  &lt;h3 id=&quot;t0kd&quot; data-align=&quot;center&quot;&gt;1.7. Чтение метаданных токена и коллекции&lt;/h3&gt;
  &lt;p id=&quot;phru&quot;&gt;Метаданные коллекции и токена хранятся на аккаунте создателя в его &lt;code&gt;Collections&lt;/code&gt; в таблице. SDKs предоставляют удобные обертки для запросов к этим конкретным таблицам.&lt;/p&gt;
  &lt;p id=&quot;sFbS&quot;&gt;Чтобы прочитать метаданные коллекции:&lt;/p&gt;
  &lt;pre id=&quot;WP9S&quot; data-lang=&quot;typescript&quot;&gt;  const collectionData = await tokenClient.getCollectionData(alice.address(), collectionName);
  console.log(&amp;#x60;Alice&amp;#x27;s collection: ${JSON.stringify(collectionData, null, 4)}&amp;#x60;);&lt;/pre&gt;
  &lt;p id=&quot;G8K3&quot;&gt;Чтобы прочитать метаданные токена:&lt;/p&gt;
  &lt;pre id=&quot;k4gk&quot; data-lang=&quot;typescript&quot;&gt;  const tokenData = await tokenClient.getTokenData(alice.address(), collectionName, tokenName);
  console.log(&amp;#x60;Alice&amp;#x27;s token data: ${JSON.stringify(tokenData, null, 4)}&amp;#x60;);&lt;/pre&gt;
  &lt;p id=&quot;A6T6&quot;&gt;Вот как &lt;code&gt;getTokenData&lt;/code&gt; запрашивает метаданные токена:&lt;/p&gt;
  &lt;pre id=&quot;zW6z&quot; data-lang=&quot;typescript&quot;&gt;async getTokenData(
  creator: MaybeHexString,
  collectionName: string,
  tokenName: string,
): Promise&amp;lt;TokenTypes.TokenData&amp;gt; {
  const creatorHex = creator instanceof HexString ? creator.hex() : creator;
  const collection: { type: Gen.MoveStructTag; data: any } = await this.aptosClient.getAccountResource(
    creatorHex,
    &amp;quot;0x3::token::Collections&amp;quot;,
  );
  const { handle } = collection.data.token_data;
  const tokenDataId = {
    creator: creatorHex,
    collection: collectionName,
    name: tokenName,
  };

  const getTokenTableItemRequest: Gen.TableItemRequest = {
    key_type: &amp;quot;0x3::token::TokenDataId&amp;quot;,
    value_type: &amp;quot;0x3::token::TokenData&amp;quot;,
    key: tokenDataId,
  };

  // Мы знаем, что ответом будет структура, содержащая TokenData, отсюда и неявное приведение.
  return this.aptosClient.getTableItem(handle, getTokenTableItemRequest);
}&lt;/pre&gt;
  &lt;h3 id=&quot;Qn7z&quot; data-align=&quot;center&quot;&gt;1.8. Чтение баланса токенов&lt;/h3&gt;
  &lt;p id=&quot;hzfx&quot;&gt;Каждый токен в Aptos является отдельным активом, активы, принадлежащие пользователю, хранятся в его &lt;code&gt;TokenStore&lt;/code&gt;. Чтобы получить баланс:&lt;/p&gt;
  &lt;pre id=&quot;CDtT&quot; data-lang=&quot;typescript&quot;&gt;  const aliceBalance1 = await tokenClient.getToken(
    alice.address(),
    collectionName,
    tokenName,
    &amp;#x60;${tokenPropertyVersion}&amp;#x60;,
  );
  console.log(&amp;#x60;Alice&amp;#x27;s token balance: ${aliceBalance1.amount}&amp;#x60;);&lt;/pre&gt;
  &lt;h3 id=&quot;B4w4&quot; data-align=&quot;center&quot;&gt;1.9. Предложение и получение токена&lt;/h3&gt;
  &lt;p id=&quot;Blxh&quot;&gt;Многие пользователи получили нежелательные токены, которые могут вызвать как минимальный дискомфорт, так и серьезные последствия. Aptos дает право каждому владельцу аккаунта решать, принимать или не принимать односторонние переводы. По умолчанию односторонние переводы не поддерживаются. Таким образом, Aptos обеспечивает основу для &lt;em&gt;предложения&lt;/em&gt; и &lt;em&gt;клейма&lt;/em&gt; токенов.&lt;/p&gt;
  &lt;p id=&quot;8KGV&quot;&gt;Чтобы предложить (отправить) токен:&lt;/p&gt;
  &lt;pre id=&quot;DO39&quot; data-lang=&quot;typescript&quot;&gt;  const txnHash3 = await tokenClient.offerToken(
    alice,
    bob.address(),
    alice.address(),
    collectionName,
    tokenName,
    1,
    tokenPropertyVersion,
  );
  await client.waitForTransaction(txnHash3, { checkSuccess: true });&lt;/pre&gt;
  &lt;p id=&quot;qXij&quot;&gt;Чтобы получить токен:&lt;/p&gt;
  &lt;pre id=&quot;vC8P&quot; data-lang=&quot;typescript&quot;&gt;  const txnHash4 = await tokenClient.claimToken(
    bob,
    alice.address(),
    alice.address(),
    collectionName,
    tokenName,
    tokenPropertyVersion,
  );
  await client.waitForTransaction(txnHash4, { checkSuccess: true });&lt;/pre&gt;
  &lt;h3 id=&quot;atkR&quot; data-align=&quot;center&quot;&gt;1.10. Безопасная односторонняя передача токена&lt;/h3&gt;
  &lt;p id=&quot;o04F&quot;&gt;Чтобы обеспечить безопасную одностороннюю передачу токена, отправитель может сначала попросить получателя подтвердить оффчейн о предстоящей передаче. Это происходит в форме запроса мультиагентной транзакции. Мультиагентные транзакции содержат несколько подписей, по одной для каждого ончейн аккаунта. Затем Move может использовать это для предоставления разрешений на уровне подписи всем подписантам. Для передачи токенов это гарантирует, что принимающая сторона действительно желает получить этот токен, не требуя использования описанной выше структуры передачи токенов.&lt;/p&gt;
  &lt;pre id=&quot;4w1b&quot; data-lang=&quot;typescript&quot;&gt;  const txnHash5 = await tokenClient.directTransferToken(
    bob,
    alice,
    alice.address(),
    collectionName,
    tokenName,
    1,
    tokenPropertyVersion,
  );
  await client.waitForTransaction(txnHash5, { checkSuccess: true });
})();&lt;/pre&gt;
  &lt;p id=&quot;DT6n&quot;&gt;Теперь пробуем запустить:&lt;/p&gt;
  &lt;pre id=&quot;5DLc&quot; data-lang=&quot;bash&quot;&gt;npx ts-node index&lt;/pre&gt;
  &lt;p id=&quot;vsfb&quot;&gt;Получаем вывод:&lt;/p&gt;
  &lt;pre id=&quot;ocCT&quot; data-lang=&quot;typescript&quot;&gt;Alice&amp;#x27;s collection: {
    &amp;quot;description&amp;quot;: &amp;quot;Alice&amp;#x27;s simple collection&amp;quot;,
    &amp;quot;maximum&amp;quot;: &amp;quot;18446744073709551615&amp;quot;,
    &amp;quot;mutability_config&amp;quot;: {
        &amp;quot;description&amp;quot;: false,
        &amp;quot;maximum&amp;quot;: false,
        &amp;quot;uri&amp;quot;: false
    },
    &amp;quot;name&amp;quot;: &amp;quot;Alice&amp;#x27;s&amp;quot;,
    &amp;quot;supply&amp;quot;: &amp;quot;1&amp;quot;,
    &amp;quot;uri&amp;quot;: &amp;quot;https://alice.com&amp;quot;
}
Alice&amp;#x27;s token data: {
    &amp;quot;default_properties&amp;quot;: {
        &amp;quot;map&amp;quot;: {
            &amp;quot;data&amp;quot;: []
        }
    },
    &amp;quot;description&amp;quot;: &amp;quot;Alice&amp;#x27;s simple token&amp;quot;,
    &amp;quot;largest_property_version&amp;quot;: &amp;quot;0&amp;quot;,
    &amp;quot;maximum&amp;quot;: &amp;quot;18446744073709551615&amp;quot;,
    &amp;quot;mutability_config&amp;quot;: {
        &amp;quot;description&amp;quot;: false,
        &amp;quot;maximum&amp;quot;: false,
        &amp;quot;properties&amp;quot;: false,
        &amp;quot;royalty&amp;quot;: false,
        &amp;quot;uri&amp;quot;: false
    },
    &amp;quot;name&amp;quot;: &amp;quot;Alice&amp;#x27;s first token&amp;quot;,
    &amp;quot;royalty&amp;quot;: {
        &amp;quot;payee_address&amp;quot;: &amp;quot;0x3bb244826f49ac293aff101564c462c8e2046912dc8f581db655e2dbb5bffee6&amp;quot;,
        &amp;quot;royalty_points_denominator&amp;quot;: &amp;quot;0&amp;quot;,
        &amp;quot;royalty_points_numerator&amp;quot;: &amp;quot;0&amp;quot;
    },
    &amp;quot;supply&amp;quot;: &amp;quot;1&amp;quot;,
    &amp;quot;uri&amp;quot;: &amp;quot;https://aptos.dev/img/nyan.jpeg&amp;quot;
}
Alice&amp;#x27;s token balance: 1&lt;/pre&gt;
  &lt;section style=&quot;background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;zdTO&quot;&gt;Готово! Достаточно просто. Полный код примера можно найти на &lt;a href=&quot;https://github.com/aptos-labs/aptos-core/blob/main/ecosystem/typescript/sdk/examples/typescript/simple_nft.ts&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt;. А &lt;a href=&quot;https://aptos.dev/tutorials/your-first-nft/&quot; target=&quot;_blank&quot;&gt;тут&lt;/a&gt; можно увидеть оригинал туториала.&lt;/p&gt;
  &lt;/section&gt;
  &lt;h2 id=&quot;9Noo&quot; data-align=&quot;center&quot;&gt;2. Sui&lt;/h2&gt;
  &lt;p id=&quot;mmwB&quot;&gt;В Sui всё представляет из себя NFT — объекты уникальны, не взаимозаменяемы и принадлежат друг другу. Так что технически достаточно простой публикации. Я решил разобрать два способа создания NFT: простой с помощью &lt;a href=&quot;https://github.com/MystenLabs/sui/tree/main/sui_programmability/examples/nfts&quot; target=&quot;_blank&quot;&gt;Sui CLI&lt;/a&gt; и более гибкий с помощью смарт-контракта на языке Move.&lt;/p&gt;
  &lt;h3 id=&quot;0P0U&quot; data-align=&quot;center&quot;&gt;2.1. Подготовка&lt;/h3&gt;
  &lt;p id=&quot;oWwe&quot;&gt;Установите &lt;a href=&quot;https://docs.sui.io/devnet/build/install&quot; target=&quot;_blank&quot;&gt;Sui&lt;/a&gt; и &lt;a href=&quot;https://git-scm.com/download/&quot; target=&quot;_blank&quot;&gt;Git&lt;/a&gt;.&lt;/p&gt;
  &lt;h3 id=&quot;bpGl&quot; data-align=&quot;center&quot;&gt;2.2. Реализация с помощью Sui CLI&lt;/h3&gt;
  &lt;p id=&quot;T5qs&quot;&gt;Вы можете создать &lt;a href=&quot;https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/devnet_nft.move#L16&quot; target=&quot;_blank&quot;&gt;NFT-подобный объект&lt;/a&gt; на Sui, используя следующую команду:&lt;/p&gt;
  &lt;pre id=&quot;2q78&quot; data-lang=&quot;bash&quot;&gt;sui client create-example-nft&lt;/pre&gt;
  &lt;p id=&quot;cmhF&quot;&gt;Вы увидите вывод, похожий на:&lt;/p&gt;
  &lt;pre id=&quot;eRf7&quot; data-lang=&quot;bash&quot;&gt;Successfully created an ExampleNFT:

----- Move Object (0x524f9fae3ca4554e01354415daf58a05e5bf26ac[1]) -----
Owner: Account Address ( 0xb02b5e57fe3572f94ad5ac2a17392bfb3261f7a0 )
Version: 1
Storage Rebate: 25
Previous Transaction: 98HbDxEwEUknQiJzyWM8AiYIM479BEKuGwxrZOGtAwk=
----- Data -----
type: 0x2::devnet_nft::DevNetNFT
description: An NFT created by the Sui Command Line Tool
id: 0x524f9fae3ca4554e01354415daf58a05e5bf26ac[1]
name: Example NFT
url: ipfs://bafkreibngqhl3gaa7daob4i2vccziay2jjlp435cf66vhono7nrvww53ty&lt;/pre&gt;
  &lt;p id=&quot;1DHt&quot;&gt;А вот так выглядит контракт DevNetNFT:&lt;/p&gt;
  &lt;pre id=&quot;Xbjj&quot; data-lang=&quot;rust&quot;&gt;module sui::devnet_nft {
    use sui::url::{Self, Url};
    use std::string;
    use sui::object::{Self, ID, UID};
    use sui::event;
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    /// Example NFT, который может заминтить кто угодно
    struct DevNetNFT has key, store {
        id: UID,
        /// Имя токена
        name: string::String,
        /// Описание токена
        description: string::String,
        /// URL токена
        url: Url,
        // Можно добавить кастомные аттрибуты
    }

    struct MintNFTEvent has copy, drop {
        // Идентификатор объекта NFT
        object_id: ID,
        // Создатель NFT
        creator: address,
        // Название NFT
        name: string::String,
    }

    /// Создать новый devnet_nft
    public entry fun mint(
        name: vector&amp;lt;u8&amp;gt;,
        description: vector&amp;lt;u8&amp;gt;,
        url: vector&amp;lt;u8&amp;gt;,
        ctx: &amp;amp;mut TxContext
    ) {
        let nft = DevNetNFT {
            id: object::new(ctx),
            name: string::utf8(name),
            description: string::utf8(description),
            url: url::new_unsafe_from_bytes(url)
        };
        let sender = tx_context::sender(ctx);
        event::emit(MintNFTEvent {
            object_id: object::uid_to_inner(&amp;amp;nft.id),
            creator: sender,
            name: nft.name,
        });
        transfer::transfer(nft, sender);
    }

    /// Обновить &amp;#x60;description&amp;#x60; у &amp;#x60;nft&amp;#x60; на &amp;#x60;new_description&amp;#x60;
    public entry fun update_description(
        nft: &amp;amp;mut DevNetNFT,
        new_description: vector&amp;lt;u8&amp;gt;,
        _: &amp;amp;mut TxContext
    ) {
        nft.description = string::utf8(new_description)
    }

    /// Навсегда удалить &amp;#x60;nft&amp;#x60;
    public entry fun burn(nft: DevNetNFT, _: &amp;amp;mut TxContext) {
        let DevNetNFT { id, name: _, description: _, url: _ } = nft;
        object::delete(id)
    }

    /// Получить NFT&amp;#x27;s &amp;#x60;name&amp;#x60;
    public fun name(nft: &amp;amp;DevNetNFT): &amp;amp;string::String {
        &amp;amp;nft.name
    }

    /// Получить NFT&amp;#x27;s &amp;#x60;description&amp;#x60;
    public fun description(nft: &amp;amp;DevNetNFT): &amp;amp;string::String {
        &amp;amp;nft.description
    }

    /// Получить NFT&amp;#x27;s &amp;#x60;url&amp;#x60;
    public fun url(nft: &amp;amp;DevNetNFT): &amp;amp;Url {
        &amp;amp;nft.url
    }
}&lt;/pre&gt;
  &lt;h3 id=&quot;IwFP&quot; data-align=&quot;center&quot;&gt;2.3. Реализация с помощью смарт-контракта&lt;/h3&gt;
  &lt;p id=&quot;aHmQ&quot;&gt;Теперь же создадим простую и очень проблемную коллекцию. За основу я взял контракт &lt;a href=&quot;https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/devnet_nft.move&quot; target=&quot;_blank&quot;&gt;DevNetNFT&lt;/a&gt; и &lt;a href=&quot;https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/nfts/sources/collection.move&quot; target=&quot;_blank&quot;&gt;пример коллекции&lt;/a&gt;. Также упомяну, что мои знания в языке Move нулевые, спасало только то, что это Rust-подобный язык.&lt;/p&gt;
  &lt;p id=&quot;Yv90&quot;&gt;Сначала создайте пустой пакет Move:&lt;/p&gt;
  &lt;pre id=&quot;xCNL&quot; data-lang=&quot;bash&quot;&gt;sui move new my_nfts&lt;/pre&gt;
  &lt;p id=&quot;Mvce&quot;&gt;Клонируйте репозиторий Sui:&lt;/p&gt;
  &lt;pre id=&quot;xL6V&quot; data-lang=&quot;bash&quot;&gt;git clone https://github.com/MystenLabs/sui.git&lt;/pre&gt;
  &lt;p id=&quot;mjuR&quot;&gt;Обновите &lt;code&gt;[dependencies]&lt;/code&gt; в файле &lt;code&gt;Move.toml&lt;/code&gt; вот так:&lt;/p&gt;
  &lt;pre id=&quot;NQPI&quot; data-lang=&quot;toml&quot;&gt;[package]
name = &amp;quot;my_nfts&amp;quot;
version = &amp;quot;0.0.1&amp;quot;

[dependencies]
Sui = { local = &amp;quot;../sui/crates/sui-framework&amp;quot; }

[addresses]
my_nfts = &amp;quot;0x0&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;HYAo&quot;&gt;А теперь создайте файл &lt;code&gt;collection.move&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;QBdm&quot; data-lang=&quot;bash&quot;&gt;touch my_nfts/sources/collection.move&lt;/pre&gt;
  &lt;p id=&quot;hnc3&quot;&gt;И обновите его (комментарии на русском языке не поддерживаются, позже их нужно будет удалить):&lt;/p&gt;
  &lt;pre id=&quot;b2pt&quot; data-lang=&quot;rust&quot;&gt;module my_nfts::collection {
    use sui::url::{Self, Url};
    use std::string;
    use sui::object::{Self, ID, UID};
    use sui::event;
    use sui::transfer;
    use sui::typed_id::{Self, TypedID};
    use sui::tx_context::{Self, TxContext};
    use sui::vec_set::{Self, VecSet};

    /// Example NFT, который может заминтить кто угодно
    struct MyNFT has key, store {
        id: UID,
        /// Имя токена
        name: string::String,
        /// Описание токена
        description: string::String,
        /// URL токена
        url: Url,
    }

    // ===== События =====

    struct MintNFTEvent has copy, drop {
        // Идентификатор объекта NFT
        object_id: ID,
        // Создатель NFT
        creator: address,
        // Название NFT
        name: string::String,
    }

    // ===== Public view функции =====

    /// Получить NFT&amp;#x27;s &amp;#x60;name&amp;#x60;
    public fun name(nft: &amp;amp;MyNFT): &amp;amp;string::String {
        &amp;amp;nft.name
    }

    /// Получить NFT&amp;#x27;s &amp;#x60;description&amp;#x60;
    public fun description(nft: &amp;amp;MyNFT): &amp;amp;string::String {
        &amp;amp;nft.description
    }

    /// Получить NFT&amp;#x27;s &amp;#x60;url&amp;#x60;
    public fun url(nft: &amp;amp;MyNFT): &amp;amp;Url {
        &amp;amp;nft.url
    }

    // ===== Entrypoints (точки входа) =====

    /// Создать новый my_nft (этот ентри метод необязателен)
    public entry fun mint_to_sender(
        name: vector&amp;lt;u8&amp;gt;,
        description: vector&amp;lt;u8&amp;gt;,
        url: vector&amp;lt;u8&amp;gt;,
        ctx: &amp;amp;mut TxContext
    ) {
        let nft = MyNFT {
            id: object::new(ctx),
            name: string::utf8(name),
            description: string::utf8(description),
            url: url::new_unsafe_from_bytes(url)
        };
        let sender = tx_context::sender(ctx);
        event::emit(MintNFTEvent {
            object_id: object::uid_to_inner(&amp;amp;nft.id),
            creator: sender,
            name: nft.name,
        });
        transfer::transfer(nft, sender);
    }

    /// Перевести &amp;#x60;nft&amp;#x60; &amp;#x60;получателю&amp;#x60;
    public entry fun transfer(
        nft: MyNFT, recipient: address, _: &amp;amp;mut TxContext
    ) {
        transfer::transfer(nft, recipient)
    }

    /// Обновить &amp;#x60;description&amp;#x60; у &amp;#x60;nft&amp;#x60; на &amp;#x60;new_description&amp;#x60;
    public entry fun update_description(
        nft: &amp;amp;mut MyNFT,
        new_description: vector&amp;lt;u8&amp;gt;,
        _: &amp;amp;mut TxContext
    ) {
        nft.description = string::utf8(new_description)
    }

    /// Навсегда удалить &amp;#x60;nft&amp;#x60;
    public entry fun burn(nft: MyNFT, _: &amp;amp;mut TxContext) {
        let MyNFT { id, name: _, description: _, url: _ } = nft;
        object::delete(id)
    }

    // ===== Коллекция =====

    // Коды ошибок

    /// Максимальная емкость, установленная для коллекции, не может превышать жесткого ограничения,
    /// которое равно DEFAULT_MAX_CAPACITY.
    const EInvalidMaxCapacity: u64 = 0;

    /// Попытка добавить объект в коллекцию, когда коллекция
    /// уже заполнена до предела.
    const EMaxCapacityExceeded: u64 = 1;

    const DEFAULT_MAX_CAPACITY: u64 = 0x10000;

    struct Collection&amp;lt;phantom T: store&amp;gt; has key {
        id: UID,
        objects: VecSet&amp;lt;ID&amp;gt;,
        max_capacity: u64,
    }

    /// Создать новую коллекцию и вернуть ее.
    public fun new&amp;lt;T: store&amp;gt;(ctx: &amp;amp;mut TxContext): Collection&amp;lt;T&amp;gt; {
        new_with_max_capacity(ctx, DEFAULT_MAX_CAPACITY)
    }

    /// Создать новую коллекцию с настраиваемым ограничением размера и вернуть ее.
    public fun new_with_max_capacity&amp;lt;T: store&amp;gt;(
        ctx: &amp;amp;mut TxContext,
        max_capacity: u64,
    ): Collection&amp;lt;T&amp;gt; {
        assert!(max_capacity &amp;lt;= DEFAULT_MAX_CAPACITY &amp;amp;&amp;amp; max_capacity &amp;gt; 0, EInvalidMaxCapacity);
        let c = Collection {
            id: object::new(ctx),
            objects: vec_set::empty(),
            max_capacity,
        };
        // Минтим 2 MyNFT и добавляем в коллекцию
        mint_and_add_nft(b&amp;quot;MyNFT #1&amp;quot;, b&amp;quot;This is MyNFT #1&amp;quot;, b&amp;quot;ipfs://bafkreibngqhl3gaa7daob4i2vccziay2jjlp435cf66vhono7nrvww53ty&amp;quot;, &amp;amp;mut c, ctx);
        mint_and_add_nft(b&amp;quot;MyNFT #2&amp;quot;, b&amp;quot;This is MyNFT #2&amp;quot;, b&amp;quot;ipfs://bafkreibngqhl3gaa7daob4i2vccziay2jjlp435cf66vhono7nrvww53ty&amp;quot;, &amp;amp;mut c, ctx);
        c
    }

    /// Создать новую коллекцию и передать ее подписавшему.
    public entry fun create&amp;lt;T: store&amp;gt;(ctx: &amp;amp;mut TxContext) {
        transfer::transfer(new&amp;lt;T&amp;gt;(ctx), tx_context::sender(ctx))
    }

    /// Вернуть размер коллекции.
    public fun size&amp;lt;T: store&amp;gt;(c: &amp;amp;Collection&amp;lt;T&amp;gt;): u64 {
        vec_set::size(&amp;amp;c.objects)
    }

    /// Заминтить MyNFT объект и добавить в коллекцию.
    public fun mint_and_add_nft&amp;lt;T: store&amp;gt;(
        name: vector&amp;lt;u8&amp;gt;,
        description: vector&amp;lt;u8&amp;gt;,
        url: vector&amp;lt;u8&amp;gt;,
        c: &amp;amp;mut Collection&amp;lt;T&amp;gt;,
        ctx: &amp;amp;mut TxContext,
    ): TypedID&amp;lt;MyNFT&amp;gt; {
        assert!(size(c) + 1 &amp;lt;= c.max_capacity, EMaxCapacityExceeded);
        let nft = MyNFT {
            id: object::new(ctx),
            name: string::utf8(name),
            description: string::utf8(description),
            url: url::new_unsafe_from_bytes(url)
        };
        let sender = tx_context::sender(ctx);
        event::emit(MintNFTEvent {
            object_id: object::uid_to_inner(&amp;amp;nft.id),
            creator: sender,
            name: nft.name,
        });

        vec_set::insert(&amp;amp;mut c.objects, object::uid_to_inner(&amp;amp;nft.id));
        let nft_id = typed_id::new(&amp;amp;nft);
        transfer::transfer_to_object(nft, c);
        nft_id
    }

    /// Проверить, содержит ли коллекция определенный объект,
    /// идентифицируемый идентификатором объекта в байтах.
    public fun contains&amp;lt;T: store&amp;gt;(c: &amp;amp;Collection&amp;lt;T&amp;gt;, id: &amp;amp;ID): bool {
        vec_set::contains(&amp;amp;c.objects, id)
    }

    /// Удалить и вернуть объект из коллекции.
    /// Прервать, если объект не найден.
    public fun remove&amp;lt;T: store&amp;gt;(c: &amp;amp;mut Collection&amp;lt;T&amp;gt;, nft: MyNFT): MyNFT {
        vec_set::remove(&amp;amp;mut c.objects, object::uid_as_inner(&amp;amp;nft.id));
        nft
    }

    /// Удалить объект из коллекции, а затем передать его подписавшему.
    public entry fun remove_and_take&amp;lt;T: key + store&amp;gt;(
        c: &amp;amp;mut Collection&amp;lt;T&amp;gt;,
        nft: MyNFT,
        ctx: &amp;amp;mut TxContext,
    ) {
        let object = remove(c, nft);
        transfer::transfer(object, tx_context::sender(ctx));
    }

    public fun transfer_to_object_id&amp;lt;T: key + store&amp;gt;(
        obj: Collection&amp;lt;T&amp;gt;,
        owner_id: &amp;amp;mut UID,
    ) {
        transfer::transfer_to_object_id(obj, owner_id)
    }
}&lt;/pre&gt;
  &lt;p id=&quot;W9rK&quot;&gt;Этот контракт — сплошной эксперимент, т. к. в итоге у него есть ряд проблем: создатель &lt;code&gt;MyNFT&lt;/code&gt; считается коллекция, а если &lt;code&gt;MyNFT&lt;/code&gt; вытащить из коллекции, принадлежность к ней исчезнет. В общем, нет функции минта :)&lt;/p&gt;
  &lt;p id=&quot;EI4f&quot;&gt;А ещё, как видите, я закомментировал методы &lt;code&gt;remove&lt;/code&gt; и &lt;code&gt;remove_and_take&lt;/code&gt;, которые как раз таки вытаскивали NFT из коллекции, потому что пока не смог исправить ошибку в методе &lt;code&gt;remove&lt;/code&gt; (&lt;em&gt;edit 20.09&lt;/em&gt;: исправил ошибку).&lt;/p&gt;
  &lt;p id=&quot;WTgC&quot;&gt;Но всё же пример получился интересным на мой взгляд. Тестируем.&lt;/p&gt;
  &lt;p id=&quot;MbkG&quot;&gt;Публикуем контракт:&lt;/p&gt;
  &lt;pre id=&quot;VZqr&quot; data-lang=&quot;bash&quot;&gt;sui client publish --path my_nfts --gas-budget 30000&lt;/pre&gt;
  &lt;p id=&quot;DlDb&quot;&gt;Вы увидите вывод, похожий на:&lt;/p&gt;
  &lt;pre id=&quot;SVOx&quot; data-lang=&quot;bash&quot;&gt;----- Certificate ----
Transaction Hash: Mpa5fQD++ej/RlNFaBK181iDsyfAwzAn2p6tYs89Ppk=
Transaction Signature: AA==@8uR5Yl+9yT3fvScztVN1+9oTXrYpEbO8HDWc/gCxXpW+Qa3Dl6Wsqa7VR9QhgradYqxW1o9H5afbuiNcf87DAg==@7izXcEge0JZjZ1Ifhjit3s1A/NcFQ7SbezzQY2+xxeM=
Signed Authorities Bitmap: RoaringBitmap&amp;lt;[1, 2, 3]&amp;gt;
Transaction Kind : Publish
----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0x585677ee8152c2365b64e218a0a90842d6b9ba45 , Owner: Immutable
Mutated Objects:
  - ID: 0x3f9594a043ff74d79f423f989d445f1317005050 , Owner: Account Address ( 0x8f624561d9659c623cf55153df227133fe409482 )
----- Publish Results ----
The newly published package object ID: 0x585677ee8152c2365b64e218a0a90842d6b9ba45

Updated Gas : Coin { id: 0x3f9594a043ff74d79f423f989d445f1317005050, value: 29850 }&lt;/pre&gt;
  &lt;p id=&quot;hbIw&quot;&gt;Вызываем метод &lt;code&gt;create&lt;/code&gt; (создаём коллекцию):&lt;/p&gt;
  &lt;pre id=&quot;jaFL&quot; data-lang=&quot;bash&quot;&gt;sui client call --function create --module collection --package &amp;lt;ID&amp;gt; --type-args &amp;lt;ID&amp;gt;::collection::MyNFT --gas-budget 30000&lt;/pre&gt;
  &lt;p id=&quot;Heat&quot;&gt;Вместо &amp;lt;ID&amp;gt; — id пакета (контракта), в моём случае это &lt;code&gt;0x585677ee8152c2365b64e218a0a90842d6b9ba45&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;z1WI&quot;&gt;Получаем примерно такой вывод:&lt;/p&gt;
  &lt;pre id=&quot;73lf&quot; data-lang=&quot;bash&quot;&gt;----- Certificate ----
Transaction Hash: 2N4LFMjqEbrV1tDEXcYzIA+pzewCtN4n+FAv4Ar4f3c=
Transaction Signature: AA==@+BGpI+WaXhzLl5Zynql2X44px8Q7+PI0ihxQUl5s/LpwvO2XvN508zRZm82rGv1Y7oKHefdY4VjWcKLXbSv7Cw==@7izXcEge0JZjZ1Ifhjit3s1A/NcFQ7SbezzQY2+xxeM=
Signed Authorities Bitmap: RoaringBitmap&amp;lt;[0, 1, 3]&amp;gt;
Transaction Kind : Call
Package ID : 0x585677ee8152c2365b64e218a0a90842d6b9ba45
Module : collection
Function : create
Arguments : []
Type Arguments : [&amp;quot;0x585677ee8152c2365b64e218a0a90842d6b9ba45::collection::MyNFT&amp;quot;]
----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0x28a887795de98bd02aad3922b5d48fa444b005cb , Owner: Object ID: ( 0xceaad4a58eb097a0ecfd4412006abfcdd55b1928 )
  - ID: 0x4f0f93e8480bc73bbbcf51800e14bad5c85deeee , Owner: Object ID: ( 0xceaad4a58eb097a0ecfd4412006abfcdd55b1928 )
  - ID: 0xceaad4a58eb097a0ecfd4412006abfcdd55b1928 , Owner: Account Address ( 0x8f624561d9659c623cf55153df227133fe409482 )
Mutated Objects:
  - ID: 0x7478f82dc5fcc49b00d97c93179b43c347a8ea83 , Owner: Account Address ( 0x8f624561d9659c623cf55153df227133fe409482 )&lt;/pre&gt;
  &lt;p id=&quot;30RV&quot;&gt;А теперь давайте посмотрим как выглядит эта коллекция в эксплорере: &lt;a href=&quot;https://explorer.devnet.sui.io/objects/0xceaad4a58eb097a0ecfd4412006abfcdd55b1928&quot; target=&quot;_blank&quot;&gt;сама коллекция&lt;/a&gt;, &lt;a href=&quot;https://explorer.devnet.sui.io/objects/0x4f0f93e8480bc73bbbcf51800e14bad5c85deeee&quot; target=&quot;_blank&quot;&gt;MyNFT #1&lt;/a&gt;, &lt;a href=&quot;https://explorer.devnet.sui.io/objects/0x28a887795de98bd02aad3922b5d48fa444b005cb&quot; target=&quot;_blank&quot;&gt;MyNFT #2&lt;/a&gt;.&lt;/p&gt;
  &lt;figure id=&quot;GkT2&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/ad/3f/ad3f9a3c-5ce1-4d06-887d-91a42b0af168.png&quot; width=&quot;692&quot; /&gt;
    &lt;figcaption&gt;Коллекция&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;figure id=&quot;2djR&quot; class=&quot;m_custom&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/95/51/95514b22-4785-4b67-ba67-016b46b31d77.png&quot; width=&quot;1279.9999999999998&quot; /&gt;
    &lt;figcaption&gt;MyNFT #1&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;section style=&quot;background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;0ghW&quot;&gt;Готово! Получился интересный эксперимент. Если когда-нибудь я бы начал делать полноценную коллекцию я бы обратил внимание на официальный пример &lt;a href=&quot;https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/nfts/sources/num.move&quot; target=&quot;_blank&quot;&gt;num&lt;/a&gt; и/или дождался пример &lt;a href=&quot;https://github.com/MystenLabs/sui/tree/main/sui_programmability/examples/nfts&quot; target=&quot;_blank&quot;&gt;ImageNFT&lt;/a&gt;, которого пока что нет.&lt;/p&gt;
  &lt;/section&gt;
  &lt;h2 id=&quot;MkKI&quot; data-align=&quot;center&quot;&gt;3. Solana&lt;/h2&gt;
  &lt;p id=&quot;cT6s&quot;&gt;Здесь же мы выберем один из самых простых способов создания NFT коллекций и будем использовать &lt;a href=&quot;https://www.metaplex.com/&quot; target=&quot;_blank&quot;&gt;Metaplex&lt;/a&gt;, а в особенности &lt;a href=&quot;https://docs.metaplex.com/developer-tools/sugar/&quot; target=&quot;_blank&quot;&gt;Sugar&lt;/a&gt;. Кстати, про NFT в Солане у меня уже была статья: &lt;a href=&quot;https://teletype.in/@scamushka/solana_nft&quot; target=&quot;_blank&quot;&gt;https://teletype.in/@scamushka/solana_nft&lt;/a&gt;.&lt;/p&gt;
  &lt;h3 id=&quot;wep7&quot; data-align=&quot;center&quot;&gt;3.1. Подготовка&lt;/h3&gt;
  &lt;p id=&quot;SKuC&quot;&gt;Установите &lt;a href=&quot;https://docs.solana.com/cli/install-solana-cli-tools&quot; target=&quot;_blank&quot;&gt;Solana Tool Suite&lt;/a&gt;, &lt;a href=&quot;https://docs.metaplex.com/developer-tools/sugar/overview/installation&quot; target=&quot;_blank&quot;&gt;Sugar&lt;/a&gt; и настройте &lt;a href=&quot;https://docs.metaplex.com/guides/cli-wallet#setting-up-a-devnet-wallet-for-testing&quot; target=&quot;_blank&quot;&gt;Solana CLI кошелёк&lt;/a&gt;.&lt;/p&gt;
  &lt;h3 id=&quot;IftP&quot; data-align=&quot;center&quot;&gt;3.2. Настройка проекта&lt;/h3&gt;
  &lt;p id=&quot;eOjV&quot;&gt;Скачайте &lt;a href=&quot;https://arweave.net/RhNCVZoqC6iO0xEL0DnsqZGPSG_CK_KeiU4vluOeIoI&quot; target=&quot;_blank&quot;&gt;архив&lt;/a&gt; с тестовыми ассетами и перекиньте папку &lt;code&gt;assets&lt;/code&gt; в новую папку. Каталог проекта должен выглядеть следующим образом:&lt;/p&gt;
  &lt;pre id=&quot;ctvX&quot;&gt;MyProject/
     assets/
         0.png
         0.json
         1.png
         1.json
         . . .&lt;/pre&gt;
  &lt;h3 id=&quot;2SX9&quot; data-align=&quot;center&quot;&gt;3.3. Создание конфига и публикация CM2&lt;/h3&gt;
  &lt;p id=&quot;0thB&quot;&gt;Запустите следующую команду из каталога вашего проекта:&lt;/p&gt;
  &lt;pre id=&quot;9NTb&quot; data-lang=&quot;bash&quot;&gt;sugar launch&lt;/pre&gt;
  &lt;p id=&quot;PtyY&quot;&gt;Отвечаем на все вопросы (везде отвечаем &lt;code&gt;yes&lt;/code&gt;) и деплоим Candy Machine. Подробное описание всех вопросов &lt;a href=&quot;https://docs.metaplex.com/developer-tools/sugar/tutorials/my-first-candy-machine#create-a-config-file&quot; target=&quot;_blank&quot;&gt;тут&lt;/a&gt;.&lt;/p&gt;
  &lt;figure id=&quot;2nT8&quot; class=&quot;m_custom&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/1a/b2/1ab291c3-78e5-4325-b167-db4b10184030.png&quot; width=&quot;1277.9284750337383&quot; /&gt;
    &lt;figcaption&gt;sugar launch&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;tGVl&quot; data-align=&quot;center&quot;&gt;3.4. Минт NFT&lt;/h3&gt;
  &lt;p id=&quot;D7Lj&quot;&gt;Чтобы убедиться, что всё работает должным образом, минтим NFT на адрес нашего кошелька:&lt;/p&gt;
  &lt;pre id=&quot;whHL&quot; data-lang=&quot;bash&quot;&gt;sugar mint&lt;/pre&gt;
  &lt;p id=&quot;iE97&quot;&gt;Получаем примерно такой вывод:&lt;/p&gt;
  &lt;pre id=&quot;WKZs&quot; data-lang=&quot;bash&quot;&gt;[1/2] 🔍 Loading candy machine Candy machine ID: Ews3L5NoAjjLEHYqEu47DqQ77nsqgNQs3NuELjBCd5bb ▪️▪️▪️▪️▪️ Done
[2/2] 🍬 Minting from candy machine

Minting to PanbgtcTiZ2PveV96t2FHSffiLHXXjMuhvoabUUKKm8 ▪️▪️▪️▪️▪️ Signature: jAUVJv4ezyumvKYWvuEsMcDtWRujCK4xFL9q8MCe7PmDiVuAGHNY5PFGKUH5hY4PnqtGMyvDjX821xxCiGAChzQ

✅ Command successful.&lt;/pre&gt;
  &lt;p id=&quot;Yjmb&quot;&gt;И на этом всё! Без заключений и каких-либо выводов ❤️&lt;/p&gt;
  &lt;h2 id=&quot;JWMa&quot; data-align=&quot;center&quot;&gt;4. Справочник&lt;/h2&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;ttcO&quot;&gt;Your First NFT (Aptos): &lt;a href=&quot;https://aptos.dev/tutorials/your-first-nft&quot; target=&quot;_blank&quot;&gt;https://aptos.dev/tutorials/your-first-nft&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(263, 48%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;fN2U&quot;&gt;Aptos Examples: &lt;a href=&quot;https://github.com/aptos-labs/aptos-core/tree/main/ecosystem/typescript/sdk/examples&quot; target=&quot;_blank&quot;&gt;https://github.com/aptos-labs/aptos-core/tree/main/ecosystem/typescript/sdk/examples&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;jsBz&quot;&gt;Sui Docs (Move): &lt;a href=&quot;https://docs.sui.io/build/move&quot; target=&quot;_blank&quot;&gt;https://docs.sui.io/build/move&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(263, 48%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;cSOU&quot;&gt;Sui NFTs Examples: &lt;a href=&quot;https://github.com/MystenLabs/sui/tree/main/sui_programmability/examples/nfts&quot; target=&quot;_blank&quot;&gt;https://github.com/MystenLabs/sui/tree/main/sui_programmability/examples/nfts&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;Vzf0&quot;&gt;Metaplex Docs (Sugar): &lt;a href=&quot;https://docs.metaplex.com/developer-tools/sugar/&quot; target=&quot;_blank&quot;&gt;https://docs.metaplex.com/developer-tools/sugar/&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;

</content></entry><entry><id>scamushka:libp2p_tutorial</id><link rel="alternate" type="text/html" href="https://teletype.in/@scamushka/libp2p_tutorial?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=scamushka"></link><title>Туториал по libp2p: создание peer-to-peer приложения на Rust</title><published>2022-08-09T18:46:51.428Z</published><updated>2022-08-10T14:54:21.533Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img4.teletype.in/files/36/16/3616359d-418e-465c-8c53-2e4f6ea3ba1c.png"></media:thumbnail><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/c6/53/c653ed71-e135-4fd3-8c12-2b80b11c436a.jpeg&quot;&gt;Всем привет! С вами ArteMm aka Скамушка ツ</summary><content type="html">
  &lt;figure id=&quot;q3M0&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/c6/53/c653ed71-e135-4fd3-8c12-2b80b11c436a.jpeg&quot; width=&quot;1280&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;jaoW&quot;&gt;&lt;strong&gt;Всем привет!&lt;/strong&gt; С вами ArteMm aka &lt;a href=&quot;https://t.me/scamushka&quot; target=&quot;_blank&quot;&gt;Скамушка&lt;/a&gt; ツ&lt;/p&gt;
  &lt;p id=&quot;Q8JQ&quot;&gt;В этой статье мы узнаем, что такое &lt;code&gt;libp2p&lt;/code&gt;, а также создадим простое p2p приложение. Это перевод/конспект &lt;a href=&quot;https://blog.logrocket.com/libp2p-tutorial-build-a-peer-to-peer-app-in-rust/&quot; target=&quot;_blank&quot;&gt;данной статьи&lt;/a&gt; с улучшениями и исправлениями ошибок от меня.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;h2 id=&quot;eJZ5&quot;&gt;Навигация по статье:&lt;/h2&gt;
    &lt;p id=&quot;GzJh&quot;&gt;&lt;strong&gt;1. &lt;a href=&quot;https://teletype.in/@scamushka/libp2p_tutorial#8jze&quot; target=&quot;_blank&quot;&gt;Введение&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;Bb6n&quot;&gt;&lt;strong&gt;2. &lt;a href=&quot;https://teletype.in/@scamushka/libp2p_tutorial#Bryh&quot; target=&quot;_blank&quot;&gt;Первоначальная настройка&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;6gmu&quot;&gt;&lt;strong&gt;3. &lt;a href=&quot;https://teletype.in/@scamushka/libp2p_tutorial#J1j4&quot; target=&quot;_blank&quot;&gt;Что такое libp2p?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;YbP4&quot;&gt;&lt;strong&gt;4. &lt;a href=&quot;https://teletype.in/@scamushka/libp2p_tutorial#ZSrf&quot; target=&quot;_blank&quot;&gt;Как работает libp2p&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;peTN&quot;&gt;&lt;strong&gt;5. &lt;a href=&quot;https://teletype.in/@scamushka/libp2p_tutorial#VivF&quot; target=&quot;_blank&quot;&gt;Создание клиента libp2p&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;aX2t&quot;&gt;&lt;strong&gt;6. &lt;a href=&quot;https://teletype.in/@scamushka/libp2p_tutorial#FfpE&quot; target=&quot;_blank&quot;&gt;Обработка ввода в libp2p&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;ZNBd&quot;&gt;&lt;strong&gt;7. &lt;a href=&quot;https://teletype.in/@scamushka/libp2p_tutorial#5Fb1&quot; target=&quot;_blank&quot;&gt;Отправка сообщений с помощью libp2p&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;Cs6g&quot;&gt;&lt;strong&gt;8. &lt;a href=&quot;https://teletype.in/@scamushka/libp2p_tutorial#sRhy&quot; target=&quot;_blank&quot;&gt;Ответы на сообщения с помощью libp2p&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;TmqJ&quot;&gt;&lt;strong&gt;9. &lt;a href=&quot;https://teletype.in/@scamushka/libp2p_tutorial#pjSs&quot; target=&quot;_blank&quot;&gt;Тестирование с помощью libp2p&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;Ae7L&quot;&gt;&lt;strong&gt;10. &lt;a href=&quot;https://teletype.in/@scamushka/libp2p_tutorial#oN5V&quot; target=&quot;_blank&quot;&gt;Заключение&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;XYLd&quot;&gt;&lt;strong&gt;11. &lt;a href=&quot;https://teletype.in/@scamushka/libp2p_tutorial#htnq&quot; target=&quot;_blank&quot;&gt;Доп. комментарий и материалы на эту тему&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;h2 id=&quot;8jze&quot; data-align=&quot;center&quot;&gt;1. Введение&lt;/h2&gt;
  &lt;p id=&quot;mPW3&quot;&gt;За последние несколько лет на сцене децентрализованного ПО произошло несколько очень интересных событий (даже помимо всей крипты и блокчейнов). Яркими примерами являются &lt;a href=&quot;https://ipfs.tech/&quot; target=&quot;_blank&quot;&gt;IPFS&lt;/a&gt;; новая платформа распределенного кодирования &lt;a href=&quot;https://radicle.xyz/&quot; target=&quot;_blank&quot;&gt;Radicle&lt;/a&gt;; децентрализованная социальная сеть &lt;a href=&quot;https://scuttlebutt.nz/&quot; target=&quot;_blank&quot;&gt;Scuttlebutt&lt;/a&gt;; и многие другие приложения в &lt;a href=&quot;https://fediverse.party/&quot; target=&quot;_blank&quot;&gt;Fediverse&lt;/a&gt;, такие как &lt;a href=&quot;https://joinmastodon.org/&quot; target=&quot;_blank&quot;&gt;Mastodon&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;kBL9&quot;&gt;В этом руководстве я покажу вам, как создать очень простое p2p приложение с помощью Rust и фантастической библиотеки &lt;a href=&quot;https://github.com/libp2p/rust-libp2p&quot; target=&quot;_blank&quot;&gt;libp2p&lt;/a&gt;, которая существует на разных стадиях зрелости для широкого спектра языков.&lt;/p&gt;
  &lt;p id=&quot;Fy9p&quot;&gt;Мы собираемся создать приложение для кулинарных рецептов с простым интерфейсом командной строки, которое позволит нам:&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;ul id=&quot;eusM&quot;&gt;
      &lt;li id=&quot;ckNg&quot;&gt;Создавать рецепты&lt;/li&gt;
      &lt;li id=&quot;18rG&quot;&gt;Публиковать рецепты&lt;/li&gt;
      &lt;li id=&quot;BrNT&quot;&gt;Перечислять локальные рецепты&lt;/li&gt;
      &lt;li id=&quot;IbOc&quot;&gt;Перечислять другие пиры, которые мы обнаружили в сети&lt;/li&gt;
      &lt;li id=&quot;xCih&quot;&gt;Перечислять опубликованные рецепты заданного пира&lt;/li&gt;
      &lt;li id=&quot;3N9z&quot;&gt;Перечислять все рецепты всех известных нам пиров&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/section&gt;
  &lt;p id=&quot;hghM&quot;&gt;Мы сделаем все это примерно в 300 строках Rust. Давайте начнем!&lt;/p&gt;
  &lt;h2 id=&quot;Bryh&quot; data-align=&quot;center&quot;&gt;2. Первоначальная настройка&lt;/h2&gt;
  &lt;p id=&quot;cWHT&quot;&gt;Чтобы следовать этому примеру, все, что вам нужно, — это последняя установка Rust (1.47+).&lt;/p&gt;
  &lt;p id=&quot;LXaU&quot;&gt;Сначала создайте новый проект Rust:&lt;/p&gt;
  &lt;pre id=&quot;fsvz&quot; data-lang=&quot;bash&quot;&gt;cargo new rust-p2p-example
cd rust-p2p-example&lt;/pre&gt;
  &lt;p id=&quot;wKMJ&quot;&gt;Затем отредактируйте файл &lt;code&gt;Cargo.toml&lt;/code&gt; и добавьте необходимые зависимости:&lt;/p&gt;
  &lt;pre id=&quot;P8uC&quot; data-lang=&quot;toml&quot;&gt;[dependencies]
libp2p = { version = &amp;quot;0.39&amp;quot;, features = [&amp;quot;tcp-tokio&amp;quot;, &amp;quot;mdns&amp;quot;] }
tokio = { version = &amp;quot;1.0&amp;quot;, features = [&amp;quot;io-util&amp;quot;, &amp;quot;io-std&amp;quot;, &amp;quot;macros&amp;quot;, &amp;quot;rt&amp;quot;, &amp;quot;rt-multi-thread&amp;quot;, &amp;quot;sync&amp;quot;, &amp;quot;fs&amp;quot;] }
serde = {version = &amp;quot;1.0&amp;quot;, features = [&amp;quot;derive&amp;quot;] }
serde_json = &amp;quot;1.0&amp;quot;
once_cell = &amp;quot;1.5&amp;quot;
log = &amp;quot;0.4&amp;quot;
pretty_env_logger = &amp;quot;0.4&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;1HrY&quot;&gt;Как упоминалось выше, мы будем использовать &lt;a href=&quot;https://github.com/libp2p/rust-libp2p&quot; target=&quot;_blank&quot;&gt;libp2p&lt;/a&gt; для части с p2p сетью. В частности, мы собираемся использовать его вместе с асинхронным рантаймом Tokio. Мы будем использовать &lt;a href=&quot;https://blog.logrocket.com/json-and-rust-why-serde_json-is-the-top-choice/&quot; target=&quot;_blank&quot;&gt;Serde для сериализации и десериализации JSON&lt;/a&gt; и пару вспомогательных библиотек для логирования и инициализации состояния.&lt;/p&gt;
  &lt;h2 id=&quot;J1j4&quot; data-align=&quot;center&quot;&gt;3. Что такое &lt;code&gt;libp2p&lt;/code&gt;?&lt;/h2&gt;
  &lt;p id=&quot;DRxO&quot;&gt;&lt;a href=&quot;https://libp2p.io/&quot; target=&quot;_blank&quot;&gt;libp2p&lt;/a&gt; — это набор протоколов для создания p2p приложений, ориентированных на модульность.&lt;/p&gt;
  &lt;p id=&quot;uaFQ&quot;&gt;Существуют реализации библиотеки для нескольких языков, таких как JavaScript, Go и Rust. Все эти библиотеки реализуют одни и те же спецификации &lt;code&gt;libp2p&lt;/code&gt;, поэтому клиент &lt;code&gt;libp2p&lt;/code&gt;, созданный с помощью Go, может беспрепятственно взаимодействовать с другим клиентом, написанным на JavaScript, если они совместимы с точки зрения выбранного стека протоколов. Эти протоколы охватывают широкий диапазон, от базовых сетевых транспортных протоколов до протоколов уровня безопасности и мультиплексирования.&lt;/p&gt;
  &lt;p id=&quot;ivoQ&quot;&gt;Мы не будем слишком углубляться в детали &lt;code&gt;libp2p&lt;/code&gt; в этой статье, но если вам интересно погрузиться глубже, &lt;a href=&quot;https://docs.libp2p.io/concepts/&quot; target=&quot;_blank&quot;&gt;официальная документация libp2p&lt;/a&gt; предлагает очень хороший обзор различных концепций, с которыми мы столкнемся на этом пути.&lt;/p&gt;
  &lt;h2 id=&quot;ZSrf&quot; data-align=&quot;center&quot;&gt;4. Как работает &lt;code&gt;libp2p&lt;/code&gt;&lt;/h2&gt;
  &lt;p id=&quot;bwOI&quot;&gt;Для начала в &lt;code&gt;main.rs&lt;/code&gt; импортируем все необходимые компоненты:&lt;/p&gt;
  &lt;pre id=&quot;ITrJ&quot; data-lang=&quot;rust&quot;&gt;use libp2p::{
    core::upgrade,
    floodsub::{Floodsub, FloodsubEvent, Topic},
    futures::StreamExt,
    identity,
    mdns::{Mdns, MdnsEvent},
    mplex,
    noise::{Keypair, NoiseConfig, X25519Spec},
    swarm::{NetworkBehaviourEventProcess, Swarm, SwarmBuilder},
    tcp::TokioTcpConfig,
    NetworkBehaviour, PeerId, Transport,
};
use log::{error, info};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use tokio::{fs, io::AsyncBufReadExt, sync::mpsc};&lt;/pre&gt;
  &lt;p id=&quot;YHUn&quot;&gt;Чтобы увидеть &lt;code&gt;libp2p&lt;/code&gt; в действии, давайте запустим наше приложение рецептов. Мы начнем с определения некоторых констант и типов, которые нам понадобятся:&lt;/p&gt;
  &lt;pre id=&quot;omAK&quot; data-lang=&quot;rust&quot;&gt;const STORAGE_FILE_PATH: &amp;amp;str = &amp;quot;./recipes.json&amp;quot;;

type Result&amp;lt;T&amp;gt; = std::result::Result&amp;lt;T, Box&amp;lt;dyn std::error::Error + Send + Sync + &amp;#x27;static&amp;gt;&amp;gt;;

static KEYS: Lazy&amp;lt;identity::Keypair&amp;gt; = Lazy::new(|| identity::Keypair::generate_ed25519());
static PEER_ID: Lazy&amp;lt;PeerId&amp;gt; = Lazy::new(|| PeerId::from(KEYS.public()));
static TOPIC: Lazy&amp;lt;Topic&amp;gt; = Lazy::new(|| Topic::new(&amp;quot;recipes&amp;quot;));&lt;/pre&gt;
  &lt;p id=&quot;EeFF&quot;&gt;Мы будем хранить наши локальные рецепты в простом файле JSON с именем &lt;code&gt;recipes.json&lt;/code&gt;, который, по ожиданиям приложения, будет находиться в той же папке, что и исполняемый файл. Мы также определяем вспомогательный тип для &lt;code&gt;Result&lt;/code&gt;, который позволяет нам распространять произвольные ошибки.&lt;/p&gt;
  &lt;p id=&quot;eXx2&quot;&gt;Затем мы используем &lt;code&gt;once_cell::Lazy&lt;/code&gt; для ленивой инициализации нескольких вещей. Прежде всего, мы используем его для генерации пары ключей и так называемого &lt;code&gt;PeerId&lt;/code&gt;, полученного из открытого ключа. Мы также создаем &lt;code&gt;Topic&lt;/code&gt;, который является еще одной ключевой концепцией &lt;code&gt;libp2p&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;Cj10&quot;&gt;Что все это значит? Если коротко, то &lt;code&gt;PeerId&lt;/code&gt; — это просто уникальный идентификатор для конкретного пира в рамках всей p2p сети. Мы получаем его из пары ключей, чтобы обеспечить его уникальность. Кроме того, пара ключей позволяет нам безопасно общаться с остальной частью сети, гарантируя, что никто не сможет выдать себя за нас.&lt;/p&gt;
  &lt;p id=&quot;HpsD&quot;&gt;С другой стороны, &lt;code&gt;Topic&lt;/code&gt; — это концепция из Floodsub, которая является реализацией &lt;a href=&quot;https://github.com/libp2p/specs/tree/master/pubsub&quot; target=&quot;_blank&quot;&gt;интерфейса pub/sub&lt;/a&gt; для &lt;code&gt;libp2p&lt;/code&gt;. &lt;code&gt;Topic&lt;/code&gt; — это то, на что мы можем &lt;code&gt;subscribe&lt;/code&gt; (подписаться) и отправлять сообщения, например, чтобы прослушивать только часть трафика в pub/sub сети.&lt;/p&gt;
  &lt;p id=&quot;wz89&quot;&gt;Также нам понадобятся некоторые типы для рецепта:&lt;/p&gt;
  &lt;pre id=&quot;mXu9&quot; data-lang=&quot;rust&quot;&gt;type Recipes = Vec&amp;lt;Recipe&amp;gt;;

#[derive(Debug, Serialize, Deserialize)]
struct Recipe {
    id: usize,
    name: String,
    ingredients: String,
    instructions: String,
    public: bool,
}&lt;/pre&gt;
  &lt;p id=&quot;FL8x&quot;&gt;И несколько типов для сообщений, которые мы планируем рассылать:&lt;/p&gt;
  &lt;pre id=&quot;EI2f&quot; data-lang=&quot;rust&quot;&gt;#[derive(Debug, Serialize, Deserialize)]
enum ListMode {
    ALL,
    One(String),
}

#[derive(Debug, Serialize, Deserialize)]
struct ListRequest {
    mode: ListMode,
}

#[derive(Debug, Serialize, Deserialize)]
struct ListResponse {
    mode: ListMode,
    data: Recipes,
    receiver: String,
}

enum EventType {
    Response(ListResponse),
    Input(String),
}&lt;/pre&gt;
  &lt;p id=&quot;G8q7&quot;&gt;Рецепт довольно прост. У него есть ID, название, некоторые ингредиенты и инструкции по его выполнению. Кроме того, мы добавляем &lt;code&gt;public&lt;/code&gt; флаг, чтобы мы могли различать, какими рецептами мы хотим поделиться, а какие оставить для себя.&lt;/p&gt;
  &lt;p id=&quot;fopg&quot;&gt;Как упоминалось в начале, есть два способа получения списков от других пиров: от всех или от одного, что представлено перечислением &lt;code&gt;ListMode&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;HlN0&quot;&gt;Типы &lt;code&gt;ListRequest&lt;/code&gt; и &lt;code&gt;ListResponse&lt;/code&gt; являются просто обертками для этого типа и данных, отправляемых с их помощью.&lt;/p&gt;
  &lt;p id=&quot;4HXu&quot;&gt;Перечисление &lt;code&gt;EventType&lt;/code&gt; различает ответ от другого пира и ввод от нас самих. Позже мы увидим, почему это различие имеет значение.&lt;/p&gt;
  &lt;h2 id=&quot;VivF&quot; data-align=&quot;center&quot;&gt;5. Создание клиента &lt;code&gt;libp2p&lt;/code&gt;&lt;/h2&gt;
  &lt;p id=&quot;bHgV&quot;&gt;Давайте приступим к написанию &lt;code&gt;main&lt;/code&gt; функции для создания peer-to-peer сети.&lt;/p&gt;
  &lt;pre id=&quot;v3nw&quot; data-lang=&quot;rust&quot;&gt;#[tokio::main]
async fn main() {
    pretty_env_logger::init();

    info!(&amp;quot;Peer Id: {}&amp;quot;, PEER_ID.clone());
    let (response_sender, mut response_rcv) = mpsc::unbounded_channel();

    let auth_keys = Keypair::&amp;lt;X25519Spec&amp;gt;::new()
        .into_authentic(&amp;amp;KEYS)
        .expect(&amp;quot;can create auth keys&amp;quot;);&lt;/pre&gt;
  &lt;p id=&quot;wldg&quot;&gt;Мы инициализируем логирование и создаем асинхронный &lt;code&gt;channel&lt;/code&gt; (канал) для связи между различными частями приложения. Позже мы будем использовать этот канал для отправки ответов от сетевого стека &lt;code&gt;libp2p&lt;/code&gt; обратно в наше приложение для их обработки.&lt;/p&gt;
  &lt;p id=&quot;0mfa&quot;&gt;Кроме того, мы создаем некоторые ключи аутентификации для криптопротокола &lt;a href=&quot;https://noiseprotocol.org/&quot; target=&quot;_blank&quot;&gt;Noise&lt;/a&gt;, которые мы будем использовать для защиты трафика внутри сети. Для этого мы создаем новую пару ключей и подписываем ее нашими идентификационными ключами с помощью функции &lt;code&gt;into_authentic&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;GELv&quot;&gt;Следующий шаг важен и включает в себя некоторые основные концепции &lt;code&gt;libp2p&lt;/code&gt;: создание так называемого &lt;a href=&quot;https://docs.libp2p.io/concepts/transport/&quot; target=&quot;_blank&quot;&gt;Transport&lt;/a&gt;.&lt;/p&gt;
  &lt;pre id=&quot;XDqb&quot; data-lang=&quot;rust&quot;&gt;    let transp = TokioTcpConfig::new()
        .upgrade(upgrade::Version::V1)
        .authenticate(NoiseConfig::xx(auth_keys).into_authenticated()) // XX Handshake паттерн, также существует IX и IK — только XX в настоящее время обеспечивает взаимодействие с другими реализациями libp2p (impls)
        .multiplex(mplex::MplexConfig::new())
        .boxed();&lt;/pre&gt;
  &lt;p id=&quot;wsij&quot;&gt;Транспорт — это набор сетевых протоколов, обеспечивающих ориентированную на соединение коммуникацию между пирами. В рамках одного приложения можно использовать несколько транспортов — например, TCP/IP и Websockets, или UDP одновременно для разных вариантов использования.&lt;/p&gt;
  &lt;p id=&quot;Jz55&quot;&gt;В этом примере мы будем использовать TCP в качестве основы, используя асинхронный TCP от Tokio. Как только TCP-соединение будет установлено, мы &lt;code&gt;upgrade&lt;/code&gt; (обновим) его, чтобы использовать &lt;code&gt;Noise&lt;/code&gt; для безопасной связи. Веб-примером этого может быть использование TLS поверх HTTP для создания безопасного соединения.&lt;/p&gt;
  &lt;p id=&quot;1VAG&quot;&gt;Мы используем &lt;code&gt;NoiseConfig:xx&lt;/code&gt; handshake паттерн, который является одним из трех вариантов, потому что только он гарантирует совместимость с другими приложениями &lt;code&gt;libp2p&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;zk5L&quot;&gt;Что хорошо в &lt;code&gt;libp2p&lt;/code&gt;, так это то, что мы можем написать Rust-клиент, а другой может написать JavaScript-клиент, и они все равно смогут легко общаться, если протоколы будут реализованы в обеих версиях библиотеки.&lt;/p&gt;
  &lt;p id=&quot;4fDI&quot;&gt;В конце мы также &lt;a href=&quot;https://docs.libp2p.io/concepts/stream-multiplexing/&quot; target=&quot;_blank&quot;&gt;мультиплексируем&lt;/a&gt; транспорт, что позволяет нам мультиплексировать несколько подпотоков или соединений на одном и том же транспорте.&lt;/p&gt;
  &lt;p id=&quot;8Uxl&quot;&gt;Довольно много теории! Но все это можно найти в &lt;a href=&quot;https://docs.libp2p.io/&quot; target=&quot;_blank&quot;&gt;документации libp2p&lt;/a&gt;. Это всего лишь один из многих способов создания p2p транспорта.&lt;/p&gt;
  &lt;p id=&quot;PVDq&quot;&gt;Следующее понятие — &lt;code&gt;NetworkBehaviour&lt;/code&gt;. Это та часть &lt;code&gt;libp2p&lt;/code&gt;, которая фактически определяет логику сети и всех пиров — например, что делать с входящими событиями и какие события отправлять.&lt;/p&gt;
  &lt;pre id=&quot;vIBO&quot; data-lang=&quot;rust&quot;&gt;    let mut behaviour = RecipeBehaviour {
        floodsub: Floodsub::new(PEER_ID.clone()),
        mdns: Mdns::new(Default::default())
            .await
            .expect(&amp;quot;can create mdns&amp;quot;),
        response_sender,
    };

    behaviour.floodsub.subscribe(TOPIC.clone());&lt;/pre&gt;
  &lt;p id=&quot;12ex&quot;&gt;В этом случае, как упоминалось выше, мы будем использовать протокол &lt;code&gt;FloodSub&lt;/code&gt; для работы с событиями. Мы также будем использовать &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc6762&quot; target=&quot;_blank&quot;&gt;mDNS&lt;/a&gt; — протокол для обнаружения других пиров в локальной сети. Мы также поместим сюда &lt;code&gt;sender&lt;/code&gt; часть нашего канала, чтобы использовать ее для распространения событий обратно в основную часть приложения.&lt;/p&gt;
  &lt;p id=&quot;gJ4q&quot;&gt;Тема (topic) &lt;code&gt;FloodSub&lt;/code&gt;, которую мы создали ранее, теперь подписана на наше поведение (behaviour), что означает, что мы будем получать события и можем отправлять события по этой теме.&lt;/p&gt;
  &lt;p id=&quot;HxZx&quot;&gt;Мы почти закончили с настройкой &lt;code&gt;libp2p&lt;/code&gt;. Последняя концепция, которая нам нужна, — это &lt;code&gt;Swarm&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;CLjb&quot; data-lang=&quot;rust&quot;&gt;    let mut swarm = SwarmBuilder::new(transp, behaviour, PEER_ID.clone())
        .executor(Box::new(|fut| {
            tokio::spawn(fut);
        }))
        .build();&lt;/pre&gt;
  &lt;p id=&quot;rEY1&quot;&gt;&lt;a href=&quot;https://docs.rs/libp2p/latest/libp2p/swarm/index.html&quot; target=&quot;_blank&quot;&gt;Swarm&lt;/a&gt; управляет соединениями, созданными с помощью транспорта, и выполняет созданное нами сетевое поведение, вызывая и получая события и предоставляя нам способ добраться до них извне.&lt;/p&gt;
  &lt;p id=&quot;aMRD&quot;&gt;Мы создаем &lt;code&gt;Swarm&lt;/code&gt; с нашим транспортом, поведением и идентификатором пира. Часть &lt;code&gt;executor&lt;/code&gt; просто говорит &lt;code&gt;Swarm&lt;/code&gt; использовать для внутреннего запуска &lt;code&gt;Tokio&lt;/code&gt; рантайм, но здесь мы также можем использовать другие асинхронные среды выполнения.&lt;/p&gt;
  &lt;p id=&quot;NbOW&quot;&gt;Осталось только запустить наш &lt;code&gt;Swarm&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;QVmU&quot; data-lang=&quot;rust&quot;&gt;    Swarm::listen_on(
        &amp;amp;mut swarm,
        &amp;quot;/ip4/0.0.0.0/tcp/0&amp;quot;
            .parse()
            .expect(&amp;quot;can get a local socket&amp;quot;),
    )
    .expect(&amp;quot;swarm can be started&amp;quot;);&lt;/pre&gt;
  &lt;p id=&quot;MswW&quot;&gt;Подобно запуску, например, TCP-сервера, мы просто вызываем &lt;code&gt;listen_on&lt;/code&gt; с локальным IP, позволяя ОС выбрать для нас порт. Это запустит &lt;code&gt;Swarm&lt;/code&gt; со всеми нашими настройками, но мы еще не определили никакой логики.&lt;/p&gt;
  &lt;p id=&quot;OKqX&quot;&gt;Давайте начнем с обработки пользовательского ввода.&lt;/p&gt;
  &lt;h2 id=&quot;FfpE&quot; data-align=&quot;center&quot;&gt;6. Обработка ввода в &lt;code&gt;libp2p&lt;/code&gt;&lt;/h2&gt;
  &lt;p id=&quot;KW9t&quot;&gt;Для пользовательского ввода мы будем полагаться на старый добрый STDIN. Итак, перед вызовом &lt;code&gt;Swarm::listen_on&lt;/code&gt; мы добавим:&lt;/p&gt;
  &lt;pre id=&quot;LwAt&quot; data-lang=&quot;rust&quot;&gt;    let mut stdin = tokio::io::BufReader::new(tokio::io::stdin()).lines();&lt;/pre&gt;
  &lt;p id=&quot;8Z0M&quot;&gt;Это определило асинхронный считыватель на STDIN, который читает поток построчно. Таким образом, если мы нажмем Enter, появится новое входящее сообщение.&lt;/p&gt;
  &lt;p id=&quot;gnYR&quot;&gt;Следующая часть — это создание нашего цикла событий, который будет слушать события от STDIN, от &lt;code&gt;Swarm&lt;/code&gt; и от нашего канала ответа, определенного выше.&lt;/p&gt;
  &lt;pre id=&quot;sdAn&quot; data-lang=&quot;rust&quot;&gt;    loop {
        let evt = {
            tokio::select! {
                line = stdin.next_line() =&amp;gt; Some(EventType::Input(line.expect(&amp;quot;can get line&amp;quot;).expect(&amp;quot;can read line from stdin&amp;quot;))),
                response = response_rcv.recv() =&amp;gt; Some(EventType::Response(response.expect(&amp;quot;response exists&amp;quot;))),
                event = swarm.select_next_some() =&amp;gt; {
                    info!(&amp;quot;Unhandled Swarm Event: {:?}&amp;quot;, event);
                    None
                },
            }
        };
        // ...
    }
}&lt;/pre&gt;
  &lt;p id=&quot;VaX8&quot;&gt;Мы используем макрос Tokio &lt;code&gt;select&lt;/code&gt; для ожидания нескольких асинхронных процессов, обрабатывая первый из них, который завершится. Мы ничего не делаем с событиями &lt;code&gt;Swarm&lt;/code&gt;; они обрабатываются в нашем &lt;code&gt;RecipeBehaviour&lt;/code&gt;, который мы рассмотрим позже, но нам все еще нужно вызывать &lt;code&gt;swarm.select_next_some()&lt;/code&gt;, чтобы продвигать &lt;code&gt;Swarm&lt;/code&gt; вперед.&lt;/p&gt;
  &lt;p id=&quot;tnw0&quot;&gt;Давайте добавим некоторую логику обработки событий вместо &lt;code&gt;// …&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;Erlj&quot; data-lang=&quot;rust&quot;&gt;        if let Some(event) = evt {
            match event {
                EventType::Response(resp) =&amp;gt; {
                   // ...
                }
                EventType::Input(line) =&amp;gt; match line.as_str() {
                    &amp;quot;ls p&amp;quot; =&amp;gt; handle_list_peers(&amp;amp;mut swarm).await,
                    cmd if cmd.starts_with(&amp;quot;ls r&amp;quot;) =&amp;gt; handle_list_recipes(cmd, &amp;amp;mut swarm).await,
                    cmd if cmd.starts_with(&amp;quot;create r&amp;quot;) =&amp;gt; handle_create_recipe(cmd).await,
                    cmd if cmd.starts_with(&amp;quot;publish r&amp;quot;) =&amp;gt; handle_publish_recipe(cmd).await,
                    _ =&amp;gt; error!(&amp;quot;unknown command&amp;quot;),
                },
            }
        }&lt;/pre&gt;
  &lt;p id=&quot;dbQr&quot;&gt;Если есть событие, мы сопоставляем его и смотрим, является ли оно событием &lt;code&gt;Response&lt;/code&gt; или &lt;code&gt;Input&lt;/code&gt;. Давайте пока рассмотрим только события &lt;code&gt;Input&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;UX8x&quot;&gt;Есть несколько вариантов. Мы поддерживаем следующие команды:&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;ul id=&quot;IXAm&quot;&gt;
      &lt;li id=&quot;j2xa&quot;&gt;&lt;code&gt;ls p&lt;/code&gt; выводит список всех известных пиров&lt;/li&gt;
      &lt;li id=&quot;MH9K&quot;&gt;&lt;code&gt;ls r&lt;/code&gt; выводит список локальных рецептов&lt;/li&gt;
      &lt;li id=&quot;eQy9&quot;&gt;&lt;code&gt;ls r {peerId}&lt;/code&gt; перечисляет опубликованные рецепты от определенного пира&lt;/li&gt;
      &lt;li id=&quot;3Rg3&quot;&gt;&lt;code&gt;ls r all&lt;/code&gt; перечисляет опубликованные рецепты от всех пиров&lt;/li&gt;
      &lt;li id=&quot;eV2n&quot;&gt;&lt;code&gt;publish r {recipeId}&lt;/code&gt; публикует заданный рецепт&lt;/li&gt;
      &lt;li id=&quot;rthb&quot;&gt;&lt;code&gt;create r {recipeName}|{recipeIngredients}|{recipeInstructions}&lt;/code&gt; создает новый рецепт с заданными данными и увеличивающимся ID&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/section&gt;
  &lt;p id=&quot;Jy89&quot;&gt;Перечисление всех рецептов от пиров в данном случае означает отправку запроса на рецепты нашим пирам, ожидание их ответа и отображение результатов. В p2p сети это может занять некоторое время, поскольку некоторые пиры могут находиться на другом конце планеты, и мы не знаем, все ли из них вообще ответят нам. Это значительно отличается от отправки запроса, например, на HTTP-сервер.&lt;/p&gt;
  &lt;p id=&quot;Oz3R&quot;&gt;Давайте сначала посмотрим на логику перечисления пиров:&lt;/p&gt;
  &lt;pre id=&quot;GuSD&quot; data-lang=&quot;rust&quot;&gt;async fn handle_list_peers(swarm: &amp;amp;mut Swarm&amp;lt;RecipeBehaviour&amp;gt;) {
    info!(&amp;quot;Discovered Peers:&amp;quot;);
    let nodes = swarm.behaviour().mdns.discovered_nodes();
    let mut unique_peers = HashSet::new();
    for peer in nodes {
        unique_peers.insert(peer);
    }
    unique_peers.iter().for_each(|p| info!(&amp;quot;{}&amp;quot;, p));
}&lt;/pre&gt;
  &lt;p id=&quot;YuDV&quot;&gt;В этом случае мы можем использовать &lt;code&gt;mDNS&lt;/code&gt; для предоставления нам всех обнаруженных нод, итерируя и отображая их.&lt;/p&gt;
  &lt;p id=&quot;LHJF&quot;&gt;Давайте рассмотрим создание и публикацию рецептов, прежде чем перейти к командам списка:&lt;/p&gt;
  &lt;pre id=&quot;ajr5&quot; data-lang=&quot;rust&quot;&gt;async fn handle_create_recipe(cmd: &amp;amp;str) {
    if let Some(rest) = cmd.strip_prefix(&amp;quot;create r&amp;quot;) {
        let elements: Vec&amp;lt;&amp;amp;str&amp;gt; = rest.split(&amp;quot;|&amp;quot;).collect();
        if elements.len() &amp;lt; 3 {
            info!(&amp;quot;too few arguments - Format: name|ingredients|instructions&amp;quot;);
        } else {
            let name = elements.get(0).expect(&amp;quot;name is there&amp;quot;);
            let ingredients = elements.get(1).expect(&amp;quot;ingredients is there&amp;quot;);
            let instructions = elements.get(2).expect(&amp;quot;instructions is there&amp;quot;);
            if let Err(e) = create_new_recipe(name, ingredients, instructions).await {
                error!(&amp;quot;error creating recipe: {}&amp;quot;, e);
            };
        }
    }
}

async fn handle_publish_recipe(cmd: &amp;amp;str) {
    if let Some(rest) = cmd.strip_prefix(&amp;quot;publish r&amp;quot;) {
        match rest.trim().parse::&amp;lt;usize&amp;gt;() {
            Ok(id) =&amp;gt; {
                if let Err(e) = publish_recipe(id).await {
                    info!(&amp;quot;error publishing recipe with id {}, {}&amp;quot;, id, e)
                } else {
                    info!(&amp;quot;Published Recipe with id: {}&amp;quot;, id);
                }
            }
            Err(e) =&amp;gt; error!(&amp;quot;invalid id: {}, {}&amp;quot;, rest.trim(), e),
        };
    }
}&lt;/pre&gt;
  &lt;p id=&quot;i3hE&quot;&gt;В обоих случаях нам нужно проанализировать строку, чтобы получить данные, разделенные &lt;code&gt;|&lt;/code&gt;, или заданный ID рецепта в случае &lt;code&gt;publish&lt;/code&gt;, регистрируя ошибку, если заданные входные данные не являются действительными.&lt;/p&gt;
  &lt;p id=&quot;FSno&quot;&gt;В случае &lt;code&gt;create&lt;/code&gt; мы вызываем вспомогательную функцию &lt;code&gt;create_new_recipe&lt;/code&gt; с заданными данными. Давайте проверим все вспомогательные функции, которые нам понадобятся для взаимодействия с нашим простым локальным JSON-хранилищем для рецептов:&lt;/p&gt;
  &lt;pre id=&quot;ZMmu&quot; data-lang=&quot;rust&quot;&gt;async fn create_new_recipe(name: &amp;amp;str, ingredients: &amp;amp;str, instructions: &amp;amp;str) -&amp;gt; Result&amp;lt;()&amp;gt; {
    let mut local_recipes = read_local_recipes().await?;
    let new_id = match local_recipes.iter().max_by_key(|r| r.id) {
        Some(v) =&amp;gt; v.id + 1,
        None =&amp;gt; 0,
    };
    local_recipes.push(Recipe {
        id: new_id,
        name: name.to_owned(),
        ingredients: ingredients.to_owned(),
        instructions: instructions.to_owned(),
        public: false,
    });
    write_local_recipes(&amp;amp;local_recipes).await?;

    info!(&amp;quot;Created recipe:&amp;quot;);
    info!(&amp;quot;Name: {}&amp;quot;, name);
    info!(&amp;quot;Ingredients: {}&amp;quot;, ingredients);
    info!(&amp;quot;Instructions:: {}&amp;quot;, instructions);

    Ok(())
}

async fn publish_recipe(id: usize) -&amp;gt; Result&amp;lt;()&amp;gt; {
    let mut local_recipes = read_local_recipes().await?;
    local_recipes
        .iter_mut()
        .filter(|r| r.id == id)
        .for_each(|r| r.public = true);
    write_local_recipes(&amp;amp;local_recipes).await?;
    Ok(())
}

async fn read_local_recipes() -&amp;gt; Result&amp;lt;Recipes&amp;gt; {
    let content = fs::read(STORAGE_FILE_PATH).await?;
    let result = serde_json::from_slice(&amp;amp;content)?;
    Ok(result)
}

async fn write_local_recipes(recipes: &amp;amp;Recipes) -&amp;gt; Result&amp;lt;()&amp;gt; {
    let json = serde_json::to_string(&amp;amp;recipes)?;
    fs::write(STORAGE_FILE_PATH, &amp;amp;json).await?;
    Ok(())
}&lt;/pre&gt;
  &lt;p id=&quot;6JTp&quot;&gt;Самыми основными строительными блоками являются &lt;code&gt;read_local_recipes&lt;/code&gt; и &lt;code&gt;write_local_recipes&lt;/code&gt;, которые просто читают и десериализуют или сериализуют и записывают рецепты из или в файл хранилища.&lt;/p&gt;
  &lt;p id=&quot;DDD7&quot;&gt;Помощник &lt;code&gt;publish_recipe&lt;/code&gt; извлекает все рецепты из файла, ищет рецепт с заданным ID и устанавливает для своего &lt;code&gt;public&lt;/code&gt; флага значение true.&lt;/p&gt;
  &lt;p id=&quot;3LmJ&quot;&gt;При создании рецепта мы также получаем все рецепты из файла, добавляем новый рецепт в конце и записываем обратно все данные, перезаписывая файл. Это не суперэффективно, но просто и работает.&lt;/p&gt;
  &lt;h2 id=&quot;5Fb1&quot; data-align=&quot;center&quot;&gt;7. Отправка сообщений с помощью &lt;code&gt;libp2p&lt;/code&gt;&lt;/h2&gt;
  &lt;p id=&quot;lsWA&quot;&gt;Далее рассмотрим &lt;code&gt;list&lt;/code&gt; команды и изучим, как мы можем отправлять сообщения другим пирам.&lt;/p&gt;
  &lt;p id=&quot;8GQN&quot;&gt;В команде &lt;code&gt;list&lt;/code&gt; есть три возможных случая:&lt;/p&gt;
  &lt;pre id=&quot;WEit&quot; data-lang=&quot;rust&quot;&gt;async fn handle_list_recipes(cmd: &amp;amp;str, swarm: &amp;amp;mut Swarm&amp;lt;RecipeBehaviour&amp;gt;) {
    let rest = cmd.strip_prefix(&amp;quot;ls r &amp;quot;);
    match rest {
        Some(&amp;quot;all&amp;quot;) =&amp;gt; {
            let req = ListRequest {
                mode: ListMode::ALL,
            };
            let json = serde_json::to_string(&amp;amp;req).expect(&amp;quot;can jsonify request&amp;quot;);
            swarm
                .behaviour_mut()
                .floodsub
                .publish(TOPIC.clone(), json.as_bytes());
        }
        Some(recipes_peer_id) =&amp;gt; {
            let req = ListRequest {
                mode: ListMode::One(recipes_peer_id.to_owned()),
            };
            let json = serde_json::to_string(&amp;amp;req).expect(&amp;quot;can jsonify request&amp;quot;);
            swarm
                .behaviour_mut()
                .floodsub
                .publish(TOPIC.clone(), json.as_bytes());
        }
        None =&amp;gt; {
            match read_local_recipes().await {
                Ok(v) =&amp;gt; {
                    info!(&amp;quot;Local Recipes ({})&amp;quot;, v.len());
                    v.iter().for_each(|r| info!(&amp;quot;{:?}&amp;quot;, r));
                }
                Err(e) =&amp;gt; error!(&amp;quot;error fetching local recipes: {}&amp;quot;, e),
            };
        }
    };
}&lt;/pre&gt;
  &lt;p id=&quot;Bko2&quot;&gt;Мы разбираем входящую команду, удаляя часть &lt;code&gt;ls r&lt;/code&gt; и проверяем, что осталось. Если в команде больше ничего нет, мы можем просто получить наши локальные рецепты и отобразить их с помощью помощников, определенных в предыдущем разделе.&lt;/p&gt;
  &lt;p id=&quot;86mN&quot;&gt;Если мы встречаем ключевое слово &lt;code&gt;all&lt;/code&gt;, мы создаем &lt;code&gt;ListRequest&lt;/code&gt; с установленным &lt;code&gt;ListMode::ALL&lt;/code&gt;, сериализуем его в JSON и, используя экземпляр &lt;code&gt;FloodSub&lt;/code&gt; в нашем &lt;code&gt;Swarm&lt;/code&gt;, публикуем его в ранее упомянутом &lt;code&gt;Topic&lt;/code&gt;&amp;#x27;е.&lt;/p&gt;
  &lt;p id=&quot;PeCd&quot;&gt;То же самое происходит, если в команде встречается ID пира, в этом случае мы просто отправим режим &lt;code&gt;ListMode::One&lt;/code&gt; с этим ID. Мы могли бы проверить, является ли этот идентификатор пира валидным или даже обнаруженным нами (discovered), но давайте не будем усложнять: если нет никого, кто бы его слушал, ничего не произойдет.&lt;/p&gt;
  &lt;p id=&quot;K1zG&quot;&gt;Это все, что нам нужно сделать для отправки сообщений в сеть. Теперь вопрос, что происходит с этими сообщениями? Где они обрабатываются?&lt;/p&gt;
  &lt;p id=&quot;SZmQ&quot;&gt;В случае p2p приложения помните, что мы одновременно являемся отправителем (&lt;code&gt;Sender&lt;/code&gt;) и получателем (&lt;code&gt;Receiver&lt;/code&gt;) событий, поэтому в нашей реализации нам нужно обрабатывать как исходящие, так и входящие события.&lt;/p&gt;
  &lt;h2 id=&quot;sRhy&quot; data-align=&quot;center&quot;&gt;8. Ответы на сообщения с помощью &lt;code&gt;libp2p&lt;/code&gt;&lt;/h2&gt;
  &lt;p id=&quot;bNSJ&quot;&gt;Наконец-то настал момент, когда в дело вступает наш &lt;code&gt;RecipeBehaviour&lt;/code&gt;. Давайте определим его:&lt;/p&gt;
  &lt;pre id=&quot;m8KL&quot; data-lang=&quot;rust&quot;&gt;#[derive(NetworkBehaviour)]
struct RecipeBehaviour {
    floodsub: Floodsub,
    mdns: Mdns,
    #[behaviour(ignore)]
    response_sender: mpsc::UnboundedSender&amp;lt;ListResponse&amp;gt;,
}&lt;/pre&gt;
  &lt;p id=&quot;IuOq&quot;&gt;Само поведение — это просто структура, но мы используем пользовательский (выводимый) макрос &lt;code&gt;NetworkBehaviour&lt;/code&gt; из &lt;code&gt;libp2p&lt;/code&gt;, поэтому нам не нужно вручную реализовывать все функции трейта самостоятельно.&lt;/p&gt;
  &lt;p id=&quot;uTlP&quot;&gt;Этот derive макрос реализует функции трейта &lt;a href=&quot;https://docs.rs/libp2p/latest/libp2p/swarm/trait.NetworkBehaviour.html&quot; target=&quot;_blank&quot;&gt;NetworkBehaviour&lt;/a&gt; для всех членов структуры, которые не аннотированы с помощью &lt;code&gt;behavior(ignore)&lt;/code&gt;. Наш канал здесь игнорируется, потому что он не имеет прямого отношения к нашему поведению.&lt;/p&gt;
  &lt;p id=&quot;2Gf4&quot;&gt;Осталось реализовать функцию &lt;code&gt;inject_event&lt;/code&gt; для &lt;code&gt;FloodsubEvent&lt;/code&gt; и &lt;code&gt;MdnsEvent&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;wKh6&quot;&gt;Давайте начнем с &lt;code&gt;mDNS&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;VHCM&quot; data-lang=&quot;rust&quot;&gt;impl NetworkBehaviourEventProcess&amp;lt;MdnsEvent&amp;gt; for RecipeBehaviour {
    fn inject_event(&amp;amp;mut self, event: MdnsEvent) {
        match event {
            MdnsEvent::Discovered(discovered_list) =&amp;gt; {
                for (peer, _addr) in discovered_list {
                    self.floodsub.add_node_to_partial_view(peer);
                }
            }
            MdnsEvent::Expired(expired_list) =&amp;gt; {
                for (peer, _addr) in expired_list {
                    if !self.mdns.has_node(&amp;amp;peer) {
                        self.floodsub.remove_node_from_partial_view(&amp;amp;peer);
                    }
                }
            }
        }
    }
}&lt;/pre&gt;
  &lt;p id=&quot;hpwY&quot;&gt;Функция &lt;code&gt;inject_event&lt;/code&gt; вызывается, когда приходит событие для этого обработчика. На стороне &lt;code&gt;mDNS&lt;/code&gt; есть только два события, &lt;code&gt;Discovered&lt;/code&gt; и &lt;code&gt;Expired&lt;/code&gt;, которые срабатывают, когда мы видим новый пир в сети или когда существующий пир исчезает. В обоих случаях мы либо добавляем его, либо удаляем из нашего «partial view» (частичного представления) &lt;code&gt;FloodSub&lt;/code&gt;, которое представляет собой список нод, на которые мы распространяем наши сообщения.&lt;/p&gt;
  &lt;p id=&quot;NyAb&quot;&gt;&lt;code&gt;Inject_event&lt;/code&gt; для событий pub/sub немного сложнее. Нам нужно реагировать на входящие &lt;code&gt;ListRequest&lt;/code&gt; и &lt;code&gt;ListResponse&lt;/code&gt; пейлоады. Если мы отправим &lt;code&gt;ListRequest&lt;/code&gt;, пир, получивший запрос, получит свои локальные, опубликованные рецепты, а затем ему потребуется способ отправить их обратно.&lt;/p&gt;
  &lt;p id=&quot;T45y&quot;&gt;Единственный способ отправить их обратно запрашивающему пиру — это опубликовать их в сети. Поскольку pub/sub — единственный механизм, который у нас есть, нам нужно реагировать как на входящие запросы, так и на входящие ответы.&lt;/p&gt;
  &lt;p id=&quot;7p67&quot;&gt;Давайте посмотрим, как это работает:&lt;/p&gt;
  &lt;pre id=&quot;7NUh&quot; data-lang=&quot;rust&quot;&gt;impl NetworkBehaviourEventProcess&amp;lt;FloodsubEvent&amp;gt; for RecipeBehaviour {
    fn inject_event(&amp;amp;mut self, event: FloodsubEvent) {
        match event {
            FloodsubEvent::Message(msg) =&amp;gt; {
                if let Ok(resp) = serde_json::from_slice::&amp;lt;ListResponse&amp;gt;(&amp;amp;msg.data) {
                    if resp.receiver == PEER_ID.to_string() {
                        info!(&amp;quot;Response from {}:&amp;quot;, msg.source);
                        resp.data.iter().for_each(|r| info!(&amp;quot;{:?}&amp;quot;, r));
                    }
                } else if let Ok(req) = serde_json::from_slice::&amp;lt;ListRequest&amp;gt;(&amp;amp;msg.data) {
                    match req.mode {
                        ListMode::ALL =&amp;gt; {
                            info!(&amp;quot;Received ALL req: {:?} from {:?}&amp;quot;, req, msg.source);
                            respond_with_public_recipes(
                                self.response_sender.clone(),
                                msg.source.to_string(),
                            );
                        }
                        ListMode::One(ref peer_id) =&amp;gt; {
                            if peer_id == &amp;amp;PEER_ID.to_string() {
                                info!(&amp;quot;Received req: {:?} from {:?}&amp;quot;, req, msg.source);
                                respond_with_public_recipes(
                                    self.response_sender.clone(),
                                    msg.source.to_string(),
                                );
                            }
                        }
                    }
                }
            }
            _ =&amp;gt; (),
        }
    }
}&lt;/pre&gt;
  &lt;p id=&quot;FmVF&quot;&gt;Мы сопоставляем входящее сообщение, пытаясь десериализовать его в запрос или ответ. В случае ответа мы просто печатаем ответ с ID вызывающего пира, который мы получаем с помощью &lt;code&gt;msg.source&lt;/code&gt;. Когда мы получаем входящий запрос, нам нужно различать случаи &lt;code&gt;ALL&lt;/code&gt; и &lt;code&gt;One&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;zXM4&quot;&gt;В случае &lt;code&gt;One&lt;/code&gt; мы проверяем, совпадает ли заданный peer ID с нашим — действительно ли запрос предназначен для нас. Если это так, мы возвращаем наши опубликованные рецепты, что также является нашим ответом в случае &lt;code&gt;ALL&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;pN9F&quot;&gt;В обоих случаях мы вызываем хелпер &lt;code&gt;response_with_public_recipes&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;PCek&quot; data-lang=&quot;rust&quot;&gt;fn respond_with_public_recipes(sender: mpsc::UnboundedSender&amp;lt;ListResponse&amp;gt;, receiver: String) {
    tokio::spawn(async move {
        match read_local_recipes().await {
            Ok(recipes) =&amp;gt; {
                let resp = ListResponse {
                    mode: ListMode::ALL,
                    receiver,
                    data: recipes.into_iter().filter(|r| r.public).collect(),
                };
                if let Err(e) = sender.send(resp) {
                    error!(&amp;quot;error sending response via channel, {}&amp;quot;, e);
                }
            }
            Err(e) =&amp;gt; error!(&amp;quot;error fetching local recipes to answer ALL request, {}&amp;quot;, e),
        }
    });
}&lt;/pre&gt;
  &lt;p id=&quot;o5Ux&quot;&gt;В этом вспомогательном методе мы используем &lt;code&gt;spawn&lt;/code&gt; у Tokio для асинхронного выполнения future, который считывает все локальные рецепты, создает &lt;code&gt;ListResponse&lt;/code&gt; из данных и отправляет эти данные через &lt;code&gt;channel_sender&lt;/code&gt; в наш цикл событий, где мы обрабатываем это следующим образом (обновите &lt;a href=&quot;https://teletype.in/@scamushka/libp2p_tutorial#Erlj&quot; target=&quot;_blank&quot;&gt;код&lt;/a&gt;, где &lt;code&gt;// …&lt;/code&gt;):&lt;/p&gt;
  &lt;pre id=&quot;023f&quot; data-lang=&quot;rust&quot;&gt;                EventType::Response(resp) =&amp;gt; {
                    let json = serde_json::to_string(&amp;amp;resp).expect(&amp;quot;can jsonify response&amp;quot;);
                    swarm
                        .behaviour_mut()
                        .floodsub
                        .publish(TOPIC.clone(), json.as_bytes());
                }&lt;/pre&gt;
  &lt;p id=&quot;30Nn&quot;&gt;Если мы замечаем «внутренне» отправленное событие через &lt;code&gt;Response&lt;/code&gt;, мы сериализуем его в JSON и отправляем в сеть.&lt;/p&gt;
  &lt;h2 id=&quot;pjSs&quot; data-align=&quot;center&quot;&gt;9. Тестирование с помощью &lt;code&gt;libp2p&lt;/code&gt;&lt;/h2&gt;
  &lt;section style=&quot;background-color:hsl(hsl(199, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;5RbL&quot;&gt;&lt;strong&gt;Linux системы (важно)&lt;/strong&gt;&lt;/p&gt;
    &lt;ul id=&quot;00u7&quot;&gt;
      &lt;li id=&quot;0zau&quot;&gt;Если возникли ошибки во время сборки, нужно установить дополнительные зависимости:&lt;/li&gt;
    &lt;/ul&gt;
    &lt;pre id=&quot;rJTM&quot; data-lang=&quot;bash&quot;&gt;sudo apt-get update &amp;amp;&amp;amp; sudo apt-get upgrade &amp;amp;&amp;amp; sudo apt-get install -y pkg-config build-essential libudev-dev&lt;/pre&gt;
    &lt;ul id=&quot;hYXW&quot;&gt;
      &lt;li id=&quot;R4AU&quot;&gt;Если используете VS Code с &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer&quot; target=&quot;_blank&quot;&gt;rust-analyzer&lt;/a&gt; и у вас возникают &lt;a href=&quot;https://github.com/dtolnay/anyhow/issues/250&quot; target=&quot;_blank&quot;&gt;ошибки&lt;/a&gt; при сборке, нужно удалить папку target и проверить локальный пакет и все его зависимости на наличие ошибок:&lt;/li&gt;
    &lt;/ul&gt;
    &lt;pre id=&quot;8Rjy&quot; data-lang=&quot;bash&quot;&gt;rm -rf target
cargo check&lt;/pre&gt;
  &lt;/section&gt;
  &lt;p id=&quot;MqIm&quot;&gt;На этом реализация закончена. Теперь давайте протестируем ее.&lt;/p&gt;
  &lt;p id=&quot;HFSm&quot;&gt;Чтобы убедиться, что наша реализация работает, запустим приложение в нескольких терминалах с помощью этой команды:&lt;/p&gt;
  &lt;pre id=&quot;PWY6&quot; data-lang=&quot;bash&quot;&gt;RUST_LOG=info cargo run&lt;/pre&gt;
  &lt;p id=&quot;D2fc&quot;&gt;Имейте в виду, что приложение ожидает наличия файла &lt;code&gt;recipes.json&lt;/code&gt; в директории, из которой вы его запускаете.&lt;/p&gt;
  &lt;p id=&quot;9GQb&quot;&gt;Когда приложение запустилось, мы получаем следующий лог, печатающий наш идентификатор пира:&lt;/p&gt;
  &lt;pre id=&quot;7pXs&quot; data-lang=&quot;bash&quot;&gt;INFO  rust_peer_to_peer_example &amp;gt; Peer Id: 12D3KooWDc1FDabQzpntvZRWeDZUL351gJRy3F4E8VN5Gx2pBCU2&lt;/pre&gt;
  &lt;p id=&quot;17vb&quot;&gt;Теперь нам нужно нажать Enter, чтобы запустить цикл событий.&lt;/p&gt;
  &lt;p id=&quot;YKdm&quot;&gt;При вводе &lt;code&gt;ls p&lt;/code&gt; мы получаем список обнаруженных нами пиров:&lt;/p&gt;
  &lt;pre id=&quot;O2cB&quot; data-lang=&quot;bash&quot;&gt;ls p
 INFO  rust_peer_to_peer_example &amp;gt; Discovered Peers:
 INFO  rust_peer_to_peer_example &amp;gt; 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG
 INFO  rust_peer_to_peer_example &amp;gt; 12D3KooWLGN85pv5XTDALGX5M6tRgQtUGMWXWasWQD6oJjMcEENA&lt;/pre&gt;
  &lt;p id=&quot;bBmR&quot;&gt;С помощью &lt;code&gt;ls r&lt;/code&gt; мы получаем локальные рецепты:&lt;/p&gt;
  &lt;pre id=&quot;sRUE&quot; data-lang=&quot;bash&quot;&gt;ls r
 INFO  rust_peer_to_peer_example &amp;gt; Local Recipes (3)
 INFO  rust_peer_to_peer_example &amp;gt; Recipe { id: 0, name: &amp;quot; Coffee&amp;quot;, ingredients: &amp;quot;Coffee&amp;quot;, instructions: &amp;quot;Make Coffee&amp;quot;, public: true }
 INFO  rust_peer_to_peer_example &amp;gt; Recipe { id: 1, name: &amp;quot; Tea&amp;quot;, ingredients: &amp;quot;Tea, Water&amp;quot;, instructions: &amp;quot;Boil Water, add tea&amp;quot;, public: false }
 INFO  rust_peer_to_peer_example &amp;gt; Recipe { id: 2, name: &amp;quot; Carrot Cake&amp;quot;, ingredients: &amp;quot;Carrots, Cake&amp;quot;, instructions: &amp;quot;Make Carrot Cake&amp;quot;, public: true }&lt;/pre&gt;
  &lt;p id=&quot;W0wT&quot;&gt;Вызов &lt;code&gt;ls r all&lt;/code&gt; запускает отправку запроса другим пирам и возвращает их рецепты:&lt;/p&gt;
  &lt;pre id=&quot;8Oq6&quot; data-lang=&quot;bash&quot;&gt;ls r all
 INFO  rust_peer_to_peer_example &amp;gt; Response from 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG:
 INFO  rust_peer_to_peer_example &amp;gt; Recipe { id: 0, name: &amp;quot; Coffee&amp;quot;, ingredients: &amp;quot;Coffee&amp;quot;, instructions: &amp;quot;Make Coffee&amp;quot;, public: true }
 INFO  rust_peer_to_peer_example &amp;gt; Recipe { id: 2, name: &amp;quot; Carrot Cake&amp;quot;, ingredients: &amp;quot;Carrots, Cake&amp;quot;, instructions: &amp;quot;Make Carrot Cake&amp;quot;, public: true }&lt;/pre&gt;
  &lt;p id=&quot;13jT&quot;&gt;То же самое произойдет, если мы используем &lt;code&gt;ls r&lt;/code&gt; с ID пира:&lt;/p&gt;
  &lt;pre id=&quot;sui9&quot; data-lang=&quot;bash&quot;&gt;ls r 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG
 INFO  rust_peer_to_peer_example &amp;gt; Response from 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG:
 INFO  rust_peer_to_peer_example &amp;gt; Recipe { id: 0, name: &amp;quot; Coffee&amp;quot;, ingredients: &amp;quot;Coffee&amp;quot;, instructions: &amp;quot;Make Coffee&amp;quot;, public: true }
 INFO  rust_peer_to_peer_example &amp;gt; Recipe { id: 2, name: &amp;quot; Carrot Cake&amp;quot;, ingredients: &amp;quot;Carrots, Cake&amp;quot;, instructions: &amp;quot;Make Carrot Cake&amp;quot;, public: true }&lt;/pre&gt;
  &lt;p id=&quot;AGs5&quot;&gt;Это работает! Вы также можете попробовать это с огромным количеством клиентов в одной сети.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;HH5W&quot;&gt;Полный код примера можно найти на &lt;a href=&quot;https://github.com/scamushka/rust-p2p-example&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
  &lt;/section&gt;
  &lt;h2 id=&quot;oN5V&quot; data-align=&quot;center&quot;&gt;10. Заключение&lt;/h2&gt;
  &lt;p id=&quot;c9A3&quot;&gt;В этой статье мы разобрали, как создать небольшое децентрализованное сетевое приложение с использованием Rust и &lt;code&gt;libp2p&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;osrW&quot;&gt;Если вы имеете опыт работы в вебе, многие сетевые концепции будут несколько знакомы, но создание p2p приложения все равно требует принципиально иного подхода к проектированию и созданию.&lt;/p&gt;
  &lt;p id=&quot;um6H&quot;&gt;Библиотека &lt;code&gt;libp2p&lt;/code&gt; является достаточно зрелой, и, благодаря популярности Rust на криптосцене, существует развивающаяся и богатая экосистема библиотек для создания мощных децентрализованных приложений.&lt;/p&gt;
  &lt;h2 id=&quot;htnq&quot; data-align=&quot;center&quot;&gt;11. Доп. комментарий и материалы на эту тему&lt;/h2&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;P9pV&quot;&gt;Будет справедливо подчеркнуть, что этот пример слишком упрощен:&lt;/p&gt;
    &lt;ol id=&quot;VFJE&quot;&gt;
      &lt;li id=&quot;e2TL&quot;&gt;Он использует только &lt;code&gt;mDNS&lt;/code&gt; для обнаружения пиров, поэтому он никогда не выйдет за пределы локальной сети&lt;/li&gt;
      &lt;li id=&quot;7IbL&quot;&gt;Использование &lt;code&gt;FloodSub&lt;/code&gt; для ответов на конкретный запрос является настоящим излишеством — ответ должен прийти на одну ноду, но он усиливается по всему оверлею (учитывая и без того неэффективную репликацию сообщений &lt;code&gt;FloodSub&lt;/code&gt;)&lt;/li&gt;
    &lt;/ol&gt;
    &lt;p id=&quot;Kx8W&quot;&gt;Поэтому можно попробовать еще пару вещей:&lt;/p&gt;
    &lt;ol id=&quot;lSr1&quot;&gt;
      &lt;li id=&quot;Sy1S&quot;&gt;Включить bootstrap ноды (как уже используется в примерах &lt;code&gt;libp2p&lt;/code&gt;)&lt;/li&gt;
      &lt;li id=&quot;UCLJ&quot;&gt;Использовать &lt;code&gt;RequestReply&lt;/code&gt; &lt;code&gt;NetworkBehaviour&lt;/code&gt; для отправки ответов на запрос списка только заинтересованным нодам (может потребоваться включить запрашивающего в список broadcast)&lt;/li&gt;
      &lt;li id=&quot;tHKJ&quot;&gt;Используйте более эффективный &lt;code&gt;GossipSub&lt;/code&gt; вместо &lt;code&gt;FloodSub&lt;/code&gt; — в основном тот же интерфейс, так что не так много изменений&lt;/li&gt;
    &lt;/ol&gt;
    &lt;p id=&quot;uJSq&quot;&gt;Автор: &lt;a href=&quot;https://blog.logrocket.com/libp2p-tutorial-build-a-peer-to-peer-app-in-rust/#comment-4773&quot; target=&quot;_blank&quot;&gt;https://blog.logrocket.com/libp2p-tutorial-build-a-peer-to-peer-app-in-rust/#comment-4773&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(263, 48%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;hOla&quot;&gt;Official Tutorials: &lt;a href=&quot;https://docs.rs/libp2p/latest/libp2p/tutorials/index.html&quot; target=&quot;_blank&quot;&gt;https://docs.rs/libp2p/latest/libp2p/tutorials/index.html&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;OFnS&quot;&gt;Official Examples: &lt;a href=&quot;https://github.com/libp2p/rust-libp2p/tree/master/examples&quot; target=&quot;_blank&quot;&gt;https://github.com/libp2p/rust-libp2p/tree/master/examples&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(263, 48%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;3L8E&quot;&gt;Libp2p Docs (Concepts): &lt;a href=&quot;https://docs.libp2p.io/concepts/&quot; target=&quot;_blank&quot;&gt;https://docs.libp2p.io/concepts/&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;Q2zq&quot;&gt;На этом всё! ❤️&lt;/p&gt;

</content></entry><entry><id>scamushka:solana_nft</id><link rel="alternate" type="text/html" href="https://teletype.in/@scamushka/solana_nft?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=scamushka"></link><title>Как NFT представлены в Solana: большой конспект без единой строчки кода</title><published>2022-07-27T12:10:39.891Z</published><updated>2022-07-27T15:22:07.415Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/9c/19/9c191c9f-b8e7-4223-ad86-e0713b322d50.png"></media:thumbnail><summary type="html">&lt;img src=&quot;https://img2.teletype.in/files/9f/05/9f0548c1-4b37-45b2-92bd-cb80bfa3b612.jpeg&quot;&gt;Всем привет! С вами ArteMm aka Скамушка ツ</summary><content type="html">
  &lt;figure id=&quot;Ppre&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/9f/05/9f0548c1-4b37-45b2-92bd-cb80bfa3b612.jpeg&quot; width=&quot;1279.9999999999998&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;6RsQ&quot;&gt;&lt;strong&gt;Всем привет!&lt;/strong&gt; С вами ArteMm aka &lt;a href=&quot;https://t.me/scamushka&quot; target=&quot;_blank&quot;&gt;Скамушка&lt;/a&gt; ツ&lt;/p&gt;
  &lt;p id=&quot;3Mju&quot;&gt;В этой статье мы разберёмся как работают нфт в солане. Это вольный перевод/конспект &lt;a href=&quot;https://lorisleiva.com/owning-digital-assets-in-solana/how-nfts-are-represented-in-solana&quot; target=&quot;_blank&quot;&gt;данной статьи&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;FO3C&quot;&gt;Поскольку солана структурирует данные иначе, чем другие блокчейны, поначалу можно запутаться в этой теме. Но в действительности ничего сложного нет!&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;h2 id=&quot;eJZ5&quot;&gt;Навигация по статье:&lt;/h2&gt;
    &lt;p id=&quot;GzJh&quot;&gt;&lt;strong&gt;1. &lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#FQmA&quot; target=&quot;_blank&quot;&gt;Различия с Ethereum&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;W9dY&quot;&gt;&lt;strong&gt;2. &lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#6Cr0&quot; target=&quot;_blank&quot;&gt;Представление обычных токенов&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;tFBF&quot;&gt;&lt;strong&gt;3. &lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#PywG&quot; target=&quot;_blank&quot;&gt;Данные в Mint и Token аккаунтах&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;Yp1m&quot;&gt;&lt;strong&gt;4. &lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#2G2U&quot; target=&quot;_blank&quot;&gt;Теперь поговорим о NFT&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;opo3&quot;&gt;&lt;strong&gt;5. &lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#03Qb&quot; target=&quot;_blank&quot;&gt;Где картинка?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;YGIJ&quot;&gt;&lt;strong&gt;6. &lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#6waS&quot; target=&quot;_blank&quot;&gt;Опять же, где картинка?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;dzyg&quot;&gt;&lt;strong&gt;7. &lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#Lyos&quot; target=&quot;_blank&quot;&gt;Зачем два хранилища метаданных?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;x8D3&quot;&gt;&lt;strong&gt;8. &lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#0auK&quot; target=&quot;_blank&quot;&gt;Печать нескольких выпусков&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;bmW2&quot;&gt;&lt;strong&gt;9. &lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#qojV&quot; target=&quot;_blank&quot;&gt;Стандарт токенов и Semi-Fungible токены&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;ul id=&quot;TMqF&quot;&gt;
      &lt;li id=&quot;EXcs&quot;&gt;&lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#K29I&quot; target=&quot;_blank&quot;&gt;&lt;strong&gt;NonFungible&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
      &lt;li id=&quot;gsy6&quot;&gt;&lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#f8fQ&quot; target=&quot;_blank&quot;&gt;&lt;strong&gt;FungibleAsset&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
      &lt;li id=&quot;8KMz&quot;&gt;&lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#LMRg&quot; target=&quot;_blank&quot;&gt;&lt;strong&gt;Fungible&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p id=&quot;eEfS&quot;&gt;&lt;strong&gt;10. &lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#SZZM&quot; target=&quot;_blank&quot;&gt;Заключение&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;TLQv&quot;&gt;&lt;strong&gt;11. &lt;a href=&quot;https://teletype.in/@scamushka/solana_nft#ydrA&quot; target=&quot;_blank&quot;&gt;Доп. материалы на эту тему&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;h2 id=&quot;FQmA&quot; data-align=&quot;center&quot;&gt;1. Различия с Ethereum&lt;/h2&gt;
  &lt;p id=&quot;4wFT&quot;&gt;В других блокчейнах (например, эфир) или в обычном коде, вы добавляете переменные в свою программу, и ваша логика обновляет эти переменные. В эфире контракт содержит как логику, так и данные, необходимые для того, чтобы этот контракт выполнял свою работу.&lt;/p&gt;
  &lt;p id=&quot;ZELt&quot;&gt;В солане же программа (=контракт) взаимодействует с &lt;a href=&quot;https://docs.solana.com/developing/programming-model/accounts&quot; target=&quot;_blank&quot;&gt;аккаунтами&lt;/a&gt; (=счетами, учётными записями), которые хранятся за пределами программы. Как и файл, аккаунты могут хранить произвольные типы данных (например, целые числа, строки, открытые ключи), а также SOL. Аккаунты также имеют метаданные, которые описывают, кому разрешен доступ к их данным и &lt;a href=&quot;https://docs.solana.com/developing/programming-model/accounts#rent&quot; target=&quot;_blank&quot;&gt;как долго может существовать аккаунт&lt;/a&gt;. Любой может читать или пополнять аккаунт, но только владелец аккаунта может списывать средства или изменять его данные.&lt;/p&gt;
  &lt;p id=&quot;sttn&quot;&gt;В общем, аккаунт либо содержит данные (например, сколько у вас токенов), либо представляет собой исполняемую программу (например, смарт-контракт). Первые называют «&lt;strong&gt;data accounts&lt;/strong&gt;», а вторые — «&lt;strong&gt;program accounts&lt;/strong&gt;». В отличие от эфира программные аккаунты не хранят состояние. Всё состояние хранится в дата аккаунтах.&lt;/p&gt;
  &lt;p id=&quot;X8CX&quot;&gt;Данные исполняемых аккаунтов используются исключительно для неизменяемого байт-кода, который применяется для обработки транзакций.&lt;/p&gt;
  &lt;h2 id=&quot;6Cr0&quot; data-align=&quot;center&quot;&gt;2. Представление обычных токенов&lt;/h2&gt;
  &lt;p id=&quot;eBvb&quot;&gt;Каждый тип токена определяется тем, что мы называем «&lt;strong&gt;Mint Account&lt;/strong&gt;». Этот аккаунт аналогичен печатной машине, поскольку его можно буквально использовать для минта токенов, что эквивалентно печати денег.&lt;/p&gt;
  &lt;p id=&quot;74Gm&quot;&gt;Затем отдельные лица могут владеть токенами через «&lt;strong&gt;Token Accounts&lt;/strong&gt;» (=банковский счёт), в которых хранится количество принадлежащих токенов.&lt;/p&gt;
  &lt;p id=&quot;Ru8b&quot;&gt;Наконец, в блокчейне нет такого понятия, как отдельны лица, поскольку люди взаимодействуют с ними через пары криптографических ключей, называемые кошельками. Открытый ключ каждого кошелька («&lt;strong&gt;Wallet Account&lt;/strong&gt;») указывает на Token Account, который хранит информацию о количество токенов, принадлежащих этому кошельку.&lt;/p&gt;
  &lt;p id=&quot;YBou&quot;&gt;Это приводит нас к следующей аналогии.&lt;/p&gt;
  &lt;figure id=&quot;BqNf&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/0b/db/0bdbb601-71b9-4e24-b402-734214bb4fcd.png&quot; width=&quot;979.7303921568629&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;4Hlf&quot;&gt;Прежде чем мы перейдем к следующему разделу, давайте рассмотрим краткий пример владения токенами в солане. Мы будем использовать токены USDC и AVDO.&lt;/p&gt;
  &lt;figure id=&quot;iU3j&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/1a/b3/1ab3afb0-b47c-44cc-ae28-97e7a50f094f.png&quot; width=&quot;980&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;yidB&quot;&gt;Как вы можете видеть, Алиса владеет частью USDC и частью AVDO через два кошелька и три токен аккаунта. С другой стороны, Боб владеет AVDO только через один кошелёк и два токен аккаунта.&lt;/p&gt;
  &lt;p id=&quot;yHGF&quot;&gt;Однако здесь появляется проблема. Представим, что Алиса хочет отправить Бобу несколько токенов AVDO. Поскольку у Боба есть два Token Account, какой аккаунт следует выбрать Алисе для внесения своих токенов? Должна ли она попросить Боба прислать ей открытый ключ выбранного им токен аккаунта? Ещё хуже, представьте, что Алиса хочет отправить Бобу токены USDC, а у Боба в настоящее время нет ни одного аккаунта для токенов USDC. Должна ли она создать новый токен аккаунт для Боба, а затем отправить ему его открытый ключ?&lt;/p&gt;
  &lt;p id=&quot;k32q&quot;&gt;Ни одна из этих проблем не делает отправку токенов невозможной, но они делают нашу жизнь сложнее, чем она должна быть.&lt;/p&gt;
  &lt;p id=&quot;4VsI&quot;&gt;При отправке токенов кому-то у вас обычно есть только открытый ключ их кошелька, и вам действительно не хочется беспокоиться о том, какой токен аккаунт использовать и существует ли он вообще.&lt;/p&gt;
  &lt;p id=&quot;K2BZ&quot;&gt;Решение этой проблемы: «&lt;strong&gt;Program Derived Addresses&lt;/strong&gt;» или сокращенно &lt;strong&gt;PDA&lt;/strong&gt;. Это открытые ключи, &lt;strong&gt;полученные из других открытых ключей&lt;/strong&gt; по специальному алгоритму.&lt;/p&gt;
  &lt;p id=&quot;1Zyj&quot;&gt;Для нас это означает, что, имея «Wallet Account» и «Mint Account», &lt;strong&gt;мы можем детерминистически найти соответствующий&lt;/strong&gt; Token Account. На самом деле эти аккаунты называются «Associated Token Accounts» (сокращенно ATA), и они управляются «&lt;a href=&quot;https://spl.solana.com/associated-token-account&quot; target=&quot;_blank&quot;&gt;Associated Token Account Program&lt;/a&gt;».&lt;/p&gt;
  &lt;p id=&quot;lR1G&quot;&gt;Таким образом, у нас есть два способа создания и использования токен аккаунтов: один детерминированный (с использованием PDA) и другой — нет (с использованием обычных токен аккаунтов).&lt;/p&gt;
  &lt;figure id=&quot;M3BG&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/4b/be/4bbe11f2-bb03-47bc-a090-c90929b546cc.png&quot; width=&quot;979.9999999999999&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;z9Ec&quot;&gt;Обратите внимание на аннотацию над PDA. Это параметры, необходимые для получения адреса связанного токен аккаунта.&lt;/p&gt;
  &lt;p id=&quot;ME7c&quot;&gt;Поскольку мы будем часто использовать эту диаграмму в данной статье, давайте немного сократим её представление. Мы будем использовать следующую диаграмму для представления общей связи между кошельками и минт аккаунтами. Они могли использовать PDAs, а могли и не использовать.&lt;/p&gt;
  &lt;figure id=&quot;Vd8j&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/b1/08/b1088607-3230-4fd5-9438-f6f484ad7a5b.png&quot; width=&quot;979.9999999999999&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;aR5D&quot;&gt;Теперь давайте быстро взглянем на наш предыдущий пример, используя только ассоциированные токен аккаунты.&lt;/p&gt;
  &lt;figure id=&quot;nfFV&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/bb/f4/bbf4f759-7de1-4e5d-9b2e-717af22d7744.png&quot; width=&quot;980.0000000000002&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;H7pW&quot;&gt;Обратите внимание, что теперь у нас только один токен аккаунт на кошелёк для одного минт аккаунта, что означает, что Алиса знает, куда отправить токены AVDO Бобу. Кроме того, Алиса знает, куда отправить Бобу USDC, даже если аккаунт еще не существует.&lt;/p&gt;
  &lt;h2 id=&quot;PywG&quot; data-align=&quot;center&quot;&gt;3. Данные в Mint и Token аккаунтах&lt;/h2&gt;
  &lt;p id=&quot;YaHR&quot;&gt;Уже скоро мы доберёмся до нфт, но сначала нам нужно понять, какие данные хранятся как в токен аккаунтах, так и в минт аккаунтах.&lt;/p&gt;
  &lt;p id=&quot;4Ljl&quot;&gt;Для этого давайте обновим нашу диаграмму, чтобы показать все данные, доступные для каждого аккаунта, а также отобразим владельца аккаунта, т. е. программу соланы, ответственную за её создание.&lt;/p&gt;
  &lt;figure id=&quot;OoUf&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/a5/d1/a5d1160c-67b1-457c-adbf-b3e8757310a8.png&quot; width=&quot;980&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;xQp7&quot;&gt;Хорошо, у нас есть несколько вещей, на которые стоит обратить внимание.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;ul id=&quot;dcpP&quot;&gt;
      &lt;li id=&quot;n7hW&quot;&gt;Во-первых, обратите внимание, как токен аккаунт (ассоциированный или нет) отслеживает как минт аккаунт, так и аккаунт кошелька. Эти связи выделены пунктирными линиями.&lt;/li&gt;
      &lt;li id=&quot;iVcN&quot;&gt;Следующим важным элементом данных в токен аккаунте является «&lt;strong&gt;Amount&lt;/strong&gt;». В нём хранится количество токенов, имеющихся на аккаунте.&lt;/li&gt;
      &lt;li id=&quot;qnIa&quot;&gt;Остальные данные также важны, но выходят за рамки этой статьи, поэтому я не буду их рассматривать. Я просто скажу, что когда вы видите атрибут &lt;em&gt;курсивом&lt;/em&gt;, это означает, что он необязателен.&lt;/li&gt;
      &lt;li id=&quot;5xWX&quot;&gt;Кстати говоря, следующий атрибут — «&lt;strong&gt;Supply&lt;/strong&gt;» — сообщает нам общее количество токенов, находящихся в настоящее время в обращении. Этот атрибут нельзя обновить вручную, он автоматически обновляется программой.&lt;/li&gt;
      &lt;li id=&quot;ao02&quot;&gt;Далее, атрибут «&lt;strong&gt;Decimals&lt;/strong&gt;» определяет, сколько знаков после запятой мы должны использовать для данного токена. Например, если токен аккаунт имеет &lt;code&gt;Amount = 250&lt;/code&gt;, а минт аккаунт имеет &lt;code&gt;Decimals = 2&lt;/code&gt;, это означает, что токен аккаунт фактически владеет &lt;code&gt;2.50&lt;/code&gt; этого токена. Таким образом, все денежные значения могут быть сохранены с использованием целых чисел.&lt;/li&gt;
      &lt;li id=&quot;wMlb&quot;&gt;Мы также пропустим остальные атрибуты, поскольку они выходят за рамки данной статьи.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/section&gt;
  &lt;p id=&quot;EKfe&quot;&gt;Чтобы приблизиться к представлению нфт, давайте немного поиграем с этими атрибутами.&lt;/p&gt;
  &lt;p id=&quot;8tIr&quot;&gt;Что, если мы создадим минт аккаунт с &lt;code&gt;Decimals = 0&lt;/code&gt; и сразу же &lt;strong&gt;заминтим один токен&lt;/strong&gt; на аккаунт кошелька?&lt;/p&gt;
  &lt;p id=&quot;KW1z&quot;&gt;Результатом будет &lt;strong&gt;минт аккаунт с одним токеном в обращении&lt;/strong&gt;, который нельзя разбить на более мелкие единицы — например, Алиса и Боб не могут иметь по 0.5 этого токена.&lt;/p&gt;
  &lt;figure id=&quot;qtmq&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/49/f1/49f1697d-a5ce-45ba-a52b-9904d9372faa.png&quot; width=&quot;980.0000000000002&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;7Hfc&quot;&gt;Единственная проблема заключается в том, что ничто не мешает «Mint Authority» продолжать минтить больше токенов в будущем. Если бы они это сделали, у нас внезапно появилось бы более одного токена в обращении и, следовательно, ими могло бы владеть более одного кошелька.&lt;/p&gt;
  &lt;p id=&quot;T0Cn&quot;&gt;Чтобы предотвратить это, &lt;strong&gt;Mint Authority должен отозвать своё право&lt;/strong&gt; на минт новых токенов сразу &lt;strong&gt;после минта первого&lt;/strong&gt;.&lt;/p&gt;
  &lt;figure id=&quot;OHO9&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/4e/aa/4eaad3b3-5aae-4c2d-94f2-1b6af783ba2b.png&quot; width=&quot;980.0000000000001&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;5QZ4&quot;&gt;В итоге мы получаем минт аккаунт, предложение которого никогда не превысит единицу и чей токен нельзя разделить или поделить.&lt;/p&gt;
  &lt;p id=&quot;bYyn&quot;&gt;Таким образом, в любой момент времени &lt;strong&gt;у этого минта может быть только один холдер токена&lt;/strong&gt;.&lt;/p&gt;
  &lt;h2 id=&quot;2G2U&quot; data-align=&quot;center&quot;&gt;4. Теперь поговорим о NFT&lt;/h2&gt;
  &lt;p id=&quot;69gA&quot;&gt;Чтобы токен был non-fungible, он должен обладать такими характеристиками, чтобы его нельзя было обменять на что-то аналогичное. Нам удалось достичь этого путем создания минт аккаунта, у которого никогда не будет более одного держателя токена. Тот, кто владеет этим токеном, владеет минт аккаунтом и, следовательно, владеет нфт.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(199, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;KEKZ&quot;&gt;Вкратце: NFT — это минт аккаунт с &lt;code&gt;Decimals = 0&lt;/code&gt;, предложение (&lt;code&gt;Supply&lt;/code&gt;) которого никогда не превысит единицу.&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;xTpl&quot;&gt;На мой взгляд, данная модель достаточно элегантна. Её представление не только встроено в её определение, но и, опираясь на токен и минт аккаунты, мы можем взаимодействовать с нфт так же, как и с токенами.&lt;/p&gt;
  &lt;h2 id=&quot;03Qb&quot; data-align=&quot;center&quot;&gt;5. Где картинка?&lt;/h2&gt;
  &lt;p id=&quot;2Bu9&quot;&gt;То, что мы объяснили до сих пор, действительно является нфт по определению, но не очень-то полезным. Где название? Где картинка?!&lt;/p&gt;
  &lt;p id=&quot;uROm&quot;&gt;Что ж, похоже, нам нужно прикрепить больше данных к нашему нфт, чтобы сделать его полезным. Вот тут и приходит на помощь Metaplex!&lt;/p&gt;
  &lt;p id=&quot;WV5V&quot;&gt;&lt;a href=&quot;https://www.metaplex.com/&quot; target=&quot;_blank&quot;&gt;Metaplex&lt;/a&gt; — компания, которая создаёт и поддерживает программы Solana. Их самая популярная программа называется «&lt;strong&gt;Token Metadata Program&lt;/strong&gt;». Как вы уже догадались, она добавляет метаданные к нашим токенам!&lt;/p&gt;
  &lt;p id=&quot;xcWz&quot;&gt;Для этого используются Program Derived Addresses (PDAs), которые позволяют нам детерминированно найти адрес, используя другие адреса.&lt;/p&gt;
  &lt;p id=&quot;B89L&quot;&gt;В этом случае Token Metadata Program создаст новый «Metadata Account», прикрепленный к этому нфт. Но как вы думаете, какой адрес он использует для получения собственного адреса? Минт аккаунт или токен аккаунт?&lt;/p&gt;
  &lt;figure id=&quot;VFDM&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/23/b3/23b324d5-1b10-4c9d-bde8-09da22f75273.png&quot; width=&quot;979.9999999999999&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;m3jl&quot;&gt;Ответ: &lt;strong&gt;минт аккаунт&lt;/strong&gt;! Минт аккаунт является наиболее важным аккаунтом для нфт. Токен аккаунт — это связь между нфт и кошельком. Если мы привяжем PDA к токен аккаунту, а затем продадим наш нфт, новый владелец потеряет все эти данные. Таким образом, &lt;strong&gt;минт аккаунт является основной точкой входа в нфт&lt;/strong&gt;.&lt;/p&gt;
  &lt;p id=&quot;uw3E&quot;&gt;Хорошо, давайте посмотрим, какими данными нас наградил Metaplex в «Metadata Account».&lt;/p&gt;
  &lt;figure id=&quot;je0K&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/a0/10/a0101713-d8f8-4286-89ce-863ea306f9d9.png&quot; width=&quot;980.0000000000001&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;9dtd&quot;&gt;Давайте пройдемся по всем этим атрибутам по очереди.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;ul id=&quot;qDmL&quot;&gt;
      &lt;li id=&quot;iyL2&quot;&gt;&lt;code&gt;Key&lt;/code&gt;: Этот атрибут является тем, что мы называем дискриминатором. Поскольку Metaplex имеет много разных типов аккаунтов в рамках Token Metadata Program, это говорит нам, с каким аккаунтом мы имеем дело. Здесь Metadata Account идентифицируется ключом &lt;code&gt;MetadataV1&lt;/code&gt;. Обратите внимание, что Token Program использует размер аккаунта вместо дискриминатора для определения этого параметра, что более эффективно, но менее гибко.&lt;/li&gt;
      &lt;li id=&quot;8dvN&quot;&gt;&lt;code&gt;Update Authority&lt;/code&gt;: Это аккаунт, который может обновлять «Metadata Account».&lt;/li&gt;
      &lt;li id=&quot;X1sE&quot;&gt;&lt;code&gt;Mint&lt;/code&gt;: Это указывает на минт аккаунт.&lt;/li&gt;
      &lt;li id=&quot;ljq9&quot;&gt;&lt;code&gt;Name&lt;/code&gt;: Ончейн имя NFT — ограничено 32 байтами.&lt;/li&gt;
      &lt;li id=&quot;SRvq&quot;&gt;&lt;code&gt;Symbol&lt;/code&gt;: Ончейн символ NFT, ограниченный 10 байтами. Часто этот параметр оставляют как пустую строку, но он может быть полезен, если вы хотите, чтобы у вашей коллекции NFT был общий символ. Например, ваш NFT дроп «Banana Blossom» может иметь символ «BNBL».&lt;/li&gt;
      &lt;li id=&quot;33EX&quot;&gt;&lt;code&gt;URI&lt;/code&gt;: URI нфт ограничен 200 байтами. &lt;strong&gt;Это один из важнейших атрибутов&lt;/strong&gt;. Он содержит URI, указывающий на оффчейн JSON-файл. Этот JSON-файл можно хранить либо на традиционном сервере (например, с помощью AWS), либо с помощью решения для постоянного хранения в другой сети (например, с помощью Arweave). Мы поговорим об этом JSON-файле чуть позже.&lt;/li&gt;
      &lt;li id=&quot;bnHl&quot;&gt;&lt;code&gt;Seller Fee Basis Points&lt;/code&gt;: Роялти, распределяемые между создателями в базисных пунктах, т. е. &lt;code&gt;550&lt;/code&gt; означает &lt;code&gt;5.5%&lt;/code&gt;.&lt;/li&gt;
      &lt;li id=&quot;MvHR&quot;&gt;&lt;code&gt;Creators&lt;/code&gt;: Массив создателей и их доля роялти. Этот массив ограничен 5 создателями. Для каждого создателя существует атрибут &lt;code&gt;verified&lt;/code&gt;, чтобы убедиться, что он подписал NFT для подтверждения его подлинности.&lt;/li&gt;
      &lt;li id=&quot;GRhi&quot;&gt;&lt;code&gt;Primary Sale Happened&lt;/code&gt;: Логическое значение, отслеживающее, был ли NFT продан ранее или нет. Это влияет на роялти.&lt;/li&gt;
      &lt;li id=&quot;aoLo&quot;&gt;&lt;code&gt;Is Mutable&lt;/code&gt;: Логическое значение, указывающее, можно ли изменить ончейн метаданные NFT. После переключения на &lt;code&gt;false&lt;/code&gt;, его нельзя вернуть.&lt;/li&gt;
      &lt;li id=&quot;Z7MP&quot;&gt;&lt;code&gt;Edition Nonce&lt;/code&gt;: Этот необязательный атрибут немного выходит за рамки, но он используется для проверки номера выпуска NFT, выпущенных ограниченным тиражом.&lt;/li&gt;
      &lt;li id=&quot;SFui&quot;&gt;&lt;code&gt;Token Standard&lt;/code&gt;: Этот необязательный атрибут определяет взаимозаменяемость токена. Подробнее об этом мы поговорим в этой статье.&lt;/li&gt;
      &lt;li id=&quot;mQZs&quot;&gt;&lt;code&gt;Collection&lt;/code&gt;: Этот необязательный атрибут ссылается на минт адрес другого NFT, который действует как NFT коллекция. Это полезно для торговых площадок, чтобы группировать NFT вместе и безопасно проверять эти коллекции.&lt;/li&gt;
      &lt;li id=&quot;PfHm&quot;&gt;&lt;code&gt;Uses&lt;/code&gt;: Этот необязательный атрибут может сделать NFT пригодными для использования. То есть вы можете загрузить в него определенное количество &amp;quot;использований&amp;quot; и использовать его, пока он не закончится. Вы даже можете сделать так, чтобы NFT уничтожался, когда он полностью израсходован.&lt;/li&gt;
      &lt;li id=&quot;LsN9&quot;&gt;&lt;code&gt;Collection Details&lt;/code&gt;: Этот необязательный атрибут позволяет нам отличать Collection NFT от обычных NFT и добавляет дополнительный контекст, например, количество NFT, связанных с Collection NFT. &lt;em&gt;Этого атрибута нет на иллюстрации&lt;/em&gt;.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/section&gt;
  &lt;p id=&quot;cafI&quot;&gt;Как видите, много интересных функций. Сейчас я не собираюсь рассматривать их все здесь. Но вы всегда можете ознакомиться с &lt;a href=&quot;https://docs.metaplex.com/programs/token-metadata/token-standard&quot; target=&quot;_blank&quot;&gt;официальной документацией Metaplex&lt;/a&gt; для получения дополнительной информации.&lt;/p&gt;
  &lt;h2 id=&quot;6waS&quot; data-align=&quot;center&quot;&gt;6. Опять же, где картинка?&lt;/h2&gt;
  &lt;p id=&quot;i2Jl&quot;&gt;Помните тот атрибут &lt;code&gt;URI&lt;/code&gt;, который указывает на оффчейн объект JSON? Так вот, этот объект JSON следует определенному стандарту, чтобы хранить еще больше данных.&lt;/p&gt;
  &lt;figure id=&quot;JiyV&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/73/e1/73e14245-08d4-498e-a648-c163c1896c72.png&quot; width=&quot;980&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;L5jm&quot;&gt;Как видите, среди прочего мы можем предоставить &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt; и, наконец, &lt;code&gt;image&lt;/code&gt;! Подобно атрибуту &lt;code&gt;URI&lt;/code&gt; у Metadata Account, этот атрибут &lt;code&gt;image&lt;/code&gt; должен быть URI, который можно использовать для загрузки цифрового актива. Также есть атрибут &lt;code&gt;animation_url&lt;/code&gt; и массив &lt;code&gt;files&lt;/code&gt; для NFT, которые имеют более настраиваемые потребности. Все эти активы могут храниться оффчейн (на традиционном сервере), либо с помощью постоянного хранилища (в другом блокчейне, например, Arweave). Рекомендую &lt;a href=&quot;https://docs.metaplex.com/programs/token-metadata/token-standard#token-standards&quot; target=&quot;_blank&quot;&gt;ознакомиться с документацией Metaplex&lt;/a&gt; для получения дополнительной информации о стандарте NFT.&lt;/p&gt;
  &lt;p id=&quot;i1yc&quot;&gt;Стоит отметить, что в этот JSON-объект можно добавить все, что угодно. Если вы планируете создать приложение, которое будет распознавать ваши собственные NFT, это может быть полезно. Но имейте в виду, что другие приложения и торговые площадки не будут знать о существовании этих данных и, следовательно, не будут их использовать.&lt;/p&gt;
  &lt;h2 id=&quot;Lyos&quot; data-align=&quot;center&quot;&gt;7. Зачем два хранилища метаданных?&lt;/h2&gt;
  &lt;p id=&quot;HjXy&quot;&gt;Вам может быть интересно, зачем нам два места для хранения данных вашего NFT? Разве мы не можем просто хранить все в Metadata Account?&lt;/p&gt;
  &lt;p id=&quot;mTt2&quot;&gt;Есть несколько проблем с этим.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;ul id=&quot;UHlz&quot;&gt;
      &lt;li id=&quot;vxc4&quot;&gt;Во-первых, хранение данных ончейн стоит денег. При разработке аккаунтов, используемых тысячами людей, вы должны помнить о цене, в которую обойдется их создание.&lt;/li&gt;
      &lt;li id=&quot;n4Qw&quot;&gt;Во-вторых, ончейн данные гораздо менее гибкие. После создания аккаунта с использованием определенной структуры его нельзя легко изменить. Поэтому, если бы нам пришлось хранить все ончейн, мы бы значительно ограничили количество данных, которые можно прикрепить к NFT.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/section&gt;
  &lt;p id=&quot;GrId&quot;&gt;В результате нам необходимо грамотно и безопасно разделить данные на две категории: on-chain и off-chain. Например, массив &lt;code&gt;Creators&lt;/code&gt; — ончейн, потому что нам нужен надежный способ узнать, действительно ли NFT был предложен и подписан данным исполнителем. Атрибут &lt;code&gt;Is Mutable&lt;/code&gt; — ончейн, потому что нам нужно гарантировать, что после того, как он будет изменен на &lt;code&gt;false&lt;/code&gt;, его нельзя будет вернуть обратно.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(199, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;lmBG&quot;&gt;В итоге, Token Metadata Program может создать гарантии и ожидания для on-chain данных, но не может делать этого для off-chain данных.&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;mIvD&quot;&gt;Однако это не обязательно означает, что оффчейн JSON-файл небезопасен и ему нельзя доверять. Блокчейны постоянного хранения, такие как Arweave, обычно используются для хранения как цифровых активов, так и метаданных JSON, которые на них ссылаются, обеспечивая неизменность оффчейн данных. Кроме того, NFT можно сделать неизменяемыми, гарантируя, что атрибут &lt;code&gt;URI&lt;/code&gt; у Metadata Account никогда не будет указывать на другое место. Это самая безопасная конфигурация NFT, поскольку гарантируется, что ее нельзя будет изменить.&lt;/p&gt;
  &lt;p id=&quot;aidF&quot;&gt;Также обратите внимание, что некоторым проектам NFT может потребоваться, чтобы их данные были изменяемыми таким образом, чтобы это было выгодно владельцам NFT. Например, если вы планируете создать NFT детеныша обезьяны, который постепенно превращается во взрослую обезьяну, вам нужно, чтобы метаданные JSON хранились на сервере, который вы контролируете, чтобы делать эти постепенные изменения. В следующий раз, когда владельцы NFT откроют свои кошельки, они увидят другое изображение, но скорее обрадуются, чем возмутятся. Я хочу сказать, что до тех пор, пока вы доверяете создателям ваших NFT, что гарантируется флагом Verified в ончейн атрибуте Creators, изменяемые оффчейн данные могут быть подлинными. Но, конечно же, не забывайте о DYOR.&lt;/p&gt;
  &lt;h2 id=&quot;0auK&quot; data-align=&quot;center&quot;&gt;8. Печать нескольких выпусков&lt;/h2&gt;
  &lt;p id=&quot;xh2j&quot;&gt;По больше части мы уже понимаем, как представлены нфт в солане. Но не до конца. Существует еще один важный аккаунт, предлагаемый Token Metadata Program, полученный из минт аккаунта (с помощью PDA).&lt;/p&gt;
  &lt;p id=&quot;UwXS&quot;&gt;Фактически, аккаунт, находящийся внутри этого PDA, может быть одного из двух различных типов. Это может быть либо «Master Edition», либо «Edition».&lt;/p&gt;
  &lt;figure id=&quot;1fZq&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/5f/df/5fdf2cad-4114-4c69-8d24-4cb6295492d6.png&quot; width=&quot;980.4941451990632&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;Y5zH&quot;&gt;«Master Edition», также известный как «Original Edition», — это NFT, которое может быть продублировано его владельцем определенное количество раз, определяемое атрибутом «Max Supply».&lt;/p&gt;
  &lt;p id=&quot;gmFh&quot;&gt;«Edition», также известный как «Print Edition», — это NFT, которое было продублировано с «Master Edition». Всякий раз, когда создается новый «Edition», он отслеживает свой родительский «Master Edition» и его номер издания. Он также увеличивает supply своего родительского «Master Edition». Как только саплай достигает максимального саплая, больше NFT не могут быть напечатаны таким образом. Обратите внимание, что «Max Supply» у «Master Edition» может быть null, что означает, что из него можно распечатать неограниченное количество NFT.&lt;/p&gt;
  &lt;p id=&quot;VkoC&quot;&gt;Также обратите внимание, что еще один — менее известный — PDA аккаунт под названием «Edition Marker» существует в «Edition» NFT, для обеспечения отсутствия дублирования между номерами изданий данного «Master Edition».&lt;/p&gt;
  &lt;figure id=&quot;obug&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/8d/1c/8d1c9f24-d3db-4813-af40-3122e8eae1fa.png&quot; width=&quot;980&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;kDJN&quot;&gt;Одним из вариантов использования этой функции является предоставление художникам возможности продавать более одной копии своих работ. Например, они могут выпустить 100 ограниченных выпусков своего 1/1 NFT, и каждый из них будет отслеживать номер своего издания ончейн.&lt;/p&gt;
  &lt;p id=&quot;UuYm&quot;&gt;Стоит отметить, что печать нескольких выпусков NFT совершенно необязательна, и большинство NFT устанавливают для своего «Max Supply» значение 0, чтобы предотвратить его использование.&lt;/p&gt;
  &lt;p id=&quot;Wvjj&quot;&gt;Хотя печать изданий является основной задачей «Master Editions» и «Editions», &lt;strong&gt;они отвечают не только за это&lt;/strong&gt;.&lt;/p&gt;
  &lt;p id=&quot;skBF&quot;&gt;Если вы помните, ранее мы говорили, что для того, чтобы «Mint Account» был NFT, нужно:&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;ul id=&quot;lH0Y&quot;&gt;
      &lt;li id=&quot;hSA9&quot;&gt;Иметь &lt;code&gt;Decimals = 0&lt;/code&gt;.&lt;/li&gt;
      &lt;li id=&quot;ccVp&quot;&gt;Заминтить ровно один токен на аккаунт кошелька.&lt;/li&gt;
      &lt;li id=&quot;WLmb&quot;&gt;Отозвать право на минт дополнительных токенов через атрибут «Mint Authority».&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/section&gt;
  &lt;p id=&quot;Tnql&quot;&gt;Что ж, Token Metadata Program использует Edition PDA, чтобы гарантировать эти свойства. При создании PDA Edition аккаунта (будь то «Master Edition» или «Edition») он проверит, что минт аккаунт имеет &lt;code&gt;Decimals = 0&lt;/code&gt; и &lt;code&gt;Supply = 1&lt;/code&gt;. Если какое-либо из этих условий не выполняется, аккаунт не будет создан. Если это удается, то он передает «Mint Authority» этому новому Edition PDA, гарантируя, что ни один кошелек никогда не сможет заминтить дополнительные токены. Это означает, &lt;strong&gt;что сам факт существования аккаунта «Master Edition» или «Edition» в минт аккаунте является доказательством его невзаимозаменяемости&lt;/strong&gt;.&lt;/p&gt;
  &lt;p id=&quot;ue5P&quot;&gt;Вот почему счета «Master Edition» и «Edition» важны в Solana NFTs и заслуживают упоминания в этой статье.&lt;/p&gt;
  &lt;p id=&quot;rLa2&quot;&gt;Вот небольшое обновление нашей диаграммы NFT. Как вы можете видеть, «Mint Authority» больше не null, а указывает на Edition PDA, который может контролироваться только Token Metadata Program с открытым исходным кодом и аудитом.&lt;/p&gt;
  &lt;figure id=&quot;jQzl&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/3b/80/3b809a2c-d028-43e2-8541-b647559c682f.png&quot; width=&quot;980.0000000000003&quot; /&gt;
  &lt;/figure&gt;
  &lt;h2 id=&quot;qojV&quot; data-align=&quot;center&quot;&gt;9. Стандарт токенов и Semi-Fungible токены&lt;/h2&gt;
  &lt;p id=&quot;OaV8&quot;&gt;Эта статья была бы неполной, если бы я не упомянул, что Token Metadata Program также поддерживает то, что мы называем «&lt;strong&gt;Semi-Fungible Tokens&lt;/strong&gt;» (полувзаимозаменяемые токены) или сокращенно SFT.&lt;/p&gt;
  &lt;p id=&quot;SgYV&quot;&gt;SFT в основном такие же, как NFT, но без невзаимозаменяемых гарантий, о которых мы говорили ранее.&lt;/p&gt;
  &lt;p id=&quot;1PDO&quot;&gt;«Но почему?», спросите вы, «я думал, что все дело в невзаимозаменяемости?».&lt;/p&gt;
  &lt;p id=&quot;uygj&quot;&gt;Ну, для NFT — да, но, если подумать, основная цель «Metadata Account» — добавление данных к токенам. Почему мы должны ограничивать эту функцию только невзаимозаменяемыми токенами?&lt;/p&gt;
  &lt;p id=&quot;J4xh&quot;&gt;Почему бы нашему взаимозаменяемому токену Avocado (AVDO) не добавить ончейн данные к своему минт аккаунту? Он мог бы использовать эти данные, чтобы сообщить децентрализованным биржам, какой символ использовать, какие внешние ссылки отображать, какой логотип показать и так далее.&lt;/p&gt;
  &lt;p id=&quot;AuPH&quot;&gt;Другим вариантом использования этого будет создание игрового актива в виде токена с &lt;code&gt;Decimals = 0&lt;/code&gt;. Например, вы можете создать токен для ресурса «Wood» в своей игре. Поскольку игроки должны иметь возможность владеть и торговать более чем одним деревом, нет особого смысла создавать один NFT для каждого отдельного куска дерева в вашей игре. Вот почему создание взаимозаменяемых игровых ресурсов при сохранении преимуществ от ончейн данных является очень ценным.&lt;/p&gt;
  &lt;p id=&quot;iQNF&quot;&gt;Таким образом, Token Metadata Program позволяет нам создавать Metadata Accounts для минт аккаунтов, которые являются взаимозаменяемыми. Именно поэтому &lt;strong&gt;ответственность за гарантию отсутствия взаимозаменяемости лежит на Edition PDA&lt;/strong&gt;.&lt;/p&gt;
  &lt;p id=&quot;GKma&quot;&gt;Чтобы облегчить нам жизнь, аккаунт метаданных отслеживает, «насколько взаимозаменяем» токен, с помощью атрибута «&lt;strong&gt;Token Standard&lt;/strong&gt;». Это может быть одно из следующих значений.&lt;/p&gt;
  &lt;h3 id=&quot;K29I&quot;&gt;&lt;code&gt;NonFungible&lt;/code&gt;&lt;/h3&gt;
  &lt;p id=&quot;piaE&quot;&gt;Если стандарт токена &lt;code&gt;NonFungible&lt;/code&gt;, мы знаем, &lt;strong&gt;что имеем дело с NFT&lt;/strong&gt;. Этот стандарт применяется тогда и только тогда, когда для этого минта был создан аккаунт «Master Edition» или «Edition».&lt;/p&gt;
  &lt;p id=&quot;XruR&quot;&gt;Это означает, что у нас есть следующие гарантии:&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;ul id=&quot;e53z&quot;&gt;
      &lt;li id=&quot;UI4L&quot;&gt;&lt;code&gt;Supply = 1&lt;/code&gt;.&lt;/li&gt;
      &lt;li id=&quot;gy2U&quot;&gt;&lt;code&gt;Decimals = 0&lt;/code&gt;.&lt;/li&gt;
      &lt;li id=&quot;LpsI&quot;&gt;Аккаунт существует под Edition PDA.&lt;/li&gt;
      &lt;li id=&quot;WzY1&quot;&gt;Mint Authority было передано Edition PDA.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/section&gt;
  &lt;figure id=&quot;jAzC&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/3b/80/3b809a2c-d028-43e2-8541-b647559c682f.png&quot; width=&quot;979.9999999999998&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;f8fQ&quot;&gt;&lt;code&gt;FungibleAsset&lt;/code&gt;&lt;/h3&gt;
  &lt;p id=&quot;SreY&quot;&gt;Если стандарт токена — &lt;code&gt;FungibleAsset&lt;/code&gt;, мы знаем, &lt;strong&gt;что имеем дело с недесятичным SFT&lt;/strong&gt;. Например, наш пример ресурса «Wood» будет обозначен как &lt;code&gt;FungibleAsset&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;dW0M&quot;&gt;Это означает, что у нас есть следующие гарантии:&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;ul id=&quot;lIOZ&quot;&gt;
      &lt;li id=&quot;C9eB&quot;&gt;&lt;code&gt;Decimals = 0&lt;/code&gt;.&lt;/li&gt;
      &lt;li id=&quot;Bvnj&quot;&gt;Под Edition PDA не существует аккаунта.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/section&gt;
  &lt;figure id=&quot;mf12&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/55/29/5529179a-6bae-454c-8e71-afb8560701ea.png&quot; width=&quot;979.9999999999999&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;LMRg&quot;&gt;&lt;code&gt;Fungible&lt;/code&gt;&lt;/h3&gt;
  &lt;p id=&quot;st5J&quot;&gt;Если стандарт токена — &lt;code&gt;Fungible&lt;/code&gt;, мы знаем, &lt;strong&gt;что имеем дело с десятичным SFT&lt;/strong&gt;. Например, токен Avocado или токен USDC подходят под эту категорию.&lt;/p&gt;
  &lt;p id=&quot;EOcY&quot;&gt;Это означает, что у нас есть следующие гарантии:&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;ul id=&quot;H7AI&quot;&gt;
      &lt;li id=&quot;8U2M&quot;&gt;&lt;code&gt;Decimals &amp;gt; 0&lt;/code&gt; (строго больше 0).&lt;/li&gt;
      &lt;li id=&quot;CXXC&quot;&gt;Под Edition PDA не существует аккаунта.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/section&gt;
  &lt;figure id=&quot;BL65&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/99/86/99860a8a-2403-4c33-ad68-6e480b706681.png&quot; width=&quot;980.0000000000001&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;SLcH&quot;&gt;Стоит отметить, что стандарт оффчейн JSON метаданных варьируется в зависимости от «Token Standard», о котором мы только что говорили. Вы можете найти определение JSON стардарта для каждого типа токена ниже.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;ul id=&quot;Bxpf&quot;&gt;
      &lt;li id=&quot;N3qH&quot;&gt;&lt;a href=&quot;https://docs.metaplex.com/programs/token-metadata/token-standard#the-non-fungible-standard&quot; target=&quot;_blank&quot;&gt;Стандарт JSON для Non Fungible токенов&lt;/a&gt;&lt;/li&gt;
      &lt;li id=&quot;FpRP&quot;&gt;&lt;a href=&quot;https://docs.metaplex.com/programs/token-metadata/token-standard#the-fungible-asset-standard&quot; target=&quot;_blank&quot;&gt;Стандарт JSON для Fungible Asset токенов&lt;/a&gt;&lt;/li&gt;
      &lt;li id=&quot;Osru&quot;&gt;&lt;a href=&quot;https://docs.metaplex.com/programs/token-metadata/token-standard#the-fungible-standard&quot; target=&quot;_blank&quot;&gt;Стандарт JSON для Fungible токенов&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/section&gt;
  &lt;h2 id=&quot;SZZM&quot; data-align=&quot;center&quot;&gt;10. Заключение&lt;/h2&gt;
  &lt;p id=&quot;RUYj&quot;&gt;Теперь мы не только имеем полное представление о том, как выглядят NFT и SFT в Solana, но и понимаем, почему вещи устроены так, как они есть, и как они сравниваются с другими привычными нам моделями.&lt;/p&gt;
  &lt;p id=&quot;Haod&quot;&gt;Эта статья была бы неполной без заключительной диаграммы, резюмирующей то, что мы здесь узнали.&lt;/p&gt;
  &lt;figure id=&quot;22VP&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/28/53/28534195-56f6-4cc1-a218-6faf5ed09ddc.png&quot; width=&quot;980.0000000000001&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;lhGL&quot;&gt;И чтобы сделать эту диаграмму еще более полезной для разработчиков, давайте также добавим смещение и размер каждого атрибута аккаунта в байтах. Я буду использовать тильду &lt;code&gt;~&lt;/code&gt; для переменных размеров аккаунта.&lt;/p&gt;
  &lt;figure id=&quot;TGXi&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/34/5d/345db337-920d-496e-a79b-a2c60314011b.png&quot; width=&quot;979.9999999999999&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;hKWl&quot;&gt;На этом всё! ❤️&lt;/p&gt;
  &lt;h2 id=&quot;ydrA&quot; data-align=&quot;center&quot;&gt;11. Доп. материалы на эту тему&lt;/h2&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;UIib&quot;&gt;Solana’s Token Program, Explained: &lt;a href=&quot;https://pencilflip.medium.com/solanas-token-program-explained-de0ddce29714&quot; target=&quot;_blank&quot;&gt;https://pencilflip.medium.com/solanas-token-program-explained-de0ddce29714&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(263, 48%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;pCRF&quot;&gt;Understanding Program Derived Addresses: &lt;a href=&quot;https://www.brianfriel.xyz/understanding-program-derived-addresses/&quot; target=&quot;_blank&quot;&gt;https://www.brianfriel.xyz/understanding-program-derived-addresses/&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;4Ppy&quot;&gt;Metaplex Docs (Token Metadata): &lt;a href=&quot;https://docs.metaplex.com/programs/token-metadata/&quot; target=&quot;_blank&quot;&gt;https://docs.metaplex.com/programs/token-metadata/&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(263, 48%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;5NZW&quot;&gt;Solana Docs (Programming Model): &lt;a href=&quot;https://docs.solana.com/developing/programming-model/overview&quot; target=&quot;_blank&quot;&gt;https://docs.solana.com/developing/programming-model/overview&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;ZsrC&quot;&gt;Solana Cookbook (Core Concepts): &lt;a href=&quot;https://solanacookbook.com/core-concepts/accounts.html&quot; target=&quot;_blank&quot;&gt;https://solanacookbook.com/core-concepts/accounts.html&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(263, 48%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;tAJq&quot;&gt;Solana Wiki (Account Model): &lt;a href=&quot;https://solana.wiki/zh-cn/docs/account-model/#account-storage&quot; target=&quot;_blank&quot;&gt;https://solana.wiki/zh-cn/docs/account-model/#account-storage&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;

</content></entry></feed>